Compare commits

...

246 Commits

Author SHA1 Message Date
Mohamed Akram 95c890ed88 Fix non-contiguous program indices 2026-03-25 20:33:51 +01:00
Tom Mrazauskas b10cec2322 chore: update type testing setup (#2139)
* chore: update type testing setup
* stricter TSConfig
* deduplicate type checking
2026-03-22 23:32:32 +01:00
Igor Savin 800c3db6b2 Remove dependency 2026-03-20 23:12:25 +01:00
Igor Savin be945bb7bd Address comments 2026-03-20 23:12:25 +01:00
Igor Savin 824e3789c8 Cleanup 2026-03-20 23:12:25 +01:00
Igor Savin f8c6677458 Run in CI 2026-03-20 23:12:25 +01:00
Igor Savin f1421eb2ab Remove grunt, add s3 publishing tests 2026-03-20 23:12:25 +01:00
Igor Savin b5ad730473 Improve error handling 2026-03-19 22:05:55 +01:00
Igor Savin ff02463429 Make CLI ESM for compatibility with new yargs 2026-03-19 22:05:55 +01:00
Igor Savin a8a1c9dd4f Adjust coverage 2026-03-19 22:05:55 +01:00
Igor Savin 243060b310 Fix CI 2026-03-19 22:05:55 +01:00
Igor Savin 60d17f546d Support section filter in benchmarks 2026-03-19 22:05:55 +01:00
Igor Savin 61659fbe63 Cleanup 2026-03-19 22:05:55 +01:00
Igor Savin c1bc257d39 Improve tests 2026-03-19 22:05:55 +01:00
Igor Savin 1b321a8bef add tstyche tests 2026-03-19 22:05:55 +01:00
Igor Savin 92e842f82e update dependencies 2026-03-19 22:05:55 +01:00
Jakob Linskeseder 6f1de2025e Explicitly set region for AWS S3 publishing
Fixes the following error:
`Error: Region is missing`
2026-03-18 21:29:06 +01:00
Jakob Linskeseder d025292e06 Migrate AWS SDK for JavaScript v2 APIs to v3
This was cherry-picked from https://github.com/handlebars-lang/handlebars.js/pull/2020.
2026-03-18 20:46:45 +01:00
Jakob Linskeseder 71350d90ec Don't fail tests if Git is not available
I need this because I run Node inside a
minimal container for security reasons.
2026-03-18 20:46:45 +01:00
Igor Savin bf93bafeca Modernize benchmarks (#2132)
* Introduce benchmarks
2026-03-17 23:43:46 +01:00
Igor Savin 70b8f1145d feat: migrate to RSPack (#2131)
* Migrate to RSPack
* Add tests to CI, address coverage
* fix: Restore AWS sdk
* add simple serve
2026-03-14 23:08:19 +01:00
Igor Savin 2f68d9425b Migrate to oxlint and oxfmt v2 (#2130)
* Migrate to oxlint and oxfmt
* Address review comments
2026-03-10 17:27:44 +01:00
Igor Savin 169ef75066 Remove deprecated helpers and fix release script (#2128)
* Remove deprecated helpers
* Use more idiomatic assertions
* Fix release script
2026-03-03 21:52:28 +01:00
Igor Savin d65683434d Convert tests to vitest (#2127)
* Convert tests to vitest
* fix git tests
* Cleanup
* Convert ignore comments
* address code review comments
2026-03-02 21:33:52 +01:00
Jakob Linskeseder 1245255892 Fix Playwright tests in CI pipeline
Also change README to use standard markdown extension.
2026-03-01 17:31:58 +01:00
Igor Savin 22e04868c0 Apply compatibility fixes 2026-03-01 17:26:37 +01:00
Igor Savin a3ac164642 Refresh Node versions used 2026-03-01 17:26:37 +01:00
William Entriken 864b721fef remove dead link 2025-08-19 22:53:19 +02:00
Wouter Van Schandevijl cc8574e631 fix README links to docs 2025-07-07 19:01:55 +02:00
Mohamed Akram f422bfdf3e Improve rendering performance
Avoid unnecessary copies via Utils.extend in hot paths.
2024-09-03 22:27:34 +02:00
Christian Clauss 25c696b889 Upgrade GitHub Actions checkout and setup-node
* https://github.com/actions/checkout/releases
* https://github.com/actions/setup-node/releases
2023-12-14 00:07:04 +01:00
Benoit Vallée 1fc4ef09c1 Fix type "RuntimeOptions" also accepting string partials 2023-09-21 22:30:51 +02:00
Mohamed Akram ce1f2abf5a Drop support for EOL Node.js versions 2023-09-20 21:51:23 +02:00
Mohamed Akram 2afecb25db Fix running integration tests on macOS 2023-09-20 21:26:53 +02:00
Jonas Thelemann cb828aa282 feat(types): set hash to be a Record<string, any>
Resolves #2001
2023-09-12 23:24:37 +02:00
Jonas Thelemann 0bc9f634ca chore(pull-request-template): correct types path
I think it's `/types` now.
2023-09-08 23:12:13 +02:00
Jakob Linskeseder ae83edd5c4 Support Map and Set in #each block
Also support Map-keys in lookup-expressions.

See #1418 #1679
2023-09-06 23:20:32 +02:00
Seyed Mohammad Mahdi Hatami 8ce2be4496 Improve error-message when partial is missing
Signed-off-by: Seyed Mohammad Mahdi Hatami <hatamik7@gmail.com>
2023-09-01 00:29:18 +02:00
Nils Knappmeier d3b93571a7 refactor: rename i to startPartIndex 2023-08-05 22:41:22 +02:00
Jakob Linskeseder 99d162cadc 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:14 +02:00
Jakob Linskeseder 7147755b55 Allow manually triggering the release-action 2023-08-02 23:03:30 +02:00
Jakob Linskeseder 9413ea99ce Format *.md files with prettier
The `npm run format` command now matches
the husky `lint-staged`-commands.

Also upgraded to prettier v3.
2023-07-30 23:38:10 +02:00
Jakob Linskeseder f202c501a3 Pass correct environment for AWS S3 publishing
See #1972
2023-07-29 21:44:00 +02:00
Jakob Linskeseder bb55dead2d Checkout submodules when publishing to AWS S3
See #1972
2023-07-29 21:24:52 +02:00
Jakob Linskeseder 6e10b1f658 Fix publishing to AWS S3
See #1972
2023-07-29 21:18:15 +02:00
Jakob Linskeseder 65f24eb8b6 Publish latest and tags to AWS S3
This recreates the logic from this removed
TravisCI job:
https://github.com/handlebars-lang/handlebars.js/blob/a9a8e403213583ca90cb7c872d3a22796c37d961/.travis.yml#L16-L25
2023-07-29 21:09:35 +02:00
Jay Linski 35f001862c Create SECURITY.md
Closes #1961.
2023-07-25 19:21:46 +02:00
Jakob Linskeseder b3ffb16e5d Fix browser tests in CI pipeline 2023-07-24 21:55:20 +02:00
biodiscus 42adee3f82 Add the link to html-bundler-webpack-plugin in README 2023-07-24 21:53:06 +02:00
Jakob Linskeseder b4a54ad86e Test with Node 20 2023-05-17 01:30:18 +02:00
Jakob Linskeseder e6686963ba Use @handlebars/parser v2
We have to remove `--failAfterWarnings` from our Rollup integration-test,
because Typescript, when targeting es5, will use global `this` for transpilation.
Rollup warns about this, since it replaces `this` with `undefined`:

```
src/index.js → dist/bundle.js...
(!) `this` has been rewritten to `undefined`
https://rollupjs.org/guide/en/#error-this-is-undefined
../../../node_modules/@handlebars/parser/dist/esm/printer.js
1: var __spreadArrays = (this && this.__spreadArrays) || function () {}
```

See https://github.com/handlebars-lang/handlebars-parser/releases/tag/v2.0.0
2022-10-29 22:09:05 +02:00
Jakob Linskeseder 262b84b08a Fix integration-tests by not using browserlist from root
The `browserlist`-config was introduced in 6bc73fdad7.

See https://webpack.js.org/configuration/target/#browserslist.
2022-10-29 21:49:15 +02:00
Jakob Linskeseder 6186128ad0 Make library compatible with workers
Pulled from 4.x branch, see #1894.
2022-10-29 21:49:15 +02:00
Jakob Linskeseder 03fe783a69 Ignore prettier formatting-commit
See https://github.blog/changelog/2022-03-24-ignore-commits-in-the-blame-view-beta/
2022-10-29 21:49:15 +02:00
Jakob Linskeseder e534a911f3 Upgrade prettier to v2
Prettier v2 has the following breaking changes:
* enforces spaces between `function` and params
* enforces trailing commas by default
2022-10-29 21:49:15 +02:00
Jakob Linskeseder f6ff3bf52b Upgrade to ECMAScript 2020
This is needed in order to use `globalThis`, see #1894.
It also made it possible to remove some old polyfills and fallbacks.
2022-10-29 21:49:15 +02:00
Jakob Linskeseder da41887377 Get rid of underscore-dependency in specs 2022-10-29 21:49:15 +02:00
Jakob Linskeseder 83baaa469f Remove support for IE11 and dead browsers
Handlebars v5 will target modern browsers,
while older browsers will still receive support
via version 4.x.
2022-10-29 21:49:15 +02:00
Jakob Linskeseder 785a63e0a8 Test on Node 18 and drop EOL Node 10
* Updated lock-file to fix https://github.com/npm/cli/issues/4859.
* Updated integration-tests to webpack 5 to fix
  https://github.com/webpack/webpack/issues/14532.
* Added `mode` to webpack-integration-tests to avoid
  the warning `The 'mode' option has not been set...`.
* Replaced outdated `grunt-bg-shell`-package to get rid of
  coffee-script warnings
2022-10-16 22:21:56 +02:00
Jakob Linskeseder 903ca504b5 Remove unused parser tests
Parser tests are not used anymore, since the parser is
now living in a separate package.
2022-10-14 13:56:17 +02:00
1337617 69dbe65c19 Improving the README.markdown document 2022-09-17 19:00:38 +02:00
Nils Knappmeier 54cbc8c6c6 update package-lock.json 2022-06-21 22:26:22 +02:00
arturmakaranka b14ff4a6fc Fix compiling of each block params in strict mode
Fixes #1820
Closes #1821
2022-05-25 22:20:04 +02:00
Jakob Linskeseder e0de1d9a4a Add CI check for unmet dependencies
Fixes #1592
2022-05-20 22:37:11 +02:00
Jakob Linskeseder 9bff03fd3b 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 20:46:30 +02:00
James Wilson a9fe6d5a03 use https instead of git for mustache submodule 2022-03-19 19:13:23 +01:00
Jakob Linskeseder ce8982fe55 Add Plop to in-the-wild list 2022-01-10 19:21:58 +01:00
Jakob Linskeseder 80809ef7fc Add more in-the-wild packages to README 2022-01-04 21:52:02 +01:00
Jakob Linskeseder 30bd78d457 Enable IDE code-highlighting for bin-file
By adding the `.js`-extension to the bin-file, IDEs will
know that js code-highlighting should be applied to the
file, which makes it easier for devs to read the code.
2022-01-02 19:19:00 +01:00
Jakob Linskeseder 0896d00c2e Remove outdated eco package from bench
The `eco`-templates (Embedded CoffeeScript templates)
had their last update over 10 years ago, so we can
remove this dependency from our benchmark.
2022-01-02 00:15:47 +01:00
Jakob Linskeseder f73be4ebf3 Remove unused jison package
This is package is not needed anymore since the parser
was moved to a standalone package in
19bdace85a.
2022-01-02 00:15:47 +01:00
Jakob Linskeseder 29ed243268 Replace shields.io with faster badgen.net
Badgen is a faster successor of shields.io:
https://github.com/badgen/badgen.net
2021-12-30 00:57:40 +01:00
Jakob Linskeseder dd6ff05079 Add npm version and stats to README 2021-12-30 00:47:47 +01:00
Jakob Linskeseder c18ed16f71 Update repository URL
Related to c295ef085f.
2021-12-30 00:47:47 +01:00
Jakob Linskeseder 7e5890cf5a Fix build-status badge in README 2021-12-30 00:08:26 +01:00
Jakob Linskeseder c7278d176a Fix security issue in y18n (CVE-2020-7774)
See https://github.com/advisories/GHSA-c4w7-xm78-47vh.
2021-12-30 00:04:27 +01:00
Jay Linski 35ea8e61bf Merge pull request #1774 from handlebars-lang/merge-4.x
Merge branch 4.x into master
2021-12-29 23:47:46 +01:00
Jakob Linskeseder a98b01c2ee Fix TypeScript linting
Also upgraded to `@definitelytyped/dtslint`.
2021-12-29 23:43:57 +01:00
Jakob Linskeseder b3b26712d9 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!
```
2021-12-29 23:29:47 +01:00
Jakob Linskeseder 37f4ea18b4 Remove AMD tests
AMD modules were removed in v5.
2021-12-29 23:21:14 +01:00
Jakob Linskeseder 48870b41f7 Update to lock-file version 2 2021-12-29 23:11:07 +01:00
Jakob Linskeseder 82c613244a Merge branch '4.x' into merge-4.x 2021-12-29 23:10:11 +01:00
Jakob Linskeseder 4da5882660 Remove non-standard package.json property
This was added in 88ee4757e7
and is not used anymore since a2687fdf50.
2021-12-29 21:48:34 +01:00
Jakob Linskeseder 476d61667d Sort in-the-wild list by name
Also changed protocol to https.
2021-12-23 23:31:15 +01:00
Jakob Linskeseder 5ba0c72fe6 Add vite-package to in-the-wild list
Resolves https://github.com/handlebars-lang/docs/pull/70
2021-12-23 23:31:15 +01:00
Jakob Linskeseder 49621c15ac Remove archived projects from in-the-wild list 2021-12-23 23:31:15 +01:00
Jakob Linskeseder 1dc2c0bffb Link to new live demo (playground) on docs site
This site is served via HTTPS and provides the latest
Handlebars.js versions.
2021-12-23 23:31:15 +01:00
Jakob Linskeseder fe58756da2 Fix typos
This was found by running an IntellijIDE
code inspection for proofreading.
2021-12-22 20:36:26 +01:00
Jakob Linskeseder b0f1a62ddb Add missing release for tag v4.2.2 2021-12-03 21:01:26 +01:00
Jakob Linskeseder 7d9450d3c3 Add missing release for tag v4.3.5
Also updated all URLs
2021-12-03 20:46:55 +01:00
Mehdi e538c29d92 Fix Grammar in "compiler-api" 2021-11-24 00:09:56 +01:00
Nils Knappmeier 6790c080c6 Merge branch '4.x'
# Conflicts:
#	components/bower.json
#	components/handlebars.js.nuspec
#	components/package.json
#	integration-testing/multi-nodejs-test/test.sh
#	package-lock.json
#	package.json
2021-02-15 10:54:38 +01:00
Pedro Lamas 2f509a8fca Add handlebars-action to Handlebars in the Wild (#1722) 2021-02-15 08:57:22 +01:00
Chris Garrett 19bdace85a [FEAT] Extract the Handlebars parser (#1713)
Extracts the parser to `@handlebars/parser`, where it can be shared
between different implementations. This means that e.g. Glimmer/Ember
will be able to iterate on new features without forcing Handlebars to
adopt them immediately, and vice versa. All implementors will be able to
absorb changes as it makes sense for them.
2021-02-15 08:56:25 +01:00
Nils Knappmeier 071003b89c docs: add note about reporting security-issues to contributing.md 2020-11-10 23:50:01 +01:00
Jay Linski 1a08e1d0a7 Update repository links
The project was moved, but some links weren't updated accordingly.
2020-08-12 08:42:19 +01:00
Nils Knappmeier 212f9d930b Merge branch '4.x'
# Conflicts:
#	integration-testing/multi-nodejs-test/test.sh
#	spec/string-params.js
#	spec/subexpressions.js
#	spec/track-ids.js
2020-05-05 00:08:00 +02:00
Jordan Mele a65a55382c Moved uglify-js from optionalDependencies to peerDependencies 2020-04-24 22:09:12 +02:00
Tim Gates b1034a6702 docs: Fix simple typo, mulitple -> multiple
There is a small typo in lib/handlebars/compiler/whitespace-control.js.

Should read `multiple` rather than `mulitple`.
2020-04-15 23:04:50 +02:00
Hannah Wolfe 8727d2015a chore: bumping yargs to v15
fixes #1673

- we no longer need to pin to yargs 14 for node 6 & 8 support
2020-04-03 21:03:29 +01:00
Hannah Wolfe a39bfa5fe3 tests: added failing test for issue #1673
- demonstrates the regression
2020-04-03 21:03:29 +01:00
Hannah Wolfe 75794c5dbc breaking: drop support for Node.js < v10
- handlebars is changing to a policy of only supporting current and active versions of Node.js
2020-04-03 20:03:37 +01:00
Hannah Wolfe 3438891bd2 Revert "Switch cmd parser to latest minimist"
This reverts commit c2f1e62031.
2020-04-03 19:26:21 +01:00
Hannah Wolfe 0ec71dcc5c Merge branch '4.x' 2020-04-03 19:24:50 +01:00
Hannah Wolfe 6f17a8e75c Merge branch '4.x' 2020-04-02 20:34:06 +01:00
Hannah Wolfe f8eade7b41 Merge branch '4.x' 2020-04-01 18:54:36 +01:00
Hannah Wolfe d36adf9343 Revert "Update release-notes.md"
This reverts commit 2fd4a3da7a.
2020-04-01 18:49:13 +01:00
Hannah Wolfe 2fd4a3da7a Update release-notes.md 2020-03-31 17:04:46 +01:00
Ilja leyberman 3800b7dd4c Update README.markdown
added a new Project (openVALIDATION) to the section  Handlebars in the Wild
2020-02-25 22:15:55 +01:00
Nils Knappmeier 2ea32e8db9 Merge branch '4.x'
# Conflicts:
#	components/bower.json
#	components/handlebars.js.nuspec
#	components/package.json
#	package.json
2020-02-05 06:13:36 +01:00
roydukkey fd3ca85d13 Fixes spelling and punctuation 2020-01-13 22:00:27 +01:00
Nils Knappmeier edfe6b899c Merge branch '4.x'
# Conflicts:
#	components/bower.json
#	components/handlebars.js.nuspec
#	components/package.json
#	package.json
2020-01-13 21:58:50 +01:00
Nils Knappmeier 0c8230c253 Merge branch '4.x'
# Conflicts:
#	components/bower.json
#	components/handlebars.js.nuspec
#	components/package.json
#	package.json
2020-01-09 00:17:28 +01:00
Nils Knappmeier d23ccf636a Merge branch '4.x'
# Conflicts:
#	Gruntfile.js
#	package-lock.json
#	package.json
2019-12-14 18:46:25 +01:00
Nils Knappmeier 8a062f19d5 Merge branch '4.x' 2019-12-08 16:41:29 +01:00
Nils Knappmeier 44ee971e44 Merge branch '4.x' 2019-12-05 08:02:40 +01:00
Nils Knappmeier 37411901da Merge branch '4.x-reformatted' into master-reformatted 2019-12-03 22:59:50 +01:00
Nils Knappmeier dfe0fcb130 Merge branch '4.x'
# Conflicts:
#	Gruntfile.js
#	lib/handlebars/compiler/compiler.js
#	lib/handlebars/compiler/javascript-compiler.js
#	lib/handlebars/helpers/block-helper-missing.js
#	lib/handlebars/helpers/each.js
#	lib/handlebars/helpers/with.js
#	lib/handlebars/runtime.js
#	spec/compiler.js
#	spec/regressions.js
#	spec/runtime.js
#	spec/string-params.js
#	spec/subexpressions.js
#	spec/track-ids.js
2019-12-03 22:48:35 +01:00
Nils Knappmeier 3fb331ef40 style: reformat all files using prettier 2019-12-03 22:39:17 +01:00
Nils Knappmeier 2f0991a022 update package-lock.json 2019-12-03 22:31:07 +01:00
Nils Knappmeier 4085e066fc Merge branch 'rebuild'
# Conflicts:
#	Gruntfile.js
#	package-lock.json
#	package.json
#	spec/amd-runtime.html
#	spec/amd.html
2019-12-03 22:28:41 +01:00
Nils Knappmeier dce6f54fdf Merge branch '4.x'
# Conflicts:
#	components/bower.json
#	components/handlebars.js.nuspec
#	components/package.json
#	package-lock.json
#	package.json
#	spec/amd-runtime.html
#	spec/amd.html
2019-11-18 08:26:56 +01:00
Nils Knappmeier 7b11f55c53 Merge branch '4.x'
# Conflicts:
#	components/bower.json
#	components/handlebars.js.nuspec
#	components/package.json
#	package.json
2019-11-14 08:10:05 +01:00
Nils Knappmeier b0bb2a42b4 Merge branch '4.x'
# Conflicts:
#	components/bower.json
#	components/handlebars.js.nuspec
#	components/package.json
#	package-lock.json
#	package.json
2019-11-09 06:47:10 +01:00
Nils Knappmeier e46c239b3d Merge branch '4.x'
# Conflicts:
#	lib/handlebars/compiler/javascript-compiler.js
#	lib/handlebars/helpers/with.js
2019-10-27 17:31:48 +01:00
Nils Knappmeier 081e1d0ad9 Merge branch '4.x'
# Conflicts:
#	lib/handlebars/compiler/javascript-compiler.js
2019-10-27 17:26:57 +01:00
kpdecker 2ef6fbd1fc Use objects for hash value tracking
The use of arrays was incorrect for the data type and causing problems when hash keys conflicted with array behaviors.

Fixes #1194

(cherry picked from commit 768ddbd661)
2019-10-27 17:25:40 +01:00
Nils Knappmeier 50e844b563 Merge branch '4.x'
# Conflicts:
#	components/bower.json
#	components/handlebars.js.nuspec
#	components/package.json
#	package.json
2019-10-22 00:02:19 +02:00
Nils Knappmeier c56995d706 deprecate decorators 2019-10-09 07:16:20 +02:00
Nils Knappmeier 566536b03d Merge branch '4.x'
# Conflicts:
#	components/bower.json
#	components/handlebars.js.nuspec
#	components/package.json
#	package-lock.json
#	package.json
2019-10-08 22:22:01 +02:00
Nils Knappmeier 6aaac4a779 add text about "possibly breaking" label 2019-10-08 21:50:18 +02:00
Vladimir Kapustin 4ae86f3282 Add handlebars-wax to Handlebars in the Wild 2019-10-05 07:57:32 +02:00
Nils Knappmeier 25bae31578 Merge branch '4.x'
# Conflicts:
#	components/bower.json
#	components/handlebars.js.nuspec
#	components/package.json
#	lib/handlebars/runtime.js
#	package.json
2019-09-24 08:56:55 +02:00
Nils Knappmeier 74795a3a7f update jsfiddle 2019-09-24 08:29:11 +02:00
Nils Knappmeier 0419daec52 fix accidental typo 2019-09-20 19:08:19 +02:00
king6cong 8df74a0119 FAQ format adjustment 2019-09-20 19:08:19 +02:00
Nils Knappmeier 35d3fdb1dd Merge branch '4.x'
# Conflicts:
#	components/bower.json
#	components/handlebars.js.nuspec
#	components/package.json
#	package-lock.json
#	package.json
2019-09-03 22:24:48 +02:00
Nils Knappmeier dce44541db Update jsfiddle-link 2019-09-03 22:10:15 +02:00
Nils Knappmeier a510d82252 Merge pull request #1530 from jbmusso/patch-2
Fix typo in compiler API doc: "mutation" field should read as "mutating"
2019-07-02 20:55:20 +02:00
Jean-Baptiste Musso 77e38330e6 Fix typo in compiler API doc: "mutation" field should read as "mutating"
The `Visitor` class expects a `boolean` property named `mutating`, instead of `mutation`.
2019-06-18 07:51:15 +02:00
Nils Knappmeier 3ce4425056 chore: udpate package-lock.json 2019-04-13 16:51:22 +02:00
Nils Knappmeier 2f8aa1c839 chore: update jsfiddle link 2019-04-13 16:49:52 +02:00
Nils Knappmeier 2f681d256f Merge branch '4.x'
# Conflicts:
#	components/bower.json
#	components/handlebars.js.nuspec
#	components/package.json
#	package.json
2019-04-13 16:48:27 +02:00
manishsaraan 1b04524a53 apiDoc added in list 2019-03-14 20:31:42 +01:00
Nils Knappmeier 3fc7e6feb2 chore: add text about security advisories to github templates 2019-02-17 20:50:34 +01:00
Nils Knappmeier 16c654579a docs: include typings in PR-template 2019-02-08 12:49:10 +01:00
Nils Knappmeier f349aa43c0 Merge branch '4.x'
# Conflicts:
#	components/bower.json
#	components/handlebars.js.nuspec
#	components/package.json
#	package.json
2019-02-07 11:06:49 +01:00
Nils Knappmeier 913b845aa9 chore: bump components/handlebars.js versions to 5.0.0-alpha.1 2019-01-02 01:18:18 +01:00
Nils Knappmeier 920bf1c5ad Merge branch '4.x' 2019-01-02 01:09:10 +01:00
Nils Knappmeier d1eca1c5cf Merge branch '4.x' 2018-12-16 00:03:20 +01:00
Sharang Dashputre d7d14ed6e5 Remove duplicate text in release-notes.md
[ci skip]
2018-09-11 06:30:47 +02:00
Nils Knappmeier 379172e236 Merge branch '4.x' 2018-09-04 20:57:30 +02:00
Nils Knappmeier 148b19182d Merge branch '4.x' 2018-05-31 23:16:44 +02:00
Qiang Li e1fa310101 unnecessary check 2018-03-31 08:22:20 +02:00
Nils Knappmeier 94419deb95 Merge branch '4.x' 2017-11-28 22:16:54 +01:00
Nils Knappmeier 63be1c8130 Merge branch '4.x' 2017-11-28 22:02:33 +01:00
Nils Knappmeier 7f3da649c0 Merge branch '4.x' 2017-11-09 10:47:04 +01:00
Nils Knappmeier f21f900cf5 Merge branch '4.x'
# Conflicts:
#	.eslintrc.js
#	lib/precompiler.js
#	package.json
#	release-notes.md
2017-11-09 10:42:54 +01:00
Lukas Drgon 999220bf2d Add jsDelivr hits badge 2017-10-17 23:23:43 +02:00
Nils Knappmeier d5caa56d6c Gracefully handle missing uglify-js dependency
closes #1391

uglify-js is an optional dependency and should be treated as such.
This commit gracefully handles MODULE_NOT_FOUND errors while loading
uglify.

- Check for existing uglify-js (and load uglify-js) only if minification
  was activated
- Use "require.resolve" to check if uglify exists. Otherwise, a missing
  dependency of uglify-js would cause the same behavior as missing
  uglify-js. (Only a warning, no error)
- The code to load and run uglify is put into a single for readability
  purposes
- Tests use a mockup Module._resolveFilename to simulate the missing module.
  This function is used by both "require" and "require.resolve", so both
  are mocked equally.
2017-10-17 22:17:37 +02:00
Nils Knappmeier ce3cd8a685 Extend compiler-api example by replacing child-compiler
closes #1376
2017-10-06 23:45:36 +02:00
Marcos Marado 33773c2cb2 Update (C) year in the LICENSE file
Welcome to 2017!
2017-10-06 23:44:33 +02:00
Nils Knappmeier 7b74175e0e Merge branch '4.x' 2017-08-23 22:31:54 +02:00
Mike c933387415 Fix broken header tag (markdown)
The # was too close to the header text.
2017-08-18 00:16:22 +02:00
John Kreitlow 1a78717286 Fix Commits links in release-notes 2017-08-03 00:22:11 +02:00
Davide Mancuso 64002dc506 Update README.markdown
Add incremental-bars to the list of projects using Handlebars
2017-05-31 21:21:47 +02:00
Nils Knappmeier fb26f69485 Merge branch '4.x'
# Conflicts:
#	CONTRIBUTING.md
#	lib/handlebars/compiler/compiler.js
#	package.json
#	spec/compiler.js
2017-05-21 14:20:32 +02:00
Nils Knappmeier be107d2ca1 Merge branch '4.x'
# Conflicts:
#	tasks/test.js
2017-05-15 00:12:54 +02:00
Nils Knappmeier 2441ba1335 Merge branch '4.x' into wycats-master 2017-05-14 22:52:24 +02:00
Nils Knappmeier 5c676a32fb Merge remote-tracking branch 'wycats/4.x' into wycats-master 2017-05-14 14:48:44 +02:00
Nils Knappmeier 228950169d Update jsfiddle-link in CONTRIBUTING.md to 4.0.8 2017-05-13 08:33:14 +02:00
Nils Knappmeier 680ec9666a Merge branch '4.x' into wycats-master
# Conflicts:
#	package.json
2017-05-13 01:00:35 +02:00
Nils Knappmeier 58ec656fbc Bump version in master-branch to 5.0-alpha.1
This is in order to make sure that the master-branch version is not
confused with the stable 4.x
2017-04-30 19:30:09 +02:00
Nils Knappmeier 77674e4f6c Merge branch '4.x' into wycats-master 2017-04-30 19:26:21 +02:00
Nils Knappmeier 714a4c4482 Merge branch '4.x' into wycats-master
# Conflicts:
#	spec/regressions.js
2017-03-25 15:04:37 +01:00
Nils Knappmeier d40cbfc70d Merge pull request #1325 from robinhouston/pr-whitespace-differences
Document whitespace compatibility difference with mustache

Fixes #1324
2017-03-14 00:05:31 +01:00
Robin Houston b6e2f76a45 Document whitespace compatibility difference with mustache 2017-03-13 22:03:14 +00:00
Yehuda Katz 1b78b5b7b2 Merge pull request #1323 from chancancode/patch-1
Update README to be more accurate
2017-03-10 15:33:03 -08:00
Godfrey Chan a5e6afb05d Update README to be more accurate
Copied from the website at @wycats' request.

Addresses #1119.
2017-03-10 15:32:34 -08:00
Nils Knappmeier 8baba76b64 Merge pull request #1315 from Turbo87/es6-parser
parser: Change suffix to use ES6 default module export
2017-02-24 08:36:30 +01:00
Tobias Bieniek 508347ea94 Parser: Change suffix to use ES6 default module export
- This export will be transpiled by Babel for the cjs distribution,
  but will enable others to  use a pure ES6 module distribution
- Instanbul: Ignore "parser.js" for coverage reporting. This file was ignored before
  via annotation, but this has no effect anymore due to the above change
- Remove istanbul annotation from `parser-prefix` (@nknapp)

Squashed by @nknapp
2017-02-24 08:28:51 +01:00
Nils Knappmeier ad341bfc1b Merge pull request #1306 from wycats/merge-4.x
Merge 4.x into master
2017-02-17 08:43:27 +01:00
Nils Knappmeier 911e676af4 Merge branch '4.x'
# Conflicts:
#	Gruntfile.js
#	lib/handlebars/exception.js
#	spec/partials.js
#	spec/regressions.js
2017-02-14 23:10:02 +01:00
Nils Knappmeier 0fa10b6e87 Merge branch '4.x'
# Conflicts:
#	Gruntfile.js
#	lib/handlebars/exception.js
#	spec/partials.js
#	spec/regressions.js
2017-02-14 22:41:46 +01:00
Joonas Lahtinen 660a117536 Avoid duplicate "sourceMappingURL=" lines.
Avoid duplicate // sourceMappingURL=... lines when minifying AND
generating a map. UglifyJS2 will write the line when minifying.
2017-02-14 21:57:00 +01:00
Nils Knappmeier c9970200a0 Fix compiler-api example (#1303)
Closes #1302 

While trying to answer #1302, I noticed that the compiler-api example did not work in (because `nameLookup` is actually at the prototype). This example has a jsfiddle that proves it is working.
It does not provide an solve the exact problem of the reporter, but it answers the question.
2017-01-29 07:11:01 +01:00
Travis Nelson 6300e57f11 Moved .eslintrc to .eslintrc.js and cleaned up rules
Extending from eslint:recommended so we stay up to date on the latest
recommended rules.
2017-01-16 23:19:57 +01:00
Ryunosuke Sato 6c89939af7 Fix README filename in package.json
There is `README.markdown` in handlebars.js repository, not a `README.md`.
2017-01-15 22:21:04 +01:00
Marcos Marado a6476739f0 add more machine-readable data about the license
This small change is meant to make LibreJS happy.

Closes: #1273
2017-01-06 11:26:04 +01:00
Nils Knappmeier 9119265204 Added ".editorconfig"-file that specifies the indent for javascript-files (#1292)
* see http://editorconfig.org/
* This file only includes very basic settings. It may be expanded in the future
2017-01-06 10:14:59 +01:00
Nils Knappmeier d117b3e0f9 Eslint upgrade to 19.0.0 and configuration fixes (#1294)
Original PR by @travnels, commit-message edited by @nknapp

* Upgraded eslint to 19.0.0
* Cleaned up duplicate rules 'no-extra-parens', 'quote-props'
* eslint rule 'no-empty-label' was replaced.
  Rule 'no-empty-label' was removed and replaced by: ‘no-labels’. ‘no-labels’ already in the project
* eslint rule 'space-after-keywords' has been replaced
  Rules 'space-after-keywords' and 'space-return-throw-case' wer removed and replaced by ‘keyword-spacing’.
* Added parsoer-option: sourceType='module'
* Add unnecessary=false to 'quote-props' to remove warnings about unnecessarily quoted property. 

Code corrections
* helpers.js: unused variable 'depthString' removed, detected by new eslint
2017-01-06 10:03:46 +01:00
Marcos Marado 2b7ed7e0bb Fix istanbul dev dependency
According to npm's documentation[1], dependencies on github
repositories are declared with the following syntax:
    "module": "person/module"

This commit changes the package.json syntax on istanbul's dependency,
to follow the documentation.

[1] https://docs.npmjs.com/files/package.json#github-urls
2017-01-01 20:59:46 +01:00
Travis Nelson 9a36966b16 require('sys') is deprecated, using 'util' instead
(node:30288) DeprecationWarning: sys is deprecated. Use util instead.
2017-01-01 20:44:56 +01:00
Nils Knappmeier 6c92579faf Template for issues and pull-request
I've noticed that many issues are answered with "Please provide a jsfiddle".
This issue template tells the reporter to do that from the start.
2016-12-30 16:36:24 +01:00
Yehuda Katz 7535e48a79 Merge pull request #1243 from lawnsea/nested-partial-blocks
Walk up data frames for nested @partial-block
2016-09-10 07:10:36 -07:00
Kevin Decker 11b99b361d Merge pull request #1244 from blikblum/fix-build-windows
Fix build in windows - WIP
2016-08-23 07:12:06 -05:00
Luiz Américo 275ab37b2e Fix build in windows 2016-08-21 17:50:20 -03:00
Kevin Decker bff3af52f2 Merge pull request #1234 from blikblum/upgarde-jison
Upgrade jison to 0.4.16
2016-08-21 14:14:27 -05:00
Kevin Decker 9fb7baddcc Merge pull request #1239 from blikblum/patch-1
Update definition of BlockStatement.path
2016-08-21 14:12:35 -05:00
Lon Ingram 21696005cd Use XML-like tags in test instead of bizarre dot delimiters 2016-08-17 13:36:16 -05:00
Lon Ingram 601ce8a69f Add test reproducing #1185 2016-08-17 13:32:50 -05:00
Lon Ingram bbe0a94d6e Walk up data frames for nested @partial-block
The root cause of #1218 is that `invokePartial` creates a stack of data frames
for nested partial blocks, but `resolvePartial` always uses the value at top of
the stack without "popping" it. The result is an infinite recursive loop, as
references to `@partial-block` in the partial at the top of the stack resolve to
itself.

So, walk up the stack of data frames when evaluating. This is accomplished by
1) setting the `partial-block` property to `noop` after use and
2) using `_parent['partial-block']` if `partial-block` is `noop`

Fix #1218
2016-08-16 18:23:53 -05:00
Luiz Américo f2211ddd65 Update definition of BlockStatement.path
As pointed in https://github.com/wycats/handlebars.js/issues/1237 BlockStatement.path accepts in practice PathExpression or Literal.  
Updates its definition to reflect this fact
2016-07-21 20:45:25 -03:00
Luiz Américo e8722e2551 stick to 0.4.16. Tests does no pass with 0.4.17 2016-07-09 11:55:03 -03:00
Luiz Américo 094a2564fe upgrade jison to 0.4.17 2016-07-09 10:12:11 -03:00
Kevin Decker a6a0e50551 Merge pull request #1210 from kabirbaidhya/patch-1
Add a new lightweight package based on handlebars in the README
2016-05-17 12:20:44 -05:00
Kevin Decker 5c9d5b5b54 Merge pull request #1213 from jbboehr/hotfix/yargs-check
Fix test failure caused by yargs
2016-05-17 12:20:24 -05:00
John Boehr f0d4159a5e Fix test failure caused by yargs
In yargs 3.2, any non-truthy return value to check will fail: https://github.com/yargs/yargs/blob/master/CHANGELOG.md#302-20150213-1650-0000
It appears that this check is no longer necessary as the other contents of the check callback no longer exist: https://github.com/wycats/handlebars.js/commit/2e7a3bd70223c0e532a24290c7d817b951354541#diff-2aff13863648736946e83d70c5e63520L86
It appears the build failed for PR #1180, not sure why it was merged
2016-04-21 15:41:07 -07:00
Kabir 2ff795cf2f Add a new lightweight package based on handlebars in the README
Add a new package [just-handlebars-helpers](https://github.com/leapfrogtechnology/just-handlebars-helpers) on the list. 

This is a lightweight package that offers a collection of common handlebars helpers. And it is [fully tested](https://codecov.io/github/leapfrogtechnology/just-handlebars-helpers)
2016-04-12 16:17:12 +05:45
Kevin Decker e8ceafa652 Merge pull request #1191 from leo/patch-1
Added cory
2016-03-11 22:23:42 -06:00
Kevin Decker 6b644802d9 Merge pull request #1180 from TrySound/yargs
Use yargs
2016-03-11 22:23:00 -06:00
kpdecker 768ddbd661 Use objects for hash value tracking
The use of arrays was incorrect for the data type and causing problems when hash keys conflicted with array behaviors.

Fixes #1194
2016-03-11 22:16:18 -06:00
Kevin Decker 06baeaebfd Merge pull request #1187 from charleso/bugfix/1186-existing-program-block-param
Ensure that existing blockParams and depths are respected on dupe programs
2016-03-07 16:04:00 -06:00
Charles O'Farrell c7be766902 Ensure that existing blockParams and depths are respected on dupe programs
Fixes #1186
2016-03-08 07:23:45 +11:00
Mr. Leo b37232fc8d Added cory 2016-02-27 12:11:25 +01:00
Bogdan Chadkin 0757843712 Use yargs 2016-02-11 20:59:42 +03:00
Kevin Decker 27212f5953 Merge pull request #1177 from gennadiylitvinyuk/patch-1
Preserve License info in Closure Compiler
2016-02-05 08:31:59 -06:00
Gennadiy Litvinyuk 7130a11539 Preserve License info in Closure Compiler
To preserve license info in Closure Compiler the license has to be JSDoc-comment (not simple comment) and have @license before license text
2016-02-05 15:03:19 +01:00
Kevin Decker 4788b03192 Merge pull request #1171 from paulfalgout/patch-1
Contributing doc fix: failing thats -> failing tests
2016-01-20 02:05:46 -06:00
Paul Falgout 0b8fd1d8a8 Contributing doc fix: failing thats -> failing tests 2016-01-19 16:21:50 -06:00
Kevin Decker 89c8b3a886 Merge pull request #1166 from timwangdev/patch-1
Update license date
2016-01-05 21:46:51 -06:00
Tim Wang b55a120e82 Update license date 2016-01-06 10:47:29 +08:00
kpdecker dfc75545fa Update jsfiddle to point to latest 2015-12-26 18:49:28 -06:00
kpdecker 400bada31f Avoid multiple babel execution cycles 2015-12-26 18:49:28 -06:00
kpdecker 834695e241 Remove cdnjs copy task 2015-12-26 18:49:28 -06:00
kpdecker 5302782c08 Drop removed amd task from watch list 2015-12-26 18:49:28 -06:00
kpdecker 118836f390 Throw exception if id tracking args are passed
Fixes #1151
2015-12-26 18:49:28 -06:00
Kevin Decker ee6fadffb4 Merge pull request #1163 from adjohnson916/patch-1
Fix typos on decorators-api.md.
2015-12-24 09:15:34 -06:00
Anders D. Johnson ec64cf9293 Fix typos on decorators-api.md. 2015-12-23 15:28:46 -06:00
kpdecker 9a94d1771f Restore babel compile options 2015-12-14 02:52:38 -06:00
kpdecker 164f1c13f7 Use istanbul fork for proper line coverage 2015-12-14 02:46:42 -06:00
kpdecker 8517352e20 Remove amd files from the build
Users can utilize the UMD library if they are still using require.js and if they need to have specific modules, they can consume the cjs or es6 modules via webpack or similar.

Fix for #1096
2015-12-14 02:31:13 -06:00
kpdecker 63fdb92472 Drop extra Error params
This was causing a difficult to diagnose failure under IE and doesn’t give us enough value to justify the change.
2015-12-14 02:24:06 -06:00
Kevin Decker 8a9c79b799 Merge pull request #1153 from ryanmurakami/master
Adding documentation for running tests
2015-12-14 01:33:23 -06:00
Ryan Lewis eb1ec52b45 moving test documentation to contributing.md 2015-12-13 21:42:54 -08:00
Ryan Lewis 21bf95c92a Merge branch 'master' of https://github.com/ryanmurakami/handlebars.js 2015-12-13 20:42:13 -08:00
Ryan Lewis cc0b239aaf Adding documentation for running tests 2015-12-13 20:40:50 -08:00
kpdecker a6121cae79 Avoid error in older browsers in test
The tests are run through the transpiler and just reverting the user of template literal is easier than adding transpiler to the test stack.
2015-12-12 17:09:24 -06:00
kpdecker 871c32a6f9 Update target browser test versions 2015-12-12 16:26:48 -06:00
kpdecker 326734b0f3 Exclude coverage check in exception conditional 2015-12-12 16:14:17 -06:00
kpdecker 2ea6119a83 Fix throw when creating exception object in Safari
https://github.com/jquery/esprima/issues/1290
2015-12-12 16:07:52 -06:00
kpdecker 8289c0bf3a Update build for modern node versions 2015-12-12 15:34:06 -06:00
kpdecker 25458fdc2a Relax depth check for context push
Fixes #1135
2015-12-12 15:27:56 -06:00
kpdecker 63a08890cc Remove semi-documented _setup and _child APIs
These were a bad idea to begin with and without the trackIds implementation they don’t make much sense.
2015-12-12 15:27:26 -06:00
Kevin Decker 0f054efcdb Merge pull request #1148 from machty/rm-trackids-stringparams
Remove stringParams and trackIds mode
2015-12-10 14:04:34 -06:00
machty 3f77b82ed0 Remove stringParams and trackIds mode
Closes #1145
2015-12-10 14:22:45 -05:00
200 changed files with 24418 additions and 31708 deletions
-24
View File
@@ -1,24 +0,0 @@
.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
-66
View File
@@ -1,66 +0,0 @@
module.exports = {
extends: ['eslint:recommended', 'plugin:compat/recommended', 'prettier'],
globals: {
self: false
},
env: {
node: true,
es6: true
},
rules: {
'no-console': 'warn',
// temporarily disabled until the violating places are fixed.
'no-func-assign': 'off',
'no-sparse-arrays': 'off',
// Best Practices //
//----------------//
'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-label-var': 'error',
'no-undef-init': 'error',
'no-use-before-define': ['error', 'nofunc'],
// ECMAScript 6 //
//--------------//
'no-var': 'error'
},
parserOptions: {
sourceType: 'module',
ecmaVersion: 6,
ecmaFeatures: {}
}
};
+6
View File
@@ -0,0 +1,6 @@
# Upgrade to Prettier 2.7
3d228334530860a6e3f99dc10777c84bf22292c1
# Format markdown files with Prettier
dfe2eaaf20f0b679d94e5a799757c4394d80f1cc
# migrate to oxlint and oxfmt
0c1d00282ca619c3416ad819bdf53c0852b32415
+9
View File
@@ -0,0 +1,9 @@
Before filing issues, please check the following points first:
- [ ] Please don't open issues for security issues. Instead, file a report at https://www.npmjs.com/advisories/report?package=handlebars
- [ ] Have a look at https://github.com/handlebars-lang/handlebars.js/blob/master/CONTRIBUTING.md
- [ ] Read the FAQ at https://github.com/handlebars-lang/handlebars.js/blob/master/FAQ.md
- [ ] Use the jsfiddle-template at https://jsfiddle.net/4nbwjaqz/4/ to reproduce problems or bugs
This will probably help you to get a solution faster.
For bugs, it would be great to have a PR with a failing test-case.
+12
View File
@@ -0,0 +1,12 @@
Before creating a pull-request, please check https://github.com/handlebars-lang/handlebars.js/blob/master/CONTRIBUTING.md first.
Generally we like to see pull requests that
- [ ] Please don't start pull requests for security issues. Instead, file a report at https://www.npmjs.com/advisories/report?package=handlebars
- [ ] 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
- [ ] Have tests
- [ ] Have the [typings](https://www.typescriptlang.org/docs/handbook/declaration-files/introduction.html) (types/index.d.ts) updated on every API change. If you need help, updating those, please mention that in the PR description.
- [ ] Don't significantly decrease the current code coverage (see coverage/lcov-report/index.html)
- [ ] Please target the `master` branch in the PR.
+1 -1
View File
@@ -1,7 +1,7 @@
version: 2
updates:
- package-ecosystem: npm
directory: "/"
directory: '/'
open-pull-requests-limit: 0
schedule:
interval: weekly
+20 -18
View File
@@ -12,12 +12,12 @@ jobs:
runs-on: 'ubuntu-latest'
steps:
- name: Checkout
uses: actions/checkout@v2
uses: actions/checkout@v6
- name: Setup Node.js
uses: actions/setup-node@v2
uses: actions/setup-node@v6
with:
node-version: '16'
node-version: '24'
- name: Install dependencies
run: npm ci
@@ -33,16 +33,16 @@ jobs:
matrix:
operating-system: ['ubuntu-latest', 'windows-latest']
# https://nodejs.org/en/about/releases/
node-version: ['10', '12', '14', '16', '17']
node-version: ['20', '22', '24']
steps:
- name: Checkout
uses: actions/checkout@v2
uses: actions/checkout@v6
with:
submodules: true
- name: Setup Node.js
uses: actions/setup-node@v2
uses: actions/setup-node@v6
with:
node-version: ${{ matrix.node-version }}
@@ -52,27 +52,27 @@ jobs:
- name: Test
run: npm run test
- name: Test (Publish)
if: matrix.node-version != '20'
run: npx vitest run --project publish
- name: Test (Integration)
# https://github.com/webpack/webpack/issues/14532
if: ${{ matrix.node-version != '17' }}
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 -
if: matrix.operating-system == 'ubuntu-latest'
run: npm run test:integration
browser:
name: Test (Browser)
runs-on: 'ubuntu-latest'
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
uses: actions/checkout@v6
with:
submodules: true
- name: Setup Node.js
uses: actions/setup-node@v2
uses: actions/setup-node@v6
with:
node-version: '16'
node-version: '24'
- name: Install dependencies
run: npm ci
@@ -83,7 +83,9 @@ jobs:
npx playwright install
- name: Build
run: npx grunt prepare
run: npm run build
- name: Test
run: npm run test:browser
run: |
npm run test:browser-smoke
npm run test:browser
+39
View File
@@ -0,0 +1,39 @@
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@v6
with:
submodules: true
- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version: '24'
- 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_REGION: 'us-east-1'
S3_ACCESS_KEY_ID: ${{ secrets.S3_ACCESS_KEY_ID }}
S3_SECRET_ACCESS_KEY: ${{ secrets.S3_SECRET_ACCESS_KEY }}
+1 -1
View File
@@ -12,8 +12,8 @@ node_modules
.nyc_output
# Generated files
lib/handlebars/compiler/parser.js
/coverage/
/dist/
/tests/bench/results/
/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
+28
View File
@@ -0,0 +1,28 @@
{
"$schema": "https://raw.githubusercontent.com/nicolo-ribaudo/oxfmt-config-schema/refs/heads/main/schema.json",
"singleQuote": true,
"tabWidth": 2,
"semi": true,
"trailingComma": "es5",
"printWidth": 80,
"ignorePatterns": [
".rvmrc",
".DS_Store",
"/tmp/",
"*.sublime-project",
"*.sublime-workspace",
"npm-debug.log",
"sauce_connect.log*",
".idea",
"yarn-error.log",
"/coverage/",
".nyc_output/",
"/dist/",
"/tests/integration/*/dist/",
"/spec/expected/",
"/spec/mustache",
"/spec/vendor",
"*.handlebars",
"*.hbs"
]
}
+150
View File
@@ -0,0 +1,150 @@
{
"$schema": "https://raw.githubusercontent.com/oxc-project/oxc/main/npm/oxlint/configuration_schema.json",
"plugins": ["eslint", "typescript", "unicorn", "oxc", "node", "vitest"],
"categories": {
"correctness": "error"
},
"rules": {
"no-console": "warn",
"no-func-assign": "off",
"no-sparse-arrays": "off",
"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-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-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",
"no-label-var": "error",
"no-use-before-define": ["error", { "functions": false }],
"no-var": "error",
"node/no-process-env": "error"
},
"ignorePatterns": [
"tmp/",
"dist/",
"coverage/",
".nyc_output/",
"handlebars-release.tgz",
"tests/integration/*/dist/",
"spec/expected/",
"spec/mustache",
"spec/vendor",
"node_modules",
"types/"
],
"overrides": [
{
"files": ["lib/**/*.js"],
"env": {
"node": false,
"browser": true
}
},
{
"files": ["spec/**/*.js"],
"globals": {
"CompilerContext": "readonly",
"Handlebars": "writable",
"handlebarsEnv": "readonly",
"expectTemplate": "readonly",
"suite": "readonly",
"test": "readonly",
"testBoth": "readonly",
"raises": "readonly",
"deepEqual": "readonly",
"start": "readonly",
"stop": "readonly",
"ok": "readonly",
"vi": "readonly",
"strictEqual": "readonly",
"define": "readonly",
"expect": "readonly",
"beforeEach": "readonly",
"afterEach": "readonly",
"describe": "readonly",
"it": "readonly"
},
"rules": {
"no-var": "off",
"dot-notation": "off",
"vitest/no-conditional-tests": "off"
}
},
{
"files": ["tasks/**/*.js"],
"rules": {
"node/no-process-env": "off",
"prefer-const": "warn",
"dot-notation": "error"
}
},
{
"files": ["tasks/tests/**/*.js"],
"globals": {
"describe": "readonly",
"it": "readonly",
"expect": "readonly",
"beforeEach": "readonly",
"afterEach": "readonly",
"vi": "readonly"
}
},
{
"files": ["tests/bench/**/*.mjs"],
"rules": {
"no-console": "off"
}
},
{
"files": ["tests/integration/multi-nodejs-test/**/*.js"],
"rules": {
"no-console": "off",
"no-var": "off"
}
},
{
"files": ["tests/browser/**/*.js"],
"env": {
"browser": true
}
},
{
"files": [
"tests/integration/webpack-babel-test/src/**/*.js",
"tests/integration/webpack-test/src/**/*.js"
],
"env": {
"browser": true
},
"rules": {
"no-var": "off"
}
}
]
}
-22
View File
@@ -1,22 +0,0 @@
.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
+8
View File
@@ -0,0 +1,8 @@
{
"$schema": "https://swc.rs/schema.json",
"module": {
"type": "commonjs",
"importInterop": "swc"
},
"sourceMaps": "inline"
}
+22 -34
View File
@@ -1,10 +1,16 @@
# How to Contribute
## Reporting Security Issues
Please refer to our [Security Policy](https://github.com/handlebars-lang/handlebars.js/blob/master/SECURITY.md).
## Reporting Issues
Please see our [FAQ](https://github.com/handlebars-lang/handlebars.js/blob/master/FAQ.md) for common issues that people run into.
Please refer to 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)
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.
@@ -12,9 +18,8 @@ Documentation issues on the [handlebarsjs.com](https://handlebarsjs.com) site sh
## 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
should merge the branch `4.x` into the master branch regularly.
- The branch `master` contains the current development version (v5).
- The branch `4.x` contains the previous stable version. Only critical bugfixes are backported there.
## Pull Requests
@@ -30,26 +35,18 @@ Generally we like to see pull requests that
## Building
To build Handlebars.js you'll need a few things installed.
- Node.js
- [Grunt](http://gruntjs.com/getting-started)
To build Handlebars.js you'll need Node.js installed.
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`.
Project dependencies may be installed via `npm install`.
To build Handlebars.js from scratch, you'll want to run `grunt`
in the root of the project. That will build Handlebars and output the
results to the dist/ folder. To re-run tests, run `grunt test` or `npm test`.
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/`.
To build Handlebars.js from scratch, run `npm run build` in the root of the project. That will compile CJS modules via SWC and bundle UMD distributions via rspack, outputting results to the dist/ folder. To run tests, use `npm test`.
If you notice any problems, please report them to the GitHub issue tracker at
[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.
@@ -73,21 +70,16 @@ npm test
## Linting and Formatting
Handlebars uses `eslint` to enforce best-practices and `prettier` to auto-format files.
We do linting and formatting in two phases:
- 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.
Handlebars uses `oxlint` for linting, `oxfmt` for formatting, and `eslint` (with `eslint-plugin-compat`) for browser API compatibility checks.
Committed files are linted and formatted in a pre-commit hook.
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 integration-test** 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).
- **npm run lint** will run all linters and fail on warnings
- **npm run format** will format all files
- **npm run check-before-pull-request** will perform all checks that our CI job does, excluding integration tests.
- **npm run test:integration** will run integration tests (bundler compatibility with webpack, rollup, etc.)
These tests only work on Linux.
## Releasing the latest version
@@ -105,12 +97,8 @@ A full release may be completed with the following:
```
npm ci
npx grunt
npm run build
npm publish
cd dist/components/
gem build handlebars-source.gemspec
gem push handlebars-source-*.gem
```
After the release, you should check that all places have really been updated. Especially verify that the `latest`-tags
@@ -132,4 +120,4 @@ in the `handlebars-lang/docs`-repo).
[generator-release]: https://github.com/walmartlabs/generator-release
[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/
[jsfiddle]: https://jsfiddle.net/4nbwjaqz/4/
+31 -39
View File
@@ -1,62 +1,54 @@
# Frequently Asked Questions
1. How can I file a bug report:
## How can I file a bug report:
See our guidelines on [reporting issues](https://github.com/handlebars-lang/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?
## 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/handlebars-lang/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?
## Why is it slower when compiling?
The Handlebars compiler must parse the template and construct a JavaScript program which can then be run. Under some environments such as older mobile devices this can have a performance impact which can be avoided by precompiling. Generally it's recommended that precompilation and the runtime library be used on all clients.
The Handlebars compiler must parse the template and construct a JavaScript program which can then be run. Under some environments such as older mobile devices this can have a performance impact which can be avoided by precompiling. Generally it's recommended that precompilation and the runtime library be used on all clients.
1. Why doesn't this work with Content Security Policy restrictions?
## Why doesn't this work with Content Security Policy restrictions?
When not using the precompiler, Handlebars generates a dynamic function for each template which can cause issues with pages that have enabled Content Policy. It's recommended that templates are precompiled or the `unsafe-eval` policy is enabled for sites that must generate dynamic templates at runtime.
When not using the precompiler, Handlebars generates a dynamic function for each template which can cause issues with pages that have enabled Content Policy. It's recommended that templates are precompiled or the `unsafe-eval` policy is enabled for sites that must generate dynamic templates at runtime.
1. How can I include script tags in my template?
## How can I include script tags in my template?
If loading the template via an inlined `<script type="text/x-handlebars">` tag then you may need to break up the script tag with an empty comment to avoid browser parser errors:
If loading the template via an inlined `<script type="text/x-handlebars">` tag then you may need to break up the script tag with an empty comment to avoid browser parser errors:
```html
<script type="text/x-handlebars">
foo
<scr{{!}}ipt src="bar"></scr{{!}}ipt>
</script>
```
```html
<script type="text/x-handlebars">
foo
<scr{{!}}ipt src="bar"></scr{{!}}ipt>
</script>
```
It's generally recommended that templates are served through external, precompiled, files, which do not suffer from this issue.
It's generally recommended that templates are served through external, precompiled, files, which do not suffer from this issue.
1. Why are my precompiled scripts throwing exceptions?
## Why are my precompiled scripts throwing exceptions?
When using the precompiler, it's important that a supporting version of the Handlebars runtime be loaded on the target page. In version 1.x there were rudimentary checks to compare the version but these did not always work. This is fixed under 2.x but the version checking does not work between these two versions. If you see unexpected errors such as `undefined is not a function` or similar, please verify that the same version is being used for both the precompiler and the client. This can be checked via:
When using the precompiler, it's important that a supporting version of the Handlebars runtime be loaded on the target page. In version 1.x there were rudimentary checks to compare the version but these did not always work. This is fixed under 2.x but the version checking does not work between these two versions. If you see unexpected errors such as `undefined is not a function` or similar, please verify that the same version is being used for both the precompiler and the client. This can be checked via:
```sh
handlebars --version
```
```sh
handlebars --version
```
If using the integrated precompiler and
If using the integrated precompiler and
```javascript
console.log(Handlebars.VERSION);
```
```javascript
console.log(Handlebars.VERSION);
```
On the client side.
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.
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/handlebars-lang/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?
## How do I load the runtime library when using AMD?
Some browsers such as particular versions of IE treat `default` as a reserved word in JavaScript source files. To safely use this you need to reference this via the `Handlebars['default']` lookup method. This is an unfortunate side effect of the shims necessary to backport the Handlebars ES6 code to all current browsers.
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/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.
If not using ES6 transpilers or accessing submodules in the build the former option should be sufficient for most use cases.
The `handlebars.runtime.js` file includes a UMD build, which exposes the library as both the module root and the `default` field for compatibility.
-239
View File
@@ -1,239 +0,0 @@
/* eslint-disable no-process-env */
module.exports = function(grunt) {
grunt.initConfig({
pkg: grunt.file.readJSON('package.json'),
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
);
}
},
files: [{ expand: true, cwd: 'dist/', src: ['*.js'], dest: 'dist/' }]
},
cdnjs: {
files: [
{ 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' }
]
}
},
babel: {
options: {
sourceMaps: 'inline',
loose: ['es6.modules'],
auxiliaryCommentBefore: 'istanbul ignore next'
},
amd: {
options: {
modules: '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/'
}
]
}
},
webpack: {
options: {
context: __dirname,
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'
}
]
},
output: {
path: 'dist/',
library: 'Handlebars',
libraryTarget: 'umd'
}
},
handlebars: {
entry: './lib/handlebars.js',
output: {
filename: 'handlebars.js'
}
},
runtime: {
entry: './lib/handlebars.runtime.js',
output: {
filename: 'handlebars.runtime.js'
}
}
},
requirejs: {
options: {
optimize: 'none',
baseUrl: 'dist/amd/'
},
dist: {
options: {
name: 'handlebars',
out: 'dist/handlebars.amd.js'
}
},
runtime: {
options: {
name: 'handlebars.runtime',
out: 'dist/handlebars.runtime.amd.js'
}
}
},
uglify: {
options: {
mangle: true,
compress: true,
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');
}
}
]
}
},
concat: {
tests: {
src: ['spec/!(require).js'],
dest: 'tmp/tests.js'
}
},
connect: {
server: {
options: {
base: '.',
hostname: '*',
port: 9999
}
}
},
bgShell: {
integrationTests: {
cmd: './tests/integration/run-integration-tests.sh',
bg: false,
fail: true
}
},
watch: {
scripts: {
options: {
atBegin: true
},
files: ['src/*', 'lib/**/*.js', 'spec/**/*.js'],
tasks: ['on-file-change']
}
}
});
// Load tasks from npm
grunt.loadNpmTasks('grunt-contrib-clean');
grunt.loadNpmTasks('grunt-contrib-concat');
grunt.loadNpmTasks('grunt-contrib-connect');
grunt.loadNpmTasks('grunt-contrib-copy');
grunt.loadNpmTasks('grunt-contrib-requirejs');
grunt.loadNpmTasks('grunt-contrib-uglify');
grunt.loadNpmTasks('grunt-contrib-watch');
grunt.loadNpmTasks('grunt-babel');
grunt.loadNpmTasks('grunt-bg-shell');
grunt.loadNpmTasks('grunt-webpack');
grunt.task.loadTasks('tasks');
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'
]);
// Requires secret properties from .travis.yaml
grunt.registerTask('extensive-tests-and-publish-to-aws', [
'default',
'bgShell: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('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',
'bgShell:integrationTests'
]);
};
-167
View File
@@ -1,167 +0,0 @@
[![CI Build Status](https://img.shields.io/github/workflow/status/handlebars-lang/handlebars.js/ci/master)](https://github.com/handlebars-lang/handlebars.js/actions)
[![jsDelivr Hits](https://data.jsdelivr.com/v1/package/npm/handlebars/badge?style=rounded)](https://www.jsdelivr.com/package/npm/handlebars)
[![Bundle size](https://img.shields.io/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](https://mustache.github.io/) 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.
Checkout the official Handlebars docs site at
[https://handlebarsjs.com/](https://handlebarsjs.com) and the live demo at [http://tryhandlebarsjs.com/](http://tryhandlebarsjs.com/).
Installing
----------
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](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
argument, which will be used to render the template.
```js
var source = "<p>Hello, my name is {{name}}. I am from {{hometown}}. I have " +
"{{kids.length}} kids:</p>" +
"<ul>{{#kids}}<li>{{name}} is {{age}}</li>{{/kids}}</ul>";
var template = Handlebars.compile(source);
var data = { "name": "Alan", "hometown": "Somewhere, TX",
"kids": [{"name": "Jimmy", "age": "12"}, {"name": "Sally", "age": "4"}]};
var result = template(data);
// Would render:
// <p>Hello, my name is Alan. I am from Somewhere, TX. I have 2 kids:</p>
// <ul>
// <li>Jimmy is 12</li>
// <li>Sally is 4</li>
// </ul>
```
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](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](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](https://mustache.github.io/mustache.5.html) defines the exact behavior of sections. In the case of name conflicts, helpers are given priority.
### Compatibility
There are a few Mustache behaviors that Handlebars does not implement.
- Handlebars deviates from Mustache slightly in that it does not perform recursive lookup by default. The compile time `compat` flag must be set to enable this functionality. Users should note that there is a performance cost for enabling this flag. The exact cost varies by template, but it's recommended that performance sensitive operations should avoid this mode and instead opt for explicit path references.
- The optional Mustache-style lambdas are not supported. Instead Handlebars provides its own lambda resolution that follows the behaviors of helpers.
- Alternative delimiters are not supported.
Supported Environments
----------------------
Handlebars has been designed to work in any ECMAScript 3 environment. This includes
- Node.js
- Chrome
- Firefox
- Safari 5+
- Opera 11+
- IE 6+
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.
Performance
-----------
In a rough performance test, precompiled Handlebars.js templates (in
the original version of Handlebars.js) rendered in about half the
time of Mustache templates. It would be a shame if it were any other
way, since they were precompiled, but the difference in architecture
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 being 5 to 7 times faster than the Mustache equivalent.
Upgrading
---------
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/handlebars-lang/handlebars.js/blob/master/FAQ.md) for known issues and common pitfalls.
Handlebars in the Wild
----------------------
* [Assemble](http://assemble.io), by [@jonschlinkert](https://github.com/jonschlinkert)
and [@doowb](https://github.com/doowb), is a static site generator that uses Handlebars.js
as its template engine.
* [Cory](https://github.com/leo/cory), by [@leo](https://github.com/leo), is another tiny static site generator
* [CoSchedule](http://coschedule.com) An editorial calendar for WordPress that uses Handlebars.js
* [dashbars](https://github.com/pismute/dashbars) A modern helper library for Handlebars.js.
* [Ember.js](http://www.emberjs.com) makes Handlebars.js the primary way to
structure your views, also with automatic data binding support.
* [Ghost](https://ghost.org/) Just a blogging platform.
* [handlebars_assets](http://github.com/leshill/handlebars_assets): A Rails Asset Pipeline gem
from Les Hill (@leshill).
* [handlebars-helpers](https://github.com/assemble/handlebars-helpers) is an extensive library
with 100+ handlebars helpers.
* [handlebars-layouts](https://github.com/shannonmoeller/handlebars-layouts) is a set of helpers which implement extendible and embeddable layout blocks as seen in other popular templating languages.
* [hbs](http://github.com/donpark/hbs): An Express.js view engine adapter for Handlebars.js,
from Don Park.
* [koa-hbs](https://github.com/jwilm/koa-hbs): [koa](https://github.com/koajs/koa) generator based
renderer for Handlebars.js.
* [jblotus](http://github.com/jblotus) created [http://tryhandlebarsjs.com](http://tryhandlebarsjs.com)
for anyone who would like to try out Handlebars.js in their browser.
* [jQuery plugin](http://71104.github.io/jquery-handlebars/): allows you to use
Handlebars.js with [jQuery](http://jquery.com/).
* [Lumbar](http://walmartlabs.github.io/lumbar) provides easy module-based template management for
handlebars projects.
* [Marionette.Handlebars](https://github.com/hashchange/marionette.handlebars) adds support for Handlebars and Mustache templates to Marionette.
* [sammy.js](http://github.com/quirkey/sammy) by Aaron Quint, a.k.a. quirkey,
supports Handlebars.js as one of its template plugins.
* [SproutCore](http://www.sproutcore.com) uses Handlebars.js as its main
templating engine, extending it with automatic data binding support.
* [YUI](http://yuilibrary.com/yui/docs/handlebars/) implements a port of handlebars
* [Swag](https://github.com/elving/swag) by [@elving](https://github.com/elving) is a growing collection of helpers for handlebars.js. Give your handlebars.js templates some swag son!
* [DOMBars](https://github.com/blakeembrey/dombars) is a DOM-based templating engine built on the Handlebars parser and runtime **DEPRECATED**
* [promised-handlebars](https://github.com/nknapp/promised-handlebars) is a wrapper for Handlebars that allows helpers to return Promises.
* [just-handlebars-helpers](https://github.com/leapfrogtechnology/just-handlebars-helpers) A fully tested lightweight package with common Handlebars helpers.
External Resources
------------------
* [Gist about Synchronous and asynchronous loading of external handlebars templates](https://gist.github.com/2287070)
Have a project using Handlebars? Send us a [pull request][pull-request]!
License
-------
Handlebars.js is released under the MIT license.
[pull-request]: https://github.com/handlebars-lang/handlebars.js/pull/new/master
+198
View File
@@ -0,0 +1,198 @@
[![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 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
[handlebarsjs.com](https://handlebarsjs.com) and try our [live demo](https://handlebarsjs.com/playground.html).
## Installing
See our [installation documentation](https://handlebarsjs.com/guide/installation/).
## Usage
In general, the syntax of Handlebars.js templates is a superset
of Mustache templates. For basic syntax, check out the [Mustache
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
argument, which will be used to render the template.
```js
var source =
'<p>Hello, my name is {{name}}. I am from {{hometown}}. I have ' +
'{{kids.length}} kids:</p>' +
'<ul>{{#kids}}<li>{{name}} is {{age}}</li>{{/kids}}</ul>';
var template = Handlebars.compile(source);
var data = {
name: 'Alan',
hometown: 'Somewhere, TX',
kids: [
{ name: 'Jimmy', age: '12' },
{ name: 'Sally', age: '4' },
],
};
var result = template(data);
// Would render:
// <p>Hello, my name is Alan. I am from Somewhere, TX. I have 2 kids:</p>
// <ul>
// <li>Jimmy is 12</li>
// <li>Sally is 4</li>
// </ul>
```
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](https://handlebarsjs.com/guide/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](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](https://mustache.github.io/mustache.5.html) defines the exact behavior of sections. In the case of name conflicts, helpers are given priority.
### Compatibility
There are a few Mustache behaviors that Handlebars does not implement.
- Handlebars deviates from Mustache slightly in that it does not perform recursive lookup by default. The compile time `compat` flag must be set to enable this functionality. Users should note that there is a performance cost for enabling this flag. The exact cost varies by template, but it's recommended that performance sensitive operations should avoid this mode and instead opt for explicit path references.
- The optional Mustache-style lambdas are not supported. Instead Handlebars provides its own lambda resolution that follows the behaviors of helpers.
- Handlebars does not allow space between the opening `{{` and a command character such as `#`, `/` or `>`. The command character must immediately follow the braces, so for example `{{> partial }}` is allowed but `{{ > partial }}` is not.
- Alternative delimiters are not supported.
## Supported Environments
Handlebars has been designed to work in any ECMAScript 2020 environment. This includes
- Node.js
- Chrome
- Firefox
- Safari
- Edge
If you need to support older environments, use Handlebars version 4.
## Performance
In a rough performance test, precompiled Handlebars.js templates (in
the original version of Handlebars.js) rendered in about half the
time of Mustache templates. It would be a shame if it were any other
way, since they were precompiled, but the difference in architecture
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 being 5 to 7 times faster than the Mustache equivalent.
### Benchmarks
The project includes a comprehensive benchmark suite (powered by [tinybench](https://github.com/tinylibs/tinybench)) that measures compilation, execution, precompilation, and end-to-end performance across templates of varying size and complexity.
```bash
# Run benchmarks (auto-labels with current git branch)
npm run bench
# Run with a custom label
npm run bench -- --label my-optimization
# Filter templates by name (regex, case-insensitive)
npm run bench -- --grep "complex|recursive"
# Run only specific sections (regex, case-insensitive)
npm run bench -- --section precompil
npm run bench -- --section "compilation|precompil"
# Compare results
npm run bench:compare
# Or specify files explicitly
npm run bench:compare -- bench/results/bench-*-main.md bench/results/bench-*-feat.md
```
Results are saved as timestamped Markdown files in `bench/results/`. Each report includes ops/sec, avg latency, p50/p75/p99 percentiles, and sample counts.
Typical workflow for comparing branches:
```bash
git checkout main && npm run bench
git checkout my-feature && npm run bench
npm run bench:compare
```
When run without arguments, `bench:compare` auto-selects two result files: if a file labelled "main" exists it is always used as the baseline, otherwise the older file is the baseline. The comparison uses p75 latency for the diff to filter outliers, and marks changes with `!` (>2%) and `!!` (>5%).
## Upgrading
See [release-notes.md](https://github.com/handlebars-lang/handlebars.js/blob/master/release-notes.md) for upgrade notes.
If you are using Handlebars in production, please regularly look for issues labeled
[possibly breaking](https://github.com/handlebars-lang/handlebars.js/issues?q=is%3Aopen+is%3Aissue+label%3A%22possibly+breaking%22).
If this label is applied to an issue, it means that the requested change is probably not a breaking change,
but since Handlebars is widely in use by a lot of people, there's always a chance that it breaks somebody's build.
## Known Issues
See [FAQ.md](https://github.com/handlebars-lang/handlebars.js/blob/master/FAQ.md) for known issues and common pitfalls.
## Handlebars in the Wild
- [apiDoc](https://github.com/apidoc/apidoc) apiDoc uses handlebars as parsing engine for api documentation view generation.
- [Assemble](https://assemble.io), by [@jonschlinkert](https://github.com/jonschlinkert) and [@doowb](https://github.com/doowb), is a static site generator that uses Handlebars.js as its template engine.
- [CoSchedule](https://coschedule.com) An editorial calendar for WordPress that uses Handlebars.js.
- [Ember.js](https://www.emberjs.com) makes Handlebars.js the primary way to structure your views, also with automatic data binding support.
- [express-handlebars](https://github.com/express-handlebars/express-handlebars) A Handlebars view engine for Express which doesn't suck.
- [express-hbs](https://github.com/TryGhost/express-hbs) Express Handlebars template engine with inheritance, partials, i18n and async helpers.
- [Ghost](https://ghost.org/) Just a blogging platform.
- [handlebars-action](https://github.com/marketplace/actions/handlebars-action) A GitHub action to transform files in your repository with Handlebars templating.
- [handlebars_assets](https://github.com/leshill/handlebars_assets) A Rails Asset Pipeline gem from Les Hill (@leshill).
- [handlebars-helpers](https://github.com/assemble/handlebars-helpers) is an extensive library with 100+ handlebars helpers.
- [handlebars-layouts](https://github.com/shannonmoeller/handlebars-layouts) is a set of helpers which implement extensible and embeddable layout blocks as seen in other popular templating languages.
- [handlebars-loader](https://github.com/pcardune/handlebars-loader) A handlebars template loader for webpack.
- [handlebars-wax](https://github.com/shannonmoeller/handlebars-wax) The missing Handlebars API. Effortless registration of data, partials, helpers, and decorators using file-system globs, modules, and plain-old JavaScript objects.
- [hbs](https://github.com/pillarjs/hbs) An Express.js view engine adapter for Handlebars.js, from Don Park.
- [html-bundler-webpack-plugin](https://github.com/webdiscus/html-bundler-webpack-plugin) The webpack plugin to compile templates, [supports Handlebars](https://github.com/webdiscus/html-bundler-webpack-plugin#using-template-handlebars).
- [incremental-bars](https://github.com/atomictag/incremental-bars) adds support for [incremental-dom](https://github.com/google/incremental-dom) as template target to Handlebars.
- [jQuery plugin](https://71104.github.io/jquery-handlebars/) allows you to use Handlebars.js with [jQuery](http://jquery.com/).
- [just-handlebars-helpers](https://github.com/leapfrogtechnology/just-handlebars-helpers) A fully tested lightweight package with common Handlebars helpers.
- [koa-hbs](https://github.com/jwilm/koa-hbs) [koa](https://github.com/koajs/koa) generator based renderer for Handlebars.js.
- [Marionette.Handlebars](https://github.com/hashchange/marionette.handlebars) adds support for Handlebars and Mustache templates to Marionette.
- [openVALIDATION](https://github.com/openvalidation/openvalidation) a natural language compiler for validation rules. Generates program code in Java, JavaScript, C#, Python and Rust with handlebars.
- [Plop](https://plopjs.com/) is a micro-generator framework that makes it easy to create files with a level of uniformity.
- [promised-handlebars](https://github.com/nknapp/promised-handlebars) is a wrapper for Handlebars that allows helpers to return Promises.
- [sammy.js](https://github.com/quirkey/sammy) by Aaron Quint, a.k.a. quirkey, supports Handlebars.js as one of its template plugins.
- [Swag](https://github.com/elving/swag) by [@elving](https://github.com/elving) is a growing collection of helpers for handlebars.js. Give your handlebars.js templates some swag son!
- [SproutCore](https://www.sproutcore.com) uses Handlebars.js as its main templating engine, extending it with automatic data binding support.
- [vite-plugin-handlebars](https://github.com/alexlafroscia/vite-plugin-handlebars) A package for Vite 2. Allows for running your HTML files through the Handlebars compiler.
- [YUI](https://yuilibrary.com/yui/docs/handlebars/) implements a port of handlebars.
## External Resources
- [Gist about Synchronous and asynchronous loading of external handlebars templates](https://gist.github.com/2287070)
Have a project using Handlebars? Send us a [pull request][pull-request]!
## License
Handlebars.js is released under the MIT license.
[pull-request]: https://github.com/handlebars-lang/handlebars.js/pull/new/master
+15
View File
@@ -0,0 +1,15 @@
# Security Policy
We recommend always using the latest versions of Handlebars and its official companion libraries to ensure your application remains as secure as possible.
## Supported Versions
| Version | Supported |
| ------- | ------------------ |
| 5.0.x | :white_check_mark: |
| 4.7.x | :white_check_mark: |
| < 4.7 | :x: |
## Reporting a Vulnerability
To report a vulnerability, please visit https://github.com/handlebars-lang/handlebars.js/security.
-6
View File
@@ -1,6 +0,0 @@
module.exports = {
rules: {
'no-console': 0,
'no-var': 0
}
};
-176
View File
@@ -1,176 +0,0 @@
#!/usr/bin/env node
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'
}
});
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)) {
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
);
});
}
+136
View File
@@ -0,0 +1,136 @@
#!/usr/bin/env node
import { createRequire } from 'node:module';
import yargs from 'yargs';
const require = createRequire(import.meta.url);
const Precompiler = require('../dist/cjs/precompiler');
const parser = yargs(process.argv.slice(2))
.usage('Precompile handlebar templates.\nUsage: $0 [template|directory]...')
.help(false)
.version(false)
.option('f', {
type: 'string',
description: 'Output File',
alias: 'output',
})
.option('map', {
type: 'string',
description: 'Source Map File',
})
.option('a', {
type: 'boolean',
description: 'Exports amd style (require.js)',
alias: 'amd',
})
.option('c', {
type: 'string',
description: 'Exports CommonJS style, path to Handlebars module',
alias: 'commonjs',
default: null,
})
.option('h', {
type: 'string',
description: 'Path to handlebar.js (only valid for amd-style)',
alias: 'handlebarPath',
default: '',
})
.option('k', {
type: 'string',
description: 'Known helpers',
alias: 'known',
})
.option('o', {
type: 'boolean',
description: 'Known helpers only',
alias: 'knownOnly',
})
.option('m', {
type: 'boolean',
description: 'Minimize output',
alias: 'min',
})
.option('n', {
type: 'string',
description: 'Template namespace',
alias: 'namespace',
default: 'Handlebars.templates',
})
.option('s', {
type: 'boolean',
description: 'Output template function only.',
alias: 'simple',
})
.option('N', {
type: 'string',
description:
'Name of passed string templates. Optional if running in a simple mode. Required when operating on multiple templates.',
alias: 'name',
})
.option('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',
})
.option('r', {
type: 'string',
description:
'Template root. Base value that will be stripped from template names.',
alias: 'root',
})
.option('p', {
type: 'boolean',
description: 'Compiling a partial template',
alias: 'partial',
})
.option('d', {
type: 'boolean',
description: 'Include data when compiling',
alias: 'data',
})
.option('e', {
type: 'string',
description: 'Template extension.',
alias: 'extension',
default: 'handlebars',
})
.option('b', {
type: 'boolean',
description:
'Removes the BOM (Byte Order Mark) from the beginning of the templates.',
alias: 'bom',
})
.option('v', {
type: 'boolean',
description: 'Prints the current compiler version',
alias: 'version',
})
.option('help', {
type: 'boolean',
description: 'Outputs this message',
})
.wrap(120);
const argv = parser.parseSync();
argv.files = argv._;
delete argv._;
Precompiler.loadTemplates(argv, function (err, opts) {
if (err) {
throw err;
}
if (opts.help || (!opts.templates.length && !opts.version)) {
parser.showHelp('log');
} else {
// cli() is async (returns a Promise), so errors would become unhandled
// rejections. Re-throw via nextTick to surface them as uncaught exceptions.
Promise.resolve(Precompiler.cli(opts)).catch((error) => {
process.nextTick(() => {
throw error;
});
});
}
});
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "handlebars",
"version": "4.7.7",
"version": "5.0.0-alpha.1",
"main": "handlebars.js",
"license": "MIT",
"dependencies": {}
+1 -3
View File
@@ -3,7 +3,5 @@
"repo": "components/handlebars.js",
"version": "1.0.0",
"main": "handlebars.js",
"scripts": [
"handlebars.js"
]
"scripts": ["handlebars.js"]
}
+32 -32
View File
@@ -1,35 +1,35 @@
{
"name": "components/handlebars.js",
"description": "Handlebars.js and Mustache are both logicless templating languages that keep the view and the code separated like we all know they should be.",
"homepage": "http://handlebarsjs.com",
"license": "MIT",
"type": "component",
"keywords": [
"handlebars",
"mustache",
"html"
],
"authors": [
{
"name": "Chris Wanstrath",
"homepage": "http://chriswanstrath.com"
}
],
"require": {
"robloach/component-installer": "*"
},
"extra": {
"component": {
"name": "handlebars",
"scripts": [
"handlebars.js"
],
"files": [
"handlebars.runtime.js"
],
"shim": {
"exports": "Handlebars"
}
}
"name": "components/handlebars.js",
"description": "Handlebars.js and Mustache are both logicless templating languages that keep the view and the code separated like we all know they should be.",
"homepage": "http://handlebarsjs.com",
"license": "MIT",
"type": "component",
"keywords": [
"handlebars",
"mustache",
"html"
],
"authors": [
{
"name": "Chris Wanstrath",
"homepage": "http://chriswanstrath.com"
}
],
"require": {
"robloach/component-installer": "*"
},
"extra": {
"component": {
"name": "handlebars",
"scripts": [
"handlebars.js"
],
"files": [
"handlebars.runtime.js"
],
"shim": {
"exports": "Handlebars"
}
}
}
}
+1 -1
View File
@@ -2,7 +2,7 @@
<package>
<metadata>
<id>handlebars.js</id>
<version>4.7.7</version>
<version>5.0.0-alpha.1</version>
<authors>handlebars.js Authors</authors>
<licenseUrl>https://github.com/handlebars-lang/handlebars.js/blob/master/LICENSE</licenseUrl>
<projectUrl>https://github.com/handlebars-lang/handlebars.js/</projectUrl>
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "handlebars",
"version": "4.7.7",
"version": "5.0.0-alpha.1",
"license": "MIT",
"jspm": {
"main": "handlebars",
+34 -35
View File
@@ -34,8 +34,8 @@ let ast = Handlebars.parseWithoutProcessing(myTemplate);
`Handlebars.parse` will parse the template with `parseWithoutProcessing` (see above) then it will update the AST to strip extraneous whitespace. The whitespace stripping functionality handles two distinct situations:
* Removes whitespace around dynamic statements that are on a line by themselves (aka "stand alone")
* Applies "whitespace control" characters (i.e. `~`) by truncating the `ContentStatement` `value` property appropriately (e.g. `\n\n{{~foo}}` would have a `ContentStatement` with a `value` of `''`)
- Removes whitespace around dynamic statements that are on a line by themselves (aka "stand alone")
- Applies "whitespace control" characters (i.e. `~`) by truncating the `ContentStatement` `value` property appropriately (e.g. `\n\n{{~foo}}` would have a `ContentStatement` with a `value` of `''`)
`Handlebars.parse` is used internally by `Handlebars.precompile` and `Handlebars.compile`.
@@ -71,7 +71,7 @@ interface Position {
interface Program <: Node {
type: "Program";
body: [ Statement ];
blockParams: [ string ];
}
```
@@ -132,7 +132,6 @@ interface PartialBlockStatement <: Statement {
`name` will be a `SubExpression` when tied to a dynamic partial, i.e. `{{> (foo) }}`, otherwise this is a path or literal whose `original` value is used to lookup the desired partial.
```java
interface ContentStatement <: Statement {
type: "ContentStatement";
@@ -148,7 +147,6 @@ interface CommentStatement <: Statement {
}
```
```java
interface Decorator <: Statement {
type: "Decorator";
@@ -209,7 +207,6 @@ interface PathExpression <: Expression {
- `parts` is an array of the names in the path. `foo.bar` would be `['foo', 'bar']`. Scope references, `.`, `..`, and `this` should be omitted from this array.
- `original` is the path as entered by the user. Separator and scope references are left untouched.
##### Literals
```java
@@ -242,7 +239,6 @@ interface NullLiteral <: Literal {
}
```
### Miscellaneous
```java
@@ -279,8 +275,8 @@ function ImportScanner() {
}
ImportScanner.prototype = new Visitor();
ImportScanner.prototype.PartialStatement = function(partial) {
this.partials.push({request: partial.name.original});
ImportScanner.prototype.PartialStatement = function (partial) {
this.partials.push({ request: partial.name.original });
Visitor.prototype.PartialStatement.call(this, partial);
};
@@ -291,7 +287,7 @@ scanner.accept(ast);
The current node's ancestors will be maintained in the `parents` array, with the most recent parent listed first.
The visitor may also be configured to operate in mutation mode by setting the `mutation` field to true. When in this mode, handler methods may return any valid AST node and it will replace the one they are currently operating on. Returning `false` will remove the given value (if valid) and returning `undefined` will leave the node in tact. This return structure only apply to mutation mode and non-mutation mode visitors are free to return whatever values they wish.
The visitor may also be configured to operate in mutation mode by setting the `mutating` field to true. When in this mode, handler methods may return any valid AST node and it will replace the one they are currently operating on. Returning `false` will remove the given value (if valid) and returning `undefined` will leave the node intact. This return structure only apply to mutation mode and non-mutation mode visitors are free to return whatever values they wish.
Implementors that may need to support mutation mode are encouraged to utilize the `acceptKey`, `acceptRequired` and `acceptArray` helpers which provide the conditional overwrite behavior as well as implement sanity checks where pertinent.
@@ -300,8 +296,7 @@ Implementors that may need to support mutation mode are encouraged to utilize th
The `Handlebars.JavaScriptCompiler` object has a number of methods that may be customized to alter the output of the compiler:
- `nameLookup(parent, name, type)`
Used to generate the code to resolve a give path component.
Used to generate the code to resolve a given path component.
- `parent` is the existing code in the path resolution
- `name` is the current path component
- `type` is the type of name being evaluated. May be one of `context`, `data`, `helper`, `decorator`, or `partial`.
@@ -309,20 +304,19 @@ The `Handlebars.JavaScriptCompiler` object has a number of methods that may be c
Note that this does not impact dynamic partials, which implementors need to be aware of. Overriding `VM.resolvePartial` may be required to support dynamic cases.
- `depthedLookup(name)`
Used to generate code that resolves parameters within any context in the stack. Is only used in `compat` mode.
Used to generate code that resolves parameters within any context in the stack. Is only used in `compat` mode.
- `compilerInfo()`
Allows for custom compiler flags used in the runtime version checking logic.
- `appendToBuffer(source, location, explicit)`
Allows for code buffer emitting code. Defaults behavior is string concatenation.
- `source` is the source code whose result is to be appending
- `location` is the location of the source in the source map.
- `explicit` is a flag signaling that the emit operation must occur, vs. the lazy evaled options otherwise.
Allows for code buffer emitting code. Defaults behavior is string concatenation.
- `source` is the source code whose result is to be appending
- `location` is the location of the source in the source map.
- `explicit` is a flag signaling that the emit operation must occur, vs. the lazy evaled options otherwise.
- `initializeBuffer()`
Allows for buffers other than the default string buffer to be used. Generally needs to be paired with a custom `appendToBuffer` implementation.
Allows for buffers other than the default string buffer to be used. Generally needs to be paired with a custom `appendToBuffer` implementation.
### Example for the compiler api.
@@ -330,7 +324,6 @@ This example changes all lookups of properties are performed by a helper (`looku
There is also [a jsfiddle with this code](https://jsfiddle.net/9D88g/162/) if you want to play around with it.
```javascript
function MyCompiler() {
Handlebars.JavaScriptCompiler.apply(this, arguments);
@@ -338,29 +331,35 @@ function MyCompiler() {
MyCompiler.prototype = new Handlebars.JavaScriptCompiler();
// Use this compile to compile BlockStatment-Blocks
MyCompiler.prototype.compiler = MyCompiler
MyCompiler.prototype.compiler = MyCompiler;
MyCompiler.prototype.nameLookup = function(parent, name, type) {
MyCompiler.prototype.nameLookup = function (parent, name, type) {
if (type === 'context') {
return this.source.functionCall('helpers.lookupLowerCase', '', [parent, JSON.stringify(name)])
return this.source.functionCall('helpers.lookupLowerCase', '', [
parent,
JSON.stringify(name),
]);
} else {
return Handlebars.JavaScriptCompiler.prototype.nameLookup.call(this, parent, name, type);
return Handlebars.JavaScriptCompiler.prototype.nameLookup.call(
this,
parent,
name,
type
);
}
}
};
var env = Handlebars.create();
env.registerHelper('lookupLowerCase', function(parent, name) {
return parent[name.toLowerCase()]
})
env.registerHelper('lookupLowerCase', function (parent, name) {
return parent[name.toLowerCase()];
});
env.JavaScriptCompiler = MyCompiler;
var template = env.compile('{{#each Test}} ({{Value}}) {{/each}}');
console.log(template({
test: [
{value: 'a'},
{value: 'b'},
{value: 'c'}
]
}));
console.log(
template({
test: [{ value: 'a' }, { value: 'b' }, { value: 'c' }],
})
);
```
+3 -1
View File
@@ -1,6 +1,8 @@
# Decorators
Decorators allow for blocks to be annotated with metadata or wrapped in functionality prior to execution of the block. This may be used to communicate with the containing helper or to setup a particular state in the system prior to running the block.
**Decorators are deprecated, please join the discussion at [#1574](https://github.com/handlebars-lang/handlebars.js/issues/1574) to see what we can do about it.**
Decorators allow for blocks to be annotated with metadata or wrapped in functionality prior to execution of the block. This may be used to communicate with the containing helper or to set up a particular state in the system prior to running the block.
Decorators are registered through similar methods as helpers, `registerDecorators` and `unregisterDecorators`. These can then be referenced via the friendly name in the template using the `{{* decorator}}` and `{{#* decorator}}{/decorator}}` syntaxes. These syntaxes are derivatives of the normal mustache syntax and as such have all of the same argument and whitespace behaviors.
+17
View File
@@ -0,0 +1,17 @@
import compat from 'eslint-plugin-compat';
export default [
{
// Ignore everything except lib/
ignores: ['**', '!lib/**'],
},
{
// Only check browser API compat in the runtime library code.
// All other linting is handled by oxlint.
...compat.configs['flat/recommended'],
files: ['lib/**/*.js'],
linterOptions: {
reportUnusedDisableDirectives: 'off',
},
},
];
-6
View File
@@ -1,6 +0,0 @@
module.exports = {
env: {
// Handlebars should run natively in the browser
node: false
}
};
+9 -8
View File
@@ -1,15 +1,16 @@
import {
parser as Parser,
parse,
parseWithoutProcessing,
Visitor,
} from '@handlebars/parser';
import runtime from './handlebars.runtime';
// Compiler imports
import AST from './handlebars/compiler/ast';
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';
import noConflict from './handlebars/no-conflict';
@@ -17,10 +18,10 @@ let _create = runtime.create;
function create() {
let hb = _create();
hb.compile = function(input, options) {
hb.compile = function (input, options) {
return compile(input, options, hb);
};
hb.precompile = function(input, options) {
hb.precompile = function (input, options) {
return precompile(input, options, hb);
};
+6 -3
View File
@@ -1,9 +1,9 @@
import { Exception } from '@handlebars/parser';
import * as base from './handlebars/base';
// Each of these augment the Handlebars object. No need to setup here.
// (This is done to easily share code between commonjs and browse envs)
import SafeString from './handlebars/safe-string';
import Exception from './handlebars/exception';
import * as Utils from './handlebars/utils';
import * as runtime from './handlebars/runtime';
@@ -19,8 +19,11 @@ function create() {
hb.Utils = Utils;
hb.escapeExpression = Utils.escapeExpression;
hb.VM = runtime;
hb.template = function(spec) {
// Spread into a plain object so that runtime functions (e.g. checkRevision)
// can be overridden by consumers. ES module namespace objects are sealed with
// getter-only properties per spec, which would prevent monkey-patching.
hb.VM = { ...runtime };
hb.template = function (spec) {
return runtime.template(spec, hb);
};
+9 -9
View File
@@ -1,5 +1,5 @@
import { Exception } from '@handlebars/parser';
import { createFrame, extend, toString } from './utils';
import Exception from './exception';
import { registerDefaultHelpers } from './helpers';
import { registerDefaultDecorators } from './decorators';
import logger from './logger';
@@ -17,7 +17,7 @@ export const REVISION_CHANGES = {
5: '== 2.0.0-alpha.x',
6: '>= 2.0.0-beta.1',
7: '>= 4.0.0 <4.3.0',
8: '>= 4.3.0'
8: '>= 4.3.0',
};
const objectType = '[object Object]';
@@ -37,7 +37,7 @@ HandlebarsEnvironment.prototype = {
logger: logger,
log: logger.log,
registerHelper: function(name, fn) {
registerHelper: function (name, fn) {
if (toString.call(name) === objectType) {
if (fn) {
throw new Exception('Arg not supported with multiple helpers');
@@ -47,11 +47,11 @@ HandlebarsEnvironment.prototype = {
this.helpers[name] = fn;
}
},
unregisterHelper: function(name) {
unregisterHelper: function (name) {
delete this.helpers[name];
},
registerPartial: function(name, partial) {
registerPartial: function (name, partial) {
if (toString.call(name) === objectType) {
extend(this.partials, name);
} else {
@@ -63,11 +63,11 @@ HandlebarsEnvironment.prototype = {
this.partials[name] = partial;
}
},
unregisterPartial: function(name) {
unregisterPartial: function (name) {
delete this.partials[name];
},
registerDecorator: function(name, fn) {
registerDecorator: function (name, fn) {
if (toString.call(name) === objectType) {
if (fn) {
throw new Exception('Arg not supported with multiple decorators');
@@ -77,7 +77,7 @@ HandlebarsEnvironment.prototype = {
this.decorators[name] = fn;
}
},
unregisterDecorator: function(name) {
unregisterDecorator: function (name) {
delete this.decorators[name];
},
/**
@@ -86,7 +86,7 @@ HandlebarsEnvironment.prototype = {
*/
resetLoggedPropertyAccesses() {
resetLoggedProperties();
}
},
};
export let log = logger.log;
+5 -5
View File
@@ -4,7 +4,7 @@ let AST = {
// a mustache is definitely a helper if:
// * it is an eligible helper, and
// * it has at least one parameter or hash segment
helperExpression: function(node) {
helperExpression: function (node) {
return (
node.type === 'SubExpression' ||
((node.type === 'MustacheStatement' ||
@@ -13,18 +13,18 @@ let AST = {
);
},
scopedId: function(path) {
scopedId: function (path) {
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) {
simpleId: function (path) {
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
-34
View File
@@ -1,34 +0,0 @@
import parser from './parser';
import WhitespaceControl from './whitespace-control';
import * as Helpers from './helpers';
import { extend } from '../utils';
export { parser };
let yy = {};
extend(yy, Helpers);
export function parseWithoutProcessing(input, options) {
// Just return if an already-compiled AST was passed in.
if (input.type === 'Program') {
return input;
}
parser.yy = yy;
// Altering the shared object here, but this is ok as parser is a sync operation
yy.locInfo = function(locInfo) {
return new yy.SourceLocation(options && options.srcName, locInfo);
};
let ast = parser.parse(input);
return ast;
}
export function parse(input, options) {
let ast = parseWithoutProcessing(input, options);
let strip = new WhitespaceControl(options);
return strip.accept(ast);
}
+30 -24
View File
@@ -4,45 +4,51 @@ import { isArray } from '../utils';
let SourceNode;
try {
/* istanbul ignore next */
/* v8 ignore next */
if (typeof define !== 'function' || !define.amd) {
// 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'); // eslint-disable-line no-undef
// The variable indirection prevents bundlers from statically resolving and bundling
// source-map (which requires Node-built-ins). The stub SourceNode below handles
// browser/bundled usage. Bundlers may emit a "Critical dependency" warning — this
// is expected and harmless.
let mod = 'source-map';
let SourceMap = require(mod);
SourceNode = SourceMap.SourceNode;
}
// oxlint-disable-next-line no-unused-vars -- Babel 5 requires named catch param
} catch (err) {
/* NOP */
}
/* istanbul ignore if: tested but not covered in istanbul due to dist build */
/* v8 ignore next -- tested but not covered due to dist build */
if (!SourceNode) {
SourceNode = function(line, column, srcFile, chunks) {
SourceNode = function (line, column, srcFile, chunks) {
this.src = '';
if (chunks) {
this.add(chunks);
}
};
/* istanbul ignore next */
/* v8 ignore next */
SourceNode.prototype = {
add: function(chunks) {
add: function (chunks) {
if (isArray(chunks)) {
chunks = chunks.join('');
}
this.src += chunks;
},
prepend: function(chunks) {
prepend: function (chunks) {
if (isArray(chunks)) {
chunks = chunks.join('');
}
this.src = chunks + this.src;
},
toStringWithSourceMap: function() {
toStringWithSourceMap: function () {
return { code: this.toString() };
},
toString: function() {
toString: function () {
return this.src;
}
},
};
}
@@ -70,32 +76,32 @@ CodeGen.prototype = {
isEmpty() {
return !this.source.length;
},
prepend: function(source, loc) {
prepend: function (source, loc) {
this.source.unshift(this.wrap(source, loc));
},
push: function(source, loc) {
push: function (source, loc) {
this.source.push(this.wrap(source, loc));
},
merge: function() {
merge: function () {
let source = this.empty();
this.each(function(line) {
this.each(function (line) {
source.add([' ', line, '\n']);
});
return source;
},
each: function(iter) {
each: function (iter) {
for (let i = 0, len = this.source.length; i < len; i++) {
iter(this.source[i]);
}
},
empty: function() {
empty: function () {
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;
}
@@ -110,12 +116,12 @@ CodeGen.prototype = {
);
},
functionCall: function(fn, type, params) {
functionCall: function (fn, type, params) {
params = this.generateList(params);
return this.wrap([fn, type ? '.' + type + '(' : '(', params, ')']);
},
quotedString: function(str) {
quotedString: function (str) {
return (
'"' +
(str + '')
@@ -129,10 +135,10 @@ CodeGen.prototype = {
);
},
objectLiteral: function(obj) {
objectLiteral: function (obj) {
let pairs = [];
Object.keys(obj).forEach(key => {
Object.keys(obj).forEach((key) => {
let value = castChunk(obj[key], this);
if (value !== 'undefined') {
pairs.push([this.quotedString(key), ':', value]);
@@ -145,7 +151,7 @@ CodeGen.prototype = {
return ret;
},
generateList: function(entries) {
generateList: function (entries) {
let ret = this.empty();
for (let i = 0, len = entries.length; i < len; i++) {
@@ -159,13 +165,13 @@ CodeGen.prototype = {
return ret;
},
generateArray: function(entries) {
generateArray: function (entries) {
let ret = this.generateList(entries);
ret.prepend('[');
ret.add(']');
return ret;
}
},
};
export default CodeGen;
+73 -133
View File
@@ -1,6 +1,4 @@
/* eslint-disable new-cap */
import Exception from '../exception';
import { Exception } from '@handlebars/parser';
import { isArray, indexOf, extend } from '../utils';
import AST from './ast';
@@ -16,7 +14,7 @@ export function Compiler() {}
Compiler.prototype = {
compiler: Compiler,
equals: function(other) {
equals: function (other) {
let len = this.opcodes.length;
if (other.opcodes.length !== len) {
return false;
@@ -47,13 +45,11 @@ Compiler.prototype = {
guid: 0,
compile: function(program, options) {
compile: function (program, options) {
this.sourceNode = [];
this.opcodes = [];
this.children = [];
this.options = options;
this.stringParams = options.stringParams;
this.trackIds = options.trackIds;
options.blockParams = options.blockParams || [];
@@ -67,7 +63,7 @@ Compiler.prototype = {
unless: true,
with: true,
log: true,
lookup: true
lookup: true,
},
options.knownHelpers
);
@@ -75,8 +71,8 @@ Compiler.prototype = {
return this.accept(program);
},
compileProgram: function(program) {
let childCompiler = new this.compiler(), // eslint-disable-line new-cap
compileProgram: function (program) {
let childCompiler = new this.compiler(),
result = childCompiler.compile(program, this.options),
guid = this.guid++;
@@ -88,8 +84,8 @@ Compiler.prototype = {
return guid;
},
accept: function(node) {
/* istanbul ignore next: Sanity code */
accept: function (node) {
/* v8 ignore next -- Sanity code */
if (!this[node.type]) {
throw new Exception('Unknown type: ' + node.type, node);
}
@@ -100,7 +96,7 @@ Compiler.prototype = {
return ret;
},
Program: function(program) {
Program: function (program) {
this.options.blockParams.unshift(program.blockParams);
let body = program.body,
@@ -117,7 +113,7 @@ Compiler.prototype = {
return this;
},
BlockStatement: function(block) {
BlockStatement: function (block) {
transformLiteralToPath(block);
let program = block.program,
@@ -162,7 +158,7 @@ Compiler.prototype = {
this.opcode('registerDecorator', params.length, path.original);
},
PartialStatement: function(partial) {
PartialStatement: function (partial) {
this.usePartial = true;
let program = partial.program;
@@ -201,11 +197,11 @@ Compiler.prototype = {
this.opcode('invokePartial', isDynamic, partialName, indent);
this.opcode('append');
},
PartialBlockStatement: function(partialBlock) {
PartialBlockStatement: function (partialBlock) {
this.PartialStatement(partialBlock);
},
MustacheStatement: function(mustache) {
MustacheStatement: function (mustache) {
this.SubExpression(mustache);
if (mustache.escaped && !this.options.noEscape) {
@@ -218,15 +214,15 @@ Compiler.prototype = {
this.DecoratorBlock(decorator);
},
ContentStatement: function(content) {
ContentStatement: function (content) {
if (content.value) {
this.opcode('appendContent', content.value);
}
},
CommentStatement: function() {},
CommentStatement: function () {},
SubExpression: function(sexpr) {
SubExpression: function (sexpr) {
transformLiteralToPath(sexpr);
let type = this.classifySexpr(sexpr);
@@ -238,7 +234,7 @@ Compiler.prototype = {
this.ambiguousSexpr(sexpr);
}
},
ambiguousSexpr: function(sexpr, program, inverse) {
ambiguousSexpr: function (sexpr, program, inverse) {
let path = sexpr.path,
name = path.parts[0],
isBlock = program != null || inverse != null;
@@ -254,14 +250,14 @@ Compiler.prototype = {
this.opcode('invokeAmbiguous', name, isBlock);
},
simpleSexpr: function(sexpr) {
simpleSexpr: function (sexpr) {
let path = sexpr.path;
path.strict = true;
this.accept(path);
this.opcode('resolvePossibleLambda');
},
helperSexpr: function(sexpr, program, inverse) {
helperSexpr: function (sexpr, program, inverse) {
let params = this.setupFullMustacheParams(sexpr, program, inverse),
path = sexpr.path,
name = path.parts[0];
@@ -287,7 +283,7 @@ Compiler.prototype = {
}
},
PathExpression: function(path) {
PathExpression: function (path) {
this.addDepth(path.depth);
this.opcode('getContext', path.depth);
@@ -314,27 +310,27 @@ Compiler.prototype = {
}
},
StringLiteral: function(string) {
StringLiteral: function (string) {
this.opcode('pushString', string.value);
},
NumberLiteral: function(number) {
NumberLiteral: function (number) {
this.opcode('pushLiteral', number.value);
},
BooleanLiteral: function(bool) {
BooleanLiteral: function (bool) {
this.opcode('pushLiteral', bool.value);
},
UndefinedLiteral: function() {
UndefinedLiteral: function () {
this.opcode('pushLiteral', 'undefined');
},
NullLiteral: function() {
NullLiteral: function () {
this.opcode('pushLiteral', 'null');
},
Hash: function(hash) {
Hash: function (hash) {
let pairs = hash.pairs,
i = 0,
l = pairs.length;
@@ -351,15 +347,15 @@ Compiler.prototype = {
},
// HELPERS
opcode: function(name) {
opcode: function (name) {
this.opcodes.push({
opcode: name,
args: slice.call(arguments, 1),
loc: this.sourceNode[0].loc
loc: this.sourceNode[0].loc,
});
},
addDepth: function(depth) {
addDepth: function (depth) {
if (!depth) {
return;
}
@@ -367,7 +363,7 @@ Compiler.prototype = {
this.useDepths = true;
},
classifySexpr: function(sexpr) {
classifySexpr: function (sexpr) {
let isSimple = AST.helpers.simpleId(sexpr.path);
let isBlockParam = isSimple && !!this.blockParamIndex(sexpr.path.parts[0]);
@@ -402,57 +398,17 @@ Compiler.prototype = {
}
},
pushParams: function(params) {
pushParams: function (params) {
for (let i = 0, l = params.length; i < l; i++) {
this.pushParam(params[i]);
}
},
pushParam: function(val) {
let value = val.value != null ? val.value : val.original || '';
if (this.stringParams) {
if (value.replace) {
value = value.replace(/^(\.?\.\/)*/g, '').replace(/\//g, '.');
}
if (val.depth) {
this.addDepth(val.depth);
}
this.opcode('getContext', val.depth || 0);
this.opcode('pushStringParam', value, val.type);
if (val.type === 'SubExpression') {
// SubExpressions get evaluated and passed in
// in string params mode.
this.accept(val);
}
} else {
if (this.trackIds) {
let blockParamIndex;
if (val.parts && !AST.helpers.scopedId(val) && !val.depth) {
blockParamIndex = this.blockParamIndex(val.parts[0]);
}
if (blockParamIndex) {
let blockParamChild = val.parts.slice(1).join('.');
this.opcode('pushId', 'BlockParam', blockParamIndex, blockParamChild);
} else {
value = val.original || value;
if (value.replace) {
value = value
.replace(/^this(?:\.|$)/, '')
.replace(/^\.\//, '')
.replace(/^\.$/, '');
}
this.opcode('pushId', val.type, value);
}
}
this.accept(val);
}
pushParam: function (val) {
this.accept(val);
},
setupFullMustacheParams: function(sexpr, program, inverse, omitEmpty) {
setupFullMustacheParams: function (sexpr, program, inverse, omitEmpty) {
let params = sexpr.params;
this.pushParams(params);
@@ -468,7 +424,7 @@ Compiler.prototype = {
return params;
},
blockParamIndex: function(name) {
blockParamIndex: function (name) {
for (
let depth = 0, len = this.options.blockParams.length;
depth < len;
@@ -480,57 +436,25 @@ Compiler.prototype = {
return [depth, param];
}
}
}
},
};
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
);
}
export function precompile(input, options = {}, env) {
validateInput(input, options);
options = options || {};
if (!('data' in options)) {
options.data = true;
}
if (options.compat) {
options.useDepths = true;
}
let ast = env.parse(input, options),
environment = new env.Compiler().compile(ast, options);
let environment = compileEnvironment(input, options, env);
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
);
}
options = extend({}, options);
if (!('data' in options)) {
options.data = true;
}
if (options.compat) {
options.useDepths = true;
}
validateInput(input, options);
let compiled;
function compileInput() {
let ast = env.parse(input, options),
environment = new env.Compiler().compile(ast, options),
let environment = compileEnvironment(input, options, env),
templateSpec = new env.JavaScriptCompiler().compile(
environment,
options,
@@ -541,25 +465,41 @@ export function compile(input, options = {}, env) {
}
// Template is only compiled on first use and cached after that point.
function ret(context, execOptions) {
return function (context, execOptions) {
if (!compiled) {
compiled = compileInput();
}
return compiled.call(this, context, execOptions);
};
}
function validateInput(input, options) {
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
);
}
ret._setup = function(setupOptions) {
if (!compiled) {
compiled = compileInput();
}
return compiled._setup(setupOptions);
};
ret._child = function(i, data, blockParams, depths) {
if (!compiled) {
compiled = compileInput();
}
return compiled._child(i, data, blockParams, depths);
};
return ret;
if (options.trackIds || options.stringParams) {
throw new Exception(
'TrackIds and stringParams are no longer supported. See Github #1145'
);
}
if (!('data' in options)) {
options.data = true;
}
if (options.compat) {
options.useDepths = true;
}
}
function compileEnvironment(input, options, env) {
let ast = env.parse(input, options);
return new env.Compiler().compile(ast, options);
}
function argEquals(a, b) {
@@ -588,7 +528,7 @@ function transformLiteralToPath(sexpr) {
depth: 0,
parts: [literal.original + ''],
original: literal.original + '',
loc: literal.loc
loc: literal.loc,
};
}
}
-219
View File
@@ -1,219 +0,0 @@
import Exception from '../exception';
function validateClose(open, close) {
close = close.path ? close.path.original : close;
if (open.path.original !== close) {
let errorNode = { loc: open.path.loc };
throw new Exception(
open.path.original + " doesn't match " + close,
errorNode
);
}
}
export function SourceLocation(source, locInfo) {
this.source = source;
this.start = {
line: locInfo.first_line,
column: locInfo.first_column
};
this.end = {
line: locInfo.last_line,
column: locInfo.last_column
};
}
export function id(token) {
if (/^\[.*\]$/.test(token)) {
return token.substring(1, token.length - 1);
} else {
return token;
}
}
export function stripFlags(open, close) {
return {
open: open.charAt(2) === '~',
close: close.charAt(close.length - 3) === '~'
};
}
export function stripComment(comment) {
return comment.replace(/^\{\{~?!-?-?/, '').replace(/-?-?~?\}\}$/, '');
}
export function preparePath(data, parts, loc) {
loc = this.locInfo(loc);
let original = data ? '@' : '',
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;
original += (parts[i].separator || '') + part;
if (!isLiteral && (part === '..' || part === '.' || part === 'this')) {
if (dig.length > 0) {
throw new Exception('Invalid path: ' + original, { loc });
} else if (part === '..') {
depth++;
}
} else {
dig.push(part);
}
}
return {
type: 'PathExpression',
data,
depth,
parts: dig,
original,
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 !== '&';
let decorator = /\*/.test(open);
return {
type: decorator ? 'Decorator' : 'MustacheStatement',
path,
params,
hash,
escaped,
strip,
loc: this.locInfo(locInfo)
};
}
export function prepareRawBlock(openRawBlock, contents, close, locInfo) {
validateClose(openRawBlock, close);
locInfo = this.locInfo(locInfo);
let program = {
type: 'Program',
body: contents,
strip: {},
loc: locInfo
};
return {
type: 'BlockStatement',
path: openRawBlock.path,
params: openRawBlock.params,
hash: openRawBlock.hash,
program,
openStrip: {},
inverseStrip: {},
closeStrip: {},
loc: locInfo
};
}
export function prepareBlock(
openBlock,
program,
inverseAndProgram,
close,
inverted,
locInfo
) {
if (close && close.path) {
validateClose(openBlock, close);
}
let decorator = /\*/.test(openBlock.open);
program.blockParams = openBlock.blockParams;
let inverse, inverseStrip;
if (inverseAndProgram) {
if (decorator) {
throw new Exception(
'Unexpected inverse block on decorator',
inverseAndProgram
);
}
if (inverseAndProgram.chain) {
inverseAndProgram.program.body[0].closeStrip = close.strip;
}
inverseStrip = inverseAndProgram.strip;
inverse = inverseAndProgram.program;
}
if (inverted) {
inverted = inverse;
inverse = program;
program = inverted;
}
return {
type: decorator ? 'DecoratorBlock' : 'BlockStatement',
path: openBlock.path,
params: openBlock.params,
hash: openBlock.hash,
program,
inverse,
openStrip: openBlock.strip,
inverseStrip,
closeStrip: close && close.strip,
loc: this.locInfo(locInfo)
};
}
export function prepareProgram(statements, loc) {
if (!loc && statements.length) {
const firstLoc = statements[0].loc,
lastLoc = statements[statements.length - 1].loc;
/* istanbul ignore else */
if (firstLoc && lastLoc) {
loc = {
source: firstLoc.source,
start: {
line: firstLoc.start.line,
column: firstLoc.start.column
},
end: {
line: lastLoc.end.line,
column: lastLoc.end.column
}
};
}
}
return {
type: 'Program',
body: statements,
strip: {},
loc: loc
};
}
export function preparePartialBlock(open, program, close, locInfo) {
validateClose(open, close);
return {
type: 'PartialBlockStatement',
name: open.path,
params: open.params,
hash: open.hash,
program,
openStrip: open.strip,
closeStrip: close && close.strip,
loc: this.locInfo(locInfo)
};
}
+114 -214
View File
@@ -1,5 +1,5 @@
import { Exception } from '@handlebars/parser';
import { COMPILER_REVISION, REVISION_CHANGES } from '../base';
import Exception from '../exception';
import { isArray } from '../utils';
import CodeGen from './code-gen';
@@ -12,25 +12,25 @@ 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 */) {
nameLookup: function (parent, name /*, type */) {
return this.internalNameLookup(parent, name);
},
depthedLookup: function(name) {
depthedLookup: function (name) {
return [
this.aliasable('container.lookup'),
'(depths, ',
JSON.stringify(name),
')'
')',
];
},
compilerInfo: function() {
compilerInfo: function () {
const revision = COMPILER_REVISION,
versions = REVISION_CHANGES[revision];
return [revision, versions];
},
appendToBuffer: function(source, location, explicit) {
appendToBuffer: function (source, location, explicit) {
// Force a source as this simplifies the merge logic.
if (!isArray(source)) {
source = [source];
@@ -50,22 +50,20 @@ JavaScriptCompiler.prototype = {
}
},
initializeBuffer: function() {
initializeBuffer: function () {
return this.quotedString('');
},
// END PUBLIC API
internalNameLookup: function(parent, name) {
internalNameLookup: function (parent, name) {
this.lookupPropertyFunctionIsUsed = true;
return ['lookupProperty(', parent, ',', JSON.stringify(name), ')'];
},
lookupPropertyFunctionIsUsed: false,
compile: function(environment, options, context, asObject) {
compile: function (environment, options, context, asObject) {
this.environment = environment;
this.options = options;
this.stringParams = this.options.stringParams;
this.trackIds = this.options.trackIds;
this.precompile = !asObject;
this.name = this.environment.name;
@@ -73,7 +71,7 @@ JavaScriptCompiler.prototype = {
this.context = context || {
decorators: [],
programs: [],
environments: []
environments: [],
};
this.preamble();
@@ -114,7 +112,7 @@ JavaScriptCompiler.prototype = {
this.source.currentLocation = firstLoc;
this.pushSource('');
/* istanbul ignore next */
/* v8 ignore next */
if (this.stackSlot || this.inlineStack.length || this.compileStack.length) {
throw new Exception('Compile completed with content left on stack');
}
@@ -125,11 +123,12 @@ JavaScriptCompiler.prototype = {
this.decorators.prepend([
'var decorators = container.decorators, ',
this.lookupPropertyFunctionVarDeclaration(),
';\n'
';\n',
]);
this.decorators.push('return fn;');
if (asObject) {
// eslint-disable-next-line no-new-func
this.decorators = Function.apply(this, [
'fn',
'props',
@@ -138,7 +137,7 @@ JavaScriptCompiler.prototype = {
'data',
'blockParams',
'depths',
this.decorators.merge()
this.decorators.merge(),
]);
} else {
this.decorators.prepend(
@@ -155,25 +154,26 @@ JavaScriptCompiler.prototype = {
if (!this.isChild) {
let ret = {
compiler: this.compilerInfo(),
main: fn
main: fn,
};
if (this.decorators) {
ret.main_d = this.decorators; // eslint-disable-line camelcase
ret.main_d = this.decorators;
ret.useDecorators = true;
}
let { programs, decorators } = this.context;
for (i = 0, l = programs.length; i < l; i++) {
if (programs[i]) {
ret[i] = programs[i];
if (decorators[i]) {
ret[i + '_d'] = decorators[i];
ret.useDecorators = true;
}
ret[i] = programs[i];
if (decorators[i]) {
ret[i + '_d'] = decorators[i];
ret.useDecorators = true;
}
}
// Release AST/compiler references only needed during compilation for dedup
this.context.environments.length = 0;
if (this.environment.usePartial) {
ret.usePartial = true;
}
@@ -212,7 +212,7 @@ JavaScriptCompiler.prototype = {
}
},
preamble: function() {
preamble: function () {
// track the last context pushed into place to allow skipping the
// getContext opcode when it would be a noop
this.lastContext = 0;
@@ -220,7 +220,7 @@ JavaScriptCompiler.prototype = {
this.decorators = new CodeGen(this.options.srcName);
},
createFunctionContext: function(asObject) {
createFunctionContext: function (asObject) {
let varDeclarations = '';
let locals = this.stackVars.concat(this.registers.list);
@@ -235,7 +235,7 @@ 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;
Object.keys(this.aliases).forEach(alias => {
Object.keys(this.aliases).forEach((alias) => {
let node = this.aliases[alias];
if (node.children && node.referenceCount > 1) {
varDeclarations += ', alias' + ++aliasCount + '=' + alias;
@@ -262,25 +262,25 @@ JavaScriptCompiler.prototype = {
if (asObject) {
params.push(source);
return Function.apply(this, params);
return Function.apply(this, params); // eslint-disable-line no-new-func
} else {
return this.source.wrap([
'function(',
params.join(','),
') {\n ',
source,
'}'
'}',
]);
}
},
mergeSource: function(varDeclarations) {
mergeSource: function (varDeclarations) {
let isSimple = this.environment.isSimple,
appendOnly = !this.forceBuffer,
appendFirst,
sourceSeen,
bufferStart,
bufferEnd;
this.source.each(line => {
this.source.each((line) => {
if (line.appendToBuffer) {
if (bufferStart) {
line.prepend(' + ');
@@ -334,7 +334,7 @@ JavaScriptCompiler.prototype = {
return this.source.merge();
},
lookupPropertyFunctionVarDeclaration: function() {
lookupPropertyFunctionVarDeclaration: function () {
return `
lookupProperty = container.lookupProperty || function(parent, propertyName) {
if (Object.prototype.hasOwnProperty.call(parent, propertyName)) {
@@ -354,7 +354,7 @@ JavaScriptCompiler.prototype = {
// `{{#this.foo}}...{{/this.foo}}`, resolve the value of `foo`, and
// replace it on the stack with the result of properly
// invoking blockHelperMissing.
blockValue: function(name) {
blockValue: function (name) {
let blockHelperMissing = this.aliasable(
'container.hooks.blockHelperMissing'
),
@@ -373,7 +373,7 @@ JavaScriptCompiler.prototype = {
// Compiler value, before: lastHelper=value of last found helper, if any
// On stack, after, if no lastHelper: same as [blockValue]
// On stack, after, if lastHelper: value
ambiguousBlockValue: function() {
ambiguousBlockValue: function () {
// We're being a bit cheeky and reusing the options value from the prior exec
let blockHelperMissing = this.aliasable(
'container.hooks.blockHelperMissing'
@@ -393,7 +393,7 @@ JavaScriptCompiler.prototype = {
current,
' = ',
this.source.functionCall(blockHelperMissing, 'call', params),
'}'
'}',
]);
},
@@ -403,7 +403,7 @@ JavaScriptCompiler.prototype = {
// On stack, after: ...
//
// Appends the string value of `content` to the current buffer
appendContent: function(content) {
appendContent: function (content) {
if (this.pendingContent) {
content = this.pendingContent + content;
} else {
@@ -422,9 +422,9 @@ JavaScriptCompiler.prototype = {
//
// If `value` is truthy, or 0, it is coerced into a string and appended
// Otherwise, the empty string is appended
append: function() {
append: function () {
if (this.isInline()) {
this.replaceStack(current => [' != null ? ', current, ' : ""']);
this.replaceStack((current) => [' != null ? ', current, ' : ""']);
this.pushSource(this.appendToBuffer(this.popStack()));
} else {
@@ -434,13 +434,13 @@ JavaScriptCompiler.prototype = {
local,
' != null) { ',
this.appendToBuffer(local, undefined, true),
' }'
' }',
]);
if (this.environment.isSimple) {
this.pushSource([
'else { ',
this.appendToBuffer("''", undefined, true),
' }'
' }',
]);
}
}
@@ -452,13 +452,13 @@ JavaScriptCompiler.prototype = {
// On stack, after: ...
//
// Escape `value` and append it to the buffer
appendEscaped: function() {
appendEscaped: function () {
this.pushSource(
this.appendToBuffer([
this.aliasable('container.escapeExpression'),
'(',
this.popStack(),
')'
')',
])
);
},
@@ -470,7 +470,7 @@ JavaScriptCompiler.prototype = {
// Compiler value, after: lastContext=depth
//
// Set the value of the `lastContext` compiler value to the depth
getContext: function(depth) {
getContext: function (depth) {
this.lastContext = depth;
},
@@ -480,7 +480,7 @@ JavaScriptCompiler.prototype = {
// On stack, after: currentContext, ...
//
// Pushes the value of the current context onto the stack.
pushContext: function() {
pushContext: function () {
this.pushStackLiteral(this.contextName(this.lastContext));
},
@@ -491,7 +491,7 @@ JavaScriptCompiler.prototype = {
//
// Looks up the value of `name` on the current context and pushes
// it onto the stack.
lookupOnContext: function(parts, falsy, strict, scoped) {
lookupOnContext: function (parts, falsy, strict, scoped) {
let i = 0;
if (!scoped && this.options.compat && !this.lastContext) {
@@ -512,7 +512,7 @@ JavaScriptCompiler.prototype = {
//
// Looks up the value of `parts` on the given block param and pushes
// it onto the stack.
lookupBlockParam: function(blockParamId, parts) {
lookupBlockParam: function (blockParamId, parts) {
this.useBlockParams = true;
this.push(['blockParams[', blockParamId[0], '][', blockParamId[1], ']']);
@@ -525,7 +525,7 @@ JavaScriptCompiler.prototype = {
// On stack, after: data, ...
//
// Push the data lookup operator
lookupData: function(depth, parts, strict) {
lookupData: function (depth, parts, strict) {
if (!depth) {
this.pushStackLiteral('data');
} else {
@@ -535,16 +535,24 @@ JavaScriptCompiler.prototype = {
this.resolvePath('data', parts, 0, true, strict);
},
resolvePath: function(type, parts, i, falsy, strict) {
resolvePath: function (type, parts, startPartIndex, 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,
startPartIndex,
type
)
);
return;
}
let len = parts.length;
for (; i < len; i++) {
for (let i = startPartIndex; 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.
@@ -566,68 +574,30 @@ JavaScriptCompiler.prototype = {
//
// If the `value` is a lambda, replace it on the stack by
// the return value of the lambda
resolvePossibleLambda: function() {
resolvePossibleLambda: function () {
this.push([
this.aliasable('container.lambda'),
'(',
this.popStack(),
', ',
this.contextName(0),
')'
')',
]);
},
// [pushStringParam]
//
// On stack, before: ...
// On stack, after: string, currentContext, ...
//
// This opcode is designed for use in string mode, which
// provides the string value of a parameter along with its
// depth rather than resolving it immediately.
pushStringParam: function(string, type) {
this.pushContext();
this.pushString(type);
// If it's a subexpression, the string result
// will be pushed after this opcode.
if (type !== 'SubExpression') {
if (typeof string === 'string') {
this.pushString(string);
} else {
this.pushStackLiteral(string);
}
}
},
emptyHash: function(omitEmpty) {
if (this.trackIds) {
this.push('{}'); // hashIds
}
if (this.stringParams) {
this.push('{}'); // hashContexts
this.push('{}'); // hashTypes
}
emptyHash: function (omitEmpty) {
this.pushStackLiteral(omitEmpty ? 'undefined' : '{}');
},
pushHash: function() {
pushHash: function () {
if (this.hash) {
this.hashes.push(this.hash);
}
this.hash = { values: {}, types: [], contexts: [], ids: [] };
this.hash = { values: {} };
},
popHash: function() {
popHash: function () {
let hash = this.hash;
this.hash = this.hashes.pop();
if (this.trackIds) {
this.push(this.objectLiteral(hash.ids));
}
if (this.stringParams) {
this.push(this.objectLiteral(hash.contexts));
this.push(this.objectLiteral(hash.types));
}
this.push(this.objectLiteral(hash.values));
},
@@ -637,7 +607,7 @@ JavaScriptCompiler.prototype = {
// On stack, after: quotedString(string), ...
//
// Push a quoted version of `string` onto the stack
pushString: function(string) {
pushString: function (string) {
this.pushStackLiteral(this.quotedString(string));
},
@@ -649,7 +619,7 @@ JavaScriptCompiler.prototype = {
// Pushes a value onto the stack. This operation prevents
// the compiler from creating a temporary variable to hold
// it.
pushLiteral: function(value) {
pushLiteral: function (value) {
this.pushStackLiteral(value);
},
@@ -661,7 +631,7 @@ JavaScriptCompiler.prototype = {
// Push a program expression onto the stack. This takes
// a compile-time guid and converts it into a runtime-accessible
// expression.
pushProgram: function(guid) {
pushProgram: function (guid) {
if (guid != null) {
this.pushStackLiteral(this.programExpression(guid));
} else {
@@ -686,9 +656,9 @@ JavaScriptCompiler.prototype = {
'fn',
'props',
'container',
options
options,
]),
' || fn;'
' || fn;',
]);
},
@@ -701,7 +671,7 @@ JavaScriptCompiler.prototype = {
// and pushes the helper's return value onto the stack.
//
// If the helper is not found, `helperMissing` is called.
invokeHelper: function(paramSize, name, isSimple) {
invokeHelper: function (paramSize, name, isSimple) {
let nonHelper = this.popStack(),
helper = this.setupHelper(paramSize, name);
@@ -722,7 +692,7 @@ JavaScriptCompiler.prototype = {
let functionLookupCode = [
'(',
this.itemsSeparatedBy(possibleFunctionCalls, '||'),
')'
')',
];
let functionCall = this.source.functionCall(
functionLookupCode,
@@ -732,7 +702,7 @@ JavaScriptCompiler.prototype = {
this.push(functionCall);
},
itemsSeparatedBy: function(items, separator) {
itemsSeparatedBy: function (items, separator) {
let result = [];
result.push(items[0]);
for (let i = 1; i < items.length; i++) {
@@ -747,7 +717,7 @@ JavaScriptCompiler.prototype = {
//
// This operation is used when the helper is known to exist,
// so a `helperMissing` fallback is not required.
invokeKnownHelper: function(paramSize, name) {
invokeKnownHelper: function (paramSize, name) {
let helper = this.setupHelper(paramSize, name);
this.push(this.source.functionCall(helper.name, 'call', helper.callParams));
},
@@ -764,7 +734,7 @@ JavaScriptCompiler.prototype = {
// This operation emits more code than the other options,
// and can be avoided by passing the `knownHelpers` and
// `knownHelpersOnly` flags at compile-time.
invokeAmbiguous: function(name, helperCall) {
invokeAmbiguous: function (name, helperCall) {
this.useRegister('helper');
let nonHelper = this.popStack();
@@ -796,7 +766,7 @@ JavaScriptCompiler.prototype = {
this.aliasable('"function"'),
' ? ',
this.source.functionCall('helper', 'call', helper.callParams),
' : helper))'
' : helper))',
]);
},
@@ -807,7 +777,7 @@ JavaScriptCompiler.prototype = {
//
// This operation pops off a context, invokes a partial with that context,
// and pushes the result of the invocation back.
invokePartial: function(isDynamic, name, indent) {
invokePartial: function (isDynamic, name, indent) {
let params = [],
options = this.setupParams(name, 1, params);
@@ -844,70 +814,28 @@ JavaScriptCompiler.prototype = {
// On stack, after: ..., hash, ...
//
// Pops a value off the stack and assigns it to the current hash
assignToHash: function(key) {
let value = this.popStack(),
context,
type,
id;
if (this.trackIds) {
id = this.popStack();
}
if (this.stringParams) {
type = this.popStack();
context = this.popStack();
}
let hash = this.hash;
if (context) {
hash.contexts[key] = context;
}
if (type) {
hash.types[key] = type;
}
if (id) {
hash.ids[key] = id;
}
hash.values[key] = value;
},
pushId: function(type, name, child) {
if (type === 'BlockParam') {
this.pushStackLiteral(
'blockParams[' +
name[0] +
'].path[' +
name[1] +
']' +
(child ? ' + ' + JSON.stringify('.' + child) : '')
);
} else if (type === 'PathExpression') {
this.pushString(name);
} else if (type === 'SubExpression') {
this.pushStackLiteral('true');
} else {
this.pushStackLiteral('null');
}
assignToHash: function (key) {
this.hash.values[key] = this.popStack();
},
// HELPERS
compiler: JavaScriptCompiler,
compileChildren: function(environment, options) {
compileChildren: function (environment, options) {
let children = environment.children,
child,
compiler;
for (let i = 0, l = children.length; i < l; i++) {
child = children[i];
compiler = new this.compiler(); // eslint-disable-line new-cap
compiler = new this.compiler();
let existing = this.matchExistingProgram(child);
if (existing == null) {
this.context.programs.push(''); // Placeholder to prevent name conflicts for nested children
let index = this.context.programs.length;
// Placeholder to prevent name conflicts for nested children
let index = this.context.programs.push('') - 1;
child.index = index;
child.name = 'program' + index;
this.context.programs[index] = compiler.compile(
@@ -932,7 +860,7 @@ JavaScriptCompiler.prototype = {
}
}
},
matchExistingProgram: function(child) {
matchExistingProgram: function (child) {
for (let i = 0, len = this.context.environments.length; i < len; i++) {
let environment = this.context.environments[i];
if (environment && environment.equals(child)) {
@@ -941,7 +869,7 @@ JavaScriptCompiler.prototype = {
}
},
programExpression: function(guid) {
programExpression: function (guid) {
let child = this.environment.children[guid],
programParams = [child.index, 'data', child.blockParams];
@@ -955,14 +883,14 @@ JavaScriptCompiler.prototype = {
return 'container.program(' + programParams.join(', ') + ')';
},
useRegister: function(name) {
useRegister: function (name) {
if (!this.registers[name]) {
this.registers[name] = true;
this.registers.list.push(name);
}
},
push: function(expr) {
push: function (expr) {
if (!(expr instanceof Literal)) {
expr = this.source.wrap(expr);
}
@@ -971,11 +899,11 @@ JavaScriptCompiler.prototype = {
return expr;
},
pushStackLiteral: function(item) {
pushStackLiteral: function (item) {
this.push(new Literal(item));
},
pushSource: function(source) {
pushSource: function (source) {
if (this.pendingContent) {
this.source.push(
this.appendToBuffer(
@@ -991,13 +919,13 @@ JavaScriptCompiler.prototype = {
}
},
replaceStack: function(callback) {
replaceStack: function (callback) {
let prefix = ['('],
stack,
createdStack,
usedLiteral;
/* istanbul ignore next */
/* v8 ignore next */
if (!this.isInline()) {
throw new Exception('replaceStack on non-inline');
}
@@ -1030,22 +958,22 @@ JavaScriptCompiler.prototype = {
this.push(prefix.concat(item, ')'));
},
incrStack: function() {
incrStack: function () {
this.stackSlot++;
if (this.stackSlot > this.stackVars.length) {
this.stackVars.push('stack' + this.stackSlot);
}
return this.topStackName();
},
topStackName: function() {
topStackName: function () {
return 'stack' + this.stackSlot;
},
flushInline: function() {
flushInline: function () {
let inlineStack = this.inlineStack;
this.inlineStack = [];
for (let i = 0, len = inlineStack.length; i < len; i++) {
let entry = inlineStack[i];
/* istanbul ignore if */
/* v8 ignore next */
if (entry instanceof Literal) {
this.compileStack.push(entry);
} else {
@@ -1055,11 +983,11 @@ JavaScriptCompiler.prototype = {
}
}
},
isInline: function() {
isInline: function () {
return this.inlineStack.length;
},
popStack: function(wrapped) {
popStack: function (wrapped) {
let inline = this.isInline(),
item = (inline ? this.inlineStack : this.compileStack).pop();
@@ -1067,7 +995,7 @@ JavaScriptCompiler.prototype = {
return item.value;
} else {
if (!inline) {
/* istanbul ignore next */
/* v8 ignore next */
if (!this.stackSlot) {
throw new Exception('Invalid stack pop');
}
@@ -1077,11 +1005,11 @@ JavaScriptCompiler.prototype = {
}
},
topStack: function() {
topStack: function () {
let stack = this.isInline() ? this.inlineStack : this.compileStack,
item = stack[stack.length - 1];
/* istanbul ignore if */
/* v8 ignore next */
if (item instanceof Literal) {
return item.value;
} else {
@@ -1089,7 +1017,7 @@ JavaScriptCompiler.prototype = {
}
},
contextName: function(context) {
contextName: function (context) {
if (this.useDepths && context) {
return 'depths[' + context + ']';
} else {
@@ -1097,15 +1025,15 @@ JavaScriptCompiler.prototype = {
}
},
quotedString: function(str) {
quotedString: function (str) {
return this.source.quotedString(str);
},
objectLiteral: function(obj) {
objectLiteral: function (obj) {
return this.source.objectLiteral(obj);
},
aliasable: function(name) {
aliasable: function (name) {
let ret = this.aliases[name];
if (ret) {
ret.referenceCount++;
@@ -1119,7 +1047,7 @@ JavaScriptCompiler.prototype = {
return ret;
},
setupHelper: function(paramSize, name, blockHelper) {
setupHelper: function (paramSize, name, blockHelper) {
let params = [],
paramsInit = this.setupHelperArgs(name, paramSize, params, blockHelper);
let foundHelper = this.nameLookup('helpers', name, 'helper'),
@@ -1133,15 +1061,12 @@ JavaScriptCompiler.prototype = {
params: params,
paramsInit: paramsInit,
name: foundHelper,
callParams: [callContext].concat(params)
callParams: [callContext].concat(params),
};
},
setupParams: function(helper, paramSize, params) {
setupParams: function (helper, paramSize, params) {
let options = {},
contexts = [],
types = [],
ids = [],
objectArgs = !params,
param;
@@ -1152,14 +1077,6 @@ JavaScriptCompiler.prototype = {
options.name = this.quotedString(helper);
options.hash = this.popStack();
if (this.trackIds) {
options.hashIds = this.popStack();
}
if (this.stringParams) {
options.hashTypes = this.popStack();
options.hashContexts = this.popStack();
}
let inverse = this.popStack(),
program = this.popStack();
@@ -1176,28 +1093,12 @@ JavaScriptCompiler.prototype = {
while (i--) {
param = this.popStack();
params[i] = param;
if (this.trackIds) {
ids[i] = this.popStack();
}
if (this.stringParams) {
types[i] = this.popStack();
contexts[i] = this.popStack();
}
}
if (objectArgs) {
options.args = this.source.generateArray(params);
}
if (this.trackIds) {
options.ids = this.source.generateArray(ids);
}
if (this.stringParams) {
options.types = this.source.generateArray(types);
options.contexts = this.source.generateArray(contexts);
}
if (this.options.data) {
options.data = 'data';
}
@@ -1207,7 +1108,7 @@ JavaScriptCompiler.prototype = {
return options;
},
setupHelperArgs: function(helper, paramSize, params, useRegister) {
setupHelperArgs: function (helper, paramSize, params, useRegister) {
let options = this.setupParams(helper, paramSize, params);
options.loc = JSON.stringify(this.source.currentLocation);
options = this.objectLiteral(options);
@@ -1221,10 +1122,10 @@ JavaScriptCompiler.prototype = {
} else {
return options;
}
}
},
};
(function() {
(function () {
const reservedWords = (
'break else new var' +
' case finally return void' +
@@ -1254,22 +1155,21 @@ JavaScriptCompiler.prototype = {
/**
* @deprecated May be removed in the next major version
*/
JavaScriptCompiler.isValidJavaScriptVariableName = function(name) {
JavaScriptCompiler.isValidJavaScriptVariableName = function (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, startPartIndex, type) {
let stack = compiler.popStack(),
i = 0,
len = parts.length;
if (requireTerminal) {
len--;
}
for (; i < len; i++) {
for (let i = startPartIndex; i < len; i++) {
stack = compiler.nameLookup(stack, parts[i], type);
}
@@ -1279,10 +1179,10 @@ function strictLookup(requireTerminal, compiler, parts, type) {
'(',
stack,
', ',
compiler.quotedString(parts[i]),
compiler.quotedString(parts[len]),
', ',
JSON.stringify(compiler.source.currentLocation),
' )'
' )',
];
} else {
return stack;
-178
View File
@@ -1,178 +0,0 @@
/* eslint-disable new-cap */
import Visitor from './visitor';
export function print(ast) {
return new PrintVisitor().accept(ast);
}
export function PrintVisitor() {
this.padding = 0;
}
PrintVisitor.prototype = new Visitor();
PrintVisitor.prototype.pad = function(string) {
let out = '';
for (let i = 0, l = this.padding; i < l; i++) {
out += ' ';
}
out += string + '\n';
return out;
};
PrintVisitor.prototype.Program = function(program) {
let out = '',
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 += ' ]';
out += this.pad(blockParams);
}
for (i = 0, l = body.length; i < l; i++) {
out += this.accept(body[i]);
}
this.padding--;
return out;
};
PrintVisitor.prototype.MustacheStatement = function(mustache) {
return this.pad('{{ ' + this.SubExpression(mustache) + ' }}');
};
PrintVisitor.prototype.Decorator = function(mustache) {
return this.pad('{{ DIRECTIVE ' + this.SubExpression(mustache) + ' }}');
};
PrintVisitor.prototype.BlockStatement = PrintVisitor.prototype.DecoratorBlock = function(
block
) {
let out = '';
out += this.pad(
(block.type === 'DecoratorBlock' ? 'DIRECTIVE ' : '') + 'BLOCK:'
);
this.padding++;
out += this.pad(this.SubExpression(block));
if (block.program) {
out += this.pad('PROGRAM:');
this.padding++;
out += this.accept(block.program);
this.padding--;
}
if (block.inverse) {
if (block.program) {
this.padding++;
}
out += this.pad('{{^}}');
this.padding++;
out += this.accept(block.inverse);
this.padding--;
if (block.program) {
this.padding--;
}
}
this.padding--;
return out;
};
PrintVisitor.prototype.PartialStatement = function(partial) {
let content = 'PARTIAL:' + partial.name.original;
if (partial.params[0]) {
content += ' ' + this.accept(partial.params[0]);
}
if (partial.hash) {
content += ' ' + this.accept(partial.hash);
}
return this.pad('{{> ' + content + ' }}');
};
PrintVisitor.prototype.PartialBlockStatement = function(partial) {
let content = 'PARTIAL BLOCK:' + partial.name.original;
if (partial.params[0]) {
content += ' ' + this.accept(partial.params[0]);
}
if (partial.hash) {
content += ' ' + this.accept(partial.hash);
}
content += ' ' + this.pad('PROGRAM:');
this.padding++;
content += this.accept(partial.program);
this.padding--;
return this.pad('{{> ' + content + ' }}');
};
PrintVisitor.prototype.ContentStatement = function(content) {
return this.pad("CONTENT[ '" + content.value + "' ]");
};
PrintVisitor.prototype.CommentStatement = function(comment) {
return this.pad("{{! '" + comment.value + "' }}");
};
PrintVisitor.prototype.SubExpression = function(sexpr) {
let params = sexpr.params,
paramStrings = [],
hash;
for (let i = 0, l = params.length; i < l; i++) {
paramStrings.push(this.accept(params[i]));
}
params = '[' + paramStrings.join(', ') + ']';
hash = sexpr.hash ? ' ' + this.accept(sexpr.hash) : '';
return this.accept(sexpr.path) + ' ' + params + hash;
};
PrintVisitor.prototype.PathExpression = function(id) {
let path = id.parts.join('/');
return (id.data ? '@' : '') + 'PATH:' + path;
};
PrintVisitor.prototype.StringLiteral = function(string) {
return '"' + string.value + '"';
};
PrintVisitor.prototype.NumberLiteral = function(number) {
return 'NUMBER{' + number.value + '}';
};
PrintVisitor.prototype.BooleanLiteral = function(bool) {
return 'BOOLEAN{' + bool.value + '}';
};
PrintVisitor.prototype.UndefinedLiteral = function() {
return 'UNDEFINED';
};
PrintVisitor.prototype.NullLiteral = function() {
return 'NULL';
};
PrintVisitor.prototype.Hash = function(hash) {
let pairs = hash.pairs,
joinedPairs = [];
for (let i = 0, l = pairs.length; i < l; i++) {
joinedPairs.push(this.accept(pairs[i]));
}
return 'HASH{' + joinedPairs.join(', ') + '}';
};
PrintVisitor.prototype.HashPair = function(pair) {
return pair.key + '=' + this.accept(pair.value);
};
/* eslint-enable new-cap */
-136
View File
@@ -1,136 +0,0 @@
import Exception from '../exception';
function Visitor() {
this.parents = [];
}
Visitor.prototype = {
constructor: Visitor,
mutating: false,
// Visits a given value. If mutating, will replace the value if necessary.
acceptKey: function(node, name) {
let value = this.accept(node[name]);
if (this.mutating) {
// 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
);
}
node[name] = value;
}
},
// Performs an accept operation with added sanity check to ensure
// required keys are not removed.
acceptRequired: function(node, name) {
this.acceptKey(node, name);
if (!node[name]) {
throw new Exception(node.type + ' requires ' + name);
}
},
// Traverses a given array. If mutating, empty respnses will be removed
// for child elements.
acceptArray: function(array) {
for (let i = 0, l = array.length; i < l; i++) {
this.acceptKey(array, i);
if (!array[i]) {
array.splice(i, 1);
i--;
l--;
}
}
},
accept: function(object) {
if (!object) {
return;
}
/* istanbul ignore next: Sanity code */
if (!this[object.type]) {
throw new Exception('Unknown type: ' + object.type, object);
}
if (this.current) {
this.parents.unshift(this.current);
}
this.current = object;
let ret = this[object.type](object);
this.current = this.parents.shift();
if (!this.mutating || ret) {
return ret;
} else if (ret !== false) {
return object;
}
},
Program: function(program) {
this.acceptArray(program.body);
},
MustacheStatement: visitSubExpression,
Decorator: visitSubExpression,
BlockStatement: visitBlock,
DecoratorBlock: visitBlock,
PartialStatement: visitPartial,
PartialBlockStatement: function(partial) {
visitPartial.call(this, partial);
this.acceptKey(partial, 'program');
},
ContentStatement: function(/* content */) {},
CommentStatement: function(/* comment */) {},
SubExpression: visitSubExpression,
PathExpression: function(/* path */) {},
StringLiteral: function(/* string */) {},
NumberLiteral: function(/* number */) {},
BooleanLiteral: function(/* bool */) {},
UndefinedLiteral: function(/* literal */) {},
NullLiteral: function(/* literal */) {},
Hash: function(hash) {
this.acceptArray(hash.pairs);
},
HashPair: function(pair) {
this.acceptRequired(pair, 'value');
}
};
function visitSubExpression(mustache) {
this.acceptRequired(mustache, 'path');
this.acceptArray(mustache.params);
this.acceptKey(mustache, 'hash');
}
function visitBlock(block) {
visitSubExpression.call(this, block);
this.acceptKey(block, 'program');
this.acceptKey(block, 'inverse');
}
function visitPartial(partial) {
this.acceptRequired(partial, 'name');
this.acceptArray(partial.params);
this.acceptKey(partial, 'hash');
}
export default Visitor;
@@ -1,234 +0,0 @@
import Visitor from './visitor';
function WhitespaceControl(options = {}) {
this.options = options;
}
WhitespaceControl.prototype = new Visitor();
WhitespaceControl.prototype.Program = function(program) {
const doStandalone = !this.options.ignoreStandalone;
let isRoot = !this.isRootSeen;
this.isRootSeen = true;
let body = program.body;
for (let i = 0, l = body.length; i < l; i++) {
let current = body[i],
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;
if (strip.close) {
omitRight(body, i, true);
}
if (strip.open) {
omitLeft(body, i, true);
}
if (doStandalone && inlineStandalone) {
omitRight(body, i);
if (omitLeft(body, i)) {
// 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];
}
}
}
if (doStandalone && openStandalone) {
omitRight((current.program || current.inverse).body);
// Strip out the previous content node if it's whitespace only
omitLeft(body, i);
}
if (doStandalone && closeStandalone) {
// Always strip the next node
omitRight(body, i);
omitLeft((current.inverse || current.program).body);
}
}
return program;
};
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;
if (inverse && inverse.chained) {
firstInverse = inverse.body[0].program;
// Walk the inverse chain to find the last inverse that is actually in the chain.
while (lastInverse.chained) {
lastInverse = lastInverse.body[lastInverse.body.length - 1].program;
}
}
let strip = {
open: block.openStrip.open,
close: block.closeStrip.close,
// Determine the standalone candiacy. Basically flag our content as being possibly standalone
// so our parent can determine if we actually are standalone
openStandalone: isNextWhitespace(program.body),
closeStandalone: isPrevWhitespace((firstInverse || program).body)
};
if (block.openStrip.close) {
omitRight(program.body, null, true);
}
if (inverse) {
let inverseStrip = block.inverseStrip;
if (inverseStrip.open) {
omitLeft(program.body, null, true);
}
if (inverseStrip.close) {
omitRight(firstInverse.body, null, true);
}
if (block.closeStrip.open) {
omitLeft(lastInverse.body, null, true);
}
// Find standalone else statments
if (
!this.options.ignoreStandalone &&
isPrevWhitespace(program.body) &&
isNextWhitespace(firstInverse.body)
) {
omitLeft(program.body);
omitRight(firstInverse.body);
}
} else if (block.closeStrip.open) {
omitLeft(program.body, null, true);
}
return strip;
};
WhitespaceControl.prototype.Decorator = WhitespaceControl.prototype.MustacheStatement = function(
mustache
) {
return mustache.strip;
};
WhitespaceControl.prototype.PartialStatement = WhitespaceControl.prototype.CommentStatement = function(
node
) {
/* istanbul ignore next */
let strip = node.strip || {};
return {
inlineStandalone: true,
open: strip.open,
close: strip.close
};
};
function isPrevWhitespace(body, i, isRoot) {
if (i === undefined) {
i = body.length;
}
// Nodes that end with newlines are considered whitespace (but are special
// cased for strip operations)
let prev = body[i - 1],
sibling = body[i - 2];
if (!prev) {
return isRoot;
}
if (prev.type === 'ContentStatement') {
return (sibling || !isRoot ? /\r?\n\s*?$/ : /(^|\r?\n)\s*?$/).test(
prev.original
);
}
}
function isNextWhitespace(body, i, isRoot) {
if (i === undefined) {
i = -1;
}
let next = body[i + 1],
sibling = body[i + 2];
if (!next) {
return isRoot;
}
if (next.type === 'ContentStatement') {
return (sibling || !isRoot ? /^\s*?\r?\n/ : /^\s*?(\r?\n|$)/).test(
next.original
);
}
}
// Marks the node to the right of the position as omitted.
// I.e. {{foo}}' ' will mark the ' ' node as omitted.
//
// If i is undefined, then the first child will be marked as such.
//
// If mulitple is truthy then all whitespace will be stripped out until non-whitespace
// content is met.
function omitRight(body, i, multiple) {
let current = body[i == null ? 0 : i + 1];
if (
!current ||
current.type !== 'ContentStatement' ||
(!multiple && current.rightStripped)
) {
return;
}
let original = current.value;
current.value = current.value.replace(
multiple ? /^\s+/ : /^[ \t]*\r?\n?/,
''
);
current.rightStripped = current.value !== original;
}
// Marks the node to the left of the position as omitted.
// I.e. ' '{{foo}} will mark the ' ' node as omitted.
//
// If i is undefined then the last child will be marked as such.
//
// If mulitple is truthy then all whitespace will be stripped out until non-whitespace
// 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)
) {
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.leftStripped = current.value !== original;
return current.leftStripped;
}
export default WhitespaceControl;
+21 -18
View File
@@ -1,22 +1,25 @@
import { extend } from '../utils';
export default function(instance) {
instance.registerDecorator('inline', function(fn, props, container, options) {
let ret = fn;
if (!props.partials) {
props.partials = {};
ret = function(context, options) {
// Create a new partials stack frame prior to exec.
let original = container.partials;
container.partials = extend({}, original, props.partials);
let ret = fn(context, options);
container.partials = original;
return ret;
};
export default function (instance) {
instance.registerDecorator(
'inline',
function (fn, props, container, options) {
let ret = fn;
if (!props.partials) {
props.partials = {};
ret = function (context, options) {
// Create a new partials stack frame prior to exec.
let original = container.partials;
container.partials = extend({}, original, props.partials);
let ret = fn(context, options);
container.partials = original;
return ret;
};
}
props.partials[options.args[0]] = options.fn;
return ret;
}
props.partials[options.args[0]] = options.fn;
return ret;
});
);
}
-68
View File
@@ -1,68 +0,0 @@
const errorProps = [
'description',
'fileName',
'lineNumber',
'endLineNumber',
'message',
'name',
'number',
'stack'
];
function Exception(message, node) {
let loc = node && node.loc,
line,
endLineNumber,
column,
endColumn;
if (loc) {
line = loc.start.line;
endLineNumber = loc.end.line;
column = loc.start.column;
endColumn = loc.end.column;
message += ' - ' + line + ':' + column;
}
let tmp = Error.prototype.constructor.call(this, message);
// Unfortunately errors are not enumerable in Chrome (at least), so `for prop in tmp` doesn't work.
for (let idx = 0; idx < errorProps.length; idx++) {
this[errorProps[idx]] = tmp[errorProps[idx]];
}
/* istanbul ignore else */
if (Error.captureStackTrace) {
Error.captureStackTrace(this, Exception);
}
try {
if (loc) {
this.lineNumber = line;
this.endLineNumber = endLineNumber;
// Work around issue under safari where we can't directly set the column value
/* istanbul ignore next */
if (Object.defineProperty) {
Object.defineProperty(this, 'column', {
value: column,
enumerable: true
});
Object.defineProperty(this, 'endColumn', {
value: endColumn,
enumerable: true
});
} else {
this.column = column;
this.endColumn = endColumn;
}
}
} catch (nop) {
/* Ignore if the browser is very particular */
}
}
Exception.prototype = new Error();
export default Exception;
+2 -1
View File
@@ -20,7 +20,8 @@ export function moveHelperToHooks(instance, helperName, keepHelper) {
if (instance.helpers[helperName]) {
instance.hooks[helperName] = instance.helpers[helperName];
if (!keepHelper) {
delete instance.helpers[helperName];
// Using delete is slow
instance.helpers[helperName] = undefined;
}
}
}
+3 -16
View File
@@ -1,7 +1,7 @@
import { appendContextPath, createFrame, isArray } from '../utils';
import { isArray } from '../utils';
export default function(instance) {
instance.registerHelper('blockHelperMissing', function(context, options) {
export default function (instance) {
instance.registerHelper('blockHelperMissing', function (context, options) {
let inverse = options.inverse,
fn = options.fn;
@@ -11,24 +11,11 @@ export default function(instance) {
return inverse(this);
} else if (isArray(context)) {
if (context.length > 0) {
if (options.ids) {
options.ids = [options.name];
}
return instance.helpers.each(context, options);
} else {
return inverse(this);
}
} else {
if (options.data && options.ids) {
let data = createFrame(options.data);
data.contextPath = appendContextPath(
options.data.contextPath,
options.name
);
options = { data: data };
}
return fn(context, options);
}
});
+24 -33
View File
@@ -1,14 +1,8 @@
import {
appendContextPath,
blockParams,
createFrame,
isArray,
isFunction
} from '../utils';
import Exception from '../exception';
import { Exception } from '@handlebars/parser';
import { createFrame, isArray, isFunction, isMap, isSet } from '../utils';
export default function(instance) {
instance.registerHelper('each', function(context, options) {
export default function (instance) {
instance.registerHelper('each', function (context, options) {
if (!options) {
throw new Exception('Must pass iterator to #each');
}
@@ -17,13 +11,7 @@ export default function(instance) {
inverse = options.inverse,
i = 0,
ret = '',
data,
contextPath;
if (options.data && options.ids) {
contextPath =
appendContextPath(options.data.contextPath, options.ids[0]) + '.';
}
data;
if (isFunction(context)) {
context = context.call(this);
@@ -33,26 +21,19 @@ export default function(instance) {
data = createFrame(options.data);
}
function execIteration(field, index, last) {
function execIteration(field, value, index, last) {
if (data) {
data.key = field;
data.index = index;
data.first = index === 0;
data.last = !!last;
if (contextPath) {
data.contextPath = contextPath + field;
}
}
ret =
ret +
fn(context[field], {
fn(value, {
data: data,
blockParams: blockParams(
[context[field], field],
[contextPath + field, null]
)
blockParams: [context[field], field],
});
}
@@ -60,9 +41,19 @@ export default function(instance) {
if (isArray(context)) {
for (let j = context.length; i < j; i++) {
if (i in context) {
execIteration(i, i, i === context.length - 1);
execIteration(i, context[i], i, i === context.length - 1);
}
}
} else if (isMap(context)) {
const j = context.size;
for (const [key, value] of context) {
execIteration(key, value, i++, i === j);
}
} else if (isSet(context)) {
const j = context.size;
for (const value of context) {
execIteration(i, value, i++, i === j);
}
} else if (typeof Symbol === 'function' && context[Symbol.iterator]) {
const newContext = [];
const iterator = context[Symbol.iterator]();
@@ -71,23 +62,23 @@ export default function(instance) {
}
context = newContext;
for (let j = context.length; i < j; i++) {
execIteration(i, i, i === context.length - 1);
execIteration(i, context[i], i, i === context.length - 1);
}
} else {
let priorKey;
Object.keys(context).forEach(key => {
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.
// an intermediate keys array.
if (priorKey !== undefined) {
execIteration(priorKey, i - 1);
execIteration(priorKey, context[priorKey], i - 1);
}
priorKey = key;
i++;
});
if (priorKey !== undefined) {
execIteration(priorKey, i - 1, true);
execIteration(priorKey, context[priorKey], i - 1, true);
}
}
}
+3 -3
View File
@@ -1,7 +1,7 @@
import Exception from '../exception';
import { Exception } from '@handlebars/parser';
export default function(instance) {
instance.registerHelper('helperMissing', function(/* [args, ]options */) {
export default function (instance) {
instance.registerHelper('helperMissing', function (/* [args, ]options */) {
if (arguments.length === 1) {
// A missing field in a {{foo}} construct.
return undefined;
+6 -6
View File
@@ -1,8 +1,8 @@
import { Exception } from '@handlebars/parser';
import { isEmpty, isFunction } from '../utils';
import Exception from '../exception';
export default function(instance) {
instance.registerHelper('if', function(conditional, options) {
export default function (instance) {
instance.registerHelper('if', function (conditional, options) {
if (arguments.length != 2) {
throw new Exception('#if requires exactly one argument');
}
@@ -11,7 +11,7 @@ export default function(instance) {
}
// 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
// The `includeZero` option may be set to treat the conditional as purely not empty based on the
// behavior of isEmpty. Effectively this determines if 0 is handled by the positive path or negative.
if ((!options.hash.includeZero && !conditional) || isEmpty(conditional)) {
return options.inverse(this);
@@ -20,14 +20,14 @@ export default function(instance) {
}
});
instance.registerHelper('unless', function(conditional, options) {
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
hash: options.hash,
});
});
}
+2 -2
View File
@@ -1,5 +1,5 @@
export default function(instance) {
instance.registerHelper('log', function(/* message, options */) {
export default function (instance) {
instance.registerHelper('log', function (/* message, options */) {
let args = [undefined],
options = arguments[arguments.length - 1];
for (let i = 0; i < arguments.length - 1; i++) {
+2 -2
View File
@@ -1,5 +1,5 @@
export default function(instance) {
instance.registerHelper('lookup', function(obj, field, options) {
export default function (instance) {
instance.registerHelper('lookup', function (obj, field, options) {
if (!obj) {
// Note for 5.0: Change to "obj == null" in 5.0
return obj;
+5 -18
View File
@@ -1,14 +1,8 @@
import {
appendContextPath,
blockParams,
createFrame,
isEmpty,
isFunction
} from '../utils';
import Exception from '../exception';
import { Exception } from '@handlebars/parser';
import { isEmpty, isFunction } from '../utils';
export default function(instance) {
instance.registerHelper('with', function(context, options) {
export default function (instance) {
instance.registerHelper('with', function (context, options) {
if (arguments.length != 2) {
throw new Exception('#with requires exactly one argument');
}
@@ -20,17 +14,10 @@ export default function(instance) {
if (!isEmpty(context)) {
let data = options.data;
if (options.data && options.ids) {
data = createFrame(options.data);
data.contextPath = appendContextPath(
options.data.contextPath,
options.ids[0]
);
}
return fn(context, {
data: data,
blockParams: blockParams([context], [data && data.contextPath])
blockParams: [context],
});
} else {
return options.inverse(this);
@@ -1,11 +0,0 @@
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);
}
+21 -23
View File
@@ -1,34 +1,32 @@
import { createNewLookupObject } from './create-new-lookup-object';
import { extend } from '../utils';
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);
// Create an object with "null"-prototype to avoid truthy results on
// prototype properties.
const propertyWhiteList = Object.create(null);
// eslint-disable-next-line no-proto
defaultPropertyWhiteList['__proto__'] = false;
propertyWhiteList['__proto__'] = false;
extend(propertyWhiteList, runtimeOptions.allowedProtoProperties);
const methodWhiteList = Object.create(null);
methodWhiteList['constructor'] = false;
methodWhiteList['__defineGetter__'] = false;
methodWhiteList['__defineSetter__'] = false;
methodWhiteList['__lookupGetter__'] = false;
extend(methodWhiteList, runtimeOptions.allowedProtoMethods);
return {
properties: {
whitelist: createNewLookupObject(
defaultPropertyWhiteList,
runtimeOptions.allowedProtoProperties
),
defaultValue: runtimeOptions.allowProtoPropertiesByDefault
whitelist: propertyWhiteList,
defaultValue: runtimeOptions.allowProtoPropertiesByDefault,
},
methods: {
whitelist: createNewLookupObject(
defaultMethodWhiteList,
runtimeOptions.allowedProtoMethods
),
defaultValue: runtimeOptions.allowProtoMethodsByDefault
}
whitelist: methodWhiteList,
defaultValue: runtimeOptions.allowProtoMethodsByDefault,
},
};
}
@@ -47,11 +45,11 @@ function checkWhiteList(protoAccessControlForType, propertyName) {
if (protoAccessControlForType.defaultValue !== undefined) {
return protoAccessControlForType.defaultValue;
}
logUnexpecedPropertyAccessOnce(propertyName);
logUnexpectedPropertyAccessOnce(propertyName);
return false;
}
function logUnexpecedPropertyAccessOnce(propertyName) {
function logUnexpectedPropertyAccessOnce(propertyName) {
if (loggedProperties[propertyName] !== true) {
loggedProperties[propertyName] = true;
logger.log(
@@ -64,7 +62,7 @@ function logUnexpecedPropertyAccessOnce(propertyName) {
}
export function resetLoggedProperties() {
Object.keys(loggedProperties).forEach(propertyName => {
Object.keys(loggedProperties).forEach((propertyName) => {
delete loggedProperties[propertyName];
});
}
+2 -2
View File
@@ -1,10 +1,10 @@
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
// This should not happen, but apparently it does in https://github.com/handlebars-lang/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 */) {
let wrapper = function (/* dynamic arguments */) {
const options = arguments[arguments.length - 1];
arguments[arguments.length - 1] = transformOptionsFn(options);
return helper.apply(this, arguments);
+3 -3
View File
@@ -5,7 +5,7 @@ let logger = {
level: 'info',
// Maps a given level value to the `methodMap` indexes above.
lookupLevel: function(level) {
lookupLevel: function (level) {
if (typeof level === 'string') {
let levelMap = indexOf(logger.methodMap, level.toLowerCase());
if (levelMap >= 0) {
@@ -19,7 +19,7 @@ let logger = {
},
// Can be overridden in the host environment
log: function(level, ...message) {
log: function (level, ...message) {
level = logger.lookupLevel(level);
if (
@@ -33,7 +33,7 @@ let logger = {
}
console[method](...message); // eslint-disable-line no-console
}
}
},
};
export default logger;
+7 -8
View File
@@ -1,11 +1,10 @@
export default function(Handlebars) {
/* istanbul ignore next */
let root = typeof global !== 'undefined' ? global : window, // eslint-disable-line no-undef
$Handlebars = root.Handlebars;
/* istanbul ignore next */
Handlebars.noConflict = function() {
if (root.Handlebars === Handlebars) {
root.Handlebars = $Handlebars;
export default function (Handlebars) {
let $Handlebars = globalThis.Handlebars;
/* v8 ignore next */
Handlebars.noConflict = function () {
if (globalThis.Handlebars === Handlebars) {
globalThis.Handlebars = $Handlebars;
}
return Handlebars;
};
+38 -60
View File
@@ -1,16 +1,16 @@
import { Exception } from '@handlebars/parser';
import * as Utils from './utils';
import Exception from './exception';
import {
COMPILER_REVISION,
createFrame,
LAST_COMPATIBLE_COMPILER_REVISION,
REVISION_CHANGES
REVISION_CHANGES,
} from './base';
import { moveHelperToHooks } from './helpers';
import { wrapHelper } from './internal/wrapHelper';
import {
createProtoAccessControl,
resultIsAllowed
resultIsAllowed,
} from './internal/proto-access';
export function checkRevision(compilerInfo) {
@@ -47,7 +47,7 @@ export function checkRevision(compilerInfo) {
}
export function template(templateSpec, env) {
/* istanbul ignore next */
/* v8 ignore next */
if (!env) {
throw new Exception('No environment passed to template');
}
@@ -68,23 +68,13 @@ export function template(templateSpec, env) {
function invokePartialWrapper(partial, context, options) {
if (options.hash) {
context = Utils.extend({}, context, options.hash);
if (options.ids) {
options.ids[0] = true;
}
}
partial = env.VM.resolvePartial.call(this, partial, context, options);
let extendedOptions = Utils.extend({}, options, {
hooks: this.hooks,
protoAccessControl: this.protoAccessControl
});
options.hooks = this.hooks;
options.protoAccessControl = this.protoAccessControl;
let result = env.VM.invokePartial.call(
this,
partial,
context,
extendedOptions
);
let result = env.VM.invokePartial.call(this, partial, context, options);
if (result == null && env.compile) {
options.partials[options.name] = env.compile(
@@ -92,7 +82,7 @@ export function template(templateSpec, env) {
templateSpec.compilerOptions,
env
);
result = options.partials[options.name](context, extendedOptions);
result = options.partials[options.name](context, options);
}
if (result != null) {
if (options.indent) {
@@ -118,15 +108,19 @@ export function template(templateSpec, env) {
// Just add water
let container = {
strict: function(obj, name, loc) {
strict: function (obj, name, loc) {
if (!obj || !(name in obj)) {
throw new Exception('"' + name + '" not defined in ' + obj, {
loc: loc
loc: loc,
});
}
return container.lookupProperty(obj, name);
},
lookupProperty: function(parent, propertyName) {
lookupProperty: function (parent, propertyName) {
if (Utils.isMap(parent)) {
return parent.get(propertyName);
}
let result = parent[propertyName];
if (result == null) {
return result;
@@ -140,7 +134,7 @@ export function template(templateSpec, env) {
}
return undefined;
},
lookup: function(depths, name) {
lookup: function (depths, name) {
const len = depths.length;
for (let i = 0; i < len; i++) {
let result = depths[i] && container.lookupProperty(depths[i], name);
@@ -149,21 +143,21 @@ export function template(templateSpec, env) {
}
}
},
lambda: function(current, context) {
lambda: function (current, context) {
return typeof current === 'function' ? current.call(context) : current;
},
escapeExpression: Utils.escapeExpression,
invokePartial: invokePartialWrapper,
fn: function(i) {
fn: function (i) {
let ret = templateSpec[i];
ret.decorator = templateSpec[i + '_d'];
return ret;
},
programs: [],
program: function(i, data, declaredBlockParams, blockParams, depths) {
program: function (i, data, declaredBlockParams, blockParams, depths) {
let programWrapper = this.programs[i],
fn = this.fn(i);
if (data || depths || blockParams || declaredBlockParams) {
@@ -182,13 +176,13 @@ export function template(templateSpec, env) {
return programWrapper;
},
data: function(value, depth) {
data: function (value, depth) {
while (value && depth--) {
value = value._parent;
}
return value;
},
mergeIfNeeded: function(param, common) {
mergeIfNeeded: function (param, common) {
let obj = param || common;
if (param && common && param !== common) {
@@ -201,13 +195,13 @@ export function template(templateSpec, env) {
nullContext: Object.seal({}),
noop: env.VM.noop,
compilerInfo: templateSpec.compiler
compilerInfo: templateSpec.compiler,
};
function ret(context, options = {}) {
let data = options.data;
ret._setup(options);
_setup(options);
if (!options.partial && templateSpec.useData) {
data = initData(context, data);
}
@@ -252,10 +246,11 @@ export function template(templateSpec, env) {
ret.isTop = true;
ret._setup = function(options) {
function _setup(options) {
if (!options.partial) {
let mergedHelpers = Utils.extend({}, env.helpers, options.helpers);
wrapHelpersToPassLookupProperty(mergedHelpers, container);
let mergedHelpers = {};
addHelpers(mergedHelpers, env.helpers, container);
addHelpers(mergedHelpers, options.helpers, container);
container.helpers = mergedHelpers;
if (templateSpec.usePartial) {
@@ -288,26 +283,8 @@ export function template(templateSpec, env) {
container.decorators = options.decorators;
container.hooks = options.hooks;
}
};
}
ret._child = function(i, data, blockParams, depths) {
if (templateSpec.useBlockParams && !blockParams) {
throw new Exception('must pass block params');
}
if (templateSpec.useDepths && !depths) {
throw new Exception('must pass parent depths');
}
return wrapProgram(
container,
i,
templateSpec[i],
data,
0,
blockParams,
depths
);
};
return ret;
}
@@ -371,9 +348,6 @@ export function invokePartial(partial, context, options) {
// Use the current closure context to save the partial-block if this partial
const currentPartialBlock = options.data && options.data['partial-block'];
options.partial = true;
if (options.ids) {
options.data.contextPath = options.ids[0] || options.data.contextPath;
}
let partialBlock;
if (options.fn && options.fn !== noop) {
@@ -400,7 +374,9 @@ export function invokePartial(partial, context, options) {
}
if (partial === undefined) {
throw new Exception('The partial ' + options.name + ' could not be found');
throw new Exception(
'The partial "' + options.name + '" could not be found'
);
} else if (partial instanceof Function) {
return partial(context, options);
}
@@ -435,16 +411,18 @@ function executeDecorators(fn, prog, container, depths, data, blockParams) {
return prog;
}
function wrapHelpersToPassLookupProperty(mergedHelpers, container) {
Object.keys(mergedHelpers).forEach(helperName => {
let helper = mergedHelpers[helperName];
function addHelpers(mergedHelpers, helpers, container) {
if (!helpers) return;
Object.keys(helpers).forEach((helperName) => {
let helper = helpers[helperName];
mergedHelpers[helperName] = passLookupPropertyOption(helper, container);
});
}
function passLookupPropertyOption(helper, container) {
const lookupProperty = container.lookupProperty;
return wrapHelper(helper, options => {
return Utils.extend({ lookupProperty }, options);
return wrapHelper(helper, (options) => {
options.lookupProperty = lookupProperty;
return options;
});
}
+1 -1
View File
@@ -3,7 +3,7 @@ function SafeString(string) {
this.string = string;
}
SafeString.prototype.toString = SafeString.prototype.toHTML = function() {
SafeString.prototype.toString = SafeString.prototype.toHTML = function () {
return '' + this.string;
};
+12 -30
View File
@@ -5,7 +5,7 @@ const escape = {
'"': '&quot;',
"'": '&#x27;',
'`': '&#x60;',
'=': '&#x3D;'
'=': '&#x3D;',
};
const badChars = /[&<>"'`=]/g,
@@ -30,32 +30,23 @@ export function extend(obj /* , ...source */) {
export let toString = Object.prototype.toString;
// Sourced from lodash
// https://github.com/bestiejs/lodash/blob/master/LICENSE.txt
/* eslint-disable func-style */
let isFunction = function(value) {
// https://github.com/lodash/lodash/blob/4.17.21/LICENSE
export function isFunction(value) {
return typeof value === 'function';
};
// fallback for older versions of Chrome and Safari
/* istanbul ignore next */
if (isFunction(/x/)) {
isFunction = function(value) {
return (
typeof value === 'function' &&
toString.call(value) === '[object Function]'
);
};
}
export { isFunction };
/* eslint-enable func-style */
/* istanbul ignore next */
export const isArray =
Array.isArray ||
function(value) {
function testTag(name) {
const tag = '[object ' + name + ']';
return function (value) {
return value && typeof value === 'object'
? toString.call(value) === '[object Array]'
? toString.call(value) === tag
: false;
};
}
export const isArray = Array.isArray;
export const isMap = testTag('Map');
export const isSet = testTag('Set');
// Older IE versions do not directly support indexOf so we must implement our own, sadly.
export function indexOf(array, value) {
@@ -105,12 +96,3 @@ export function createFrame(object) {
frame._parent = object;
return frame;
}
export function blockParams(params, ids) {
params.path = ids;
return params;
}
export function appendContextPath(contextPath, id) {
return (contextPath ? contextPath + '.' : '') + id;
}
+4 -5
View File
@@ -1,15 +1,14 @@
// USAGE:
// var handlebars = require('handlebars');
/* eslint-env node */
/* eslint-disable no-var */
// var local = handlebars.create();
var handlebars = require('../dist/cjs/handlebars')['default'];
var printer = require('../dist/cjs/handlebars/compiler/printer');
handlebars.PrintVisitor = printer.PrintVisitor;
handlebars.print = printer.print;
var parser = require('@handlebars/parser');
handlebars.PrintVisitor = parser.PrintVisitor;
handlebars.print = parser.print;
module.exports = handlebars;
@@ -19,7 +18,7 @@ function extension(module, filename) {
var templateString = fs.readFileSync(filename, 'utf8');
module.exports = handlebars.compile(templateString);
}
/* istanbul ignore else */
/* v8 ignore next 4 */
if (typeof require !== 'undefined' && require.extensions) {
require.extensions['.handlebars'] = extension;
require.extensions['.hbs'] = extension;
+32 -29
View File
@@ -1,17 +1,16 @@
/* eslint-env node */
/* eslint-disable no-console */
import Async from 'neo-async';
import fs from 'fs';
import * as Handlebars from './handlebars';
import Handlebars from './handlebars';
import { basename } from 'path';
import { SourceMapConsumer, SourceNode } from 'source-map';
module.exports.loadTemplates = function(opts, callback) {
loadStrings(opts, function(err, strings) {
module.exports.loadTemplates = function (opts, callback) {
loadStrings(opts, function (err, strings) {
if (err) {
callback(err);
} else {
loadFiles(opts, function(err, files) {
loadFiles(opts, function (err, files) {
if (err) {
callback(err);
} else {
@@ -37,7 +36,7 @@ function loadStrings(opts, callback) {
Async.map(
strings,
function(string, callback) {
function (string, callback) {
if (string !== '-') {
callback(undefined, string);
} else {
@@ -45,19 +44,19 @@ function loadStrings(opts, callback) {
let buffer = '';
process.stdin.setEncoding('utf8');
process.stdin.on('data', function(chunk) {
process.stdin.on('data', function (chunk) {
buffer += chunk;
});
process.stdin.on('end', function() {
process.stdin.on('end', function () {
callback(undefined, buffer);
});
}
},
function(err, strings) {
function (err, strings) {
strings = strings.map((string, index) => ({
name: names[index],
path: names[index],
source: string
source: string,
}));
callback(err, strings);
}
@@ -68,20 +67,23 @@ function loadFiles(opts, callback) {
// Build file extension pattern
let extension = (opts.extension || 'handlebars').replace(
/[\\^$*+?.():=!|{}\-[\]]/g,
function(arg) {
function (arg) {
return '\\' + arg;
}
);
extension = new RegExp('\\.' + extension + '$');
let ret = [],
queue = (opts.files || []).map(template => ({ template, root: opts.root }));
queue = (opts.files || []).map((template) => ({
template,
root: opts.root,
}));
Async.whilst(
() => queue.length,
function(callback) {
function (callback) {
let { template: path, root } = queue.shift();
fs.stat(path, function(err, stat) {
fs.stat(path, function (err, stat) {
if (err) {
return callback(
new Handlebars.Exception(`Unable to open template file "${path}"`)
@@ -91,12 +93,12 @@ function loadFiles(opts, callback) {
if (stat.isDirectory()) {
opts.hasDirectory = true;
fs.readdir(path, function(err, children) {
/* istanbul ignore next : Race condition that being too lazy to test */
fs.readdir(path, function (err, children) {
/* v8 ignore next -- Race condition that being too lazy to test */
if (err) {
return callback(err);
}
children.forEach(function(file) {
children.forEach(function (file) {
let childPath = path + '/' + file;
if (
@@ -110,8 +112,8 @@ function loadFiles(opts, callback) {
callback();
});
} else {
fs.readFile(path, 'utf8', function(err, data) {
/* istanbul ignore next : Race condition that being too lazy to test */
fs.readFile(path, 'utf8', function (err, data) {
/* v8 ignore next -- Race condition that being too lazy to test */
if (err) {
return callback(err);
}
@@ -132,7 +134,7 @@ function loadFiles(opts, callback) {
ret.push({
path: path,
name: name,
source: data
source: data,
});
callback();
@@ -140,7 +142,7 @@ function loadFiles(opts, callback) {
}
});
},
function(err) {
function (err) {
if (err) {
callback(err);
} else {
@@ -150,7 +152,7 @@ function loadFiles(opts, callback) {
);
}
module.exports.cli = function(opts) {
module.exports.cli = async function (opts) {
if (opts.version) {
console.log(Handlebars.VERSION);
return;
@@ -219,10 +221,10 @@ module.exports.cli = function(opts) {
output.add('{};\n');
}
opts.templates.forEach(function(template) {
for (const template of opts.templates) {
let options = {
knownHelpers: known,
knownHelpersOnly: opts.o
knownHelpersOnly: opts.o,
};
if (opts.map) {
@@ -236,11 +238,12 @@ 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);
let consumer = await new SourceMapConsumer(precompiled.map);
precompiled = SourceNode.fromStringWithSourceMap(
precompiled.code,
consumer
);
consumer.destroy();
}
if (opts.simple) {
@@ -259,10 +262,10 @@ module.exports.cli = function(opts) {
template.name,
"'] = template(",
precompiled,
');\n'
');\n',
]);
}
});
}
// Output the content
if (!opts.simple) {
@@ -335,7 +338,7 @@ function minify(output, sourceMapFile) {
return require('uglify-js').minify(output.code, {
sourceMap: {
content: output.map,
url: sourceMapFile
}
url: sourceMapFile,
},
});
}
-9
View File
@@ -1,9 +0,0 @@
module.exports = {
'check-coverage': true,
branches: 100,
lines: 100,
functions: 100,
statements: 100,
exclude: ['**/spec/**', '**/handlebars/compiler/parser.js'],
reporter: 'html'
};
+17662 -21766
View File
File diff suppressed because it is too large Load Diff
+95 -103
View File
@@ -1,117 +1,26 @@
{
"name": "handlebars",
"barename": "handlebars",
"version": "4.7.7",
"version": "5.0.0-alpha.1",
"description": "Handlebars provides the power necessary to let you build semantic templates effectively with no frustration",
"homepage": "https://www.handlebarsjs.com/",
"keywords": [
"handlebars",
"html",
"mustache",
"template",
"html"
"template"
],
"homepage": "https://handlebarsjs.com/",
"license": "MIT",
"author": "Yehuda Katz",
"repository": {
"type": "git",
"url": "https://github.com/handlebars-lang/handlebars.js.git"
},
"author": "Yehuda Katz",
"license": "MIT",
"readmeFilename": "README.md",
"engines": {
"node": ">=0.4.7"
},
"dependencies": {
"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": {
"@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",
"chai-diff": "^1.0.1",
"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": "^6.7.2",
"eslint-config-prettier": "^6.7.0",
"eslint-plugin-compat": "^3.13.0",
"eslint-plugin-es5": "^1.4.1",
"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",
"grunt-contrib-connect": "^1",
"grunt-contrib-copy": "^1",
"grunt-contrib-requirejs": "^1",
"grunt-contrib-uglify": "^1",
"grunt-contrib-watch": "^1.1.0",
"grunt-webpack": "^1.0.8",
"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",
"webpack-dev-server": "^1.12.1"
},
"main": "lib/index.js",
"types": "types/index.d.ts",
"browser": {
".": "./dist/cjs/handlebars.js",
"./runtime": "./dist/cjs/handlebars.runtime.js"
},
"bin": {
"handlebars": "bin/handlebars"
},
"scripts": {
"build": "grunt build",
"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",
"directories": {
"lib": "dist/amd"
},
"buildConfig": {
"minify": true
}
"handlebars": "bin/handlebars.mjs"
},
"files": [
"bin",
"dist/*.js",
"dist/amd/**/*.js",
"dist/cjs/**/*.js",
"lib",
"release-notes.md",
@@ -119,19 +28,102 @@
"types/*.d.ts",
"runtime.d.ts"
],
"main": "lib/index.js",
"browser": "./dist/cjs/handlebars.js",
"types": "types/index.d.ts",
"scripts": {
"clean": "node -e \"require('fs').rmSync('dist',{recursive:true,force:true});require('fs').rmSync('tmp',{recursive:true,force:true})\"",
"build:cjs": "swc lib --out-dir dist/cjs --strip-leading-paths --ignore \"**/index.js\"",
"build:bundles": "rspack build",
"build": "npm run clean && npm run build:cjs && npm run build:bundles",
"release": "npm run build",
"publish:aws": "npm run build && npm run test:tasks && node tasks/publish-to-aws.js",
"format": "oxfmt --write . && oxlint --fix .",
"lint": "npm run lint:oxlint && npm run lint:format && npm run lint:types && npm run lint:compat",
"lint:oxlint": "oxlint --max-warnings 0 .",
"lint:format": "oxfmt --check .",
"lint:types": "tstyche",
"lint:compat": "eslint",
"test": "npm run build && vitest run --project node --project tasks --project rspack --coverage",
"test:browser": "vitest run --project browser",
"test:unit": "vitest run --project node",
"test:tasks": "vitest run --project tasks",
"test:publish": "npm run build && vitest run --project publish",
"test:browser-smoke": "playwright test --config tests/browser/playwright.config.js",
"test:serve": "npx serve -l 9999 .",
"test:integration": "npm run build && ./tests/integration/run-integration-tests.sh",
"bench": "node tests/bench/perf.mjs",
"bench:compare": "node tests/bench/compare.mjs",
"bench:size": "node tests/bench/size.mjs",
"--- combined tasks ---": "",
"check-before-pull-request": "concurrently --kill-others-on-fail npm:lint npm:test"
},
"dependencies": {
"@handlebars/parser": "^2.1.0",
"neo-async": "^2.6.2",
"source-map": "^0.7.6",
"yargs": "^18.0.0"
},
"devDependencies": {
"@aws-sdk/client-s3": "^3.1011.0",
"@playwright/test": "^1.58.2",
"@rspack/cli": "^1.7.8",
"@rspack/core": "^1.7.8",
"@swc/cli": "^0.8.0",
"@swc/core": "^1.15.18",
"@vitest/browser": "^4.0.18",
"@vitest/browser-playwright": "^4.0.18",
"@vitest/coverage-v8": "^4.0.18",
"cli-testlab": "^6.0.0",
"concurrently": "^5.0.0",
"eslint": "^10.0.3",
"eslint-plugin-compat": "^7.0.1",
"fs-extra": "^8.1.0",
"husky": "^3.1.0",
"lint-staged": "^16.3.2",
"mock-stdin": "^0.3.0",
"oxfmt": "^0.36.0",
"oxlint": "^1.51.0",
"semver": "^5.0.1",
"tinybench": "^6.0.0",
"tstyche": "^6.2.0",
"typescript": "^5.9.3",
"uglify-js": "^3.19.3",
"vitest": "^4.0.18"
},
"peerDependencies": {
"uglify-js": "^3.19.3"
},
"husky": {
"hooks": {
"pre-commit": "lint-staged"
}
},
"lint-staged": {
"*.{js,css,json}": [
"prettier --write",
"git add"
"*.{js,css,json,md}": [
"oxfmt --write"
],
"*.js": [
"eslint --fix",
"git add"
"oxlint --fix"
]
},
"browserslist": [
"last 2 versions",
"Firefox ESR",
"not dead",
"not IE 11",
"maintained node versions"
],
"engines": {
"node": ">=20"
},
"jspm": {
"main": "handlebars",
"directories": {
"lib": "dist/cjs"
},
"buildConfig": {
"minify": true
}
}
}
-5
View File
@@ -1,5 +0,0 @@
module.exports = {
tabWidth: 2,
semi: true,
singleQuote: true
};
+286 -275
View File
File diff suppressed because it is too large Load Diff
+88
View File
@@ -0,0 +1,88 @@
const { rspack } = require('@rspack/core');
const path = require('path');
const fs = require('fs');
const pkg = require('./package.json');
const license = fs.readFileSync(path.resolve(__dirname, 'LICENSE'), 'utf8');
const banner = `/*!
@license magnet:?xt=urn:btih:d3d9a9a6595521f9666a5e94cc830dab83b65699&dn=expat.txt Expat
${pkg.name} v${pkg.version}
${license}
*/`;
function createConfig(entry, filename, minimize) {
const plugins = [];
if (!minimize) {
// For non-minified builds, use BannerPlugin to add the license header
plugins.push(new rspack.BannerPlugin({ banner, raw: true }));
}
return {
mode: minimize ? 'production' : 'none',
context: __dirname,
entry,
output: {
path: path.resolve(__dirname, 'dist'),
filename,
library: {
name: 'Handlebars',
type: 'umd',
export: 'default',
},
globalObject: 'this',
clean: false,
},
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
use: {
loader: 'builtin:swc-loader',
options: {
jsc: {
parser: { syntax: 'ecmascript' },
},
},
},
},
],
},
optimization: {
minimize,
minimizer: minimize
? [
new rspack.SwcJsMinimizerRspackPlugin({
extractComments: false,
minimizerOptions: {
compress: { passes: 2 },
mangle: true,
format: {
comments: false,
// Prepend the license banner in the minified output
preamble: banner,
},
},
}),
]
: [],
},
plugins,
target: ['web', 'browserslist'],
devtool: false,
};
}
module.exports = [
createConfig('./lib/handlebars.js', 'handlebars.js', false),
createConfig('./lib/handlebars.runtime.js', 'handlebars.runtime.js', false),
createConfig('./lib/handlebars.js', 'handlebars.min.js', true),
createConfig(
'./lib/handlebars.runtime.js',
'handlebars.runtime.min.js',
true
),
];
+2 -4
View File
@@ -1,5 +1,3 @@
import Handlebars = require('handlebars')
import Handlebars = require('handlebars');
declare module "handlebars/runtime" {
}
declare module 'handlebars/runtime' {}
-47
View File
@@ -1,47 +0,0 @@
{
"extends": [
"../.eslintrc.js",
"plugin:es5/no-es2015",
"prettier"
],
"plugins": [
"es5"
],
"globals": {
"CompilerContext": true,
"Handlebars": true,
"handlebarsEnv": true,
"shouldCompileTo": true,
"shouldCompileToWithPartials": true,
"shouldThrow": true,
"expectTemplate": true,
"compileWithPartials": true,
"console": true,
"require": true,
"suite": true,
"equal": true,
"equals": true,
"test": true,
"testBoth": true,
"raises": true,
"deepEqual": true,
"start": true,
"stop": true,
"ok": true,
"sinon": true,
"strictEqual": true,
"define": true,
"expect": true,
"chai": true
},
"env": {
"mocha": true
},
"rules": {
// Disabling for tests, for now.
"no-path-concat": "off",
"no-var": "off",
"dot-notation": "off"
}
}
-108
View File
@@ -1,108 +0,0 @@
<html>
<head>
<title>Mocha</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="/node_modules/mocha/mocha.css" />
<style>
.headless .suite > h1,
.headless .test.pass {
display: none;
}
</style>
<script>
// Show only errors in "headless", non-interactive mode.
if (/headless=true/.test(location.href)) {
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/vendor/json2.js"></script>
<script src="/spec/vendor/require.js"></script>
<script src="/spec/env/common.js"></script>
<script>
var requireFailure;
requirejs.config({
paths: {
'handlebars.runtime': '/dist/handlebars.runtime.amd'
}
});
requirejs.onError = function (err) {
requireFailure = err;
};
</script>
<script>
onload = function(){
require(['handlebars.runtime'], function(Handlebars) {
describe('runtime', function() {
it('should load', function() {
equal(!!Handlebars['default'].template, true);
equal(!!Handlebars['default'].VERSION, true);
});
});
mocha.globals(['mochaResults'])
// The test harness leaks under FF. We should have decent global leak coverage from other tests
if (!navigator.userAgent.match(/Firefox\/([\d.]+)/)) {
mocha.checkLeaks();
}
var runner = mocha.run();
// Reporting to test-runner
var failedTests = [];
runner.on('end', function(){
window.mochaResults = runner.stats;
window.mochaResults.reports = failedTests;
});
runner.on('fail', logFailure);
// Inject any require initilizer failures into the first test so they are properly
// reported.
if (requireFailure) {
runner.on('hook end', function(hook){
if (requireFailure) {
runner.uncaught(requireFailure);
requireFailure = undefined;
}
});
}
function logFailure(test, err){
var flattenTitles = function(test){
var titles = [];
while (test.parent.title){
titles.push(test.parent.title);
test = test.parent;
}
return titles.reverse();
};
failedTests.push({
name: test.title,
result: false,
message: err.message,
stack: err.stack,
titles: flattenTitles(test)
});
}
});
};
</script>
</head>
<body>
<div id="mocha"></div>
</body>
</html>
-128
View File
@@ -1,128 +0,0 @@
<!doctype html>
<html>
<head>
<title>Mocha</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="/node_modules/mocha/mocha.css" />
<style>
.headless .suite > h1,
.headless .test.pass {
display: none;
}
</style>
<script>
// Show only errors in "headless", non-interactive mode.
if (/headless=true/.test(location.href)) {
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/vendor/json2.js"></script>
<script src="/spec/vendor/require.js"></script>
<script src="/spec/env/common.js"></script>
<script>
var requireFailure;
requirejs.config({
paths: {
handlebars: '/dist/handlebars.amd',
tests: '/tmp/tests'
}
});
requirejs.onError = function (err) {
requireFailure = err;
};
var CompilerContext = {
compile: function(template, options) {
var templateSpec = handlebarsEnv.precompile(template, options);
return handlebarsEnv.template(safeEval(templateSpec));
},
compileWithPartial: function(template, options) {
return handlebarsEnv.compile(template, options);
}
};
function safeEval(templateSpec) {
try {
var ret;
eval('ret = ' + templateSpec);
return ret;
} catch (err) {
console.error(templateSpec);
throw err;
}
}
</script>
<script>
onload = function(){
require(['handlebars'], function(Handlebars) {
window.Handlebars = Handlebars['default'];
require(['tests'], function(Handlebars) {
mocha.globals(['mochaResults'])
// The test harness leaks under FF. We should have decent global leak coverage from other tests
if (!navigator.userAgent.match(/Firefox\/([\d.]+)/)) {
mocha.checkLeaks();
}
var runner = mocha.run();
// Reporting to test-runner
var failedTests = [];
runner.on('end', function(){
window.mochaResults = runner.stats;
window.mochaResults.reports = failedTests;
});
runner.on('fail', logFailure);
// Inject any require initilizer failures into the first test so they are properly
// reported.
if (requireFailure) {
runner.on('hook end', function(hook){
if (requireFailure) {
runner.uncaught(requireFailure);
requireFailure = undefined;
}
});
}
function logFailure(test, err){
function flattenTitles(test){
var titles = [];
while (test.parent.title){
titles.push(test.parent.title);
test = test.parent;
}
return titles.reverse();
}
failedTests.push({
name: test.title,
result: false,
message: err.message,
stack: err.stack,
titles: flattenTitles(test)
});
}
});
});
};
</script>
</head>
<body>
<div id="mocha"></div>
</body>
</html>
+83 -334
View File
@@ -1,178 +1,174 @@
describe('ast', function() {
describe('ast', function () {
if (!Handlebars.AST) {
return;
}
var AST = Handlebars.AST;
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"
);
describe('BlockStatement', function () {
it('should throw on mustache mismatch', function () {
expect(function () {
handlebarsEnv.parse('\n {{#foo}}{{/bar}}');
}).toThrow("foo doesn't match bar - 2:5");
});
});
describe('helpers', function() {
describe('#helperExpression', function() {
it('should handle mustache statements', function() {
equals(
describe('helpers', function () {
describe('#helperExpression', function () {
it('should handle mustache statements', function () {
expect(
AST.helpers.helperExpression({
type: 'MustacheStatement',
params: [],
hash: undefined
}),
false
);
equals(
hash: undefined,
})
).toBe(false);
expect(
AST.helpers.helperExpression({
type: 'MustacheStatement',
params: [1],
hash: undefined
}),
true
);
equals(
hash: undefined,
})
).toBe(true);
expect(
AST.helpers.helperExpression({
type: 'MustacheStatement',
params: [],
hash: {}
}),
true
);
hash: {},
})
).toBe(true);
});
it('should handle block statements', function() {
equals(
it('should handle block statements', function () {
expect(
AST.helpers.helperExpression({
type: 'BlockStatement',
params: [],
hash: undefined
}),
false
);
equals(
hash: undefined,
})
).toBe(false);
expect(
AST.helpers.helperExpression({
type: 'BlockStatement',
params: [1],
hash: undefined
}),
true
);
equals(
hash: undefined,
})
).toBe(true);
expect(
AST.helpers.helperExpression({
type: 'BlockStatement',
params: [],
hash: {}
}),
hash: {},
})
).toBe(true);
});
it('should handle subexpressions', function () {
expect(AST.helpers.helperExpression({ type: 'SubExpression' })).toBe(
true
);
});
it('should handle subexpressions', function() {
equals(AST.helpers.helperExpression({ type: 'SubExpression' }), true);
});
it('should work with non-helper nodes', function() {
equals(AST.helpers.helperExpression({ type: 'Program' }), false);
it('should work with non-helper nodes', function () {
expect(AST.helpers.helperExpression({ type: 'Program' })).toBe(false);
equals(
AST.helpers.helperExpression({ type: 'PartialStatement' }),
expect(AST.helpers.helperExpression({ type: 'PartialStatement' })).toBe(
false
);
equals(
AST.helpers.helperExpression({ type: 'ContentStatement' }),
expect(AST.helpers.helperExpression({ type: 'ContentStatement' })).toBe(
false
);
equals(
AST.helpers.helperExpression({ type: 'CommentStatement' }),
expect(AST.helpers.helperExpression({ type: 'CommentStatement' })).toBe(
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' }),
expect(AST.helpers.helperExpression({ type: 'PathExpression' })).toBe(
false
);
equals(AST.helpers.helperExpression({ type: 'NullLiteral' }), false);
equals(AST.helpers.helperExpression({ type: 'Hash' }), false);
equals(AST.helpers.helperExpression({ type: 'HashPair' }), false);
expect(AST.helpers.helperExpression({ type: 'StringLiteral' })).toBe(
false
);
expect(AST.helpers.helperExpression({ type: 'NumberLiteral' })).toBe(
false
);
expect(AST.helpers.helperExpression({ type: 'BooleanLiteral' })).toBe(
false
);
expect(AST.helpers.helperExpression({ type: 'UndefinedLiteral' })).toBe(
false
);
expect(AST.helpers.helperExpression({ type: 'NullLiteral' })).toBe(
false
);
expect(AST.helpers.helperExpression({ type: 'Hash' })).toBe(false);
expect(AST.helpers.helperExpression({ type: 'HashPair' })).toBe(false);
});
});
});
describe('Line Numbers', function() {
describe('Line Numbers', function () {
var ast, body;
function testColumns(node, firstLine, lastLine, firstColumn, lastColumn) {
equals(node.loc.start.line, firstLine);
equals(node.loc.start.column, firstColumn);
equals(node.loc.end.line, lastLine);
equals(node.loc.end.column, lastColumn);
expect(node.loc.start.line).toBe(firstLine);
expect(node.loc.start.column).toBe(firstColumn);
expect(node.loc.end.line).toBe(lastLine);
expect(node.loc.end.column).toBe(lastColumn);
}
/* 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
' 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;
it('gets ContentNode line numbers', function() {
it('gets ContentNode line numbers', function () {
var contentNode = body[0];
testColumns(contentNode, 1, 1, 0, 7);
});
it('gets MustacheStatement line numbers', function() {
it('gets MustacheStatement line numbers', function () {
var mustacheNode = body[1];
testColumns(mustacheNode, 1, 1, 7, 21);
});
it('gets line numbers correct when newlines appear', function() {
it('gets line numbers correct when newlines appear', function () {
testColumns(body[2], 1, 2, 21, 8);
});
it('gets MustacheStatement line numbers correct across newlines', 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() {
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() {
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);
});
it('correctly records the line numbers of an inverse of a block helper', function() {
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);
});
it('correctly records the line number of chained inverses', function() {
it('correctly records the line number of chained inverses', function () {
var chainInverseNode = body[7];
testColumns(chainInverseNode.program, 8, 9, 9, 0);
@@ -181,251 +177,4 @@ describe('ast', function() {
testColumns(chainInverseNode.inverse.body[0].inverse, 10, 11, 8, 0);
});
});
describe('whitespace control', function() {
describe('parse', function() {
it('mustache', function() {
var ast = Handlebars.parse(' {{~comment~}} ');
equals(ast.body[0].value, '');
equals(ast.body[2].value, '');
});
it('block statements', function() {
var ast = Handlebars.parse(' {{# comment~}} \nfoo\n {{~/comment}}');
equals(ast.body[0].value, '');
equals(ast.body[1].program.body[0].value, 'foo');
});
});
describe('parseWithoutProcessing', function() {
it('mustache', function() {
var ast = Handlebars.parseWithoutProcessing(' {{~comment~}} ');
equals(ast.body[0].value, ' ');
equals(ast.body[2].value, ' ');
});
it('block statements', function() {
var ast = Handlebars.parseWithoutProcessing(
' {{# comment~}} \nfoo\n {{~/comment}}'
);
equals(ast.body[0].value, ' ');
equals(ast.body[1].program.body[0].value, ' \nfoo\n ');
});
});
});
describe('standalone flags', function() {
describe('mustache', function() {
it('does not mark mustaches as standalone', function() {
var ast = Handlebars.parse(' {{comment}} ');
equals(!!ast.body[0].value, true);
equals(!!ast.body[2].value, true);
});
});
describe('blocks - parseWithoutProcessing', function() {
it('block mustaches', function() {
var ast = Handlebars.parseWithoutProcessing(
' {{# comment}} \nfoo\n {{else}} \n bar \n {{/comment}} '
),
block = ast.body[1];
equals(ast.body[0].value, ' ');
equals(block.program.body[0].value, ' \nfoo\n ');
equals(block.inverse.body[0].value, ' \n bar \n ');
equals(ast.body[2].value, ' ');
});
it('initial block mustaches', function() {
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}}'
),
block = ast.body[0];
equals(block.program.body[0].value, ' \n');
equals(block.program.body[1].path.original, 'foo');
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}}'
),
body = ast.body[0].program.body,
block = body[1];
equals(body[0].value, ' \n');
equals(block.program.body[0].value, ' \nfoo\n ');
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];
equals(ast.body[0].omit, undefined);
equals(block.program.body[0].value, ' \nfoo\n ');
equals(block.inverse.body[0].value, ' \n bar \n ');
equals(ast.body[2].value, ' ');
});
});
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];
equals(ast.body[0].value, '');
equals(block.program.body[0].value, 'foo\n');
equals(block.inverse.body[0].value, ' bar \n');
equals(ast.body[2].value, '');
});
it('marks initial block mustaches as standalone', function() {
var ast = Handlebars.parse('{{# comment}} \nfoo\n {{/comment}}'),
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];
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];
equals(body[0].value, '');
equals(block.program.body[0].value, 'foo\n');
equals(block.inverse.body[0].value, ' bar \n');
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];
equals(body[0].omit, undefined);
equals(block.program.body[0].value, ' \nfoo\n');
equals(block.inverse.body[0].value, ' bar \n ');
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];
equals(block.program.body[0].value, ' \nfoo\n');
equals(block.inverse.body[0].value, ' bar \n ');
equals(body[0].omit, undefined);
});
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];
equals(ast.body[0].omit, undefined);
equals(block.program.body[0].value, 'foo\n');
equals(block.inverse.body[0].value, ' bar \n');
equals(ast.body[2].value, '');
});
});
describe('partials - parseWithoutProcessing', function() {
it('simple partial', function() {
var ast = Handlebars.parseWithoutProcessing('{{> partial }} ');
equals(ast.body[1].value, ' ');
});
it('indented partial', function() {
var ast = Handlebars.parseWithoutProcessing(' {{> partial }} ');
equals(ast.body[0].value, ' ');
equals(ast.body[1].indent, '');
equals(ast.body[2].value, ' ');
});
});
describe('partials', function() {
it('marks partial as standalone', function() {
var ast = Handlebars.parse('{{> partial }} ');
equals(ast.body[1].value, '');
});
it('marks indented partial as standalone', function() {
var ast = Handlebars.parse(' {{> partial }} ');
equals(ast.body[0].value, '');
equals(ast.body[1].indent, ' ');
equals(ast.body[2].value, '');
});
it('marks those around content as not standalone', function() {
var ast = Handlebars.parse('a{{> partial }}');
equals(ast.body[0].omit, undefined);
ast = Handlebars.parse('{{> partial }}a');
equals(ast.body[1].omit, undefined);
});
});
describe('comments - parseWithoutProcessing', function() {
it('simple comment', function() {
var ast = Handlebars.parseWithoutProcessing('{{! comment }} ');
equals(ast.body[1].value, ' ');
});
it('indented comment', function() {
var ast = Handlebars.parseWithoutProcessing(' {{! comment }} ');
equals(ast.body[0].value, ' ');
equals(ast.body[2].value, ' ');
});
});
describe('comments', function() {
it('marks comment as standalone', function() {
var ast = Handlebars.parse('{{! comment }} ');
equals(ast.body[1].value, '');
});
it('marks indented comment as standalone', function() {
var ast = Handlebars.parse(' {{! comment }} ');
equals(ast.body[0].value, '');
equals(ast.body[2].value, '');
});
it('marks those around content as not standalone', function() {
var ast = Handlebars.parse('a{{! comment }}');
equals(ast.body[0].omit, undefined);
ast = Handlebars.parse('{{! comment }}a');
equals(ast.body[1].omit, undefined);
});
});
});
});
+108 -129
View File
@@ -1,17 +1,9 @@
global.handlebarsEnv = null;
beforeEach(function() {
global.handlebarsEnv = Handlebars.create();
});
describe('basic context', function() {
it('most basic', function() {
expectTemplate('{{foo}}')
.withInput({ foo: 'foo' })
.toCompileTo('foo');
describe('basic context', function () {
it('most basic', function () {
expectTemplate('{{foo}}').withInput({ foo: 'foo' }).toCompileTo('foo');
});
it('escaping', function() {
it('escaping', function () {
expectTemplate('\\{{foo}}')
.withInput({ foo: 'food' })
.toCompileTo('{{foo}}');
@@ -33,23 +25,21 @@ describe('basic context', function() {
.toCompileTo('\\\\ food');
});
it('compiling with a basic context', function() {
it('compiling with a basic context', function () {
expectTemplate('Goodbye\n{{cruel}}\n{{world}}!')
.withInput({
cruel: 'cruel',
world: 'world'
world: 'world',
})
.withMessage('It works if all the required keys are provided')
.toCompileTo('Goodbye\ncruel\nworld!');
});
it('compiling with a string context', function() {
expectTemplate('{{.}}{{length}}')
.withInput('bye')
.toCompileTo('bye3');
it('compiling with a string context', function () {
expectTemplate('{{.}}{{length}}').withInput('bye').toCompileTo('bye3');
});
it('compiling with an undefined context', function() {
it('compiling with an undefined context', function () {
expectTemplate('Goodbye\n{{cruel}}\n{{world.bar}}!')
.withInput(undefined)
.toCompileTo('Goodbye\n\n!');
@@ -59,11 +49,11 @@ describe('basic context', function() {
.toCompileTo('Goodbye');
});
it('comments', function() {
it('comments', function () {
expectTemplate('{{! Goodbye}}Goodbye\n{{cruel}}\n{{world}}!')
.withInput({
cruel: 'cruel',
world: 'world'
world: 'world',
})
.withMessage('comments are ignored')
.toCompileTo('Goodbye\ncruel\nworld!');
@@ -87,12 +77,12 @@ describe('basic context', function() {
);
});
it('boolean', function() {
it('boolean', function () {
var string = '{{#goodbye}}GOODBYE {{/goodbye}}cruel {{world}}!';
expectTemplate(string)
.withInput({
goodbye: true,
world: 'world'
world: 'world',
})
.withMessage('booleans show the contents when true')
.toCompileTo('GOODBYE cruel world!');
@@ -100,41 +90,37 @@ describe('basic context', function() {
expectTemplate(string)
.withInput({
goodbye: false,
world: 'world'
world: 'world',
})
.withMessage('booleans do not show the contents when false')
.toCompileTo('cruel world!');
});
it('zeros', function() {
it('zeros', function () {
expectTemplate('num1: {{num1}}, num2: {{num2}}')
.withInput({
num1: 42,
num2: 0
num2: 0,
})
.toCompileTo('num1: 42, num2: 0');
expectTemplate('num: {{.}}')
.withInput(0)
.toCompileTo('num: 0');
expectTemplate('num: {{.}}').withInput(0).toCompileTo('num: 0');
expectTemplate('num: {{num1/num2}}')
.withInput({ num1: { num2: 0 } })
.toCompileTo('num: 0');
});
it('false', function() {
it('false', function () {
/* eslint-disable no-new-wrappers */
expectTemplate('val1: {{val1}}, val2: {{val2}}')
.withInput({
val1: false,
val2: new Boolean(false)
val2: new Boolean(false),
})
.toCompileTo('val1: false, val2: false');
expectTemplate('val: {{.}}')
.withInput(false)
.toCompileTo('val: false');
expectTemplate('val: {{.}}').withInput(false).toCompileTo('val: false');
expectTemplate('val: {{val1/val2}}')
.withInput({ val1: { val2: false } })
@@ -143,7 +129,7 @@ describe('basic context', function() {
expectTemplate('val1: {{{val1}}}, val2: {{{val2}}}')
.withInput({
val1: false,
val2: new Boolean(false)
val2: new Boolean(false),
})
.toCompileTo('val1: false, val2: false');
@@ -153,10 +139,10 @@ describe('basic context', function() {
/* eslint-enable */
});
it('should handle undefined and null', function() {
it('should handle undefined and null', function () {
expectTemplate('{{awesome undefined null}}')
.withInput({
awesome: function(_undefined, _null, options) {
awesome: function (_undefined, _null, options) {
return (
(_undefined === undefined) +
' ' +
@@ -164,34 +150,34 @@ describe('basic context', function() {
' ' +
typeof options
);
}
},
})
.toCompileTo('true true object');
expectTemplate('{{undefined}}')
.withInput({
undefined: function() {
undefined: function () {
return 'undefined!';
}
},
})
.toCompileTo('undefined!');
expectTemplate('{{null}}')
.withInput({
null: function() {
null: function () {
return 'null!';
}
},
})
.toCompileTo('null!');
});
it('newlines', function() {
it('newlines', function () {
expectTemplate("Alan's\nTest").toCompileTo("Alan's\nTest");
expectTemplate("Alan's\rTest").toCompileTo("Alan's\rTest");
});
it('escaping text', function() {
it('escaping text', function () {
expectTemplate("Awesome's")
.withMessage(
"text is escaped so that it doesn't get caught on single quotes"
@@ -216,7 +202,7 @@ describe('basic context', function() {
.toCompileTo(" ' ' ");
});
it('escaping expressions', function() {
it('escaping expressions', function () {
expectTemplate('{{{awesome}}}')
.withInput({ awesome: "&'\\<>" })
.withMessage("expressions with 3 handlebars aren't escaped")
@@ -238,140 +224,140 @@ describe('basic context', function() {
.toCompileTo('Escaped, &lt;b&gt; looks like: &amp;lt;b&amp;gt;');
});
it("functions returning safestrings shouldn't be escaped", function() {
it("functions returning safestrings shouldn't be escaped", function () {
expectTemplate('{{awesome}}')
.withInput({
awesome: function() {
awesome: function () {
return new Handlebars.SafeString("&'\\<>");
}
},
})
.withMessage("functions returning safestrings aren't escaped")
.toCompileTo("&'\\<>");
});
it('functions', function() {
it('functions', function () {
expectTemplate('{{awesome}}')
.withInput({
awesome: function() {
awesome: function () {
return 'Awesome';
}
},
})
.withMessage('functions are called and render their output')
.toCompileTo('Awesome');
expectTemplate('{{awesome}}')
.withInput({
awesome: function() {
awesome: function () {
return this.more;
},
more: 'More awesome'
more: 'More awesome',
})
.withMessage('functions are bound to the context')
.toCompileTo('More awesome');
});
it('functions with context argument', function() {
it('functions with context argument', function () {
expectTemplate('{{awesome frank}}')
.withInput({
awesome: function(context) {
awesome: function (context) {
return context;
},
frank: 'Frank'
frank: 'Frank',
})
.withMessage('functions are called with context arguments')
.toCompileTo('Frank');
});
it('pathed functions with context argument', function() {
it('pathed functions with context argument', function () {
expectTemplate('{{bar.awesome frank}}')
.withInput({
bar: {
awesome: function(context) {
awesome: function (context) {
return context;
}
},
},
frank: 'Frank'
frank: 'Frank',
})
.withMessage('functions are called with context arguments')
.toCompileTo('Frank');
});
it('depthed functions with context argument', function() {
it('depthed functions with context argument', function () {
expectTemplate('{{#with frank}}{{../awesome .}}{{/with}}')
.withInput({
awesome: function(context) {
awesome: function (context) {
return context;
},
frank: 'Frank'
frank: 'Frank',
})
.withMessage('functions are called with context arguments')
.toCompileTo('Frank');
});
it('block functions with context argument', function() {
it('block functions with context argument', function () {
expectTemplate('{{#awesome 1}}inner {{.}}{{/awesome}}')
.withInput({
awesome: function(context, options) {
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() {
it('depthed block functions with context argument', function () {
expectTemplate(
'{{#with value}}{{#../awesome 1}}inner {{.}}{{/../awesome}}{{/with}}'
)
.withInput({
value: true,
awesome: function(context, options) {
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() {
it('block functions without context argument', function () {
expectTemplate('{{#awesome}}inner{{/awesome}}')
.withInput({
awesome: function(options) {
awesome: function (options) {
return options.fn(this);
}
},
})
.withMessage('block functions are called with options')
.toCompileTo('inner');
});
it('pathed block functions without context argument', function() {
it('pathed block functions without context argument', function () {
expectTemplate('{{#foo.awesome}}inner{{/foo.awesome}}')
.withInput({
foo: {
awesome: function() {
awesome: function () {
return this;
}
}
},
},
})
.withMessage('block functions are called with options')
.toCompileTo('inner');
});
it('depthed block functions without context argument', function() {
it('depthed block functions without context argument', function () {
expectTemplate(
'{{#with value}}{{#../awesome}}inner{{/../awesome}}{{/with}}'
)
.withInput({
value: true,
awesome: function() {
awesome: function () {
return this;
}
},
})
.withMessage('block functions are called with options')
.toCompileTo('inner');
});
it('paths with hyphens', function() {
it('paths with hyphens', function () {
expectTemplate('{{foo-bar}}')
.withInput({ 'foo-bar': 'baz' })
.withMessage('Paths can contain hyphens (-)')
@@ -388,21 +374,28 @@ describe('basic context', function() {
.toCompileTo('baz');
});
it('nested paths', function() {
it('nested paths', function () {
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() {
it('nested paths with Map', function () {
expectTemplate('Goodbye {{alan/expression}} world!')
.withInput({ alan: new Map([['expression', 'beautiful']]) })
.withMessage('Nested paths access nested objects')
.toCompileTo('Goodbye beautiful world!');
});
it('nested paths with empty string value', function () {
expectTemplate('Goodbye {{alan/expression}} world!')
.withInput({ alan: { expression: '' } })
.withMessage('Nested paths access nested objects with empty string')
.toCompileTo('Goodbye world!');
});
it('literal paths', function() {
it('literal paths', function () {
expectTemplate('Goodbye {{[@alan]/expression}} world!')
.withInput({ '@alan': { expression: 'beautiful' } })
.withMessage('Literal paths can be used')
@@ -414,7 +407,7 @@ describe('basic context', function() {
.toCompileTo('Goodbye beautiful world!');
});
it('literal references', function() {
it('literal references', function () {
expectTemplate('Goodbye {{[foo bar]}} world!')
.withInput({ 'foo bar': 'beautiful' })
.toCompileTo('Goodbye beautiful world!');
@@ -440,24 +433,22 @@ describe('basic context', function() {
.toCompileTo('Goodbye beautiful world!');
});
it("that current context path ({{.}}) doesn't hit helpers", function() {
it("that current context path ({{.}}) doesn't hit helpers", function () {
expectTemplate('test: {{.}}')
.withInput(null)
.withHelpers({ helper: 'awesome' })
.toCompileTo('test: ');
});
it('complex but empty paths', function() {
it('complex but empty paths', function () {
expectTemplate('{{person/name}}')
.withInput({ person: { name: null } })
.toCompileTo('');
expectTemplate('{{person/name}}')
.withInput({ person: {} })
.toCompileTo('');
expectTemplate('{{person/name}}').withInput({ person: {} }).toCompileTo('');
});
it('this keyword in paths', function() {
it('this keyword in paths', function () {
expectTemplate('{{#goodbyes}}{{this}}{{/goodbyes}}')
.withInput({ goodbyes: ['goodbye', 'Goodbye', 'GOODBYE'] })
.withMessage('This keyword in paths evaluates to current context')
@@ -465,32 +456,30 @@ describe('basic context', function() {
expectTemplate('{{#hellos}}{{this/text}}{{/hellos}}')
.withInput({
hellos: [{ text: 'hello' }, { text: 'Hello' }, { text: 'HELLO' }]
hellos: [{ text: 'hello' }, { text: 'Hello' }, { text: 'HELLO' }],
})
.withMessage('This keyword evaluates in more complex paths')
.toCompileTo('helloHelloHELLO');
});
it('this keyword nested inside path', function() {
it('this keyword nested inside path', function () {
expectTemplate('{{#hellos}}{{text/this/foo}}{{/hellos}}').toThrow(
Error,
'Invalid path: text/this - 1:13'
);
expectTemplate('{{[this]}}')
.withInput({ this: 'bar' })
.toCompileTo('bar');
expectTemplate('{{[this]}}').withInput({ this: 'bar' }).toCompileTo('bar');
expectTemplate('{{text/[this]}}')
.withInput({ text: { this: 'bar' } })
.toCompileTo('bar');
});
it('this keyword in helpers', function() {
it('this keyword in helpers', function () {
var helpers = {
foo: function(value) {
foo: function (value) {
return 'bar ' + value;
}
},
};
expectTemplate('{{#goodbyes}}{{foo this}}{{/goodbyes}}')
@@ -501,14 +490,14 @@ describe('basic context', function() {
expectTemplate('{{#hellos}}{{foo this/text}}{{/hellos}}')
.withInput({
hellos: [{ text: 'hello' }, { text: 'Hello' }, { text: 'HELLO' }]
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() {
it('this keyword nested inside helpers param', function () {
expectTemplate('{{#hellos}}{{foo text/this/foo}}{{/hellos}}').toThrow(
Error,
'Invalid path: text/this - 1:17'
@@ -516,79 +505,69 @@ describe('basic context', function() {
expectTemplate('{{foo [this]}}')
.withInput({
foo: function(value) {
foo: function (value) {
return value;
},
this: 'bar'
this: 'bar',
})
.toCompileTo('bar');
expectTemplate('{{foo text/[this]}}')
.withInput({
foo: function(value) {
foo: function (value) {
return value;
},
text: { this: 'bar' }
text: { this: 'bar' },
})
.toCompileTo('bar');
});
it('pass string literals', function() {
it('pass string literals', function () {
expectTemplate('{{"foo"}}').toCompileTo('');
expectTemplate('{{"foo"}}')
.withInput({ foo: 'bar' })
.toCompileTo('bar');
expectTemplate('{{"foo"}}').withInput({ foo: 'bar' }).toCompileTo('bar');
expectTemplate('{{#"foo"}}{{.}}{{/"foo"}}')
.withInput({
foo: ['bar', 'baz']
foo: ['bar', 'baz'],
})
.toCompileTo('barbaz');
});
it('pass number literals', function() {
it('pass number literals', function () {
expectTemplate('{{12}}').toCompileTo('');
expectTemplate('{{12}}')
.withInput({ '12': 'bar' })
.toCompileTo('bar');
expectTemplate('{{12}}').withInput({ 12: 'bar' }).toCompileTo('bar');
expectTemplate('{{12.34}}').toCompileTo('');
expectTemplate('{{12.34}}')
.withInput({ '12.34': 'bar' })
.toCompileTo('bar');
expectTemplate('{{12.34}}').withInput({ 12.34: 'bar' }).toCompileTo('bar');
expectTemplate('{{12.34 1}}')
.withInput({
'12.34': function(arg) {
12.34: function (arg) {
return 'bar' + arg;
}
},
})
.toCompileTo('bar1');
});
it('pass boolean literals', function() {
it('pass boolean literals', function () {
expectTemplate('{{true}}').toCompileTo('');
expectTemplate('{{true}}')
.withInput({ '': 'foo' })
.toCompileTo('');
expectTemplate('{{true}}').withInput({ '': 'foo' }).toCompileTo('');
expectTemplate('{{false}}')
.withInput({ false: 'foo' })
.toCompileTo('foo');
expectTemplate('{{false}}').withInput({ false: 'foo' }).toCompileTo('foo');
});
it('should handle literals in subexpression', function() {
it('should handle literals in subexpression', function () {
expectTemplate('{{foo (false)}}')
.withInput({
false: function() {
false: function () {
return 'bar';
}
},
})
.withHelper('foo', function(arg) {
.withHelper('foo', function (arg) {
return arg;
})
.toCompileTo('bar');
+102 -106
View File
@@ -1,5 +1,5 @@
describe('blocks', function() {
it('array', function() {
describe('blocks', function () {
it('array', function () {
var string = '{{#goodbyes}}{{text}}! {{/goodbyes}}cruel {{world}}!';
expectTemplate(string)
@@ -7,9 +7,9 @@ describe('blocks', function() {
goodbyes: [
{ text: 'goodbye' },
{ text: 'Goodbye' },
{ text: 'GOODBYE' }
{ text: 'GOODBYE' },
],
world: 'world'
world: 'world',
})
.withMessage('Arrays iterate over the contents when not empty')
.toCompileTo('goodbye! Goodbye! GOODBYE! cruel world!');
@@ -17,13 +17,13 @@ describe('blocks', function() {
expectTemplate(string)
.withInput({
goodbyes: [],
world: 'world'
world: 'world',
})
.withMessage('Arrays ignore the contents when empty')
.toCompileTo('cruel world!');
});
it('array without data', function() {
it('array without data', function () {
expectTemplate(
'{{#goodbyes}}{{text}}{{/goodbyes}} {{#goodbyes}}{{text}}{{/goodbyes}}'
)
@@ -31,15 +31,15 @@ describe('blocks', function() {
goodbyes: [
{ text: 'goodbye' },
{ text: 'Goodbye' },
{ text: 'GOODBYE' }
{ text: 'GOODBYE' },
],
world: 'world'
world: 'world',
})
.withCompileOptions({ compat: false })
.toCompileTo('goodbyeGoodbyeGOODBYE goodbyeGoodbyeGOODBYE');
});
it('array with @index', function() {
it('array with @index', function () {
expectTemplate(
'{{#goodbyes}}{{@index}}. {{text}}! {{/goodbyes}}cruel {{world}}!'
)
@@ -47,15 +47,15 @@ describe('blocks', function() {
goodbyes: [
{ text: 'goodbye' },
{ text: 'Goodbye' },
{ text: 'GOODBYE' }
{ text: 'GOODBYE' },
],
world: 'world'
world: 'world',
})
.withMessage('The @index variable is used')
.toCompileTo('0. goodbye! 1. Goodbye! 2. GOODBYE! cruel world!');
});
it('empty block', function() {
it('empty block', function () {
var string = '{{#goodbyes}}{{/goodbyes}}cruel {{world}}!';
expectTemplate(string)
@@ -63,9 +63,9 @@ describe('blocks', function() {
goodbyes: [
{ text: 'goodbye' },
{ text: 'Goodbye' },
{ text: 'GOODBYE' }
{ text: 'GOODBYE' },
],
world: 'world'
world: 'world',
})
.withMessage('Arrays iterate over the contents when not empty')
.toCompileTo('cruel world!');
@@ -73,21 +73,21 @@ describe('blocks', function() {
expectTemplate(string)
.withInput({
goodbyes: [],
world: 'world'
world: 'world',
})
.withMessage('Arrays ignore the contents when empty')
.toCompileTo('cruel world!');
});
it('block with complex lookup', function() {
it('block with complex lookup', function () {
expectTemplate('{{#goodbyes}}{{text}} cruel {{../name}}! {{/goodbyes}}')
.withInput({
name: 'Alan',
goodbyes: [
{ text: 'goodbye' },
{ text: 'Goodbye' },
{ text: 'GOODBYE' }
]
{ text: 'GOODBYE' },
],
})
.withMessage(
'Templates can access variables in contexts up the stack with relative path syntax'
@@ -97,37 +97,37 @@ describe('blocks', function() {
);
});
it('multiple blocks with complex lookup', function() {
it('multiple blocks with complex lookup', function () {
expectTemplate('{{#goodbyes}}{{../name}}{{../name}}{{/goodbyes}}')
.withInput({
name: 'Alan',
goodbyes: [
{ text: 'goodbye' },
{ text: 'Goodbye' },
{ text: 'GOODBYE' }
]
{ text: 'GOODBYE' },
],
})
.toCompileTo('AlanAlanAlanAlanAlanAlan');
});
it('block with complex lookup using nested context', function() {
it('block with complex lookup using nested context', function () {
expectTemplate(
'{{#goodbyes}}{{text}} cruel {{foo/../name}}! {{/goodbyes}}'
).toThrow(Error);
});
it('block with deep nested complex lookup', function() {
it('block with deep nested complex lookup', function () {
expectTemplate(
'{{#outer}}Goodbye {{#inner}}cruel {{../sibling}} {{../../omg}}{{/inner}}{{/outer}}'
)
.withInput({
omg: 'OMG!',
outer: [{ sibling: 'sad', inner: [{ text: 'goodbye' }] }]
outer: [{ sibling: 'sad', inner: [{ text: 'goodbye' }] }],
})
.toCompileTo('Goodbye cruel sad OMG!');
});
it('works with cached blocks', function() {
it('works with cached blocks', function () {
expectTemplate(
'{{#each person}}{{#with .}}{{first}} {{last}}{{/with}}{{/each}}'
)
@@ -135,14 +135,14 @@ describe('blocks', function() {
.withInput({
person: [
{ first: 'Alan', last: 'Johnson' },
{ first: 'Alan', last: 'Johnson' }
]
{ first: 'Alan', last: 'Johnson' },
],
})
.toCompileTo('Alan JohnsonAlan Johnson');
});
describe('inverted sections', function() {
it('inverted sections with unset value', function() {
describe('inverted sections', function () {
it('inverted sections with unset value', function () {
expectTemplate(
'{{#goodbyes}}{{this}}{{/goodbyes}}{{^goodbyes}}Right On!{{/goodbyes}}'
)
@@ -150,7 +150,7 @@ describe('blocks', function() {
.toCompileTo('Right On!');
});
it('inverted section with false value', function() {
it('inverted section with false value', function () {
expectTemplate(
'{{#goodbyes}}{{this}}{{/goodbyes}}{{^goodbyes}}Right On!{{/goodbyes}}'
)
@@ -159,7 +159,7 @@ describe('blocks', function() {
.toCompileTo('Right On!');
});
it('inverted section with empty set', function() {
it('inverted section with empty set', function () {
expectTemplate(
'{{#goodbyes}}{{this}}{{/goodbyes}}{{^goodbyes}}Right On!{{/goodbyes}}'
)
@@ -168,13 +168,13 @@ describe('blocks', function() {
.toCompileTo('Right On!');
});
it('block inverted sections', function() {
it('block inverted sections', function () {
expectTemplate('{{#people}}{{name}}{{^}}{{none}}{{/people}}')
.withInput({ none: 'No people' })
.toCompileTo('No people');
});
it('chained inverted sections', function() {
it('chained inverted sections', function () {
expectTemplate('{{#people}}{{name}}{{else if none}}{{none}}{{/people}}')
.withInput({ none: 'No people' })
.toCompileTo('No people');
@@ -192,24 +192,24 @@ describe('blocks', function() {
.toCompileTo('No people');
});
it('chained inverted sections with mismatch', function() {
it('chained inverted sections with mismatch', function () {
expectTemplate(
'{{#people}}{{name}}{{else if none}}{{none}}{{/if}}'
).toThrow(Error);
});
it('block inverted sections with empty arrays', function() {
it('block inverted sections with empty arrays', function () {
expectTemplate('{{#people}}{{name}}{{^}}{{none}}{{/people}}')
.withInput({
none: 'No people',
people: []
people: [],
})
.toCompileTo('No people');
});
});
describe('standalone sections', function() {
it('block standalone else sections', function() {
describe('standalone sections', function () {
it('block standalone else sections', function () {
expectTemplate('{{#people}}\n{{name}}\n{{^}}\n{{none}}\n{{/people}}\n')
.withInput({ none: 'No people' })
.toCompileTo('No people\n');
@@ -223,7 +223,7 @@ describe('blocks', function() {
.toCompileTo('No people\n');
});
it('block standalone else sections can be disabled', function() {
it('block standalone else sections can be disabled', function () {
expectTemplate('{{#people}}\n{{name}}\n{{^}}\n{{none}}\n{{/people}}\n')
.withInput({ none: 'No people' })
.withCompileOptions({ ignoreStandalone: true })
@@ -235,7 +235,7 @@ describe('blocks', function() {
.toCompileTo('\nNo people\n\n');
});
it('block standalone chained else sections', function() {
it('block standalone chained else sections', function () {
expectTemplate(
'{{#people}}\n{{name}}\n{{else if none}}\n{{none}}\n{{/people}}\n'
)
@@ -249,17 +249,17 @@ describe('blocks', function() {
.toCompileTo('No people\n');
});
it('should handle nesting', function() {
it('should handle nesting', function () {
expectTemplate('{{#data}}\n{{#if true}}\n{{.}}\n{{/if}}\n{{/data}}\nOK.')
.withInput({
data: [1, 3, 5]
data: [1, 3, 5],
})
.toCompileTo('1\n3\n5\nOK.');
});
});
describe('compat mode', function() {
it('block with deep recursive lookup lookup', function() {
describe('compat mode', function () {
it('block with deep recursive lookup lookup', function () {
expectTemplate(
'{{#outer}}Goodbye {{#inner}}cruel {{omg}}{{/inner}}{{/outer}}'
)
@@ -268,108 +268,108 @@ describe('blocks', function() {
.toCompileTo('Goodbye cruel OMG!');
});
it('block with deep recursive pathed lookup', function() {
it('block with deep recursive pathed lookup', function () {
expectTemplate(
'{{#outer}}Goodbye {{#inner}}cruel {{omg.yes}}{{/inner}}{{/outer}}'
)
.withInput({
omg: { yes: 'OMG!' },
outer: [{ inner: [{ yes: 'no', text: 'goodbye' }] }]
outer: [{ inner: [{ yes: 'no', text: 'goodbye' }] }],
})
.withCompileOptions({ compat: true })
.toCompileTo('Goodbye cruel OMG!');
});
it('block with missed recursive lookup', function() {
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' }] }]
outer: [{ inner: [{ yes: 'no', text: 'goodbye' }] }],
})
.withCompileOptions({ compat: true })
.toCompileTo('Goodbye cruel ');
});
});
describe('decorators', function() {
it('should apply mustache decorators', function() {
describe('decorators', function () {
it('should apply mustache decorators', function () {
expectTemplate('{{#helper}}{{*decorator}}{{/helper}}')
.withHelper('helper', function(options) {
.withHelper('helper', function (options) {
return options.fn.run;
})
.withDecorator('decorator', function(fn) {
.withDecorator('decorator', function (fn) {
fn.run = 'success';
return fn;
})
.toCompileTo('success');
});
it('should apply allow undefined return', function() {
it('should apply allow undefined return', function () {
expectTemplate('{{#helper}}{{*decorator}}suc{{/helper}}')
.withHelper('helper', function(options) {
.withHelper('helper', function (options) {
return options.fn() + options.fn.run;
})
.withDecorator('decorator', function(fn) {
.withDecorator('decorator', function (fn) {
fn.run = 'cess';
})
.toCompileTo('success');
});
it('should apply block decorators', function() {
it('should apply block decorators', function () {
expectTemplate(
'{{#helper}}{{#*decorator}}success{{/decorator}}{{/helper}}'
)
.withHelper('helper', function(options) {
.withHelper('helper', function (options) {
return options.fn.run;
})
.withDecorator('decorator', function(fn, props, container, options) {
.withDecorator('decorator', function (fn, props, container, options) {
fn.run = options.fn();
return fn;
})
.toCompileTo('success');
});
it('should support nested decorators', function() {
it('should support nested decorators', function () {
expectTemplate(
'{{#helper}}{{#*decorator}}{{#*nested}}suc{{/nested}}cess{{/decorator}}{{/helper}}'
)
.withHelper('helper', function(options) {
.withHelper('helper', function (options) {
return options.fn.run;
})
.withDecorators({
decorator: function(fn, props, container, options) {
decorator: function (fn, props, container, options) {
fn.run = options.fn.nested + options.fn();
return fn;
},
nested: function(fn, props, container, options) {
nested: function (fn, props, container, options) {
props.nested = options.fn();
}
},
})
.toCompileTo('success');
});
it('should apply multiple decorators', function() {
it('should apply multiple decorators', function () {
expectTemplate(
'{{#helper}}{{#*decorator}}suc{{/decorator}}{{#*decorator}}cess{{/decorator}}{{/helper}}'
)
.withHelper('helper', function(options) {
.withHelper('helper', function (options) {
return options.fn.run;
})
.withDecorator('decorator', function(fn, props, container, options) {
.withDecorator('decorator', function (fn, props, container, options) {
fn.run = (fn.run || '') + options.fn();
return fn;
})
.toCompileTo('success');
});
it('should access parent variables', function() {
it('should access parent variables', function () {
expectTemplate('{{#helper}}{{*decorator foo}}{{/helper}}')
.withHelper('helper', function(options) {
.withHelper('helper', function (options) {
return options.fn.run;
})
.withDecorator('decorator', function(fn, props, container, options) {
.withDecorator('decorator', function (fn, props, container, options) {
fn.run = options.args;
return fn;
})
@@ -377,79 +377,75 @@ describe('blocks', function() {
.toCompileTo('success');
});
it('should work with root program', function() {
it('should work with root program', function () {
var run;
expectTemplate('{{*decorator "success"}}')
.withDecorator('decorator', function(fn, props, container, options) {
equals(options.args[0], 'success');
.withDecorator('decorator', function (fn, props, container, options) {
expect(options.args[0]).toBe('success');
run = true;
return fn;
})
.withInput({ foo: 'success' })
.toCompileTo('');
equals(run, true);
expect(run).toBe(true);
});
it('should fail when accessing variables from root', function() {
it('should fail when accessing variables from root', function () {
var run;
expectTemplate('{{*decorator foo}}')
.withDecorator('decorator', function(fn, props, container, options) {
equals(options.args[0], undefined);
.withDecorator('decorator', function (fn, props, container, options) {
expect(options.args[0]).toBeUndefined();
run = true;
return fn;
})
.withInput({ foo: 'fail' })
.toCompileTo('');
equals(run, true);
expect(run).toBe(true);
});
describe('registration', function() {
it('unregisters', function() {
describe('registration', function () {
it('unregisters', function () {
handlebarsEnv.decorators = {};
handlebarsEnv.registerDecorator('foo', function() {
handlebarsEnv.registerDecorator('foo', function () {
return 'fail';
});
equals(!!handlebarsEnv.decorators.foo, true);
expect(handlebarsEnv.decorators.foo).toBeTruthy();
handlebarsEnv.unregisterDecorator('foo');
equals(handlebarsEnv.decorators.foo, undefined);
expect(handlebarsEnv.decorators.foo).toBeUndefined();
});
it('allows multiple globals', function() {
it('allows multiple globals', function () {
handlebarsEnv.decorators = {};
handlebarsEnv.registerDecorator({
foo: function() {},
bar: function() {}
foo: function () {},
bar: function () {},
});
equals(!!handlebarsEnv.decorators.foo, true);
equals(!!handlebarsEnv.decorators.bar, true);
expect(handlebarsEnv.decorators.foo).toBeTruthy();
expect(handlebarsEnv.decorators.bar).toBeTruthy();
handlebarsEnv.unregisterDecorator('foo');
handlebarsEnv.unregisterDecorator('bar');
equals(handlebarsEnv.decorators.foo, undefined);
equals(handlebarsEnv.decorators.bar, undefined);
expect(handlebarsEnv.decorators.foo).toBeUndefined();
expect(handlebarsEnv.decorators.bar).toBeUndefined();
});
it('fails with multiple and args', function() {
shouldThrow(
function() {
handlebarsEnv.registerDecorator(
{
world: function() {
return 'world!';
},
testHelper: function() {
return 'found it!';
}
it('fails with multiple and args', function () {
expect(function () {
handlebarsEnv.registerDecorator(
{
world: function () {
return 'world!';
},
{}
);
},
Error,
'Arg not supported with multiple decorators'
);
testHelper: function () {
return 'found it!';
},
},
{}
);
}).toThrow('Arg not supported with multiple decorators');
});
});
});
+205 -154
View File
@@ -1,12 +1,12 @@
describe('builtin helpers', function() {
describe('#if', function() {
it('if', function() {
describe('builtin helpers', function () {
describe('#if', function () {
it('if', function () {
var string = '{{#if goodbye}}GOODBYE {{/if}}cruel {{world}}!';
expectTemplate(string)
.withInput({
goodbye: true,
world: 'world'
world: 'world',
})
.withMessage('if with boolean argument shows the contents when true')
.toCompileTo('GOODBYE cruel world!');
@@ -14,7 +14,7 @@ describe('builtin helpers', function() {
expectTemplate(string)
.withInput({
goodbye: 'dummy',
world: 'world'
world: 'world',
})
.withMessage('if with string argument shows the contents')
.toCompileTo('GOODBYE cruel world!');
@@ -22,7 +22,7 @@ describe('builtin helpers', function() {
expectTemplate(string)
.withInput({
goodbye: false,
world: 'world'
world: 'world',
})
.withMessage(
'if with boolean argument does not show the contents when false'
@@ -37,7 +37,7 @@ describe('builtin helpers', function() {
expectTemplate(string)
.withInput({
goodbye: ['foo'],
world: 'world'
world: 'world',
})
.withMessage('if with non-empty array shows the contents')
.toCompileTo('GOODBYE cruel world!');
@@ -45,7 +45,7 @@ describe('builtin helpers', function() {
expectTemplate(string)
.withInput({
goodbye: [],
world: 'world'
world: 'world',
})
.withMessage('if with empty array does not show the contents')
.toCompileTo('cruel world!');
@@ -53,7 +53,7 @@ describe('builtin helpers', function() {
expectTemplate(string)
.withInput({
goodbye: 0,
world: 'world'
world: 'world',
})
.withMessage('if with zero does not show the contents')
.toCompileTo('cruel world!');
@@ -63,21 +63,21 @@ describe('builtin helpers', function() {
)
.withInput({
goodbye: 0,
world: 'world'
world: 'world',
})
.withMessage('if with zero does not show the contents')
.toCompileTo('GOODBYE cruel world!');
});
it('if with function argument', function() {
it('if with function argument', function () {
var string = '{{#if goodbye}}GOODBYE {{/if}}cruel {{world}}!';
expectTemplate(string)
.withInput({
goodbye: function() {
goodbye: function () {
return true;
},
world: 'world'
world: 'world',
})
.withMessage(
'if with function shows the contents when function returns true'
@@ -86,10 +86,10 @@ describe('builtin helpers', function() {
expectTemplate(string)
.withInput({
goodbye: function() {
goodbye: function () {
return this.world;
},
world: 'world'
world: 'world',
})
.withMessage(
'if with function shows the contents when function returns string'
@@ -98,10 +98,10 @@ describe('builtin helpers', function() {
expectTemplate(string)
.withInput({
goodbye: function() {
goodbye: function () {
return false;
},
world: 'world'
world: 'world',
})
.withMessage(
'if with function does not show the contents when returns false'
@@ -110,10 +110,10 @@ describe('builtin helpers', function() {
expectTemplate(string)
.withInput({
goodbye: function() {
goodbye: function () {
return this.foo;
},
world: 'world'
world: 'world',
})
.withMessage(
'if with function does not show the contents when returns undefined'
@@ -121,61 +121,61 @@ describe('builtin helpers', function() {
.toCompileTo('cruel world!');
});
it('should not change the depth list', function() {
it('should not change the depth list', function () {
expectTemplate(
'{{#with foo}}{{#if goodbye}}GOODBYE cruel {{../world}}!{{/if}}{{/with}}'
)
.withInput({
foo: { goodbye: true },
world: 'world'
world: 'world',
})
.toCompileTo('GOODBYE cruel world!');
});
});
describe('#with', function() {
it('with', function() {
describe('#with', function () {
it('with', function () {
expectTemplate('{{#with person}}{{first}} {{last}}{{/with}}')
.withInput({
person: {
first: 'Alan',
last: 'Johnson'
}
last: 'Johnson',
},
})
.toCompileTo('Alan Johnson');
});
it('with with function argument', function() {
it('with with function argument', function () {
expectTemplate('{{#with person}}{{first}} {{last}}{{/with}}')
.withInput({
person: function() {
person: function () {
return {
first: 'Alan',
last: 'Johnson'
last: 'Johnson',
};
}
},
})
.toCompileTo('Alan Johnson');
});
it('with with else', function() {
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() {
it('with provides block parameter', function () {
expectTemplate('{{#with person as |foo|}}{{foo.first}} {{last}}{{/with}}')
.withInput({
person: {
first: 'Alan',
last: 'Johnson'
}
last: 'Johnson',
},
})
.toCompileTo('Alan Johnson');
});
it('works when data is disabled', function() {
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 })
@@ -183,14 +183,14 @@ describe('builtin helpers', function() {
});
});
describe('#each', function() {
beforeEach(function() {
handlebarsEnv.registerHelper('detectDataInsideEach', function(options) {
describe('#each', function () {
beforeEach(function () {
handlebarsEnv.registerHelper('detectDataInsideEach', function (options) {
return options.data && options.data.exclaim;
});
});
it('each', function() {
it('each', function () {
var string = '{{#each goodbyes}}{{text}}! {{/each}}cruel {{world}}!';
expectTemplate(string)
@@ -198,9 +198,9 @@ describe('builtin helpers', function() {
goodbyes: [
{ text: 'goodbye' },
{ text: 'Goodbye' },
{ text: 'GOODBYE' }
{ text: 'GOODBYE' },
],
world: 'world'
world: 'world',
})
.withMessage(
'each with array argument iterates over the contents when not empty'
@@ -210,21 +210,21 @@ describe('builtin helpers', function() {
expectTemplate(string)
.withInput({
goodbyes: [],
world: 'world'
world: 'world',
})
.withMessage('each with array argument ignores the contents when empty')
.toCompileTo('cruel world!');
});
it('each without data', function() {
it('each without data', function () {
expectTemplate('{{#each goodbyes}}{{text}}! {{/each}}cruel {{world}}!')
.withInput({
goodbyes: [
{ text: 'goodbye' },
{ text: 'Goodbye' },
{ text: 'GOODBYE' }
{ text: 'GOODBYE' },
],
world: 'world'
world: 'world',
})
.withRuntimeOptions({ data: false })
.withCompileOptions({ data: false })
@@ -237,13 +237,13 @@ describe('builtin helpers', function() {
.toCompileTo('cruelworld');
});
it('each without context', function() {
it('each without context', function () {
expectTemplate('{{#each goodbyes}}{{text}}! {{/each}}cruel {{world}}!')
.withInput(undefined)
.toCompileTo('cruel !');
});
it('each with an object and @key', function() {
it('each with an object and @key', function () {
var string =
'{{#each goodbyes}}{{@key}}. {{text}}! {{/each}}cruel {{world}}!';
@@ -257,27 +257,23 @@ describe('builtin helpers', function() {
// 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 actual = CompilerContext.compile(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!';
equals(
actual === expected1 || actual === expected2,
true,
'each with object argument iterates over the contents when not empty'
);
expect([expected1, expected2]).toContain(actual);
expectTemplate(string)
.withInput({
goodbyes: {},
world: 'world'
world: 'world',
})
.toCompileTo('cruel world!');
});
it('each with @index', function() {
it('each with @index', function () {
expectTemplate(
'{{#each goodbyes}}{{@index}}. {{text}}! {{/each}}cruel {{world}}!'
)
@@ -285,15 +281,15 @@ describe('builtin helpers', function() {
goodbyes: [
{ text: 'goodbye' },
{ text: 'Goodbye' },
{ text: 'GOODBYE' }
{ text: 'GOODBYE' },
],
world: 'world'
world: 'world',
})
.withMessage('The @index variable is used')
.toCompileTo('0. goodbye! 1. Goodbye! 2. GOODBYE! cruel world!');
});
it('each with nested @index', function() {
it('each with nested @index', function () {
expectTemplate(
'{{#each goodbyes}}{{@index}}. {{text}}! {{#each ../goodbyes}}{{@index}} {{/each}}After {{@index}} {{/each}}{{@index}}cruel {{world}}!'
)
@@ -301,9 +297,9 @@ describe('builtin helpers', function() {
goodbyes: [
{ text: 'goodbye' },
{ text: 'Goodbye' },
{ text: 'GOODBYE' }
{ text: 'GOODBYE' },
],
world: 'world'
world: 'world',
})
.withMessage('The @index variable is used')
.toCompileTo(
@@ -311,20 +307,29 @@ describe('builtin helpers', function() {
);
});
it('each with block params', function() {
it('each with block params', function () {
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'
world: 'world',
})
.toCompileTo(
'0. goodbye! 0 0 0 1 After 0 1. Goodbye! 1 0 1 1 After 1 cruel world!'
);
});
it('each object with @index', function() {
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 () {
expectTemplate(
'{{#each goodbyes}}{{@index}}. {{text}}! {{/each}}cruel {{world}}!'
)
@@ -332,15 +337,15 @@ describe('builtin helpers', function() {
goodbyes: {
a: { text: 'goodbye' },
b: { text: 'Goodbye' },
c: { text: 'GOODBYE' }
c: { text: 'GOODBYE' },
},
world: 'world'
world: 'world',
})
.withMessage('The @index variable is used')
.toCompileTo('0. goodbye! 1. Goodbye! 2. GOODBYE! cruel world!');
});
it('each with @first', function() {
it('each with @first', function () {
expectTemplate(
'{{#each goodbyes}}{{#if @first}}{{text}}! {{/if}}{{/each}}cruel {{world}}!'
)
@@ -348,15 +353,15 @@ describe('builtin helpers', function() {
goodbyes: [
{ text: 'goodbye' },
{ text: 'Goodbye' },
{ text: 'GOODBYE' }
{ text: 'GOODBYE' },
],
world: 'world'
world: 'world',
})
.withMessage('The @first variable is used')
.toCompileTo('goodbye! cruel world!');
});
it('each with nested @first', function() {
it('each with nested @first', function () {
expectTemplate(
'{{#each goodbyes}}({{#if @first}}{{text}}! {{/if}}{{#each ../goodbyes}}{{#if @first}}{{text}}!{{/if}}{{/each}}{{#if @first}} {{text}}!{{/if}}) {{/each}}cruel {{world}}!'
)
@@ -364,9 +369,9 @@ describe('builtin helpers', function() {
goodbyes: [
{ text: 'goodbye' },
{ text: 'Goodbye' },
{ text: 'GOODBYE' }
{ text: 'GOODBYE' },
],
world: 'world'
world: 'world',
})
.withMessage('The @first variable is used')
.toCompileTo(
@@ -374,19 +379,19 @@ describe('builtin helpers', function() {
);
});
it('each object with @first', function() {
it('each object with @first', function () {
expectTemplate(
'{{#each goodbyes}}{{#if @first}}{{text}}! {{/if}}{{/each}}cruel {{world}}!'
)
.withInput({
goodbyes: { foo: { text: 'goodbye' }, bar: { text: 'Goodbye' } },
world: 'world'
world: 'world',
})
.withMessage('The @first variable is used')
.toCompileTo('goodbye! cruel world!');
});
it('each with @last', function() {
it('each with @last', function () {
expectTemplate(
'{{#each goodbyes}}{{#if @last}}{{text}}! {{/if}}{{/each}}cruel {{world}}!'
)
@@ -394,27 +399,27 @@ describe('builtin helpers', function() {
goodbyes: [
{ text: 'goodbye' },
{ text: 'Goodbye' },
{ text: 'GOODBYE' }
{ text: 'GOODBYE' },
],
world: 'world'
world: 'world',
})
.withMessage('The @last variable is used')
.toCompileTo('GOODBYE! cruel world!');
});
it('each object with @last', function() {
it('each object with @last', function () {
expectTemplate(
'{{#each goodbyes}}{{#if @last}}{{text}}! {{/if}}{{/each}}cruel {{world}}!'
)
.withInput({
goodbyes: { foo: { text: 'goodbye' }, bar: { text: 'Goodbye' } },
world: 'world'
world: 'world',
})
.withMessage('The @last variable is used')
.toCompileTo('Goodbye! cruel world!');
});
it('each with nested @last', function() {
it('each with nested @last', function () {
expectTemplate(
'{{#each goodbyes}}({{#if @last}}{{text}}! {{/if}}{{#each ../goodbyes}}{{#if @last}}{{text}}!{{/if}}{{/each}}{{#if @last}} {{text}}!{{/if}}) {{/each}}cruel {{world}}!'
)
@@ -422,9 +427,9 @@ describe('builtin helpers', function() {
goodbyes: [
{ text: 'goodbye' },
{ text: 'Goodbye' },
{ text: 'GOODBYE' }
{ text: 'GOODBYE' },
],
world: 'world'
world: 'world',
})
.withMessage('The @last variable is used')
.toCompileTo(
@@ -432,19 +437,19 @@ describe('builtin helpers', function() {
);
});
it('each with function argument', function() {
it('each with function argument', function () {
var string = '{{#each goodbyes}}{{text}}! {{/each}}cruel {{world}}!';
expectTemplate(string)
.withInput({
goodbyes: function() {
goodbyes: function () {
return [
{ text: 'goodbye' },
{ text: 'Goodbye' },
{ text: 'GOODBYE' }
{ text: 'GOODBYE' },
];
},
world: 'world'
world: 'world',
})
.withMessage(
'each with array function argument iterates over the contents when not empty'
@@ -454,7 +459,7 @@ describe('builtin helpers', function() {
expectTemplate(string)
.withInput({
goodbyes: [],
world: 'world'
world: 'world',
})
.withMessage(
'each with array function argument ignores the contents when empty'
@@ -462,7 +467,7 @@ describe('builtin helpers', function() {
.toCompileTo('cruel world!');
});
it('each object when last key is an empty string', function() {
it('each object when last key is an empty string', function () {
expectTemplate(
'{{#each goodbyes}}{{@index}}. {{text}}! {{/each}}cruel {{world}}!'
)
@@ -470,15 +475,15 @@ describe('builtin helpers', function() {
goodbyes: {
a: { text: 'goodbye' },
b: { text: 'Goodbye' },
'': { text: 'GOODBYE' }
'': { text: 'GOODBYE' },
},
world: 'world'
world: 'world',
})
.withMessage('Empty string key is not skipped')
.toCompileTo('0. goodbye! 1. Goodbye! 2. GOODBYE! cruel world!');
});
it('data passed to helpers', function() {
it('data passed to helpers', function () {
expectTemplate(
'{{#each letters}}{{this}}{{detectDataInsideEach}}{{/each}}'
)
@@ -486,26 +491,70 @@ describe('builtin helpers', function() {
.withMessage('should output data')
.withRuntimeOptions({
data: {
exclaim: '!'
}
exclaim: '!',
},
})
.toCompileTo('a!b!c!');
});
it('each on implicit context', function() {
it('each on implicit context', function () {
expectTemplate('{{#each}}{{text}}! {{/each}}cruel world!').toThrow(
handlebarsEnv.Exception,
'Must pass iterator to #each'
);
});
it('each on Map', function () {
var map = new Map([
[1, 'one'],
[2, 'two'],
[3, 'three'],
]);
expectTemplate('{{#each map}}{{@key}}(i{{@index}}) {{.}} {{/each}}')
.withInput({ map: map })
.toCompileTo('1(i0) one 2(i1) two 3(i2) three ');
expectTemplate('{{#each map}}{{#if @first}}{{.}}{{/if}}{{/each}}')
.withInput({ map: map })
.toCompileTo('one');
expectTemplate('{{#each map}}{{#if @last}}{{.}}{{/if}}{{/each}}')
.withInput({ map: map })
.toCompileTo('three');
expectTemplate('{{#each map}}{{.}}{{/each}}not-in-each')
.withInput({ map: new Map() })
.toCompileTo('not-in-each');
});
it('each on Set', function () {
var set = new Set([1, 2, 3]);
expectTemplate('{{#each set}}{{@key}}(i{{@index}}) {{.}} {{/each}}')
.withInput({ set: set })
.toCompileTo('0(i0) 1 1(i1) 2 2(i2) 3 ');
expectTemplate('{{#each set}}{{#if @first}}{{.}}{{/if}}{{/each}}')
.withInput({ set: set })
.toCompileTo('1');
expectTemplate('{{#each set}}{{#if @last}}{{.}}{{/if}}{{/each}}')
.withInput({ set: set })
.toCompileTo('3');
expectTemplate('{{#each set}}{{.}}{{/each}}not-in-each')
.withInput({ set: new Set() })
.toCompileTo('not-in-each');
});
if (global.Symbol && global.Symbol.iterator) {
it('each on iterable', function() {
it('each on iterable', function () {
function Iterator(arr) {
this.arr = arr;
this.index = 0;
}
Iterator.prototype.next = function() {
Iterator.prototype.next = function () {
var value = this.arr[this.index];
var done = this.index === this.arr.length;
if (!done) {
@@ -516,7 +565,7 @@ describe('builtin helpers', function() {
function Iterable(arr) {
this.arr = arr;
}
Iterable.prototype[global.Symbol.iterator] = function() {
Iterable.prototype[global.Symbol.iterator] = function () {
return new Iterator(this.arr);
};
var string = '{{#each goodbyes}}{{text}}! {{/each}}cruel {{world}}!';
@@ -526,9 +575,9 @@ describe('builtin helpers', function() {
goodbyes: new Iterable([
{ text: 'goodbye' },
{ text: 'Goodbye' },
{ text: 'GOODBYE' }
{ text: 'GOODBYE' },
]),
world: 'world'
world: 'world',
})
.withMessage(
'each with array argument iterates over the contents when not empty'
@@ -538,7 +587,7 @@ describe('builtin helpers', function() {
expectTemplate(string)
.withInput({
goodbyes: new Iterable([]),
world: 'world'
world: 'world',
})
.withMessage(
'each with array argument ignores the contents when empty'
@@ -548,27 +597,27 @@ describe('builtin helpers', function() {
}
});
describe('#log', function() {
describe('#log', function () {
/* eslint-disable no-console */
if (typeof console === 'undefined') {
return;
}
var $log, $info, $error;
beforeEach(function() {
beforeEach(function () {
$log = console.log;
$info = console.info;
$error = console.error;
});
afterEach(function() {
afterEach(function () {
console.log = $log;
console.info = $info;
console.error = $error;
});
it('should call logger at default level', function() {
it('should call logger at default level', function () {
var levelArg, logArg;
handlebarsEnv.log = function(level, arg) {
handlebarsEnv.log = function (level, arg) {
levelArg = level;
logArg = arg;
};
@@ -577,13 +626,13 @@ describe('builtin helpers', function() {
.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'");
expect(levelArg).toBe(1);
expect(logArg).toBe('whee');
});
it('should call logger at data level', function() {
it('should call logger at data level', function () {
var levelArg, logArg;
handlebarsEnv.log = function(level, arg) {
handlebarsEnv.log = function (level, arg) {
levelArg = level;
logArg = arg;
};
@@ -593,21 +642,21 @@ describe('builtin helpers', function() {
.withRuntimeOptions({ data: { level: '03' } })
.withCompileOptions({ data: true })
.toCompileTo('');
equals('03', levelArg);
equals('whee', logArg);
expect(levelArg).toBe('03');
expect(logArg).toBe('whee');
});
it('should output to info', function() {
it('should output to info', function () {
var called;
console.info = function(info) {
equals('whee', info);
console.info = function (info) {
expect(info).toBe('whee');
called = true;
console.info = $info;
console.log = $log;
};
console.log = function(log) {
equals('whee', log);
console.log = function (log) {
expect(log).toBe('whee');
called = true;
console.info = $info;
console.log = $log;
@@ -616,14 +665,14 @@ describe('builtin helpers', function() {
expectTemplate('{{log blah}}')
.withInput({ blah: 'whee' })
.toCompileTo('');
equals(true, called);
expect(called).toBe(true);
});
it('should log at data level', function() {
it('should log at data level', function () {
var called;
console.error = function(log) {
equals('whee', log);
console.error = function (log) {
expect(log).toBe('whee');
called = true;
console.error = $error;
};
@@ -633,15 +682,15 @@ describe('builtin helpers', function() {
.withRuntimeOptions({ data: { level: '03' } })
.withCompileOptions({ data: true })
.toCompileTo('');
equals(true, called);
expect(called).toBe(true);
});
it('should handle missing logger', function() {
it('should handle missing logger', function () {
var called = false;
console.error = undefined;
console.log = function(log) {
equals('whee', log);
console.log = function (log) {
expect(log).toBe('whee');
called = true;
console.log = $log;
};
@@ -651,14 +700,14 @@ describe('builtin helpers', function() {
.withRuntimeOptions({ data: { level: '03' } })
.withCompileOptions({ data: true })
.toCompileTo('');
equals(true, called);
expect(called).toBe(true);
});
it('should handle string log levels', function() {
it('should handle string log levels', function () {
var called;
console.error = function(log) {
equals('whee', log);
console.error = function (log) {
expect(log).toBe('whee');
called = true;
};
@@ -667,7 +716,7 @@ describe('builtin helpers', function() {
.withRuntimeOptions({ data: { level: 'error' } })
.withCompileOptions({ data: true })
.toCompileTo('');
equals(true, called);
expect(called).toBe(true);
called = false;
@@ -676,44 +725,48 @@ describe('builtin helpers', function() {
.withRuntimeOptions({ data: { level: 'ERROR' } })
.withCompileOptions({ data: true })
.toCompileTo('');
equals(true, called);
expect(called).toBe(true);
});
it('should handle hash log levels', function() {
it('should handle hash log levels', function () {
var called;
console.error = function(log) {
equals('whee', log);
console.error = function (log) {
expect(log).toBe('whee');
called = true;
};
expectTemplate('{{log blah level="error"}}')
.withInput({ blah: 'whee' })
.toCompileTo('');
equals(true, called);
expect(called).toBe(true);
});
it('should handle hash log levels', function() {
it('should handle hash log levels', function () {
var called = false;
console.info = console.log = console.error = console.debug = function() {
called = true;
console.info = console.log = console.error = console.debug = $log;
};
console.info =
console.log =
console.error =
console.debug =
function () {
called = true;
console.info = console.log = console.error = console.debug = $log;
};
expectTemplate('{{log blah level="debug"}}')
.withInput({ blah: 'whee' })
.toCompileTo('');
equals(false, called);
expect(called).toBe(false);
});
it('should pass multiple log arguments', function() {
it('should pass multiple log arguments', function () {
var called;
console.info = console.log = function(log1, log2, log3) {
equals('whee', log1);
equals('foo', log2);
equals(1, log3);
console.info = console.log = function (log1, log2, log3) {
expect(log1).toBe('whee');
expect(log2).toBe('foo');
expect(log3).toBe(1);
called = true;
console.log = $log;
};
@@ -721,34 +774,32 @@ describe('builtin helpers', function() {
expectTemplate('{{log blah "foo" 1}}')
.withInput({ blah: 'whee' })
.toCompileTo('');
equals(true, called);
expect(called).toBe(true);
});
it('should pass zero log arguments', function() {
it('should pass zero log arguments', function () {
var called;
console.info = console.log = function() {
expect(arguments.length).to.equal(0);
console.info = console.log = function () {
expect(arguments.length).toBe(0);
called = true;
console.log = $log;
};
expectTemplate('{{log}}')
.withInput({ blah: 'whee' })
.toCompileTo('');
expect(called).to.be.true();
expectTemplate('{{log}}').withInput({ blah: 'whee' }).toCompileTo('');
expect(called).toBe(true);
});
/* eslint-enable no-console */
});
describe('#lookup', function() {
it('should lookup arbitrary content', function() {
describe('#lookup', function () {
it('should lookup arbitrary content', function () {
expectTemplate('{{#each goodbyes}}{{lookup ../data .}}{{/each}}')
.withInput({ goodbyes: [0, 1], data: ['foo', 'bar'] })
.toCompileTo('foobar');
});
it('should not fail on undefined value', function() {
it('should not fail on undefined value', function () {
expectTemplate('{{#each goodbyes}}{{lookup ../bar .}}{{/each}}')
.withInput({ goodbyes: [0, 1], data: ['foo', 'bar'] })
.toCompileTo('');
+92 -120
View File
@@ -1,188 +1,160 @@
describe('compiler', function() {
describe('compiler', function () {
if (!Handlebars.compile) {
return;
}
describe('#equals', function() {
describe('#equals', function () {
function compile(string) {
var ast = Handlebars.parse(string);
return new Handlebars.Compiler().compile(ast, {});
}
it('should treat as equal', 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(
it('should treat as equal', function () {
expect(compile('foo').equals(compile('foo'))).toBe(true);
expect(compile('{{foo}}').equals(compile('{{foo}}'))).toBe(true);
expect(compile('{{foo.bar}}').equals(compile('{{foo.bar}}'))).toBe(true);
expect(
compile('{{foo.bar baz "foo" true false bat=1}}').equals(
compile('{{foo.bar baz "foo" true false bat=1}}')
),
true
);
equal(
)
).toBe(true);
expect(
compile('{{foo.bar (baz bat=1)}}').equals(
compile('{{foo.bar (baz bat=1)}}')
),
true
);
equal(
compile('{{#foo}} {{/foo}}').equals(compile('{{#foo}} {{/foo}}')),
true
);
)
).toBe(true);
expect(
compile('{{#foo}} {{/foo}}').equals(compile('{{#foo}} {{/foo}}'))
).toBe(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(
it('should treat as not equal', function () {
expect(compile('foo').equals(compile('bar'))).toBe(false);
expect(compile('{{foo}}').equals(compile('{{bar}}'))).toBe(false);
expect(compile('{{foo.bar}}').equals(compile('{{bar.bar}}'))).toBe(false);
expect(
compile('{{foo.bar baz bat=1}}').equals(
compile('{{foo.bar bar bat=1}}')
),
false
);
equal(
)
).toBe(false);
expect(
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
);
)
).toBe(false);
expect(
compile('{{#foo}} {{/foo}}').equals(compile('{{#bar}} {{/bar}}'))
).toBe(false);
expect(
compile('{{#foo}} {{/foo}}').equals(compile('{{#foo}} {{foo}}{{/foo}}'))
).toBe(false);
});
});
describe('#compile', function() {
it('should fail with invalid input', function() {
shouldThrow(
function() {
Handlebars.compile(null);
},
Error,
describe('#compile', function () {
it('should fail with invalid input', function () {
expect(function () {
Handlebars.compile(null);
}).toThrow(
'You must pass a string or Handlebars AST to Handlebars.compile. You passed null'
);
shouldThrow(
function() {
Handlebars.compile({});
},
Error,
expect(function () {
Handlebars.compile({});
}).toThrow(
'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() {
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.'
);
expect.unreachable('Statement must throw exception');
} catch (err) {
equal(
err.message,
"if doesn't match def - 2:5",
'Checking error message'
);
expect(err.message).toBe("if doesn't match def - 2:5");
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)
// Since this was neither working in Handlebars 3 nor in 4.0.5, we only check the column for other browsers.
equal(err.column, 5, 'Checking error column');
expect(err.column).toBe(5);
}
equal(err.lineNumber, 2, 'Checking error row');
expect(err.lineNumber).toBe(2);
}
});
it('should include the location as enumerable property', 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.'
);
expect.unreachable('Statement must throw exception');
} catch (err) {
equal(
Object.prototype.propertyIsEnumerable.call(err, 'column'),
true,
'Checking error column'
expect(Object.prototype.propertyIsEnumerable.call(err, 'column')).toBe(
true
);
}
});
it('can utilize AST instance', function() {
equal(
it('can utilize AST instance', function () {
expect(
Handlebars.compile({
type: 'Program',
body: [{ type: 'ContentStatement', value: 'Hello' }]
})(),
'Hello'
body: [{ type: 'ContentStatement', value: 'Hello' }],
})()
).toBe('Hello');
});
it('can pass through an empty string', function () {
expect(Handlebars.compile('')()).toBe('');
});
it('throws on desupported options', function () {
expect(function () {
Handlebars.compile('Dudes', { trackIds: true });
}).toThrow(
'TrackIds and stringParams are no longer supported. See Github #1145'
);
expect(function () {
Handlebars.compile('Dudes', { stringParams: true });
}).toThrow(
'TrackIds and stringParams are no longer supported. See Github #1145'
);
});
it('can pass through an empty string', function() {
equal(Handlebars.compile('')(), '');
});
it('should not modify the options.data property(GH-1327)', function() {
it('should not modify the options.data property(GH-1327)', function () {
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)
);
expect(options).toStrictEqual({ data: [{ a: 'foo' }, { a: 'bar' }] });
});
it('should not modify the options.knownHelpers property(GH-1327)', function() {
it('should not modify the options.knownHelpers property(GH-1327)', function () {
var options = { knownHelpers: {} };
Handlebars.compile('{{#each data}}{{@index}}:{{a}} {{/each}}', options)();
equal(
JSON.stringify(options, 0, 2),
JSON.stringify({ knownHelpers: {} }, 0, 2)
);
expect(options).toStrictEqual({ knownHelpers: {} });
});
});
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'
describe('#precompile', function () {
it('should fail with invalid input', function () {
expect(function () {
Handlebars.precompile(null);
}).toThrow(
'You must pass a string or Handlebars AST to Handlebars.compile. You passed null'
);
shouldThrow(
function() {
Handlebars.precompile({});
},
Error,
'You must pass a string or Handlebars AST to Handlebars.precompile. You passed [object Object]'
expect(function () {
Handlebars.precompile({});
}).toThrow(
'You must pass a string or Handlebars AST to Handlebars.compile. You passed [object Object]'
);
});
it('can utilize AST instance', function() {
equal(
/return "Hello"/.test(
Handlebars.precompile({
type: 'Program',
body: [{ type: 'ContentStatement', value: 'Hello' }]
})
),
true
);
it('can utilize AST instance', function () {
expect(
Handlebars.precompile({
type: 'Program',
body: [{ type: 'ContentStatement', value: 'Hello' }],
})
).toMatch(/return "Hello"/);
});
it('can pass through an empty string', function() {
equal(/return ""/.test(Handlebars.precompile('')), true);
it('can pass through an empty string', function () {
expect(Handlebars.precompile('')).toMatch(/return ""/);
});
});
});
+55 -55
View File
@@ -1,8 +1,8 @@
describe('data', function() {
it('passing in data to a compiled function that expects data - works with helpers', function() {
describe('data', function () {
it('passing in data to a compiled function that expects data - works with helpers', function () {
expectTemplate('{{hello}}')
.withCompileOptions({ data: true })
.withHelper('hello', function(options) {
.withHelper('hello', function (options) {
return options.data.adjective + ' ' + this.noun;
})
.withRuntimeOptions({ data: { adjective: 'happy' } })
@@ -11,17 +11,17 @@ describe('data', function() {
.toCompileTo('happy cat');
});
it('data can be looked up via @foo', function() {
it('data can be looked up via @foo', function () {
expectTemplate('{{@hello}}')
.withRuntimeOptions({ data: { hello: 'hello' } })
.withMessage('@foo retrieves template data')
.toCompileTo('hello');
});
it('deep @foo triggers automatic top-level data', function() {
it('deep @foo triggers automatic top-level data', function () {
var helpers = Handlebars.createFrame(handlebarsEnv.helpers);
helpers.let = function(options) {
helpers.let = function (options) {
var frame = Handlebars.createFrame(options.data);
for (var prop in options.hash) {
@@ -41,83 +41,83 @@ describe('data', function() {
.toCompileTo('Hello world');
});
it('parameter data can be looked up via @foo', function() {
it('parameter data can be looked up via @foo', function () {
expectTemplate('{{hello @world}}')
.withRuntimeOptions({ data: { world: 'world' } })
.withHelper('hello', function(noun) {
.withHelper('hello', function (noun) {
return 'Hello ' + noun;
})
.withMessage('@foo as a parameter retrieves template data')
.toCompileTo('Hello world');
});
it('hash values can be looked up via @foo', function() {
it('hash values can be looked up via @foo', function () {
expectTemplate('{{hello noun=@world}}')
.withRuntimeOptions({ data: { world: 'world' } })
.withHelper('hello', function(options) {
.withHelper('hello', function (options) {
return 'Hello ' + options.hash.noun;
})
.withMessage('@foo as a parameter retrieves template data')
.toCompileTo('Hello world');
});
it('nested parameter data can be looked up via @foo.bar', function() {
it('nested parameter data can be looked up via @foo.bar', function () {
expectTemplate('{{hello @world.bar}}')
.withRuntimeOptions({ data: { world: { bar: 'world' } } })
.withHelper('hello', function(noun) {
.withHelper('hello', function (noun) {
return 'Hello ' + noun;
})
.withMessage('@foo as a parameter retrieves template data')
.toCompileTo('Hello world');
});
it('nested parameter data does not fail with @world.bar', function() {
it('nested parameter data does not fail with @world.bar', function () {
expectTemplate('{{hello @world.bar}}')
.withRuntimeOptions({ data: { foo: { bar: 'world' } } })
.withHelper('hello', function(noun) {
.withHelper('hello', function (noun) {
return 'Hello ' + noun;
})
.withMessage('@foo as a parameter retrieves template data')
.toCompileTo('Hello undefined');
});
it('parameter data throws when using complex scope references', function() {
it('parameter data throws when using complex scope references', function () {
expectTemplate(
'{{#goodbyes}}{{text}} cruel {{@foo/../name}}! {{/goodbyes}}'
).toThrow(Error);
});
it('data can be functions', function() {
it('data can be functions', function () {
expectTemplate('{{@hello}}')
.withRuntimeOptions({
data: {
hello: function() {
hello: function () {
return 'hello';
}
}
},
},
})
.toCompileTo('hello');
});
it('data can be functions with params', function() {
it('data can be functions with params', function () {
expectTemplate('{{@hello "hello"}}')
.withRuntimeOptions({
data: {
hello: function(arg) {
hello: function (arg) {
return arg;
}
}
},
},
})
.toCompileTo('hello');
});
it('data is inherited downstream', function() {
it('data is inherited downstream', function () {
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) {
.withHelper('let', function (options) {
var frame = Handlebars.createFrame(options.data);
for (var prop in options.hash) {
if (prop in options.hash) {
@@ -131,11 +131,11 @@ describe('data', function() {
.toCompileTo('2hello world1');
});
it('passing in data to a compiled function that expects data - works with helpers in partials', function() {
it('passing in data to a compiled function that expects data - works with helpers in partials', function () {
expectTemplate('{{>myPartial}}')
.withCompileOptions({ data: true })
.withPartial('myPartial', '{{hello}}')
.withHelper('hello', function(options) {
.withHelper('hello', function (options) {
return options.data.adjective + ' ' + this.noun;
})
.withInput({ noun: 'cat' })
@@ -144,10 +144,10 @@ describe('data', function() {
.toCompileTo('happy cat');
});
it('passing in data to a compiled function that expects data - works with helpers and parameters', function() {
it('passing in data to a compiled function that expects data - works with helpers and parameters', function () {
expectTemplate('{{hello world}}')
.withCompileOptions({ data: true })
.withHelper('hello', function(noun, options) {
.withHelper('hello', function (noun, options) {
return options.data.adjective + ' ' + noun + (this.exclaim ? '!' : '');
})
.withInput({ exclaim: true, world: 'world' })
@@ -156,15 +156,15 @@ describe('data', function() {
.toCompileTo('happy world!');
});
it('passing in data to a compiled function that expects data - works with block helpers', function() {
it('passing in data to a compiled function that expects data - works with block helpers', function () {
expectTemplate('{{#hello}}{{world}}{{/hello}}')
.withCompileOptions({
data: true
data: true,
})
.withHelper('hello', function(options) {
.withHelper('hello', function (options) {
return options.fn(this);
})
.withHelper('world', function(options) {
.withHelper('world', function (options) {
return options.data.adjective + ' world' + (this.exclaim ? '!' : '');
})
.withInput({ exclaim: true })
@@ -173,13 +173,13 @@ describe('data', function() {
.toCompileTo('happy world!');
});
it('passing in data to a compiled function that expects data - works with block helpers that use ..', function() {
it('passing in data to a compiled function that expects data - works with block helpers that use ..', function () {
expectTemplate('{{#hello}}{{world ../zomg}}{{/hello}}')
.withCompileOptions({ data: true })
.withHelper('hello', function(options) {
.withHelper('hello', function (options) {
return options.fn({ exclaim: '?' });
})
.withHelper('world', function(thing, options) {
.withHelper('world', function (thing, options) {
return options.data.adjective + ' ' + thing + (this.exclaim || '');
})
.withInput({ exclaim: true, zomg: 'world' })
@@ -188,13 +188,13 @@ describe('data', function() {
.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() {
it('passing in data to a compiled function that expects data - data is passed to with block helpers where children use ..', function () {
expectTemplate('{{#hello}}{{world ../zomg}}{{/hello}}')
.withCompileOptions({ data: true })
.withHelper('hello', function(options) {
.withHelper('hello', function (options) {
return options.data.accessData + ' ' + options.fn({ exclaim: '?' });
})
.withHelper('world', function(thing, options) {
.withHelper('world', function (thing, options) {
return options.data.adjective + ' ' + thing + (this.exclaim || '');
})
.withInput({ exclaim: true, zomg: 'world' })
@@ -203,41 +203,41 @@ describe('data', function() {
.toCompileTo('#win happy world?');
});
it('you can override inherited data when invoking a helper', function() {
it('you can override inherited data when invoking a helper', function () {
expectTemplate('{{#hello}}{{world zomg}}{{/hello}}')
.withCompileOptions({ data: true })
.withHelper('hello', function(options) {
.withHelper('hello', function (options) {
return options.fn(
{ exclaim: '?', zomg: 'world' },
{ data: { adjective: 'sad' } }
);
})
.withHelper('world', function(thing, options) {
.withHelper('world', function (thing, options) {
return options.data.adjective + ' ' + thing + (this.exclaim || '');
})
.withInput({ exclaim: true, zomg: 'planet' })
.withRuntimeOptions({ data: { adjective: 'happy' } })
.withMessage('Overriden data output by helper')
.withMessage('Overridden data output by helper')
.toCompileTo('sad world?');
});
it('you can override inherited data when invoking a helper with depth', function() {
it('you can override inherited data when invoking a helper with depth', function () {
expectTemplate('{{#hello}}{{world ../zomg}}{{/hello}}')
.withCompileOptions({ data: true })
.withHelper('hello', function(options) {
.withHelper('hello', function (options) {
return options.fn({ exclaim: '?' }, { data: { adjective: 'sad' } });
})
.withHelper('world', function(thing, options) {
.withHelper('world', function (thing, options) {
return options.data.adjective + ' ' + thing + (this.exclaim || '');
})
.withInput({ exclaim: true, zomg: 'world' })
.withRuntimeOptions({ data: { adjective: 'happy' } })
.withMessage('Overriden data output by helper')
.withMessage('Overridden data output by helper')
.toCompileTo('sad world?');
});
describe('@root', function() {
it('the root context can be looked up via @root', function() {
describe('@root', function () {
it('the root context can be looked up via @root', function () {
expectTemplate('{{@root.foo}}')
.withInput({ foo: 'hello' })
.withRuntimeOptions({ data: {} })
@@ -248,7 +248,7 @@ describe('data', function() {
.toCompileTo('hello');
});
it('passed root values take priority', function() {
it('passed root values take priority', function () {
expectTemplate('{{@root.foo}}')
.withInput({ foo: 'should not be used' })
.withRuntimeOptions({ data: { root: { foo: 'hello' } } })
@@ -256,21 +256,21 @@ describe('data', function() {
});
});
describe('nesting', function() {
it('the root context can be looked up via @root', function() {
describe('nesting', function () {
it('the root context can be looked up via @root', function () {
expectTemplate(
'{{#helper}}{{#helper}}{{@./depth}} {{@../depth}} {{@../../depth}}{{/helper}}{{/helper}}'
)
.withInput({ foo: 'hello' })
.withHelper('helper', function(options) {
.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
}
depth: 0,
},
})
.toCompileTo('2 1 0');
});
+6
View File
@@ -0,0 +1,6 @@
// Pre-setup for browser tests. Must run before the main setup file
// imports the Handlebars library, so that noConflict() captures this value.
globalThis.Handlebars = 'no-conflict';
// Polyfill Node.js 'global' for specs that reference it at module level
globalThis.global = globalThis;
+27
View File
@@ -0,0 +1,27 @@
import './common.js';
import Handlebars from '../../lib/handlebars.js';
globalThis.Handlebars = Handlebars;
globalThis.CompilerContext = {
browser: true,
compile: function (template, options) {
var templateSpec = handlebarsEnv.precompile(template, options);
return handlebarsEnv.template(safeEval(templateSpec));
},
compileWithPartial: function (template, options) {
return handlebarsEnv.compile(template, options);
},
};
function safeEval(templateSpec) {
/* eslint-disable no-eval, no-console */
try {
return eval('(' + templateSpec + ')');
} catch (err) {
console.error(templateSpec);
throw err;
}
/* eslint-enable no-eval, no-console */
}
+3 -14
View File
@@ -3,20 +3,9 @@ require('./common');
var fs = require('fs'),
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';
var filename = 'dist/handlebars.js';
if (global.minimizedTest) {
filename = 'dist/handlebars.min.js';
}
var distHandlebars = fs.readFileSync(
require.resolve('../../' + filename),
'utf-8'
@@ -26,13 +15,13 @@ vm.runInThisContext(distHandlebars, filename);
global.CompilerContext = {
browser: true,
compile: function(template, options) {
compile: function (template, options) {
var templateSpec = handlebarsEnv.precompile(template, options);
return handlebarsEnv.template(safeEval(templateSpec));
},
compileWithPartial: function(template, options) {
compileWithPartial: function (template, options) {
return handlebarsEnv.compile(template, options);
}
},
};
function safeEval(templateSpec) {
+42 -148
View File
@@ -1,127 +1,6 @@
var global = (function() {
return this;
})();
var global = globalThis;
var AssertError;
if (Error.captureStackTrace) {
AssertError = function AssertError(message, caller) {
Error.prototype.constructor.call(this, message);
this.message = message;
if (Error.captureStackTrace) {
Error.captureStackTrace(this, caller || AssertError);
}
};
AssertError.prototype = new Error();
} else {
AssertError = Error;
}
/**
* @deprecated Use "expectTemplate(template)...toCompileTo(output)" instead
*/
global.shouldCompileTo = function(string, hashOrArray, expected, message) {
shouldCompileToWithPartials(string, hashOrArray, false, 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
);
}
};
/**
* @deprecated Use "expectTemplate(template)...toCompileTo(output)" instead
*/
global.compileWithPartials = function(string, hashOrArray, partials) {
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]); // input
ary.push({ helpers: hashOrArray[1], partials: hashOrArray[2] });
options =
typeof hashOrArray[3] === 'object'
? hashOrArray[3]
: { compat: hashOrArray[3] };
if (hashOrArray[4] != null) {
options.data = !!hashOrArray[4];
ary[1].data = hashOrArray[4];
}
} else {
ary = [hashOrArray];
}
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
);
}
};
/**
* @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 {
callback();
failed = true;
} catch (caught) {
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 (failed) {
throw new AssertError('It failed to throw', shouldThrow);
}
};
global.expectTemplate = function(templateAsString) {
global.expectTemplate = function (templateAsString) {
return new HandlebarsTestBench(templateAsString);
};
@@ -137,38 +16,38 @@ function HandlebarsTestBench(templateAsString) {
this.runtimeOptions = {};
}
HandlebarsTestBench.prototype.withInput = function(input) {
HandlebarsTestBench.prototype.withInput = function (input) {
this.input = input;
return this;
};
HandlebarsTestBench.prototype.withHelper = function(name, helperFunction) {
HandlebarsTestBench.prototype.withHelper = function (name, helperFunction) {
this.helpers[name] = helperFunction;
return this;
};
HandlebarsTestBench.prototype.withHelpers = function(helperFunctions) {
HandlebarsTestBench.prototype.withHelpers = function (helperFunctions) {
var self = this;
Object.keys(helperFunctions).forEach(function(name) {
Object.keys(helperFunctions).forEach(function (name) {
self.withHelper(name, helperFunctions[name]);
});
return this;
};
HandlebarsTestBench.prototype.withPartial = function(name, partialAsString) {
HandlebarsTestBench.prototype.withPartial = function (name, partialAsString) {
this.partials[name] = partialAsString;
return this;
};
HandlebarsTestBench.prototype.withPartials = function(partials) {
HandlebarsTestBench.prototype.withPartials = function (partials) {
var self = this;
Object.keys(partials).forEach(function(name) {
Object.keys(partials).forEach(function (name) {
self.withPartial(name, partials[name]);
});
return this;
};
HandlebarsTestBench.prototype.withDecorator = function(
HandlebarsTestBench.prototype.withDecorator = function (
name,
decoratorFunction
) {
@@ -176,45 +55,56 @@ HandlebarsTestBench.prototype.withDecorator = function(
return this;
};
HandlebarsTestBench.prototype.withDecorators = function(decorators) {
HandlebarsTestBench.prototype.withDecorators = function (decorators) {
var self = this;
Object.keys(decorators).forEach(function(name) {
Object.keys(decorators).forEach(function (name) {
self.withDecorator(name, decorators[name]);
});
return this;
};
HandlebarsTestBench.prototype.withCompileOptions = function(compileOptions) {
HandlebarsTestBench.prototype.withCompileOptions = function (compileOptions) {
this.compileOptions = compileOptions;
return this;
};
HandlebarsTestBench.prototype.withRuntimeOptions = function(runtimeOptions) {
HandlebarsTestBench.prototype.withRuntimeOptions = function (runtimeOptions) {
this.runtimeOptions = runtimeOptions;
return this;
};
HandlebarsTestBench.prototype.withMessage = function(message) {
HandlebarsTestBench.prototype.withMessage = function (message) {
this.message = message;
return this;
};
HandlebarsTestBench.prototype.toCompileTo = function(expectedOutputAsString) {
expect(this._compileAndExecute()).to.equal(
expectedOutputAsString,
this.message
);
HandlebarsTestBench.prototype.toCompileTo = function (expectedOutputAsString) {
expect(this._compileAndExecute()).toBe(expectedOutputAsString);
};
// see chai "to.throw" (https://www.chaijs.com/api/bdd/#method_throw)
HandlebarsTestBench.prototype.toThrow = function(errorLike, errMsgMatcher) {
HandlebarsTestBench.prototype.toThrow = function (errorLike, errMsgMatcher) {
var self = this;
expect(function() {
var caught;
try {
self._compileAndExecute();
}).to.throw(errorLike, errMsgMatcher, this.message);
} catch (e) {
caught = e;
}
expect(caught).toBeDefined();
if (typeof errorLike === 'function') {
expect(caught).toBeInstanceOf(errorLike);
if (errMsgMatcher) {
expect(caught.message).toMatch(errMsgMatcher);
}
} else if (errorLike) {
// errorLike is a string or regex message matcher (single-argument form)
expect(caught.message).toMatch(errorLike);
}
};
HandlebarsTestBench.prototype._compileAndExecute = function() {
HandlebarsTestBench.prototype._compileAndExecute = function () {
var compile =
Object.keys(this.partials).length > 0
? CompilerContext.compileWithPartial
@@ -226,10 +116,10 @@ HandlebarsTestBench.prototype._compileAndExecute = function() {
return template(this.input, combinedRuntimeOptions);
};
HandlebarsTestBench.prototype._combineRuntimeOptions = function() {
HandlebarsTestBench.prototype._combineRuntimeOptions = function () {
var self = this;
var combinedRuntimeOptions = {};
Object.keys(this.runtimeOptions).forEach(function(key) {
Object.keys(this.runtimeOptions).forEach(function (key) {
combinedRuntimeOptions[key] = self.runtimeOptions[key];
});
combinedRuntimeOptions.helpers = this.helpers;
@@ -237,3 +127,7 @@ HandlebarsTestBench.prototype._combineRuntimeOptions = function() {
combinedRuntimeOptions.decorators = this.decorators;
return combinedRuntimeOptions;
};
beforeEach(function () {
global.handlebarsEnv = Handlebars.create();
});
+3 -11
View File
@@ -1,23 +1,15 @@
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 = {
compile: function(template, options) {
compile: function (template, options) {
var templateSpec = handlebarsEnv.precompile(template, options);
return handlebarsEnv.template(safeEval(templateSpec));
},
compileWithPartial: function(template, options) {
compileWithPartial: function (template, options) {
return handlebarsEnv.compile(template, options);
}
},
};
function safeEval(templateSpec) {
-63
View File
@@ -1,63 +0,0 @@
/* eslint-disable no-console */
var fs = require('fs'),
Mocha = require('mocha'),
path = require('path');
var errors = 0,
testDir = path.dirname(__dirname),
grep = process.argv[2];
// Lazy hack, but whatever
if (grep === '--min') {
global.minimizedTest = true;
grep = undefined;
}
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() {
run('./browser', function() {
/* eslint-disable no-process-exit */
process.exit(errors);
/* eslint-enable no-process-exit */
});
});
} else {
run('./runtime', function() {
run('./browser', function() {
run('./node', function() {
/* eslint-disable no-process-exit */
process.exit(errors);
/* eslint-enable no-process-exit */
});
});
});
}
function run(env, callback) {
var mocha = new Mocha();
mocha.ui('bdd');
mocha.files = files.slice();
if (grep) {
mocha.grep(grep);
}
files.forEach(function(name) {
delete require.cache[name];
});
console.log('Running env: ' + env);
require(env);
mocha.run(function(errorCount) {
errors += errorCount;
callback();
});
}
-70
View File
@@ -1,70 +0,0 @@
require('./common');
var fs = require('fs'),
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';
var filename = 'dist/handlebars.runtime.js';
if (global.minimizedTest) {
filename = 'dist/handlebars.runtime.min.js';
}
vm.runInThisContext(
fs.readFileSync(__dirname + '/../../' + filename),
filename
);
var parse = require('../../dist/cjs/handlebars/compiler/base').parse;
var compiler = require('../../dist/cjs/handlebars/compiler/compiler');
var JavaScriptCompiler = require('../../dist/cjs/handlebars/compiler/javascript-compiler');
global.CompilerContext = {
browser: true,
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
);
};
handlebarsEnv.parse = parse;
handlebarsEnv.Compiler = compiler.Compiler;
handlebarsEnv.JavaScriptCompiler = JavaScriptCompiler;
var templateSpec = handlebarsEnv.precompile(template, options);
return handlebarsEnv.template(safeEval(templateSpec));
},
compileWithPartial: function(template, options) {
// Hack the compiler on to the environment for these specific tests
handlebarsEnv.compile = function(compileTemplate, compileOptions) {
return compiler.compile(compileTemplate, compileOptions, handlebarsEnv);
};
handlebarsEnv.parse = parse;
handlebarsEnv.Compiler = compiler.Compiler;
handlebarsEnv.JavaScriptCompiler = JavaScriptCompiler;
return handlebarsEnv.compile(template, options);
}
};
function safeEval(templateSpec) {
/* eslint-disable no-eval, no-console */
try {
return eval('(' + templateSpec + ')');
} catch (err) {
console.error(templateSpec);
throw err;
}
/* eslint-enable no-eval, no-console */
}
+3 -3
View File
@@ -1,9 +1,9 @@
Precompile handlebar templates.
Usage: handlebars [template|directory]...
Usage: handlebars.mjs [template|directory]...
Options:
-f, --output Output File [string]
--map Source Map File [string]
--map Source Map File [string]
-a, --amd Exports amd style (require.js) [boolean]
-c, --commonjs Exports CommonJS style, path to Handlebars module [string] [default: null]
-h, --handlebarPath Path to handlebar.js (only valid for amd-style) [string] [default: ""]
@@ -22,4 +22,4 @@ Options:
-e, --extension Template extension. [string] [default: "handlebars"]
-b, --bom Removes the BOM (Byte Order Mark) from the beginning of the templates. [boolean]
-v, --version Prints the current compiler version [boolean]
--help Outputs this message [boolean]
--help Outputs this message [boolean]
+4 -4
View File
@@ -1,6 +1,6 @@
define(['handlebars.runtime'], function(Handlebars) {
Handlebars = Handlebars["default"]; var template = Handlebars.template, templates = Handlebars.templates = Handlebars.templates || {};
return templates['known.helpers'] = template({"1":function(container,depth0,helpers,partials,data) {
return templates['known.helpers'] = template({"0":function(container,depth0,helpers,partials,data) {
var stack1, lookupProperty = container.lookupProperty || function(parent, propertyName) {
if (Object.prototype.hasOwnProperty.call(parent, propertyName)) {
return parent[propertyName];
@@ -8,8 +8,8 @@ return parent[propertyName];
return undefined
};
return " <div>Some known helper</div>\n"
+ ((stack1 = lookupProperty(helpers,"anotherHelper").call(depth0 != null ? depth0 : (container.nullContext || {}),true,{"name":"anotherHelper","hash":{},"fn":container.program(2, data, 0),"inverse":container.noop,"data":data,"loc":{"start":{"line":3,"column":4},"end":{"line":5,"column":22}}})) != null ? stack1 : "");
},"2":function(container,depth0,helpers,partials,data) {
+ ((stack1 = lookupProperty(helpers,"anotherHelper").call(depth0 != null ? depth0 : (container.nullContext || {}),true,{"name":"anotherHelper","hash":{},"fn":container.program(1, data, 0),"inverse":container.noop,"data":data,"loc":{"start":{"line":3,"column":4},"end":{"line":5,"column":22}}})) != null ? stack1 : "");
},"1":function(container,depth0,helpers,partials,data) {
return " <div>Another known helper</div>\n";
},"compiler":[8,">= 4.3.0"],"main":function(container,depth0,helpers,partials,data) {
var stack1, lookupProperty = container.lookupProperty || function(parent, propertyName) {
@@ -18,7 +18,7 @@ return parent[propertyName];
}
return undefined
};
return ((stack1 = lookupProperty(helpers,"someHelper").call(depth0 != null ? depth0 : (container.nullContext || {}),true,{"name":"someHelper","hash":{},"fn":container.program(1, data, 0),"inverse":container.noop,"data":data,"loc":{"start":{"line":1,"column":0},"end":{"line":6,"column":15}}})) != null ? stack1 : "");
return ((stack1 = lookupProperty(helpers,"someHelper").call(depth0 != null ? depth0 : (container.nullContext || {}),true,{"name":"someHelper","hash":{},"fn":container.program(0, data, 0),"inverse":container.noop,"data":data,"loc":{"start":{"line":1,"column":0},"end":{"line":6,"column":15}}})) != null ? stack1 : "");
},"useData":true});
});
+213 -217
View File
File diff suppressed because it is too large Load Diff
+48 -92
View File
@@ -1,99 +1,55 @@
<!doctype html>
<html>
<head>
<title>Mocha</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="/node_modules/mocha/mocha.css" />
<style>
.headless .suite > h1,
.headless .test.pass {
display: none;
}
</style>
<script>
// Show only errors in "headless", non-interactive mode.
if (/headless=true/.test(location.href)) {
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="/dist/handlebars.js"></script>
<script src="/spec/vendor/json2.js"></script>
<script src="/spec/env/common.js"></script>
<script>
var CompilerContext = {
compile: function(template, options) {
var templateSpec = handlebarsEnv.precompile(template, options);
return handlebarsEnv.template(safeEval(templateSpec));
},
compileWithPartial: function(template, options) {
return handlebarsEnv.compile(template, options);
}
};
function safeEval(templateSpec) {
try {
var ret;
eval('ret = ' + templateSpec);
return ret;
} catch (err) {
console.error(templateSpec);
throw err;
}
}
</script>
<script src="/tmp/tests.js"></script>
<script>
onload = function(){
mocha.globals(['mochaResults'])
// The test harness leaks under FF. We should have decent global leak coverage from other tests
if (!navigator.userAgent.match(/Firefox\/([\d.]+)/)) {
mocha.checkLeaks();
}
var runner = mocha.run();
// Reporting to test-runner
var failedTests = [];
runner.on('end', function(){
window.mochaResults = runner.stats;
window.mochaResults.reports = failedTests;
});
runner.on('fail', logFailure);
function logFailure(test, err){
var flattenTitles = function(test){
var titles = [];
while (test.parent.title){
titles.push(test.parent.title);
test = test.parent;
}
return titles.reverse();
};
failedTests.push({
name: test.title,
result: false,
message: err.message,
stack: err.stack,
titles: flattenTitles(test)
});
};
};
</script>
<title>Handlebars UMD Smoke Test</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
</head>
<body>
<div id="mocha"></div>
<h1>Handlebars UMD Smoke Test</h1>
<pre id="results"></pre>
<script src="/dist/handlebars.js"></script>
<script>
var results = document.getElementById('results');
var failures = 0;
var tests = 0;
function assert(condition, message) {
tests++;
if (!condition) {
failures++;
results.textContent += 'FAIL: ' + message + '\n';
} else {
results.textContent += 'PASS: ' + message + '\n';
}
}
try {
assert(typeof Handlebars !== 'undefined', 'Handlebars is defined');
assert(
typeof Handlebars.compile === 'function',
'Handlebars.compile exists'
);
assert(
typeof Handlebars.template === 'function',
'Handlebars.template exists'
);
assert(
typeof Handlebars.VERSION === 'string',
'Handlebars.VERSION exists'
);
var template = Handlebars.compile('Hello {{name}}!');
var output = template({ name: 'World' });
assert(output === 'Hello World!', 'Basic compilation works: ' + output);
} catch (e) {
failures++;
results.textContent += 'ERROR: ' + e.message + '\n';
}
results.textContent +=
'\n' + tests + ' tests, ' + failures + ' failures\n';
window.mochaResults = { passes: tests - failures, failures: failures };
</script>
</body>
</html>
+55 -50
View File
@@ -1,87 +1,92 @@
describe('javascript-compiler api', function() {
describe('javascript-compiler api', function () {
if (!Handlebars.JavaScriptCompiler) {
return;
}
describe('#nameLookup', function() {
describe('#nameLookup', function () {
var $superName;
beforeEach(function() {
beforeEach(function () {
$superName = handlebarsEnv.JavaScriptCompiler.prototype.nameLookup;
});
afterEach(function() {
afterEach(function () {
handlebarsEnv.JavaScriptCompiler.prototype.nameLookup = $superName;
});
it('should allow override', function() {
handlebarsEnv.JavaScriptCompiler.prototype.nameLookup = function(
it('should allow override', function () {
handlebarsEnv.JavaScriptCompiler.prototype.nameLookup = function (
parent,
name
) {
return parent + '.bar_' + name;
};
/* eslint-disable camelcase */
expectTemplate('{{foo}}')
.withInput({ bar_foo: 'food' })
.toCompileTo('food');
/* eslint-enable camelcase */
});
// Tests nameLookup dot vs. bracket behavior. Bracket is required in certain cases
// to avoid errors in older browsers.
it('should handle reserved words', function() {
it('should handle reserved words', function () {
expectTemplate('{{foo}} {{~null~}}')
.withInput({ foo: 'food' })
.toCompileTo('food');
});
});
describe('#compilerInfo', function() {
var $superCheck, $superInfo;
beforeEach(function() {
$superCheck = handlebarsEnv.VM.checkRevision;
$superInfo = handlebarsEnv.JavaScriptCompiler.prototype.compilerInfo;
});
afterEach(function() {
handlebarsEnv.VM.checkRevision = $superCheck;
handlebarsEnv.JavaScriptCompiler.prototype.compilerInfo = $superInfo;
});
it('should allow compilerInfo override', function() {
handlebarsEnv.JavaScriptCompiler.prototype.compilerInfo = function() {
return 'crazy';
};
handlebarsEnv.VM.checkRevision = function(compilerInfo) {
if (compilerInfo !== 'crazy') {
throw new Error("It didn't work");
}
};
expectTemplate('{{foo}} ')
.withInput({ foo: 'food' })
.toCompileTo('food ');
});
});
describe('buffer', function() {
// Monkey-patching VM.checkRevision is not possible when VM is an ESM
// namespace object (browser mode), so skip these tests in that context.
(CompilerContext.browser ? describe.skip : describe)(
'#compilerInfo',
function () {
var $superCheck, $superInfo;
beforeEach(function () {
$superCheck = handlebarsEnv.VM.checkRevision;
$superInfo = handlebarsEnv.JavaScriptCompiler.prototype.compilerInfo;
});
afterEach(function () {
handlebarsEnv.VM.checkRevision = $superCheck;
handlebarsEnv.JavaScriptCompiler.prototype.compilerInfo = $superInfo;
});
it('should allow compilerInfo override', function () {
handlebarsEnv.JavaScriptCompiler.prototype.compilerInfo = function () {
return 'crazy';
};
handlebarsEnv.VM.checkRevision = function (compilerInfo) {
if (compilerInfo !== 'crazy') {
throw new Error("It didn't work");
}
};
expectTemplate('{{foo}} ')
.withInput({ foo: 'food' })
.toCompileTo('food ');
});
}
);
describe('buffer', function () {
var $superAppend, $superCreate;
beforeEach(function() {
beforeEach(function () {
handlebarsEnv.JavaScriptCompiler.prototype.forceBuffer = true;
$superAppend = handlebarsEnv.JavaScriptCompiler.prototype.appendToBuffer;
$superCreate =
handlebarsEnv.JavaScriptCompiler.prototype.initializeBuffer;
});
afterEach(function() {
afterEach(function () {
handlebarsEnv.JavaScriptCompiler.prototype.forceBuffer = false;
handlebarsEnv.JavaScriptCompiler.prototype.appendToBuffer = $superAppend;
handlebarsEnv.JavaScriptCompiler.prototype.initializeBuffer = $superCreate;
handlebarsEnv.JavaScriptCompiler.prototype.initializeBuffer =
$superCreate;
});
it('should allow init buffer override', function() {
handlebarsEnv.JavaScriptCompiler.prototype.initializeBuffer = function() {
return this.quotedString('foo_');
};
it('should allow init buffer override', function () {
handlebarsEnv.JavaScriptCompiler.prototype.initializeBuffer =
function () {
return this.quotedString('foo_');
};
expectTemplate('{{foo}} ')
.withInput({ foo: 'food' })
.toCompileTo('foo_food ');
});
it('should allow append buffer override', function() {
handlebarsEnv.JavaScriptCompiler.prototype.appendToBuffer = function(
it('should allow append buffer override', function () {
handlebarsEnv.JavaScriptCompiler.prototype.appendToBuffer = function (
string
) {
return $superAppend.call(this, [string, ' + "_foo"']);
@@ -92,27 +97,27 @@ describe('javascript-compiler api', function() {
});
});
describe('#isValidJavaScriptVariableName', function() {
describe('#isValidJavaScriptVariableName', function () {
// It is there and accessible and could be used by someone. That's why we don't remove it
// it 4.x. But if we keep it, we add a test
// This test should not encourage you to use the function. It is not needed any more
// and might be removed in 5.0
['test', 'abc123', 'abc_123'].forEach(function(validVariableName) {
it("should return true for '" + validVariableName + "'", function() {
['test', 'abc123', 'abc_123'].forEach(function (validVariableName) {
it("should return true for '" + validVariableName + "'", function () {
expect(
handlebarsEnv.JavaScriptCompiler.isValidJavaScriptVariableName(
validVariableName
)
).to.be.true();
).toBe(true);
});
});
[('123test', 'abc()', 'abc.cde')].forEach(function(invalidVariableName) {
it("should return true for '" + invalidVariableName + "'", function() {
[('123test', 'abc()', 'abc.cde')].forEach(function (invalidVariableName) {
it("should return true for '" + invalidVariableName + "'", function () {
expect(
handlebarsEnv.JavaScriptCompiler.isValidJavaScriptVariableName(
invalidVariableName
)
).to.be.false();
).toBe(false);
});
});
});
-455
View File
@@ -1,455 +0,0 @@
describe('parser', function() {
if (!Handlebars.print) {
return;
}
function astFor(template) {
var ast = Handlebars.parse(template);
return Handlebars.print(ast);
}
it('parses simple mustaches', function() {
equals(astFor('{{123}}'), '{{ NUMBER{123} [] }}\n');
equals(astFor('{{"foo"}}'), '{{ "foo" [] }}\n');
equals(astFor('{{false}}'), '{{ BOOLEAN{false} [] }}\n');
equals(astFor('{{true}}'), '{{ BOOLEAN{true} [] }}\n');
equals(astFor('{{foo}}'), '{{ PATH:foo [] }}\n');
equals(astFor('{{foo?}}'), '{{ PATH:foo? [] }}\n');
equals(astFor('{{foo_}}'), '{{ PATH:foo_ [] }}\n');
equals(astFor('{{foo-}}'), '{{ PATH:foo- [] }}\n');
equals(astFor('{{foo:}}'), '{{ PATH:foo: [] }}\n');
});
it('parses simple mustaches with data', function() {
equals(astFor('{{@foo}}'), '{{ @PATH:foo [] }}\n');
});
it('parses simple mustaches with data paths', function() {
equals(astFor('{{@../foo}}'), '{{ @PATH:foo [] }}\n');
});
it('parses mustaches with paths', function() {
equals(astFor('{{foo/bar}}'), '{{ PATH:foo/bar [] }}\n');
});
it('parses mustaches with this/foo', function() {
equals(astFor('{{this/foo}}'), '{{ PATH:foo [] }}\n');
});
it('parses mustaches with - in a path', function() {
equals(astFor('{{foo-bar}}'), '{{ PATH:foo-bar [] }}\n');
});
it('parses mustaches with escaped [] in a path', function() {
equals(astFor('{{[foo[\\]]}}'), '{{ PATH:foo[] [] }}\n');
});
it('parses escaped \\\\ in path', function() {
equals(astFor('{{[foo\\\\]}}'), '{{ PATH:foo\\ [] }}\n');
});
it('parses mustaches with parameters', function() {
equals(astFor('{{foo bar}}'), '{{ PATH:foo [PATH:bar] }}\n');
});
it('parses mustaches with string parameters', function() {
equals(astFor('{{foo bar "baz" }}'), '{{ PATH:foo [PATH:bar, "baz"] }}\n');
});
it('parses mustaches with NUMBER parameters', function() {
equals(astFor('{{foo 1}}'), '{{ PATH:foo [NUMBER{1}] }}\n');
});
it('parses mustaches with BOOLEAN parameters', function() {
equals(astFor('{{foo true}}'), '{{ PATH:foo [BOOLEAN{true}] }}\n');
equals(astFor('{{foo false}}'), '{{ PATH:foo [BOOLEAN{false}] }}\n');
});
it('parses mustaches with undefined and null paths', function() {
equals(astFor('{{undefined}}'), '{{ UNDEFINED [] }}\n');
equals(astFor('{{null}}'), '{{ NULL [] }}\n');
});
it('parses mustaches with undefined and null parameters', function() {
equals(
astFor('{{foo undefined null}}'),
'{{ PATH:foo [UNDEFINED, NULL] }}\n'
);
});
it('parses mustaches with DATA parameters', function() {
equals(astFor('{{foo @bar}}'), '{{ PATH:foo [@PATH:bar] }}\n');
});
it('parses mustaches with hash arguments', function() {
equals(astFor('{{foo bar=baz}}'), '{{ PATH:foo [] HASH{bar=PATH:baz} }}\n');
equals(astFor('{{foo bar=1}}'), '{{ PATH:foo [] HASH{bar=NUMBER{1}} }}\n');
equals(
astFor('{{foo bar=true}}'),
'{{ PATH:foo [] HASH{bar=BOOLEAN{true}} }}\n'
);
equals(
astFor('{{foo bar=false}}'),
'{{ PATH:foo [] HASH{bar=BOOLEAN{false}} }}\n'
);
equals(
astFor('{{foo bar=@baz}}'),
'{{ PATH:foo [] HASH{bar=@PATH:baz} }}\n'
);
equals(
astFor('{{foo bar=baz bat=bam}}'),
'{{ PATH:foo [] HASH{bar=PATH:baz, bat=PATH:bam} }}\n'
);
equals(
astFor('{{foo bar=baz bat="bam"}}'),
'{{ PATH:foo [] HASH{bar=PATH:baz, bat="bam"} }}\n'
);
equals(astFor("{{foo bat='bam'}}"), '{{ PATH:foo [] HASH{bat="bam"} }}\n');
equals(
astFor('{{foo omg bar=baz bat="bam"}}'),
'{{ PATH:foo [PATH:omg] HASH{bar=PATH:baz, bat="bam"} }}\n'
);
equals(
astFor('{{foo omg bar=baz bat="bam" baz=1}}'),
'{{ PATH:foo [PATH:omg] HASH{bar=PATH:baz, bat="bam", baz=NUMBER{1}} }}\n'
);
equals(
astFor('{{foo omg bar=baz bat="bam" baz=true}}'),
'{{ PATH:foo [PATH:omg] HASH{bar=PATH:baz, bat="bam", baz=BOOLEAN{true}} }}\n'
);
equals(
astFor('{{foo omg bar=baz bat="bam" baz=false}}'),
'{{ PATH:foo [PATH:omg] HASH{bar=PATH:baz, bat="bam", baz=BOOLEAN{false}} }}\n'
);
});
it('parses contents followed by a mustache', function() {
equals(
astFor('foo bar {{baz}}'),
"CONTENT[ 'foo bar ' ]\n{{ PATH:baz [] }}\n"
);
});
it('parses a partial', function() {
equals(astFor('{{> foo }}'), '{{> PARTIAL:foo }}\n');
equals(astFor('{{> "foo" }}'), '{{> PARTIAL:foo }}\n');
equals(astFor('{{> 1 }}'), '{{> PARTIAL:1 }}\n');
});
it('parses a partial with context', function() {
equals(astFor('{{> foo bar}}'), '{{> PARTIAL:foo PATH:bar }}\n');
});
it('parses a partial with hash', function() {
equals(
astFor('{{> foo bar=bat}}'),
'{{> PARTIAL:foo HASH{bar=PATH:bat} }}\n'
);
});
it('parses a partial with context and hash', function() {
equals(
astFor('{{> foo bar bat=baz}}'),
'{{> PARTIAL:foo PATH:bar HASH{bat=PATH:baz} }}\n'
);
});
it('parses a partial with a complex name', function() {
equals(
astFor('{{> shared/partial?.bar}}'),
'{{> PARTIAL:shared/partial?.bar }}\n'
);
});
it('parsers partial blocks', function() {
equals(
astFor('{{#> foo}}bar{{/foo}}'),
"{{> PARTIAL BLOCK:foo PROGRAM:\n CONTENT[ 'bar' ]\n }}\n"
);
});
it('should handle parser block mismatch', function() {
shouldThrow(
function() {
astFor('{{#> goodbyes}}{{/hellos}}');
},
Error,
/goodbyes doesn't match hellos/
);
});
it('parsers partial blocks with arguments', function() {
equals(
astFor('{{#> foo context hash=value}}bar{{/foo}}'),
"{{> PARTIAL BLOCK:foo PATH:context HASH{hash=PATH:value} PROGRAM:\n CONTENT[ 'bar' ]\n }}\n"
);
});
it('parses a comment', function() {
equals(
astFor('{{! this is a comment }}'),
"{{! ' this is a comment ' }}\n"
);
});
it('parses a multi-line comment', function() {
equals(
astFor('{{!\nthis is a multi-line comment\n}}'),
"{{! '\nthis is a multi-line comment\n' }}\n"
);
});
it('parses an inverse section', function() {
equals(
astFor('{{#foo}} bar {{^}} baz {{/foo}}'),
"BLOCK:\n PATH:foo []\n PROGRAM:\n CONTENT[ ' bar ' ]\n {{^}}\n CONTENT[ ' baz ' ]\n"
);
});
it('parses an inverse (else-style) section', function() {
equals(
astFor('{{#foo}} bar {{else}} baz {{/foo}}'),
"BLOCK:\n PATH:foo []\n PROGRAM:\n CONTENT[ ' bar ' ]\n {{^}}\n CONTENT[ ' baz ' ]\n"
);
});
it('parses multiple inverse sections', function() {
equals(
astFor('{{#foo}} bar {{else if bar}}{{else}} baz {{/foo}}'),
"BLOCK:\n PATH:foo []\n PROGRAM:\n CONTENT[ ' bar ' ]\n {{^}}\n BLOCK:\n PATH:if [PATH:bar]\n PROGRAM:\n {{^}}\n CONTENT[ ' baz ' ]\n"
);
});
it('parses empty blocks', function() {
equals(astFor('{{#foo}}{{/foo}}'), 'BLOCK:\n PATH:foo []\n PROGRAM:\n');
});
it('parses empty blocks with empty inverse section', function() {
equals(
astFor('{{#foo}}{{^}}{{/foo}}'),
'BLOCK:\n PATH:foo []\n PROGRAM:\n {{^}}\n'
);
});
it('parses empty blocks with empty inverse (else-style) section', function() {
equals(
astFor('{{#foo}}{{else}}{{/foo}}'),
'BLOCK:\n PATH:foo []\n PROGRAM:\n {{^}}\n'
);
});
it('parses non-empty blocks with empty inverse section', function() {
equals(
astFor('{{#foo}} bar {{^}}{{/foo}}'),
"BLOCK:\n PATH:foo []\n PROGRAM:\n CONTENT[ ' bar ' ]\n {{^}}\n"
);
});
it('parses non-empty blocks with empty inverse (else-style) section', function() {
equals(
astFor('{{#foo}} bar {{else}}{{/foo}}'),
"BLOCK:\n PATH:foo []\n PROGRAM:\n CONTENT[ ' bar ' ]\n {{^}}\n"
);
});
it('parses empty blocks with non-empty inverse section', function() {
equals(
astFor('{{#foo}}{{^}} bar {{/foo}}'),
"BLOCK:\n PATH:foo []\n PROGRAM:\n {{^}}\n CONTENT[ ' bar ' ]\n"
);
});
it('parses empty blocks with non-empty inverse (else-style) section', function() {
equals(
astFor('{{#foo}}{{else}} bar {{/foo}}'),
"BLOCK:\n PATH:foo []\n PROGRAM:\n {{^}}\n CONTENT[ ' bar ' ]\n"
);
});
it('parses a standalone inverse section', function() {
equals(
astFor('{{^foo}}bar{{/foo}}'),
"BLOCK:\n PATH:foo []\n {{^}}\n CONTENT[ 'bar' ]\n"
);
});
it('throws on old inverse section', function() {
shouldThrow(function() {
astFor('{{else foo}}bar{{/foo}}');
}, Error);
});
it('parses block with block params', function() {
equals(
astFor('{{#foo as |bar baz|}}content{{/foo}}'),
"BLOCK:\n PATH:foo []\n PROGRAM:\n BLOCK PARAMS: [ bar baz ]\n CONTENT[ 'content' ]\n"
);
});
it('parses inverse block with block params', function() {
equals(
astFor('{{^foo as |bar baz|}}content{{/foo}}'),
"BLOCK:\n PATH:foo []\n {{^}}\n BLOCK PARAMS: [ bar baz ]\n CONTENT[ 'content' ]\n"
);
});
it('parses chained inverse block with block params', function() {
equals(
astFor('{{#foo}}{{else foo as |bar baz|}}content{{/foo}}'),
"BLOCK:\n PATH:foo []\n PROGRAM:\n {{^}}\n BLOCK:\n PATH:foo []\n PROGRAM:\n BLOCK PARAMS: [ bar baz ]\n CONTENT[ 'content' ]\n"
);
});
it("raises if there's a Parse error", function() {
shouldThrow(
function() {
astFor('foo{{^}}bar');
},
Error,
/Parse error on line 1/
);
shouldThrow(
function() {
astFor('{{foo}');
},
Error,
/Parse error on line 1/
);
shouldThrow(
function() {
astFor('{{foo &}}');
},
Error,
/Parse error on line 1/
);
shouldThrow(
function() {
astFor('{{#goodbyes}}{{/hellos}}');
},
Error,
/goodbyes doesn't match hellos/
);
shouldThrow(
function() {
astFor('{{{{goodbyes}}}} {{{{/hellos}}}}');
},
Error,
/goodbyes doesn't match hellos/
);
});
it('should handle invalid paths', function() {
shouldThrow(
function() {
astFor('{{foo/../bar}}');
},
Error,
/Invalid path: foo\/\.\. - 1:2/
);
shouldThrow(
function() {
astFor('{{foo/./bar}}');
},
Error,
/Invalid path: foo\/\. - 1:2/
);
shouldThrow(
function() {
astFor('{{foo/this/bar}}');
},
Error,
/Invalid path: foo\/this - 1:2/
);
});
it('knows how to report the correct line number in errors', function() {
shouldThrow(
function() {
astFor('hello\nmy\n{{foo}');
},
Error,
/Parse error on line 3/
);
shouldThrow(
function() {
astFor('hello\n\nmy\n\n{{foo}');
},
Error,
/Parse error on line 5/
);
});
it('knows how to report the correct line number in errors when the first character is a newline', function() {
shouldThrow(
function() {
astFor('\n\nhello\n\nmy\n\n{{foo}');
},
Error,
/Parse error on line 7/
);
});
describe('externally compiled AST', function() {
it('can pass through an already-compiled AST', function() {
equals(
astFor({
type: 'Program',
body: [{ type: 'ContentStatement', value: 'Hello' }]
}),
"CONTENT[ 'Hello' ]\n"
);
});
});
describe('directives', function() {
it('should parse block directives', function() {
equals(
astFor('{{#* foo}}{{/foo}}'),
'DIRECTIVE BLOCK:\n PATH:foo []\n PROGRAM:\n'
);
});
it('should parse directives', function() {
equals(astFor('{{* foo}}'), '{{ DIRECTIVE PATH:foo [] }}\n');
});
it('should fail if directives have inverse', function() {
shouldThrow(
function() {
astFor('{{#* foo}}{{^}}{{/foo}}');
},
Error,
/Unexpected inverse/
);
});
});
it('GH1024 - should track program location properly', function() {
var p = Handlebars.parse(
'\n' +
' {{#if foo}}\n' +
' {{bar}}\n' +
' {{else}} {{baz}}\n' +
'\n' +
' {{/if}}\n' +
' '
);
// We really need a deep equals but for now this should be stable...
equals(
JSON.stringify(p.loc),
JSON.stringify({
start: { line: 1, column: 0 },
end: { line: 7, column: 4 }
})
);
equals(
JSON.stringify(p.body[1].program.loc),
JSON.stringify({
start: { line: 2, column: 13 },
end: { line: 4, column: 7 }
})
);
equals(
JSON.stringify(p.body[1].inverse.loc),
JSON.stringify({
start: { line: 4, column: 15 },
end: { line: 6, column: 5 }
})
);
});
});
+129 -132
View File
@@ -1,12 +1,12 @@
describe('partials', function() {
it('basic partials', function() {
describe('partials', function () {
it('basic partials', function () {
var string = 'Dudes: {{#dudes}}{{> dude}}{{/dudes}}';
var partial = '{{name}} ({{url}}) ';
var hash = {
dudes: [
{ name: 'Yehuda', url: 'http://yehuda' },
{ name: 'Alan', url: 'http://alan' }
]
{ name: 'Alan', url: 'http://alan' },
],
};
expectTemplate(string)
@@ -22,19 +22,19 @@ describe('partials', function() {
.toCompileTo('Dudes: Yehuda (http://yehuda) Alan (http://alan) ');
});
it('dynamic partials', function() {
it('dynamic partials', function () {
var string = 'Dudes: {{#dudes}}{{> (partial)}}{{/dudes}}';
var partial = '{{name}} ({{url}}) ';
var hash = {
dudes: [
{ name: 'Yehuda', url: 'http://yehuda' },
{ name: 'Alan', url: 'http://alan' }
]
{ name: 'Alan', url: 'http://alan' },
],
};
var helpers = {
partial: function() {
partial: function () {
return 'dude';
}
},
};
expectTemplate(string)
@@ -52,41 +52,44 @@ describe('partials', function() {
.toCompileTo('Dudes: Yehuda (http://yehuda) Alan (http://alan) ');
});
it('failing dynamic partials', function() {
it('failing dynamic partials', function () {
expectTemplate('Dudes: {{#dudes}}{{> (partial)}}{{/dudes}}')
.withInput({
dudes: [
{ name: 'Yehuda', url: 'http://yehuda' },
{ name: 'Alan', url: 'http://alan' }
]
{ name: 'Alan', url: 'http://alan' },
],
})
.withHelper('partial', function() {
.withHelper('partial', function () {
return 'missing';
})
.withPartial('dude', '{{name}} ({{url}}) ')
.toThrow(Handlebars.Exception, 'The partial missing could not be found');
.toThrow(
Handlebars.Exception,
'The partial "missing" could not be found'
);
});
it('partials with context', function() {
it('partials with context', function () {
expectTemplate('Dudes: {{>dude dudes}}')
.withInput({
dudes: [
{ name: 'Yehuda', url: 'http://yehuda' },
{ name: 'Alan', url: 'http://alan' }
]
{ name: 'Alan', url: 'http://alan' },
],
})
.withPartial('dude', '{{#this}}{{name}} ({{url}}) {{/this}}')
.withMessage('Partials can be passed a context')
.toCompileTo('Dudes: Yehuda (http://yehuda) Alan (http://alan) ');
});
it('partials with no context', function() {
it('partials with no context', function () {
var partial = '{{name}} ({{url}}) ';
var hash = {
dudes: [
{ name: 'Yehuda', url: 'http://yehuda' },
{ name: 'Alan', url: 'http://alan' }
]
{ name: 'Alan', url: 'http://alan' },
],
};
expectTemplate('Dudes: {{#dudes}}{{>dude}}{{/dudes}}')
@@ -102,50 +105,50 @@ describe('partials', function() {
.toCompileTo('Dudes: foo () foo () ');
});
it('partials with string context', function() {
it('partials with string context', function () {
expectTemplate('Dudes: {{>dude "dudes"}}')
.withPartial('dude', '{{.}}')
.toCompileTo('Dudes: dudes');
});
it('partials with undefined context', function() {
it('partials with undefined context', function () {
expectTemplate('Dudes: {{>dude dudes}}')
.withPartial('dude', '{{foo}} Empty')
.toCompileTo('Dudes: Empty');
});
it('partials with duplicate parameters', function() {
it('partials with duplicate parameters', function () {
expectTemplate('Dudes: {{>dude dudes foo bar=baz}}').toThrow(
Error,
'Unsupported number of partial arguments: 2 - 1:7'
);
});
it('partials with parameters', function() {
it('partials with parameters', function () {
expectTemplate('Dudes: {{#dudes}}{{> dude others=..}}{{/dudes}}')
.withInput({
foo: 'bar',
dudes: [
{ name: 'Yehuda', url: 'http://yehuda' },
{ name: 'Alan', url: 'http://alan' }
]
{ name: 'Alan', url: 'http://alan' },
],
})
.withPartial('dude', '{{others.foo}}{{name}} ({{url}}) ')
.withMessage('Basic partials output based on current context.')
.toCompileTo('Dudes: barYehuda (http://yehuda) barAlan (http://alan) ');
});
it('partial in a partial', function() {
it('partial in a partial', function () {
expectTemplate('Dudes: {{#dudes}}{{>dude}}{{/dudes}}')
.withInput({
dudes: [
{ name: 'Yehuda', url: 'http://yehuda' },
{ name: 'Alan', url: 'http://alan' }
]
{ name: 'Alan', url: 'http://alan' },
],
})
.withPartials({
dude: '{{name}} {{> url}} ',
url: '<a href="{{url}}">{{url}}</a>'
url: '<a href="{{url}}">{{url}}</a>',
})
.withMessage('Partials are rendered inside of other partials')
.toCompileTo(
@@ -153,32 +156,29 @@ describe('partials', function() {
);
});
it('rendering undefined partial throws an exception', function() {
it('rendering undefined partial throws an exception', function () {
expectTemplate('{{> whatever}}').toThrow(
Handlebars.Exception,
'The partial whatever could not be found'
'The partial "whatever" could not be found'
);
});
it('registering undefined partial throws an exception', function() {
shouldThrow(
function() {
var undef;
handlebarsEnv.registerPartial('undefined_test', undef);
},
Handlebars.Exception,
it('registering undefined partial throws an exception', function () {
expect(function () {
handlebarsEnv.registerPartial('undefined_test', undefined);
}).toThrow(
'Attempting to register a partial called "undefined_test" as undefined'
);
});
it('rendering template partial in vm mode throws an exception', function() {
it('rendering template partial in vm mode throws an exception', function () {
expectTemplate('{{> whatever}}').toThrow(
Handlebars.Exception,
'The partial whatever could not be found'
'The partial "whatever" could not be found'
);
});
it('rendering function partial in vm mode', function() {
it('rendering function partial in vm mode', function () {
function partial(context) {
return context.name + ' (' + context.url + ') ';
}
@@ -186,15 +186,15 @@ describe('partials', function() {
.withInput({
dudes: [
{ name: 'Yehuda', url: 'http://yehuda' },
{ name: 'Alan', url: 'http://alan' }
]
{ name: 'Alan', url: 'http://alan' },
],
})
.withPartial('dude', partial)
.withMessage('Function partials output based in VM.')
.toCompileTo('Dudes: Yehuda (http://yehuda) Alan (http://alan) ');
});
it('GH-14: a partial preceding a selector', function() {
it('GH-14: a partial preceding a selector', function () {
expectTemplate('Dudes: {{>dude}} {{anotherDude}}')
.withInput({ name: 'Jeepers', anotherDude: 'Creepers' })
.withPartial('dude', '{{name}}')
@@ -202,7 +202,7 @@ describe('partials', function() {
.toCompileTo('Dudes: Jeepers Creepers');
});
it('Partials with slash paths', function() {
it('Partials with slash paths', function () {
expectTemplate('Dudes: {{> shared/dude}}')
.withInput({ name: 'Jeepers', anotherDude: 'Creepers' })
.withPartial('shared/dude', '{{name}}')
@@ -210,7 +210,7 @@ describe('partials', function() {
.toCompileTo('Dudes: Jeepers');
});
it('Partials with slash and point paths', function() {
it('Partials with slash and point paths', function () {
expectTemplate('Dudes: {{> shared/dude.thing}}')
.withInput({ name: 'Jeepers', anotherDude: 'Creepers' })
.withPartial('shared/dude.thing', '{{name}}')
@@ -218,7 +218,7 @@ describe('partials', function() {
.toCompileTo('Dudes: Jeepers');
});
it('Global Partials', function() {
it('Global Partials', function () {
handlebarsEnv.registerPartial('globalTest', '{{anotherDude}}');
expectTemplate('Dudes: {{> shared/dude}} {{> globalTest}}')
@@ -228,13 +228,13 @@ describe('partials', function() {
.toCompileTo('Dudes: Jeepers Creepers');
handlebarsEnv.unregisterPartial('globalTest');
equals(handlebarsEnv.partials.globalTest, undefined);
expect(handlebarsEnv.partials.globalTest).toBeUndefined();
});
it('Multiple partial registration', function() {
it('Multiple partial registration', function () {
handlebarsEnv.registerPartial({
'shared/dude': '{{name}}',
globalTest: '{{anotherDude}}'
globalTest: '{{anotherDude}}',
});
expectTemplate('Dudes: {{> shared/dude}} {{> globalTest}}')
@@ -244,7 +244,7 @@ describe('partials', function() {
.toCompileTo('Dudes: Jeepers Creepers');
});
it('Partials with integer path', function() {
it('Partials with integer path', function () {
expectTemplate('Dudes: {{> 404}}')
.withInput({ name: 'Jeepers', anotherDude: 'Creepers' })
.withPartial(404, '{{name}}')
@@ -252,7 +252,7 @@ describe('partials', function() {
.toCompileTo('Dudes: Jeepers');
});
it('Partials with complex path', function() {
it('Partials with complex path', function () {
expectTemplate('Dudes: {{> 404/asdf?.bar}}')
.withInput({ name: 'Jeepers', anotherDude: 'Creepers' })
.withPartial('404/asdf?.bar', '{{name}}')
@@ -260,7 +260,7 @@ describe('partials', function() {
.toCompileTo('Dudes: Jeepers');
});
it('Partials with escaped', function() {
it('Partials with escaped', function () {
expectTemplate('Dudes: {{> [+404/asdf?.bar]}}')
.withInput({ name: 'Jeepers', anotherDude: 'Creepers' })
.withPartial('+404/asdf?.bar', '{{name}}')
@@ -268,7 +268,7 @@ describe('partials', function() {
.toCompileTo('Dudes: Jeepers');
});
it('Partials with string', function() {
it('Partials with string', function () {
expectTemplate("Dudes: {{> '+404/asdf?.bar'}}")
.withInput({ name: 'Jeepers', anotherDude: 'Creepers' })
.withPartial('+404/asdf?.bar', '{{name}}')
@@ -276,19 +276,19 @@ describe('partials', function() {
.toCompileTo('Dudes: Jeepers');
});
it('should handle empty partial', function() {
it('should handle empty partial', function () {
expectTemplate('Dudes: {{#dudes}}{{> dude}}{{/dudes}}')
.withInput({
dudes: [
{ name: 'Yehuda', url: 'http://yehuda' },
{ name: 'Alan', url: 'http://alan' }
]
{ name: 'Alan', url: 'http://alan' },
],
})
.withPartial('dude', '')
.toCompileTo('Dudes: ');
});
it('throw on missing partial', function() {
it('throw on missing partial', function () {
var compile = handlebarsEnv.compile;
var compileWithPartial = CompilerContext.compileWithPartial;
handlebarsEnv.compile = undefined;
@@ -300,18 +300,18 @@ describe('partials', function() {
CompilerContext.compileWithPartial = compileWithPartial;
});
describe('partial blocks', function() {
it('should render partial block as default', function() {
describe('partial blocks', function () {
it('should render partial block as default', function () {
expectTemplate('{{#> dude}}success{{/dude}}').toCompileTo('success');
});
it('should execute default block with proper context', function() {
it('should execute default block with proper context', function () {
expectTemplate('{{#> dude context}}{{value}}{{/dude}}')
.withInput({ context: { value: 'success' } })
.toCompileTo('success');
});
it('should propagate block parameters to default block', function() {
it('should propagate block parameters to default block', function () {
expectTemplate(
'{{#with context as |me|}}{{#> dude}}{{me.value}}{{/dude}}{{/with}}'
)
@@ -319,79 +319,76 @@ describe('partials', function() {
.toCompileTo('success');
});
it('should not use partial block if partial exists', function() {
it('should not use partial block if partial exists', function () {
expectTemplate('{{#> dude}}fail{{/dude}}')
.withPartials({ dude: 'success' })
.toCompileTo('success');
});
it('should render block from partial', function() {
it('should render block from partial', function () {
expectTemplate('{{#> dude}}success{{/dude}}')
.withPartials({ dude: '{{> @partial-block }}' })
.toCompileTo('success');
});
it('should be able to render the partial-block twice', function() {
it('should be able to render the partial-block twice', function () {
expectTemplate('{{#> dude}}success{{/dude}}')
.withPartials({ dude: '{{> @partial-block }} {{> @partial-block }}' })
.toCompileTo('success success');
});
it('should render block from partial with context', function() {
it('should render block from partial with context', function () {
expectTemplate('{{#> dude}}{{value}}{{/dude}}')
.withInput({ context: { value: 'success' } })
.withPartials({
dude: '{{#with context}}{{> @partial-block }}{{/with}}'
dude: '{{#with context}}{{> @partial-block }}{{/with}}',
})
.toCompileTo('success');
});
it('should be able to access the @data frame from a partial-block', function() {
it('should be able to access the @data frame from a partial-block', function () {
expectTemplate('{{#> dude}}in-block: {{@root/value}}{{/dude}}')
.withInput({ value: 'success' })
.withPartials({
dude:
'<code>before-block: {{@root/value}} {{> @partial-block }}</code>'
dude: '<code>before-block: {{@root/value}} {{> @partial-block }}</code>',
})
.toCompileTo('<code>before-block: success in-block: success</code>');
});
it('should allow the #each-helper to be used along with partial-blocks', function() {
it('should allow the #each-helper to be used along with partial-blocks', function () {
expectTemplate(
'<template>{{#> list value}}value = {{.}}{{/list}}</template>'
)
.withInput({
value: ['a', 'b', 'c']
value: ['a', 'b', 'c'],
})
.withPartials({
list:
'<list>{{#each .}}<item>{{> @partial-block}}</item>{{/each}}</list>'
list: '<list>{{#each .}}<item>{{> @partial-block}}</item>{{/each}}</list>',
})
.toCompileTo(
'<template><list><item>value = a</item><item>value = b</item><item>value = c</item></list></template>'
);
});
it('should render block from partial with context (twice)', function() {
it('should render block from partial with context (twice)', function () {
expectTemplate('{{#> dude}}{{value}}{{/dude}}')
.withInput({ context: { value: 'success' } })
.withPartials({
dude:
'{{#with context}}{{> @partial-block }} {{> @partial-block }}{{/with}}'
dude: '{{#with context}}{{> @partial-block }} {{> @partial-block }}{{/with}}',
})
.toCompileTo('success success');
});
it('should render block from partial with context', function() {
it('should render block from partial with context', function () {
expectTemplate('{{#> dude}}{{../context/value}}{{/dude}}')
.withInput({ context: { value: 'success' } })
.withPartials({
dude: '{{#with context}}{{> @partial-block }}{{/with}}'
dude: '{{#with context}}{{> @partial-block }}{{/with}}',
})
.toCompileTo('success');
});
it('should render block from partial with block params', function() {
it('should render block from partial with block params', function () {
expectTemplate(
'{{#with context as |me|}}{{#> dude}}{{me.value}}{{/dude}}{{/with}}'
)
@@ -400,52 +397,52 @@ describe('partials', function() {
.toCompileTo('success');
});
it('should render nested partial blocks', function() {
it('should render nested partial blocks', function () {
expectTemplate('<template>{{#> outer}}{{value}}{{/outer}}</template>')
.withInput({ value: 'success' })
.withPartials({
outer:
'<outer>{{#> nested}}<outer-block>{{> @partial-block}}</outer-block>{{/nested}}</outer>',
nested: '<nested>{{> @partial-block}}</nested>'
nested: '<nested>{{> @partial-block}}</nested>',
})
.toCompileTo(
'<template><outer><nested><outer-block>success</outer-block></nested></outer></template>'
);
});
it('should render nested partial blocks at different nesting levels', function() {
it('should render nested partial blocks at different nesting levels', function () {
expectTemplate('<template>{{#> outer}}{{value}}{{/outer}}</template>')
.withInput({ value: 'success' })
.withPartials({
outer:
'<outer>{{#> nested}}<outer-block>{{> @partial-block}}</outer-block>{{/nested}}{{> @partial-block}}</outer>',
nested: '<nested>{{> @partial-block}}</nested>'
nested: '<nested>{{> @partial-block}}</nested>',
})
.toCompileTo(
'<template><outer><nested><outer-block>success</outer-block></nested>success</outer></template>'
);
});
it('should render nested partial blocks at different nesting levels (twice)', function() {
it('should render nested partial blocks at different nesting levels (twice)', function () {
expectTemplate('<template>{{#> outer}}{{value}}{{/outer}}</template>')
.withInput({ value: 'success' })
.withPartials({
outer:
'<outer>{{#> nested}}<outer-block>{{> @partial-block}} {{> @partial-block}}</outer-block>{{/nested}}{{> @partial-block}}+{{> @partial-block}}</outer>',
nested: '<nested>{{> @partial-block}}</nested>'
nested: '<nested>{{> @partial-block}}</nested>',
})
.toCompileTo(
'<template><outer><nested><outer-block>success success</outer-block></nested>success+success</outer></template>'
);
});
it('should render nested partial blocks (twice at each level)', function() {
it('should render nested partial blocks (twice at each level)', function () {
expectTemplate('<template>{{#> outer}}{{value}}{{/outer}}</template>')
.withInput({ value: 'success' })
.withPartials({
outer:
'<outer>{{#> nested}}<outer-block>{{> @partial-block}} {{> @partial-block}}</outer-block>{{/nested}}</outer>',
nested: '<nested>{{> @partial-block}}{{> @partial-block}}</nested>'
nested: '<nested>{{> @partial-block}}{{> @partial-block}}</nested>',
})
.toCompileTo(
'<template><outer>' +
@@ -455,60 +452,60 @@ describe('partials', function() {
});
});
describe('inline partials', function() {
it('should define inline partials for template', function() {
describe('inline partials', function () {
it('should define inline partials for template', function () {
expectTemplate(
'{{#*inline "myPartial"}}success{{/inline}}{{> myPartial}}'
).toCompileTo('success');
});
it('should overwrite multiple partials in the same template', function() {
it('should overwrite multiple partials in the same template', function () {
expectTemplate(
'{{#*inline "myPartial"}}fail{{/inline}}{{#*inline "myPartial"}}success{{/inline}}{{> myPartial}}'
).toCompileTo('success');
});
it('should define inline partials for block', function() {
it('should define inline partials for block', function () {
expectTemplate(
'{{#with .}}{{#*inline "myPartial"}}success{{/inline}}{{> myPartial}}{{/with}}'
).toCompileTo('success');
expectTemplate(
'{{#with .}}{{#*inline "myPartial"}}success{{/inline}}{{/with}}{{> myPartial}}'
).toThrow(Error, /myPartial could not/);
).toThrow(Error, /"myPartial" could not/);
});
it('should override global partials', function() {
it('should override global partials', function () {
expectTemplate(
'{{#*inline "myPartial"}}success{{/inline}}{{> myPartial}}'
)
.withPartials({
myPartial: function() {
myPartial: function () {
return 'fail';
}
},
})
.toCompileTo('success');
});
it('should override template partials', function() {
it('should override template partials', function () {
expectTemplate(
'{{#*inline "myPartial"}}fail{{/inline}}{{#with .}}{{#*inline "myPartial"}}success{{/inline}}{{> myPartial}}{{/with}}'
).toCompileTo('success');
});
it('should override partials down the entire stack', function() {
it('should override partials down the entire stack', function () {
expectTemplate(
'{{#with .}}{{#*inline "myPartial"}}success{{/inline}}{{#with .}}{{#with .}}{{> myPartial}}{{/with}}{{/with}}{{/with}}'
).toCompileTo('success');
});
it('should define inline partials for partial call', function() {
it('should define inline partials for partial call', function () {
expectTemplate('{{#*inline "myPartial"}}success{{/inline}}{{> dude}}')
.withPartials({ dude: '{{> myPartial }}' })
.toCompileTo('success');
});
it('should define inline partials in partial block call', function() {
it('should define inline partials in partial block call', function () {
expectTemplate(
'{{#> dude}}{{#*inline "myPartial"}}success{{/inline}}{{/dude}}'
)
@@ -516,7 +513,7 @@ describe('partials', function() {
.toCompileTo('success');
});
it('should render nested inline partials', function() {
it('should render nested inline partials', function () {
expectTemplate(
'{{#*inline "outer"}}{{#>inner}}<outer-block>{{>@partial-block}}</outer-block>{{/inner}}{{/inline}}' +
'{{#*inline "inner"}}<inner>{{>@partial-block}}</inner>{{/inline}}' +
@@ -526,7 +523,7 @@ describe('partials', function() {
.toCompileTo('<inner><outer-block>success</outer-block></inner>');
});
it('should render nested inline partials with partial-blocks on different nesting levels', function() {
it('should render nested inline partials with partial-blocks on different nesting levels', function () {
expectTemplate(
'{{#*inline "outer"}}{{#>inner}}<outer-block>{{>@partial-block}}</outer-block>{{/inner}}{{>@partial-block}}{{/inline}}' +
'{{#*inline "inner"}}<inner>{{>@partial-block}}</inner>{{/inline}}' +
@@ -538,7 +535,7 @@ describe('partials', function() {
);
});
it('should render nested inline partials (twice at each level)', function() {
it('should render nested inline partials (twice at each level)', function () {
expectTemplate(
'{{#*inline "outer"}}{{#>inner}}<outer-block>{{>@partial-block}} {{>@partial-block}}</outer-block>{{/inner}}{{/inline}}' +
'{{#*inline "inner"}}<inner>{{>@partial-block}}{{>@partial-block}}</inner>{{/inline}}' +
@@ -551,56 +548,56 @@ describe('partials', function() {
});
});
it('should pass compiler flags', function() {
it('should pass compiler flags', function () {
if (Handlebars.compile) {
var env = Handlebars.create();
env.registerPartial('partial', '{{foo}}');
var template = env.compile('{{foo}} {{> partial}}', { noEscape: true });
equal(template({ foo: '<' }), '< <');
expect(template({ foo: '<' })).toBe('< <');
}
});
describe('standalone partials', function() {
it('indented partials', function() {
describe('standalone partials', function () {
it('indented partials', function () {
expectTemplate('Dudes:\n{{#dudes}}\n {{>dude}}\n{{/dudes}}')
.withInput({
dudes: [
{ name: 'Yehuda', url: 'http://yehuda' },
{ name: 'Alan', url: 'http://alan' }
]
{ name: 'Alan', url: 'http://alan' },
],
})
.withPartial('dude', '{{name}}\n')
.toCompileTo('Dudes:\n Yehuda\n Alan\n');
});
it('nested indented partials', function() {
it('nested indented partials', function () {
expectTemplate('Dudes:\n{{#dudes}}\n {{>dude}}\n{{/dudes}}')
.withInput({
dudes: [
{ name: 'Yehuda', url: 'http://yehuda' },
{ name: 'Alan', url: 'http://alan' }
]
{ name: 'Alan', url: 'http://alan' },
],
})
.withPartials({
dude: '{{name}}\n {{> url}}',
url: '{{url}}!\n'
url: '{{url}}!\n',
})
.toCompileTo(
'Dudes:\n Yehuda\n http://yehuda!\n Alan\n http://alan!\n'
);
});
it('prevent nested indented partials', function() {
it('prevent nested indented partials', function () {
expectTemplate('Dudes:\n{{#dudes}}\n {{>dude}}\n{{/dudes}}')
.withInput({
dudes: [
{ name: 'Yehuda', url: 'http://yehuda' },
{ name: 'Alan', url: 'http://alan' }
]
{ name: 'Alan', url: 'http://alan' },
],
})
.withPartials({
dude: '{{name}}\n {{> url}}',
url: '{{url}}!\n'
url: '{{url}}!\n',
})
.withCompileOptions({ preventIndent: true })
.toCompileTo(
@@ -609,15 +606,15 @@ describe('partials', function() {
});
});
describe('compat mode', function() {
it('partials can access parents', function() {
describe('compat mode', function () {
it('partials can access parents', function () {
expectTemplate('Dudes: {{#dudes}}{{> dude}}{{/dudes}}')
.withInput({
root: 'yes',
dudes: [
{ name: 'Yehuda', url: 'http://yehuda' },
{ name: 'Alan', url: 'http://alan' }
]
{ name: 'Alan', url: 'http://alan' },
],
})
.withPartials({ dude: '{{name}} ({{url}}) {{root}} ' })
.withCompileOptions({ compat: true })
@@ -626,14 +623,14 @@ describe('partials', function() {
);
});
it('partials can access parents with custom context', function() {
it('partials can access parents with custom context', function () {
expectTemplate('Dudes: {{#dudes}}{{> dude "test"}}{{/dudes}}')
.withInput({
root: 'yes',
dudes: [
{ name: 'Yehuda', url: 'http://yehuda' },
{ name: 'Alan', url: 'http://alan' }
]
{ name: 'Alan', url: 'http://alan' },
],
})
.withPartials({ dude: '{{name}} ({{url}}) {{root}} ' })
.withCompileOptions({ compat: true })
@@ -642,14 +639,14 @@ describe('partials', function() {
);
});
it('partials can access parents without data', function() {
it('partials can access parents without data', function () {
expectTemplate('Dudes: {{#dudes}}{{> dude}}{{/dudes}}')
.withInput({
root: 'yes',
dudes: [
{ name: 'Yehuda', url: 'http://yehuda' },
{ name: 'Alan', url: 'http://alan' }
]
{ name: 'Alan', url: 'http://alan' },
],
})
.withPartials({ dude: '{{name}} ({{url}}) {{root}} ' })
.withRuntimeOptions({ data: false })
@@ -659,17 +656,17 @@ describe('partials', function() {
);
});
it('partials inherit compat', function() {
it('partials inherit compat', function () {
expectTemplate('Dudes: {{> dude}}')
.withInput({
root: 'yes',
dudes: [
{ name: 'Yehuda', url: 'http://yehuda' },
{ name: 'Alan', url: 'http://alan' }
]
{ name: 'Alan', url: 'http://alan' },
],
})
.withPartials({
dude: '{{#dudes}}{{name}} ({{url}}) {{root}} {{/dudes}}'
dude: '{{#dudes}}{{name}} ({{url}}) {{root}} {{/dudes}}',
})
.withCompileOptions({ compat: true })
.toCompileTo(
+217 -231
View File
@@ -1,5 +1,5 @@
/* eslint-disable no-console */
describe('precompiler', function() {
describe('precompiler', function () {
// NOP Under non-node environments
if (typeof process === 'undefined') {
return;
@@ -19,7 +19,7 @@ describe('precompiler', function() {
emptyTemplate = {
path: __dirname + '/artifacts/empty.handlebars',
name: 'empty',
source: ''
source: '',
},
file,
content,
@@ -33,19 +33,19 @@ describe('precompiler', function() {
* @param {Error} loadError the error that should be thrown if uglify is loaded
* @param {function} callback a callback-function to run when the mock is active.
*/
function mockRequireUglify(loadError, callback) {
async function mockRequireUglify(loadError, callback) {
var Module = require('module');
var _resolveFilename = Module._resolveFilename;
delete require.cache[require.resolve('uglify-js')];
delete require.cache[require.resolve('../dist/cjs/precompiler')];
Module._resolveFilename = function(request, mod) {
Module._resolveFilename = function (request, mod) {
if (request === 'uglify-js') {
throw loadError;
}
return _resolveFilename.call(this, request, mod);
};
try {
callback();
await callback();
} finally {
Module._resolveFilename = _resolveFilename;
delete require.cache[require.resolve('uglify-js')];
@@ -53,7 +53,7 @@ describe('precompiler', function() {
}
}
beforeEach(function() {
beforeEach(function () {
precompile = Handlebars.precompile;
minify = uglify.minify;
writeFileSync = fs.writeFileSync;
@@ -61,21 +61,21 @@ describe('precompiler', function() {
// Mock stdout and stderr
logFunction = console.log;
log = '';
console.log = function() {
console.log = function () {
log += Array.prototype.join.call(arguments, '');
};
errorLogFunction = console.error;
errorLog = '';
console.error = function() {
console.error = function () {
errorLog += Array.prototype.join.call(arguments, '');
};
fs.writeFileSync = function(_file, _content) {
fs.writeFileSync = function (_file, _content) {
file = _file;
content = _content;
};
});
afterEach(function() {
afterEach(function () {
Handlebars.precompile = precompile;
uglify.minify = minify;
fs.writeFileSync = writeFileSync;
@@ -83,326 +83,312 @@ describe('precompiler', function() {
console.error = errorLogFunction;
});
it('should output version', function() {
Precompiler.cli({ templates: [], version: true });
equals(log, Handlebars.VERSION);
it('should output version', async function () {
await Precompiler.cli({ templates: [], version: true });
expect(log).toBe(Handlebars.VERSION);
});
it('should throw if lacking templates', function() {
shouldThrow(
function() {
Precompiler.cli({ templates: [] });
},
Handlebars.Exception,
it('should throw if lacking templates', async function () {
await expect(Precompiler.cli({ templates: [] })).rejects.toThrow(
'Must define at least one template or directory.'
);
});
it('should handle empty/filtered directories', function() {
Precompiler.cli({ hasDirectory: true, templates: [] });
it('should handle empty/filtered directories', async function () {
Handlebars.precompile = function () {
return 'simple';
};
await Precompiler.cli({ hasDirectory: true, templates: [] });
// Success is not throwing
});
it('should throw when combining simple and minimized', function() {
shouldThrow(
function() {
Precompiler.cli({ templates: [__dirname], simple: true, min: true });
},
Handlebars.Exception,
'Unable to minimize simple output'
);
it('should throw when combining simple and minimized', async function () {
await expect(
Precompiler.cli({ templates: [__dirname], simple: true, min: true })
).rejects.toThrow('Unable to minimize simple output');
});
it('should throw when combining simple and multiple templates', function() {
shouldThrow(
function() {
Precompiler.cli({
templates: [
__dirname + '/artifacts/empty.handlebars',
__dirname + '/artifacts/empty.handlebars'
],
simple: true
});
},
Handlebars.Exception,
'Unable to output multiple templates in simple mode'
);
it('should throw when combining simple and multiple templates', async function () {
await expect(
Precompiler.cli({
templates: [
__dirname + '/artifacts/empty.handlebars',
__dirname + '/artifacts/empty.handlebars',
],
simple: true,
})
).rejects.toThrow('Unable to output multiple templates in simple mode');
});
it('should throw when missing name', function() {
shouldThrow(
function() {
Precompiler.cli({ templates: [{ source: '' }], amd: true });
},
Handlebars.Exception,
'Name missing for template'
);
it('should throw when missing name', async function () {
await expect(
Precompiler.cli({ templates: [{ source: '' }], amd: true })
).rejects.toThrow('Name missing for template');
});
it('should throw when combining simple and directories', function() {
shouldThrow(
function() {
Precompiler.cli({ hasDirectory: true, templates: [1], simple: true });
},
Handlebars.Exception,
'Unable to output multiple templates in simple mode'
);
it('should throw when combining simple and directories', async function () {
await expect(
Precompiler.cli({ hasDirectory: true, templates: [1], simple: true })
).rejects.toThrow('Unable to output multiple templates in simple mode');
});
it('should output simple templates', function() {
Handlebars.precompile = function() {
it('should output simple templates', async function () {
Handlebars.precompile = function () {
return 'simple';
};
Precompiler.cli({ templates: [emptyTemplate], simple: true });
equal(log, 'simple\n');
await Precompiler.cli({ templates: [emptyTemplate], simple: true });
expect(log).toBe('simple\n');
});
it('should default to simple templates', function() {
Handlebars.precompile = function() {
it('should default to simple templates', async function () {
Handlebars.precompile = function () {
return 'simple';
};
Precompiler.cli({ templates: [{ source: '' }] });
equal(log, 'simple\n');
await Precompiler.cli({ templates: [{ source: '' }] });
expect(log).toBe('simple\n');
});
it('should output amd templates', function() {
Handlebars.precompile = function() {
it('should output amd templates', async function () {
Handlebars.precompile = function () {
return 'amd';
};
Precompiler.cli({ templates: [emptyTemplate], amd: true });
equal(/template\(amd\)/.test(log), true);
await Precompiler.cli({ templates: [emptyTemplate], amd: true });
expect(log).toMatch(/template\(amd\)/);
});
it('should output multiple amd', function() {
Handlebars.precompile = function() {
it('should output multiple amd', async function () {
Handlebars.precompile = function () {
return 'amd';
};
Precompiler.cli({
await Precompiler.cli({
templates: [emptyTemplate, emptyTemplate],
amd: true,
namespace: 'foo'
namespace: 'foo',
});
equal(/templates = foo = foo \|\|/.test(log), true);
equal(/return templates/.test(log), true);
equal(/template\(amd\)/.test(log), true);
expect(log).toMatch(/templates = foo = foo \|\|/);
expect(log).toMatch(/return templates/);
expect(log).toMatch(/template\(amd\)/);
});
it('should output amd partials', function() {
Handlebars.precompile = function() {
it('should output amd partials', async function () {
Handlebars.precompile = function () {
return 'amd';
};
Precompiler.cli({ templates: [emptyTemplate], amd: true, partial: true });
equal(/return Handlebars\.partials\['empty'\]/.test(log), true);
equal(/template\(amd\)/.test(log), true);
await Precompiler.cli({
templates: [emptyTemplate],
amd: true,
partial: true,
});
expect(log).toMatch(/return Handlebars\.partials\['empty'\]/);
expect(log).toMatch(/template\(amd\)/);
});
it('should output multiple amd partials', function() {
Handlebars.precompile = function() {
it('should output multiple amd partials', async function () {
Handlebars.precompile = function () {
return 'amd';
};
Precompiler.cli({
await Precompiler.cli({
templates: [emptyTemplate, emptyTemplate],
amd: true,
partial: true
partial: true,
});
equal(/return Handlebars\.partials\[/.test(log), false);
equal(/template\(amd\)/.test(log), true);
expect(log).not.toMatch(/return Handlebars\.partials\[/);
expect(log).toMatch(/template\(amd\)/);
});
it('should output commonjs templates', function() {
Handlebars.precompile = function() {
it('should output commonjs templates', async function () {
Handlebars.precompile = function () {
return 'commonjs';
};
Precompiler.cli({ templates: [emptyTemplate], commonjs: true });
equal(/template\(commonjs\)/.test(log), true);
await Precompiler.cli({ templates: [emptyTemplate], commonjs: true });
expect(log).toMatch(/template\(commonjs\)/);
});
it('should set data flag', function() {
Handlebars.precompile = function(data, options) {
equal(options.data, true);
it('should set data flag', async function () {
Handlebars.precompile = function (data, options) {
expect(options.data).toBe(true);
return 'simple';
};
Precompiler.cli({ templates: [emptyTemplate], simple: true, data: true });
equal(log, 'simple\n');
});
it('should set known helpers', function() {
Handlebars.precompile = function(data, options) {
equal(options.knownHelpers.foo, true);
return 'simple';
};
Precompiler.cli({ templates: [emptyTemplate], simple: true, known: 'foo' });
equal(log, 'simple\n');
});
it('should output to file system', function() {
Handlebars.precompile = function() {
return 'simple';
};
Precompiler.cli({
await Precompiler.cli({
templates: [emptyTemplate],
simple: true,
output: 'file!'
data: true,
});
equal(file, 'file!');
equal(content, 'simple\n');
equal(log, '');
expect(log).toBe('simple\n');
});
it('should output minimized templates', function() {
Handlebars.precompile = function() {
it('should set known helpers', async function () {
Handlebars.precompile = function (data, options) {
expect(options.knownHelpers.foo).toBe(true);
return 'simple';
};
await Precompiler.cli({
templates: [emptyTemplate],
simple: true,
known: 'foo',
});
expect(log).toBe('simple\n');
});
it('should output to file system', async function () {
Handlebars.precompile = function () {
return 'simple';
};
await Precompiler.cli({
templates: [emptyTemplate],
simple: true,
output: 'file!',
});
expect(file).toBe('file!');
expect(content).toBe('simple\n');
expect(log).toBe('');
});
it('should output minimized templates', async function () {
Handlebars.precompile = function () {
return 'amd';
};
uglify.minify = function() {
uglify.minify = function () {
return { code: 'min' };
};
Precompiler.cli({ templates: [emptyTemplate], min: true });
equal(log, 'min');
await Precompiler.cli({ templates: [emptyTemplate], min: true });
expect(log).toBe('min');
});
it('should omit minimization gracefully, if uglify-js is missing', function() {
it('should omit minimization gracefully, if uglify-js is missing', async function () {
var error = new Error("Cannot find module 'uglify-js'");
error.code = 'MODULE_NOT_FOUND';
mockRequireUglify(error, function() {
await mockRequireUglify(error, async function () {
var Precompiler = require('../dist/cjs/precompiler');
Handlebars.precompile = function() {
Handlebars.precompile = function () {
return 'amd';
};
Precompiler.cli({ templates: [emptyTemplate], min: true });
equal(/template\(amd\)/.test(log), true);
equal(/\n/.test(log), true);
equal(/Code minimization is disabled/.test(errorLog), true);
await Precompiler.cli({ templates: [emptyTemplate], min: true });
expect(log).toMatch(/template\(amd\)/);
expect(log).toMatch(/\n/);
expect(errorLog).toMatch(/Code minimization is disabled/);
});
});
it('should fail on errors (other than missing module) while loading uglify-js', function() {
mockRequireUglify(new Error('Mock Error'), function() {
shouldThrow(
function() {
var Precompiler = require('../dist/cjs/precompiler');
Handlebars.precompile = function() {
return 'amd';
};
Precompiler.cli({ templates: [emptyTemplate], min: true });
},
Error,
'Mock Error'
);
it('should fail on errors (other than missing module) while loading uglify-js', async function () {
await mockRequireUglify(new Error('Mock Error'), async function () {
var Precompiler = require('../dist/cjs/precompiler');
Handlebars.precompile = function () {
return 'amd';
};
await expect(
Precompiler.cli({ templates: [emptyTemplate], min: true })
).rejects.toThrow('Mock Error');
});
});
it('should output map', function() {
Precompiler.cli({ templates: [emptyTemplate], map: 'foo.js.map' });
it('should output map', async function () {
await Precompiler.cli({ templates: [emptyTemplate], map: 'foo.js.map' });
equal(file, 'foo.js.map');
equal(log.match(/sourceMappingURL=/g).length, 1);
expect(file).toBe('foo.js.map');
expect(log.match(/sourceMappingURL=/g).length).toBe(1);
});
it('should output map', function() {
Precompiler.cli({
it('should output map with minification', async function () {
await Precompiler.cli({
templates: [emptyTemplate],
min: true,
map: 'foo.js.map'
map: 'foo.js.map',
});
equal(file, 'foo.js.map');
equal(log.match(/sourceMappingURL=/g).length, 1);
expect(file).toBe('foo.js.map');
expect(log.match(/sourceMappingURL=/g).length).toBe(1);
});
describe('#loadTemplates', function() {
it('should throw on missing template', function(done) {
Precompiler.loadTemplates({ files: ['foo'] }, function(err) {
equal(err.message, 'Unable to open template file "foo"');
done();
describe('#loadTemplates', function () {
function loadTemplatesAsync(inputOpts) {
return new Promise(function (resolve, reject) {
Precompiler.loadTemplates(inputOpts, function (err, opts) {
if (err) {
reject(err);
} else {
resolve(opts);
}
});
});
});
it('should enumerate directories by extension', function(done) {
Precompiler.loadTemplates(
{ files: [__dirname + '/artifacts'], extension: 'hbs' },
function(err, opts) {
equal(opts.templates.length, 2);
equal(opts.templates[0].name, 'example_2');
}
done(err);
}
);
it('should throw on missing template', async function () {
try {
await loadTemplatesAsync({ files: ['foo'] });
throw new Error('should have thrown');
} catch (err) {
expect(err.message).toBe('Unable to open template file "foo"');
}
});
it('should enumerate all templates by extension', function(done) {
Precompiler.loadTemplates(
{ files: [__dirname + '/artifacts'], extension: 'handlebars' },
function(err, opts) {
equal(opts.templates.length, 5);
equal(opts.templates[0].name, 'bom');
equal(opts.templates[1].name, 'empty');
equal(opts.templates[2].name, 'example_1');
done(err);
}
);
it('should enumerate directories by extension', async function () {
var opts = await loadTemplatesAsync({
files: [__dirname + '/artifacts'],
extension: 'hbs',
});
expect(opts.templates.length).toBe(2);
expect(opts.templates[0].name).toBe('example_2');
});
it('should handle regular expression characters in extensions', function(done) {
Precompiler.loadTemplates(
{ files: [__dirname + '/artifacts'], extension: 'hb(s' },
function(err) {
// Success is not throwing
done(err);
}
);
it('should enumerate all templates by extension', async function () {
var opts = await loadTemplatesAsync({
files: [__dirname + '/artifacts'],
extension: 'handlebars',
});
expect(opts.templates.length).toBe(5);
expect(opts.templates[0].name).toBe('bom');
expect(opts.templates[1].name).toBe('empty');
expect(opts.templates[2].name).toBe('example_1');
});
it('should handle BOM', function(done) {
var opts = {
it('should handle regular expression characters in extensions', async function () {
await loadTemplatesAsync({
files: [__dirname + '/artifacts'],
extension: 'hb(s',
});
// Success is not throwing
});
it('should handle BOM', async function () {
var opts = await loadTemplatesAsync({
files: [__dirname + '/artifacts/bom.handlebars'],
extension: 'handlebars',
bom: true
};
Precompiler.loadTemplates(opts, function(err, opts) {
equal(opts.templates[0].source, 'a');
done(err);
bom: true,
});
expect(opts.templates[0].source).toBe('a');
});
it('should handle different root', function(done) {
var opts = {
it('should handle different root', async function () {
var opts = await loadTemplatesAsync({
files: [__dirname + '/artifacts/empty.handlebars'],
simple: true,
root: 'foo/'
};
Precompiler.loadTemplates(opts, function(err, opts) {
equal(opts.templates[0].name, __dirname + '/artifacts/empty');
done(err);
root: 'foo/',
});
expect(opts.templates[0].name).toBe(__dirname + '/artifacts/empty');
});
it('should accept string inputs', function(done) {
var opts = { string: '' };
Precompiler.loadTemplates(opts, function(err, opts) {
equal(opts.templates[0].name, undefined);
equal(opts.templates[0].source, '');
done(err);
});
it('should accept string inputs', async function () {
var opts = await loadTemplatesAsync({ string: '' });
expect(opts.templates[0].name).toBeUndefined();
expect(opts.templates[0].source).toBe('');
});
it('should accept string array inputs', function(done) {
var opts = { string: ['', 'bar'], name: ['beep', 'boop'] };
Precompiler.loadTemplates(opts, function(err, opts) {
equal(opts.templates[0].name, 'beep');
equal(opts.templates[0].source, '');
equal(opts.templates[1].name, 'boop');
equal(opts.templates[1].source, 'bar');
done(err);
it('should accept string array inputs', async function () {
var opts = await loadTemplatesAsync({
string: ['', 'bar'],
name: ['beep', 'boop'],
});
expect(opts.templates[0].name).toBe('beep');
expect(opts.templates[0].source).toBe('');
expect(opts.templates[1].name).toBe('boop');
expect(opts.templates[1].source).toBe('bar');
});
it('should accept stdin input', function(done) {
it('should accept stdin input', async function () {
var stdin = require('mock-stdin').stdin();
Precompiler.loadTemplates({ string: '-' }, function(err, opts) {
equal(opts.templates[0].source, 'foo');
done(err);
});
var promise = loadTemplatesAsync({ string: '-' });
stdin.send('fo');
stdin.send('o');
stdin.end();
var opts = await promise;
expect(opts.templates[0].source).toBe('foo');
});
it('error on name missing', function(done) {
var opts = { string: ['', 'bar'] };
Precompiler.loadTemplates(opts, function(err) {
equal(
err.message,
it('error on name missing', async function () {
try {
await loadTemplatesAsync({ string: ['', 'bar'] });
throw new Error('should have thrown');
} catch (err) {
expect(err.message).toBe(
'Number of names did not match the number of string inputs'
);
done();
});
}
});
it('should complete when no args are passed', function(done) {
Precompiler.loadTemplates({}, function(err, opts) {
equal(opts.templates.length, 0);
done(err);
});
it('should complete when no args are passed', async function () {
var opts = await loadTemplatesAsync({});
expect(opts.templates.length).toBe(0);
});
});
});
+111 -109
View File
@@ -1,24 +1,24 @@
describe('Regressions', function() {
it('GH-94: Cannot read property of undefined', function() {
describe('Regressions', function () {
it('GH-94: Cannot read property of undefined', function () {
expectTemplate('{{#books}}{{title}}{{author.name}}{{/books}}')
.withInput({
books: [
{
title: 'The origin of species',
author: {
name: 'Charles Darwin'
}
name: 'Charles Darwin',
},
},
{
title: 'Lazarillo de Tormes'
}
]
title: 'Lazarillo de Tormes',
},
],
})
.withMessage('Renders without an undefined property error')
.toCompileTo('The origin of speciesCharles DarwinLazarillo de Tormes');
});
it("GH-150: Inverted sections print when they shouldn't", function() {
it("GH-150: Inverted sections print when they shouldn't", function () {
var string = '{{^set}}not set{{/set}} :: {{#set}}set{{/set}}';
expectTemplate(string)
@@ -43,14 +43,14 @@ describe('Regressions', function() {
.toCompileTo(' :: set');
});
it('GH-158: Using array index twice, breaks the template', function() {
it('GH-158: Using array index twice, breaks the template', function () {
expectTemplate('{{arr.[0]}}, {{arr.[1]}}')
.withInput({ arr: [1, 2] })
.withMessage('it works as expected')
.toCompileTo('1, 2');
});
it("bug reported by @fat where lambdas weren't being properly resolved", function() {
it("bug reported by @fat where lambdas weren't being properly resolved", function () {
var string =
'<strong>This is a slightly more complicated {{thing}}.</strong>.\n' +
'{{! Just ignore this business. }}\n' +
@@ -67,17 +67,17 @@ describe('Regressions', function() {
'{{/hasThings}}';
var data = {
thing: function() {
thing: function () {
return 'blah';
},
things: [
{ className: 'one', word: '@fat' },
{ className: 'two', word: '@dhg' },
{ className: 'three', word: '@sayrer' }
{ className: 'three', word: '@sayrer' },
],
hasThings: function() {
hasThings: function () {
return true;
}
},
};
var output =
@@ -89,36 +89,34 @@ describe('Regressions', function() {
'<li class=three>@sayrer</li>\n' +
'</ul>.\n';
expectTemplate(string)
.withInput(data)
.toCompileTo(output);
expectTemplate(string).withInput(data).toCompileTo(output);
});
it('GH-408: Multiple loops fail', function() {
it('GH-408: Multiple loops fail', function () {
expectTemplate(
'{{#.}}{{name}}{{/.}}{{#.}}{{name}}{{/.}}{{#.}}{{name}}{{/.}}'
)
.withInput([
{ name: 'John Doe', location: { city: 'Chicago' } },
{ name: 'Jane Doe', location: { city: 'New York' } }
{ name: 'Jane Doe', location: { city: 'New York' } },
])
.withMessage('It should output multiple times')
.toCompileTo('John DoeJane DoeJohn DoeJane DoeJohn DoeJane Doe');
});
it('GS-428: Nested if else rendering', function() {
it('GS-428: Nested if else rendering', function () {
var succeedingTemplate =
'{{#inverse}} {{#blk}} Unexpected {{/blk}} {{else}} {{#blk}} Expected {{/blk}} {{/inverse}}';
var failingTemplate =
'{{#inverse}} {{#blk}} Unexpected {{/blk}} {{else}} {{#blk}} Expected {{/blk}} {{/inverse}}';
var helpers = {
blk: function(block) {
blk: function (block) {
return block.fn('');
},
inverse: function(block) {
inverse: function (block) {
return block.inverse('');
}
},
};
expectTemplate(succeedingTemplate)
@@ -130,34 +128,30 @@ describe('Regressions', function() {
.toCompileTo(' Expected ');
});
it('GH-458: Scoped this identifier', function() {
expectTemplate('{{./foo}}')
.withInput({ foo: 'bar' })
.toCompileTo('bar');
it('GH-458: Scoped this identifier', function () {
expectTemplate('{{./foo}}').withInput({ foo: 'bar' }).toCompileTo('bar');
});
it('GH-375: Unicode line terminators', function() {
it('GH-375: Unicode line terminators', function () {
expectTemplate('\u2028').toCompileTo('\u2028');
});
it('GH-534: Object prototype aliases', function() {
it('GH-534: Object prototype aliases', function () {
/* eslint-disable no-extend-native */
Object.prototype[0xd834] = true;
expectTemplate('{{foo}}')
.withInput({ foo: 'bar' })
.toCompileTo('bar');
expectTemplate('{{foo}}').withInput({ foo: 'bar' }).toCompileTo('bar');
delete Object.prototype[0xd834];
/* eslint-enable no-extend-native */
});
it('GH-437: Matching escaping', function() {
it('GH-437: Matching escaping', function () {
expectTemplate('{{{a}}').toThrow(Error, /Parse error on/);
expectTemplate('{{a}}}').toThrow(Error, /Parse error on/);
});
it('GH-676: Using array in escaping mustache fails', function() {
it('GH-676: Using array in escaping mustache fails', function () {
var data = { arr: [1, 2] };
expectTemplate('{{arr}}')
@@ -166,7 +160,7 @@ describe('Regressions', function() {
.toCompileTo(data.arr.toString());
});
it('Mustache man page', function() {
it('Mustache man page', function () {
expectTemplate(
'Hello {{name}}. You have just won ${{value}}!{{#in_ca}} Well, ${{taxed_value}}, after taxes.{{/in_ca}}'
)
@@ -174,7 +168,7 @@ describe('Regressions', function() {
name: 'Chris',
value: 10000,
taxed_value: 10000 - 10000 * 0.4,
in_ca: true
in_ca: true,
})
.withMessage('the hello world mustache example works')
.toCompileTo(
@@ -182,69 +176,67 @@ describe('Regressions', function() {
);
});
it('GH-731: zero context rendering', function() {
it('GH-731: zero context rendering', function () {
expectTemplate('{{#foo}} This is {{bar}} ~ {{/foo}}')
.withInput({
foo: 0,
bar: 'OK'
bar: 'OK',
})
.toCompileTo(' This is ~ ');
});
it('GH-820: zero pathed rendering', function() {
expectTemplate('{{foo.bar}}')
.withInput({ foo: 0 })
.toCompileTo('');
it('GH-820: zero pathed rendering', function () {
expectTemplate('{{foo.bar}}').withInput({ foo: 0 }).toCompileTo('');
});
it('GH-837: undefined values for helpers', function() {
it('GH-837: undefined values for helpers', function () {
expectTemplate('{{str bar.baz}}')
.withHelpers({
str: function(value) {
str: function (value) {
return value + '';
}
},
})
.toCompileTo('undefined');
});
it('GH-926: Depths and de-dupe', function() {
it('GH-926: Depths and de-dupe', function () {
expectTemplate(
'{{#if dater}}{{#each data}}{{../name}}{{/each}}{{else}}{{#each notData}}{{../name}}{{/each}}{{/if}}'
)
.withInput({
name: 'foo',
data: [1],
notData: [1]
notData: [1],
})
.toCompileTo('foo');
});
it('GH-1021: Each empty string key', function() {
it('GH-1021: Each empty string key', function () {
expectTemplate('{{#each data}}Key: {{@key}}\n{{/each}}')
.withInput({
data: {
'': 'foo',
name: 'Chris',
value: 10000
}
value: 10000,
},
})
.toCompileTo('Key: \nKey: name\nKey: value\n');
});
it('GH-1054: Should handle simple safe string responses', function() {
it('GH-1054: Should handle simple safe string responses', function () {
expectTemplate('{{#wrap}}{{>partial}}{{/wrap}}')
.withHelpers({
wrap: function(options) {
wrap: function (options) {
return new Handlebars.SafeString(options.fn());
}
},
})
.withPartials({
partial: '{{#wrap}}<partial>{{/wrap}}'
partial: '{{#wrap}}<partial>{{/wrap}}',
})
.toCompileTo('<partial>');
});
it('GH-1065: Sparse arrays', function() {
it('GH-1065: Sparse arrays', function () {
var array = [];
array[1] = 'foo';
array[3] = 'bar';
@@ -253,11 +245,11 @@ describe('Regressions', function() {
.toCompileTo('1foo3bar');
});
it('GH-1093: Undefined helper context', function() {
it('GH-1093: Undefined helper context', function () {
expectTemplate('{{#each obj}}{{{helper}}}{{.}}{{/each}}')
.withInput({ obj: { foo: undefined, bar: 'bat' } })
.withHelpers({
helper: function() {
helper: function () {
// It's valid to execute a block against an undefined context, but
// helpers can not do so, so we expect to have an empty object here;
for (var name in this) {
@@ -267,43 +259,43 @@ describe('Regressions', function() {
}
// And to make IE happy, check for the known string as length is not enumerated.
return this === 'bat' ? 'found' : 'not';
}
},
})
.toCompileTo('notfoundbat');
});
it('should support multiple levels of inline partials', function() {
it('should support multiple levels of inline partials', function () {
expectTemplate(
'{{#> layout}}{{#*inline "subcontent"}}subcontent{{/inline}}{{/layout}}'
)
.withPartials({
doctype: 'doctype{{> content}}',
layout:
'{{#> doctype}}{{#*inline "content"}}layout{{> subcontent}}{{/inline}}{{/doctype}}'
'{{#> doctype}}{{#*inline "content"}}layout{{> subcontent}}{{/inline}}{{/doctype}}',
})
.toCompileTo('doctypelayoutsubcontent');
});
it('GH-1089: should support failover content in multiple levels of inline partials', function() {
it('GH-1089: should support failover content in multiple levels of inline partials', function () {
expectTemplate('{{#> layout}}{{/layout}}')
.withPartials({
doctype: 'doctype{{> content}}',
layout:
'{{#> doctype}}{{#*inline "content"}}layout{{#> subcontent}}subcontent{{/subcontent}}{{/inline}}{{/doctype}}'
'{{#> doctype}}{{#*inline "content"}}layout{{#> subcontent}}subcontent{{/subcontent}}{{/inline}}{{/doctype}}',
})
.toCompileTo('doctypelayoutsubcontent');
});
it('GH-1099: should support greater than 3 nested levels of inline partials', function() {
it('GH-1099: should support greater than 3 nested levels of inline partials', function () {
expectTemplate('{{#> layout}}Outer{{/layout}}')
.withPartials({
layout: '{{#> inner}}Inner{{/inner}}{{> @partial-block }}',
inner: ''
inner: '',
})
.toCompileTo('Outer');
});
it('GH-1135 : Context handling within each iteration', function() {
it('GH-1135 : Context handling within each iteration', function () {
expectTemplate(
'{{#each array}}\n' +
' 1. IF: {{#if true}}{{../name}}-{{../../name}}-{{../../../name}}{{/if}}\n' +
@@ -312,18 +304,18 @@ describe('Regressions', function() {
)
.withInput({ array: [1], name: 'John' })
.withHelpers({
myif: function(conditional, options) {
myif: function (conditional, options) {
if (conditional) {
return options.fn(this);
} else {
return options.inverse(this);
}
}
},
})
.toCompileTo(' 1. IF: John--\n' + ' 2. MYIF: John==\n');
});
it('GH-1186: Support block params for existing programs', function() {
it('GH-1186: Support block params for existing programs', function () {
expectTemplate(
'{{#*inline "test"}}{{> @partial-block }}{{/inline}}' +
'{{#>test }}{{#each listOne as |item|}}{{ item }}{{/each}}{{/test}}' +
@@ -331,62 +323,72 @@ describe('Regressions', function() {
)
.withInput({
listOne: ['a'],
listTwo: ['b']
listTwo: ['b'],
})
.withMessage('')
.toCompileTo('ab');
});
it('GH-1319: "unless" breaks when "each" value equals "null"', function() {
it('should allow hash with protected array names', function () {
var obj = { array: [1], name: 'John' };
var helpers = {
helpa: function (options) {
return options.hash.length;
},
};
expectTemplate('{{helpa length="foo"}}')
.withInput(obj)
.withHelpers(helpers)
.toCompileTo('foo');
});
it('GH-1319: "unless" breaks when "each" value equals "null"', function () {
expectTemplate(
'{{#each list}}{{#unless ./prop}}parent={{../value}} {{/unless}}{{/each}}'
)
.withInput({
value: 'parent',
list: [null, 'a']
list: [null, 'a'],
})
.withMessage('')
.toCompileTo('parent=parent parent=parent ');
});
it('GH-1341: 4.0.7 release breaks {{#if @partial-block}} usage', function() {
it('GH-1341: 4.0.7 release breaks {{#if @partial-block}} usage', function () {
expectTemplate('template {{>partial}} template')
.withPartials({
partialWithBlock:
'{{#if @partial-block}} block {{> @partial-block}} block {{/if}}',
partial: '{{#> partialWithBlock}} partial {{/partialWithBlock}}'
partial: '{{#> partialWithBlock}} partial {{/partialWithBlock}}',
})
.toCompileTo('template block partial block template');
});
describe('GH-1561: 4.3.x should still work with precompiled templates from 4.0.0 <= x < 4.3.0', function() {
it('should compile and execute templates', function() {
describe('GH-1561: 4.3.x should still work with precompiled templates from 4.0.0 <= x < 4.3.0', function () {
it('should compile and execute templates', function () {
var newHandlebarsInstance = Handlebars.create();
registerTemplate(newHandlebarsInstance, compiledTemplateVersion7());
newHandlebarsInstance.registerHelper('loud', function(value) {
newHandlebarsInstance.registerHelper('loud', function (value) {
return value.toUpperCase();
});
var result = newHandlebarsInstance.templates['test.hbs']({
name: 'yehuda'
name: 'yehuda',
});
equals(result.trim(), 'YEHUDA');
expect(result.trim()).toBe('YEHUDA');
});
it('should call "helperMissing" if a helper is missing', function() {
it('should call "helperMissing" if a helper is missing', function () {
var newHandlebarsInstance = Handlebars.create();
shouldThrow(
function() {
registerTemplate(newHandlebarsInstance, compiledTemplateVersion7());
newHandlebarsInstance.templates['test.hbs']({});
},
Handlebars.Exception,
'Missing helper: "loud"'
);
expect(function () {
registerTemplate(newHandlebarsInstance, compiledTemplateVersion7());
newHandlebarsInstance.templates['test.hbs']({});
}).toThrow('Missing helper: "loud"');
});
it('should pass "options.lookupProperty" to "lookup"-helper, even with old templates', function() {
it('should pass "options.lookupProperty" to "lookup"-helper, even with old templates', function () {
var newHandlebarsInstance = Handlebars.create();
registerTemplate(
newHandlebarsInstance,
@@ -398,9 +400,9 @@ describe('Regressions', function() {
expect(
newHandlebarsInstance.templates['test.hbs']({
property: 'a',
test: { a: 'b' }
test: { a: 'b' },
})
).to.equal('b');
).toBe('b');
});
function registerTemplate(Handlebars, compileTemplate) {
@@ -412,7 +414,7 @@ describe('Regressions', function() {
function compiledTemplateVersion7() {
return {
compiler: [7, '>= 4.0.0'],
main: function(container, depth0, helpers, partials, data) {
main: function (container, depth0, helpers, partials, data) {
return (
container.escapeExpression(
(
@@ -427,7 +429,7 @@ describe('Regressions', function() {
) + '\n\n'
);
},
useData: true
useData: true,
};
}
@@ -435,7 +437,7 @@ describe('Regressions', function() {
// This is the compiled version of "{{lookup test property}}"
return {
compiler: [7, '>= 4.0.0'],
main: function(container, depth0, helpers, partials, data) {
main: function (container, depth0, helpers, partials, data) {
return container.escapeExpression(
helpers.lookup.call(
depth0 != null ? depth0 : container.nullContext || {},
@@ -444,56 +446,56 @@ describe('Regressions', function() {
{
name: 'lookup',
hash: {},
data: data
data: data,
}
)
);
},
useData: true
useData: true,
};
}
});
it('should allow hash with protected array names', function() {
it('should allow hash with protected array names', function () {
expectTemplate('{{helpa length="foo"}}')
.withInput({ array: [1], name: 'John' })
.withHelpers({
helpa: function(options) {
helpa: function (options) {
return options.hash.length;
}
},
})
.toCompileTo('foo');
});
describe('GH-1598: Performance degradation for partials since v4.3.0', function() {
describe('GH-1598: Performance degradation for partials since v4.3.0', function () {
// Do not run test for runs without compiler
if (!Handlebars.compile) {
return;
}
var newHandlebarsInstance;
beforeEach(function() {
beforeEach(function () {
newHandlebarsInstance = Handlebars.create();
});
afterEach(function() {
sinon.restore();
afterEach(function () {
vi.restoreAllMocks();
});
it('should only compile global partials once', function() {
var templateSpy = sinon.spy(newHandlebarsInstance, 'template');
it('should only compile global partials once', function () {
var templateSpy = vi.spyOn(newHandlebarsInstance, 'template');
newHandlebarsInstance.registerPartial({
dude: 'I am a partial'
dude: 'I am a partial',
});
var string = 'Dudes: {{> dude}} {{> dude}}';
newHandlebarsInstance.compile(string)(); // This should compile template + partial once
newHandlebarsInstance.compile(string)(); // This should only compile template
equal(templateSpy.callCount, 3);
sinon.restore();
expect(templateSpy).toHaveBeenCalledTimes(3);
vi.restoreAllMocks();
});
});
describe("GH-1639: TypeError: Cannot read property 'apply' of undefined\" when handlebars version > 4.6.0 (undocumented, deprecated usage)", function() {
it('should treat undefined helpers like non-existing helpers', function() {
describe("GH-1639: TypeError: Cannot read property 'apply' of undefined\" when handlebars version > 4.6.0 (undocumented, deprecated usage)", function () {
it('should treat undefined helpers like non-existing helpers', function () {
expectTemplate('{{foo}}')
.withHelper('foo', undefined)
.withInput({ foo: 'bar' })
+7 -7
View File
@@ -1,23 +1,23 @@
if (typeof require !== 'undefined' && require.extensions['.handlebars']) {
describe('Require', function() {
it('Load .handlebars files with require()', function() {
describe('Require', function () {
it('Load .handlebars files with require()', function () {
var template = require('./artifacts/example_1');
equal(template, require('./artifacts/example_1.handlebars'));
expect(template).toBe(require('./artifacts/example_1.handlebars'));
var expected = 'foo\n';
var result = template({ foo: 'foo' });
equal(result, expected);
expect(result).toBe(expected);
});
it('Load .hbs files with require()', function() {
it('Load .hbs files with require()', function () {
var template = require('./artifacts/example_2');
equal(template, require('./artifacts/example_2.hbs'));
expect(template).toBe(require('./artifacts/example_2.hbs'));
var expected = 'Hello, World!\n';
var result = template({ name: 'World' });
equal(result, expected);
expect(result).toBe(expected);
});
});
}
+37 -101
View File
@@ -1,119 +1,55 @@
describe('runtime', function() {
describe('#template', function() {
it('should throw on invalid templates', function() {
shouldThrow(
function() {
Handlebars.template({});
},
Error,
'Unknown template object: object'
);
shouldThrow(
function() {
Handlebars.template();
},
Error,
'Unknown template object: undefined'
);
shouldThrow(
function() {
Handlebars.template('');
},
Error,
'Unknown template object: string'
);
describe('runtime', function () {
describe('#template', function () {
it('should throw on invalid templates', function () {
expect(function () {
Handlebars.template({});
}).toThrow('Unknown template object: object');
expect(function () {
Handlebars.template();
}).toThrow('Unknown template object: undefined');
expect(function () {
Handlebars.template('');
}).toThrow('Unknown template object: string');
});
it('should throw on version mismatch', function() {
shouldThrow(
function() {
Handlebars.template({
main: {},
compiler: [Handlebars.COMPILER_REVISION + 1]
});
},
Error,
it('should throw on version mismatch', function () {
expect(function () {
Handlebars.template({
main: {},
compiler: [Handlebars.COMPILER_REVISION + 1],
});
}).toThrow(
/Template was precompiled with a newer version of Handlebars than the current runtime/
);
shouldThrow(
function() {
Handlebars.template({
main: {},
compiler: [Handlebars.LAST_COMPATIBLE_COMPILER_REVISION - 1]
});
},
Error,
expect(function () {
Handlebars.template({
main: {},
compiler: [Handlebars.LAST_COMPATIBLE_COMPILER_REVISION - 1],
});
}).toThrow(
/Template was precompiled with an older version of Handlebars than the current runtime/
);
shouldThrow(
function() {
Handlebars.template({
main: {}
});
},
Error,
expect(function () {
Handlebars.template({
main: {},
});
}).toThrow(
/Template was precompiled with an older version of Handlebars than the current runtime/
);
});
});
describe('#child', function() {
if (!Handlebars.compile) {
return;
}
it('should throw for depthed methods without depths', function() {
shouldThrow(
function() {
var template = Handlebars.compile('{{#foo}}{{../bar}}{{/foo}}');
// Calling twice to hit the non-compiled case.
template._setup({});
template._setup({});
template._child(1);
},
Error,
'must pass parent depths'
);
});
it('should throw for block param methods without params', function() {
shouldThrow(
function() {
var template = Handlebars.compile('{{#foo as |foo|}}{{foo}}{{/foo}}');
// Calling twice to hit the non-compiled case.
template._setup({});
template._setup({});
template._child(1);
},
Error,
'must pass block params'
);
});
it('should expose child template', function() {
var template = Handlebars.compile('{{#foo}}bar{{/foo}}');
// Calling twice to hit the non-compiled case.
equal(template._child(1)(), 'bar');
equal(template._child(1)(), 'bar');
});
it('should render depthed content', function() {
var template = Handlebars.compile('{{#foo}}{{../bar}}{{/foo}}');
// Calling twice to hit the non-compiled case.
equal(template._child(1, undefined, [], [{ bar: 'baz' }])(), 'baz');
});
});
describe('#noConflict', function() {
if (!CompilerContext.browser) {
return;
}
it('should reset on no conflict', function() {
describe('#noConflict', function () {
it('should reset on no conflict', function () {
if (!CompilerContext.browser) {
return;
}
var reset = Handlebars;
Handlebars.noConflict();
equal(Handlebars, 'no-conflict');
expect(Handlebars).toBe('no-conflict');
Handlebars = 'really, none';
reset.noConflict();
equal(Handlebars, 'really, none');
expect(Handlebars).toBe('really, none');
Handlebars = reset;
});
+151 -126
View File
@@ -1,25 +1,23 @@
describe('security issues', function() {
describe('GH-1495: Prevent Remote Code Execution via constructor', function() {
it('should not allow constructors to be accessed', function() {
describe('security issues', function () {
describe('GH-1495: Prevent Remote Code Execution via constructor', function () {
it('should not allow constructors to be accessed', function () {
expectTemplate('{{lookup (lookup this "constructor") "name"}}')
.withInput({})
.toCompileTo('');
expectTemplate('{{constructor.name}}')
.withInput({})
.toCompileTo('');
expectTemplate('{{constructor.name}}').withInput({}).toCompileTo('');
});
it('GH-1603: should not allow constructors to be accessed (lookup via toString)', function() {
it('GH-1603: should not allow constructors to be accessed (lookup via toString)', function () {
expectTemplate('{{lookup (lookup this (list "constructor")) "name"}}')
.withInput({})
.withHelper('list', function(element) {
.withHelper('list', function (element) {
return [element];
})
.toCompileTo('');
});
it('should allow the "constructor" property to be accessed if it is an "ownProperty"', function() {
it('should allow the "constructor" property to be accessed if it is an "ownProperty"', function () {
expectTemplate('{{constructor.name}}')
.withInput({ constructor: { name: 'here we go' } })
.toCompileTo('here we go');
@@ -29,79 +27,79 @@ describe('security issues', function() {
.toCompileTo('here we go');
});
it('should allow the "constructor" property to be accessed if it is an "own property"', function() {
it('should allow the "constructor" property to be accessed if it is an "own property"', function () {
expectTemplate('{{lookup (lookup this "constructor") "name"}}')
.withInput({ constructor: { name: 'here we go' } })
.toCompileTo('here we go');
});
});
describe('GH-1558: Prevent explicit call of helperMissing-helpers', function() {
describe('GH-1558: Prevent explicit call of helperMissing-helpers', function () {
if (!Handlebars.compile) {
return;
}
describe('without the option "allowExplicitCallOfHelperMissing"', function() {
it('should throw an exception when calling "{{helperMissing}}" ', function() {
describe('without the option "allowExplicitCallOfHelperMissing"', function () {
it('should throw an exception when calling "{{helperMissing}}" ', function () {
expectTemplate('{{helperMissing}}').toThrow(Error);
});
it('should throw an exception when calling "{{#helperMissing}}{{/helperMissing}}" ', function() {
it('should throw an exception when calling "{{#helperMissing}}{{/helperMissing}}" ', function () {
expectTemplate('{{#helperMissing}}{{/helperMissing}}').toThrow(Error);
});
it('should throw an exception when calling "{{blockHelperMissing "abc" .}}" ', function() {
it('should throw an exception when calling "{{blockHelperMissing "abc" .}}" ', function () {
var functionCalls = [];
expect(function() {
expect(function () {
var template = Handlebars.compile('{{blockHelperMissing "abc" .}}');
template({
fn: function() {
fn: function () {
functionCalls.push('called');
}
},
});
}).to.throw(Error);
expect(functionCalls.length).to.equal(0);
}).toThrow();
expect(functionCalls.length).toBe(0);
});
it('should throw an exception when calling "{{#blockHelperMissing .}}{{/blockHelperMissing}}"', function() {
it('should throw an exception when calling "{{#blockHelperMissing .}}{{/blockHelperMissing}}"', function () {
expectTemplate('{{#blockHelperMissing .}}{{/blockHelperMissing}}')
.withInput({
fn: function() {
fn: function () {
return 'functionInData';
}
},
})
.toThrow(Error);
});
});
describe('with the option "allowCallsToHelperMissing" set to true', function() {
it('should not throw an exception when calling "{{helperMissing}}" ', function() {
describe('with the option "allowCallsToHelperMissing" set to true', function () {
it('should not throw an exception when calling "{{helperMissing}}" ', function () {
var template = Handlebars.compile('{{helperMissing}}');
template({}, { allowCallsToHelperMissing: true });
});
it('should not throw an exception when calling "{{#helperMissing}}{{/helperMissing}}" ', function() {
it('should not throw an exception when calling "{{#helperMissing}}{{/helperMissing}}" ', function () {
var template = Handlebars.compile(
'{{#helperMissing}}{{/helperMissing}}'
);
template({}, { allowCallsToHelperMissing: true });
});
it('should not throw an exception when calling "{{blockHelperMissing "abc" .}}" ', function() {
it('should not throw an exception when calling "{{blockHelperMissing "abc" .}}" ', function () {
var functionCalls = [];
var template = Handlebars.compile('{{blockHelperMissing "abc" .}}');
template(
{
fn: function() {
fn: function () {
functionCalls.push('called');
}
},
},
{ allowCallsToHelperMissing: true }
);
equals(functionCalls.length, 1);
expect(functionCalls.length).toBe(1);
});
it('should not throw an exception when calling "{{#blockHelperMissing .}}{{/blockHelperMissing}}"', function() {
it('should not throw an exception when calling "{{#blockHelperMissing .}}{{/blockHelperMissing}}"', function () {
var template = Handlebars.compile(
'{{#blockHelperMissing true}}sdads{{/blockHelperMissing}}'
);
@@ -110,24 +108,27 @@ describe('security issues', function() {
});
});
describe('GH-1563', function() {
it('should not allow to access constructor after overriding via __defineGetter__', function() {
if ({}.__defineGetter__ == null || {}.__lookupGetter__ == null) {
return this.skip(); // Browser does not support this exploit anyway
describe('GH-1563', function () {
var browserSupportsExploit =
{}.__defineGetter__ != null && {}.__lookupGetter__ != null;
it.skipIf(!browserSupportsExploit)(
'should not allow to access constructor after overriding via __defineGetter__',
function () {
expectTemplate(
'{{__defineGetter__ "undefined" valueOf }}' +
'{{#with __lookupGetter__ }}' +
'{{__defineGetter__ "propertyIsEnumerable" (this.bind (this.bind 1)) }}' +
'{{constructor.name}}' +
'{{/with}}'
)
.withInput({})
.toThrow(/Missing helper: "__defineGetter__"/);
}
expectTemplate(
'{{__defineGetter__ "undefined" valueOf }}' +
'{{#with __lookupGetter__ }}' +
'{{__defineGetter__ "propertyIsEnumerable" (this.bind (this.bind 1)) }}' +
'{{constructor.name}}' +
'{{/with}}'
)
.withInput({})
.toThrow(/Missing helper: "__defineGetter__"/);
});
);
});
describe('GH-1595: dangerous properties', function() {
describe('GH-1595: dangerous properties', function () {
var templates = [
'{{constructor}}',
'{{__defineGetter__}}',
@@ -138,66 +139,70 @@ describe('security issues', function() {
'{{lookup this "__defineGetter__"}}',
'{{lookup this "__defineSetter__"}}',
'{{lookup this "__lookupGetter__"}}',
'{{lookup this "__proto__"}}'
'{{lookup this "__proto__"}}',
];
templates.forEach(function(template) {
describe('access should be denied to ' + template, function() {
it('by default', function() {
expectTemplate(template)
.withInput({})
.toCompileTo('');
templates.forEach(function (template) {
describe('access should be denied to ' + template, function () {
it('by default', function () {
expectTemplate(template).withInput({}).toCompileTo('');
});
it(' with proto-access enabled', function() {
it(' with proto-access enabled', function () {
expectTemplate(template)
.withInput({})
.withRuntimeOptions({
allowProtoPropertiesByDefault: true,
allowProtoMethodsByDefault: true
allowProtoMethodsByDefault: true,
})
.toCompileTo('');
});
});
});
});
describe('GH-1631: disallow access to prototype functions', function() {
describe('GH-1631: disallow access to prototype functions', function () {
function TestClass() {}
TestClass.prototype.aProperty = 'propertyValue';
TestClass.prototype.aMethod = function() {
TestClass.prototype.aMethod = function () {
return 'returnValue';
};
beforeEach(function() {
beforeEach(function () {
handlebarsEnv.resetLoggedPropertyAccesses();
});
afterEach(function() {
sinon.restore();
afterEach(function () {
vi.restoreAllMocks();
});
describe('control access to prototype methods via "allowedProtoMethods"', function() {
describe('control access to prototype methods via "allowedProtoMethods"', function () {
checkProtoMethodAccess({});
describe('in compat mode', function() {
describe('in compat mode', function () {
checkProtoMethodAccess({ compat: true });
});
function checkProtoMethodAccess(compileOptions) {
it('should be prohibited by default and log a warning', function() {
var spy = sinon.spy(console, 'error');
it('should be prohibited by default and log a warning', function () {
var spy = vi
.spyOn(console, 'error')
.mockImplementation(function () {});
expectTemplate('{{aMethod}}')
.withInput(new TestClass())
.withCompileOptions(compileOptions)
.toCompileTo('');
expect(spy.calledOnce).to.be.true();
expect(spy.args[0][0]).to.match(/Handlebars: Access has been denied/);
expect(spy).toHaveBeenCalledTimes(1);
expect(spy.mock.calls[0][0]).toMatch(
/Handlebars: Access has been denied/
);
});
it('should only log the warning once', function() {
var spy = sinon.spy(console, 'error');
it('should only log the warning once', function () {
var spy = vi
.spyOn(console, 'error')
.mockImplementation(function () {});
expectTemplate('{{aMethod}}')
.withInput(new TestClass())
@@ -209,191 +214,211 @@ describe('security issues', function() {
.withCompileOptions(compileOptions)
.toCompileTo('');
expect(spy.calledOnce).to.be.true();
expect(spy.args[0][0]).to.match(/Handlebars: Access has been denied/);
expect(spy).toHaveBeenCalledTimes(1);
expect(spy.mock.calls[0][0]).toMatch(
/Handlebars: Access has been denied/
);
});
it('can be allowed, which disables the warning', function() {
var spy = sinon.spy(console, 'error');
it('can be allowed, which disables the warning', function () {
var spy = vi
.spyOn(console, 'error')
.mockImplementation(function () {});
expectTemplate('{{aMethod}}')
.withInput(new TestClass())
.withCompileOptions(compileOptions)
.withRuntimeOptions({
allowedProtoMethods: {
aMethod: true
}
aMethod: true,
},
})
.toCompileTo('returnValue');
expect(spy.callCount).to.equal(0);
expect(spy).not.toHaveBeenCalled();
});
it('can be turned on by default, which disables the warning', function() {
var spy = sinon.spy(console, 'error');
it('can be turned on by default, which disables the warning', function () {
var spy = vi
.spyOn(console, 'error')
.mockImplementation(function () {});
expectTemplate('{{aMethod}}')
.withInput(new TestClass())
.withCompileOptions(compileOptions)
.withRuntimeOptions({
allowProtoMethodsByDefault: true
allowProtoMethodsByDefault: true,
})
.toCompileTo('returnValue');
expect(spy.callCount).to.equal(0);
expect(spy).not.toHaveBeenCalled();
});
it('can be turned off by default, which disables the warning', function() {
var spy = sinon.spy(console, 'error');
it('can be turned off by default, which disables the warning', function () {
var spy = vi
.spyOn(console, 'error')
.mockImplementation(function () {});
expectTemplate('{{aMethod}}')
.withInput(new TestClass())
.withCompileOptions(compileOptions)
.withRuntimeOptions({
allowProtoMethodsByDefault: false
allowProtoMethodsByDefault: false,
})
.toCompileTo('');
expect(spy.callCount).to.equal(0);
expect(spy).not.toHaveBeenCalled();
});
it('can be turned off, if turned on by default', function() {
it('can be turned off, if turned on by default', function () {
expectTemplate('{{aMethod}}')
.withInput(new TestClass())
.withCompileOptions(compileOptions)
.withRuntimeOptions({
allowProtoMethodsByDefault: true,
allowedProtoMethods: {
aMethod: false
}
aMethod: false,
},
})
.toCompileTo('');
});
}
it('should cause the recursive lookup by default (in "compat" mode)', function() {
it('should cause the recursive lookup by default (in "compat" mode)', function () {
expectTemplate('{{#aString}}{{trim}}{{/aString}}')
.withInput({ aString: ' abc ', trim: 'trim' })
.withCompileOptions({ compat: true })
.toCompileTo('trim');
});
it('should not cause the recursive lookup if allowed through options(in "compat" mode)', function() {
it('should not cause the recursive lookup if allowed through options(in "compat" mode)', function () {
expectTemplate('{{#aString}}{{trim}}{{/aString}}')
.withInput({ aString: ' abc ', trim: 'trim' })
.withCompileOptions({ compat: true })
.withRuntimeOptions({
allowedProtoMethods: {
trim: true
}
trim: true,
},
})
.toCompileTo('abc');
});
});
describe('control access to prototype non-methods via "allowedProtoProperties" and "allowProtoPropertiesByDefault', function() {
describe('control access to prototype non-methods via "allowedProtoProperties" and "allowProtoPropertiesByDefault', function () {
checkProtoPropertyAccess({});
describe('in compat-mode', function() {
describe('in compat-mode', function () {
checkProtoPropertyAccess({ compat: true });
});
describe('in strict-mode', function() {
describe('in strict-mode', function () {
checkProtoPropertyAccess({ strict: true });
});
function checkProtoPropertyAccess(compileOptions) {
it('should be prohibited by default and log a warning', function() {
var spy = sinon.spy(console, 'error');
it('should be prohibited by default and log a warning', function () {
var spy = vi
.spyOn(console, 'error')
.mockImplementation(function () {});
expectTemplate('{{aProperty}}')
.withInput(new TestClass())
.withCompileOptions(compileOptions)
.toCompileTo('');
expect(spy.calledOnce).to.be.true();
expect(spy.args[0][0]).to.match(/Handlebars: Access has been denied/);
expect(spy).toHaveBeenCalledTimes(1);
expect(spy.mock.calls[0][0]).toMatch(
/Handlebars: Access has been denied/
);
});
it('can be explicitly prohibited by default, which disables the warning', function() {
var spy = sinon.spy(console, 'error');
it('can be explicitly prohibited by default, which disables the warning', function () {
var spy = vi
.spyOn(console, 'error')
.mockImplementation(function () {});
expectTemplate('{{aProperty}}')
.withInput(new TestClass())
.withCompileOptions(compileOptions)
.withRuntimeOptions({
allowProtoPropertiesByDefault: false
allowProtoPropertiesByDefault: false,
})
.toCompileTo('');
expect(spy.callCount).to.equal(0);
expect(spy).not.toHaveBeenCalled();
});
it('can be turned on, which disables the warning', function() {
var spy = sinon.spy(console, 'error');
it('can be turned on, which disables the warning', function () {
var spy = vi
.spyOn(console, 'error')
.mockImplementation(function () {});
expectTemplate('{{aProperty}}')
.withInput(new TestClass())
.withCompileOptions(compileOptions)
.withRuntimeOptions({
allowedProtoProperties: {
aProperty: true
}
aProperty: true,
},
})
.toCompileTo('propertyValue');
expect(spy.callCount).to.equal(0);
expect(spy).not.toHaveBeenCalled();
});
it('can be turned on by default, which disables the warning', function() {
var spy = sinon.spy(console, 'error');
it('can be turned on by default, which disables the warning', function () {
var spy = vi
.spyOn(console, 'error')
.mockImplementation(function () {});
expectTemplate('{{aProperty}}')
.withInput(new TestClass())
.withCompileOptions(compileOptions)
.withRuntimeOptions({
allowProtoPropertiesByDefault: true
allowProtoPropertiesByDefault: true,
})
.toCompileTo('propertyValue');
expect(spy.callCount).to.equal(0);
expect(spy).not.toHaveBeenCalled();
});
it('can be turned off, if turned on by default', function() {
it('can be turned off, if turned on by default', function () {
expectTemplate('{{aProperty}}')
.withInput(new TestClass())
.withCompileOptions(compileOptions)
.withRuntimeOptions({
allowProtoPropertiesByDefault: true,
allowedProtoProperties: {
aProperty: false
}
aProperty: false,
},
})
.toCompileTo('');
});
}
});
describe('compatibility with old runtimes, that do not provide the function "container.lookupProperty"', function() {
describe('compatibility with old runtimes, that do not provide the function "container.lookupProperty"', function () {
beforeEach(function simulateRuntimeWithoutLookupProperty() {
var oldTemplateMethod = handlebarsEnv.template;
sinon.replace(handlebarsEnv, 'template', function(templateSpec) {
templateSpec.main = wrapToAdjustContainer(templateSpec.main);
return oldTemplateMethod.call(this, templateSpec);
});
vi.spyOn(handlebarsEnv, 'template').mockImplementation(
function (templateSpec) {
templateSpec.main = wrapToAdjustContainer(templateSpec.main);
return oldTemplateMethod.call(this, templateSpec);
}
);
});
afterEach(function() {
sinon.restore();
afterEach(function () {
vi.restoreAllMocks();
});
it('should work with simple properties', function() {
it('should work with simple properties', function () {
expectTemplate('{{aProperty}}')
.withInput({ aProperty: 'propertyValue' })
.toCompileTo('propertyValue');
});
it('should work with Array.prototype.length', function() {
it('should work with Array.prototype.length', function () {
expectTemplate('{{anArray.length}}')
.withInput({ anArray: ['a', 'b', 'c'] })
.toCompileTo('3');
@@ -401,21 +426,21 @@ describe('security issues', function() {
});
});
describe('escapes template variables', function() {
it('in compat mode', function() {
describe('escapes template variables', function () {
it('in compat mode', function () {
expectTemplate("{{'a\\b'}}")
.withCompileOptions({ compat: true })
.withInput({ 'a\\b': 'c' })
.toCompileTo('c');
});
it('in default mode', function() {
it('in default mode', function () {
expectTemplate("{{'a\\b'}}")
.withCompileOptions()
.withInput({ 'a\\b': 'c' })
.toCompileTo('c');
});
it('in default mode', function() {
it('in default mode', function () {
expectTemplate("{{'a\\b'}}")
.withCompileOptions({ strict: true })
.withInput({ 'a\\b': 'c' })
+17 -12
View File
@@ -3,42 +3,47 @@ try {
var SourceMap = require('source-map'),
SourceMapConsumer = SourceMap.SourceMapConsumer;
}
} catch (err) {
} catch {
/* NOP for in browser */
}
describe('source-map', function() {
describe('source-map', function () {
if (!Handlebars.precompile || !SourceMap) {
return;
}
it('should safely include source map info', function() {
it('should safely include source map info', function () {
var template = Handlebars.precompile('{{hello}}', {
destName: 'dest.js',
srcName: 'src.hbs'
srcName: 'src.hbs',
});
equal(!!template.code, true);
equal(!!template.map, !CompilerContext.browser);
expect(template.code).toBeTruthy();
if (CompilerContext.browser) {
expect(template.map).toBeFalsy();
} else {
expect(template.map).toBeTruthy();
}
});
it('should map source properly', function() {
it('should map source properly', async function () {
var templateSource =
' b{{hello}} \n {{bar}}a {{#block arg hash=(subex 1 subval)}}{{/block}}',
template = Handlebars.precompile(templateSource, {
destName: 'dest.js',
srcName: 'src.hbs'
srcName: 'src.hbs',
});
if (template.map) {
var consumer = new SourceMapConsumer(template.map),
var consumer = await new SourceMapConsumer(template.map),
lines = template.code.split('\n'),
srcLines = templateSource.split('\n'),
generated = grepLine('" b"', lines),
source = grepLine(' b', srcLines);
var mapped = consumer.originalPositionFor(generated);
equal(mapped.line, source.line);
equal(mapped.column, source.column);
expect(mapped.line).toBe(source.line);
expect(mapped.column).toBe(source.column);
consumer.destroy();
}
});
});
@@ -49,7 +54,7 @@ function grepLine(token, lines) {
if (column >= 0) {
return {
line: i + 1,
column: column
column: column,
};
}
}

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