Compare commits

..

36 Commits

Author SHA1 Message Date
Jakob Linskeseder dbe04946ce Improve security mitigations
Instead of validating the AST in the parser, fix the compiler instead by
handling the types in a safe way.
2026-03-30 00:31:02 +02:00
Jakob Linskeseder d069c1caf1 Fix Ruby component publishing documentation
Docker images has to use root because it tries
to write to `/.local` which would fail otherwise.
2026-03-26 22:26:07 +01:00
Jakob Linskeseder 6714e07a6a Fix Composer component definition
This aligns the Composer component with
https://github.com/components/handlebars.js/blob/master/composer.json
2026-03-26 22:15:07 +01:00
handlebars-lang dce542c9a6 v4.7.9 2026-03-26 20:44:05 +00:00
handlebars-lang 8a41389ba5 Update release notes 2026-03-26 20:43:42 +00:00
Jakob Linskeseder 68d8df5a88 Fix security issues
Fixes GHSA-2w6w-674q-4c4q, GHSA-xhpv-hc6g-r9c6, GHSA-3mfm-83xf-c92r, GHSA-2qvq-rjwj-gvw9, GHSA-9cx6-37pm-9jff, GHSA-7rx3-28cr-v5wh, GHSA-442j-39wm-28r2, GHSA-xjpj-3mr7-gcpf
2026-03-26 21:29:35 +01:00
Jakob Linskeseder b2a083136b Fix browser tests
Pinned the browser tests to the latest Playwright version that still works with Node 16.
We need Node 16 for a successful build.
2026-03-25 22:08:13 +01:00
Jakob Linskeseder 9f98c16298 Fix release script
We have to pin Grunt to 1.5.3, since this
is the latest version to support Node 10,
which is needed for the release process.

Also improved the Dockerfile and added
a warning related to "yo release".
2026-03-25 21:44:59 +01:00
Mohamed Akram 45443b4290 Revert "Improve partial indenting performance"
This reverts commit 08fddee033.

The change caused performance regressions in some cases and performance
was not consistent across engines.
2026-03-25 20:31:53 +01:00
Jakob Linskeseder 8841a5f6d3 Fix CI errors with linting 2026-02-07 22:22:17 +01:00
Tyler Blackham e0137c26f2 fix: enable shell mode for spawn to resolve Windows EINVAL issue 2026-02-07 22:12:25 +01:00
Mohamed Akram e914d6037f Improve rendering performance
Avoid unnecessary copies via Utils.extend in hot paths.
2024-09-03 22:08:38 +02:00
Christian Clauss 7de4b41c34 Upgrade GitHub Actions checkout and setup-node on 4.x branch
Like #2024 but on another branch.
2023-12-14 00:07:16 +01:00
Benoit Vallée eab1d141cb Fix type "RuntimeOptions" also accepting string partials 2023-09-21 22:30:08 +02:00
Jonas Thelemann de4414d7fc feat(types): set hash to be a Record<string, any>
Resolves #2001
2023-09-12 23:23:59 +02:00
Mohamed Akram 08fddee033 Improve partial indenting performance 2023-09-07 22:42:30 +02:00
Mohamed Akram 4512766919 Fix non-contiguous program indices 2023-09-07 22:24:57 +02:00
Nils Knappmeier e497a35d7f refactor: rename i to startPartIndex 2023-08-05 19:47:17 +02:00
Nils Knappmeier 8c9f866655 update package-lock.json to correct version 2023-08-05 00:14:57 +02:00
Jakob Linskeseder 520e1d5f08 Fix links in README
This commit backports changes from `master`.
2023-08-02 23:37:05 +02:00
Jakob Linskeseder 02423780a9 Fix link to homepage
When visiting the www-variant, a `SSL_ERROR_INTERNAL_ERROR_ALERT`
error is displayed by the browser.
2023-08-02 23:35:42 +02:00
Jakob Linskeseder be92d2f254 Backport: publish latest and tags to AWS S3
In order to automate publishing 4.x releases to AWS S3,
we also need this workflow on the 4.x branch.

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

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

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

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

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

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

Backport from `master`-branch.
2021-12-30 00:58:48 +01:00
208 changed files with 25113 additions and 25161 deletions
+24
View File
@@ -0,0 +1,24 @@
.rvmrc
.DS_Store
/tmp/
*.sublime-project
*.sublime-workspace
npm-debug.log
.idea
yarn-error.log
node_modules
/handlebars-release.tgz
.nyc_output
# Generated files
lib/handlebars/compiler/parser.js
/coverage/
/dist/
/tests/integration/*/dist/
# Third-party or files that must remain unchanged
/spec/expected/
/spec/vendor
# JS-Snippets
src/*.js
+66
View File
@@ -0,0 +1,66 @@
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
@@ -1,6 +0,0 @@
# Upgrade to Prettier 2.7
3d228334530860a6e3f99dc10777c84bf22292c1
# Format markdown files with Prettier
dfe2eaaf20f0b679d94e5a799757c4394d80f1cc
# migrate to oxlint and oxfmt
0c1d00282ca619c3416ad819bdf53c0852b32415
-9
View File
@@ -1,9 +0,0 @@
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
@@ -1,12 +0,0 @@
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
+18 -20
View File
@@ -12,12 +12,12 @@ jobs:
runs-on: 'ubuntu-latest'
steps:
- name: Checkout
uses: actions/checkout@v6
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v6
uses: actions/setup-node@v4
with:
node-version: '24'
node-version: '16'
- 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: ['20', '22', '24']
node-version: ['16', '18', '20', '22']
steps:
- name: Checkout
uses: actions/checkout@v6
uses: actions/checkout@v4
with:
submodules: true
- name: Setup Node.js
uses: actions/setup-node@v6
uses: actions/setup-node@v4
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)
if: matrix.operating-system == 'ubuntu-latest'
run: npm run test:integration
# https://github.com/webpack/webpack/issues/14532
if: ${{ matrix.node-version == '16' }}
run: |
cd ./tests/integration/rollup-test && ./test.sh && cd -
cd ./tests/integration/webpack-babel-test && ./test.sh && cd -
cd ./tests/integration/webpack-test && ./test.sh && cd -
browser:
name: Test (Browser)
runs-on: ubuntu-latest
runs-on: 'ubuntu-22.04'
steps:
- name: Checkout
uses: actions/checkout@v6
uses: actions/checkout@v4
with:
submodules: true
- name: Setup Node.js
uses: actions/setup-node@v6
uses: actions/setup-node@v4
with:
node-version: '24'
node-version: '16'
- name: Install dependencies
run: npm ci
@@ -83,9 +83,7 @@ jobs:
npx playwright install
- name: Build
run: npm run build
run: npx grunt prepare
- name: Test
run: |
npm run test:browser-smoke
npm run test:browser
run: npm run test:browser
+4 -5
View File
@@ -15,14 +15,14 @@ jobs:
environment: 'builds.handlebarsjs.com.s3.amazonaws.com'
steps:
- name: Checkout
uses: actions/checkout@v6
uses: actions/checkout@v2
with:
submodules: true
- name: Setup Node.js
uses: actions/setup-node@v6
uses: actions/setup-node@v2
with:
node-version: '24'
node-version: '16'
- name: Install dependencies
run: npm ci
@@ -33,7 +33,6 @@ jobs:
git config --global user.name "handlebars-lang"
npm run publish:aws
env:
S3_BUCKET_NAME: 'builds.handlebarsjs.com'
S3_REGION: 'us-east-1'
S3_BUCKET_NAME: "builds.handlebarsjs.com"
S3_ACCESS_KEY_ID: ${{ secrets.S3_ACCESS_KEY_ID }}
S3_SECRET_ACCESS_KEY: ${{ secrets.S3_SECRET_ACCESS_KEY }}
+2 -1
View File
@@ -12,8 +12,9 @@ node_modules
.nyc_output
# Generated files
lib/handlebars/compiler/parser.js
/coverage/
/dist/
/tests/bench/results/
/test-results/
/tests/integration/*/dist/
/spec/tmp/*
-28
View File
@@ -1,28 +0,0 @@
{
"$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
@@ -1,150 +0,0 @@
{
"$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
@@ -0,0 +1,22 @@
.rvmrc
.DS_Store
/tmp/
*.sublime-project
*.sublime-workspace
npm-debug.log
sauce_connect.log*
.idea
yarn-error.log
node_modules
/handlebars-release.tgz
.nyc_output
# Generated files
lib/handlebars/compiler/parser.js
/coverage/
/dist/
/tests/integration/*/dist/
# Third-party or files that must remain unchanged
/spec/expected/
/spec/vendor
-8
View File
@@ -1,8 +0,0 @@
{
"$schema": "https://swc.rs/schema.json",
"module": {
"type": "commonjs",
"importInterop": "swc"
},
"sourceMaps": "inline"
}
+111 -28
View File
@@ -1,16 +1,10 @@
# 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 refer to our [FAQ](https://github.com/handlebars-lang/handlebars.js/blob/master/FAQ.md) for common issues that people run into.
Please see our [FAQ](https://github.com/handlebars-lang/handlebars.js/blob/master/FAQ.md) for common issues that people run into.
Should you run into other issues with the project, please don't hesitate to let us know by filing an [issue][issue]!
In general, we are going to ask for an **example** of the problem failing, which can be as simple as a jsfiddle/jsbin/etc. We've put together a jsfiddle **[template][jsfiddle]** to ease this. (We will keep this link up to date as new releases occur, so feel free to check back here).
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.
@@ -18,8 +12,9 @@ Documentation issues on the [handlebarsjs.com](https://handlebarsjs.com) site sh
## Branches
- The branch `master` contains the current development version (v5).
- The branch `4.x` contains the previous stable version. Only critical bugfixes are backported there.
- The branch `4.x` contains the currently released version. Bugfixes should be made in this branch.
- The branch `master` contains the next version. A release date is not yet specified. Maintainers
should merge the branch `4.x` into the master branch regularly.
## Pull Requests
@@ -35,13 +30,21 @@ Generally we like to see pull requests that
## Building
To build Handlebars.js you'll need Node.js installed.
To build Handlebars.js you'll need a few things installed.
- Node.js
- [Grunt](http://gruntjs.com/getting-started)
Before building, you need to make sure that the Git submodule `spec/mustache` is included (i.e. the directory `spec/mustache` should not be empty). To include it, if using Git version 1.6.5 or newer, use `git clone --recursive` rather than `git clone`. Or, if you already cloned without `--recursive`, use `git submodule update --init`.
Project dependencies may be installed via `npm install`.
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`.
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/`.
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).
@@ -70,36 +73,116 @@ npm test
## Linting and Formatting
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.
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.
You can use the following scripts to make sure that the CI job does not fail:
- **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.
- **npm run lint** will run `eslint` and fail on warnings
- **npm run format** will run `prettier` on all files
- **npm run check-before-pull-request** will perform all most checks that our CI job does in its build-job, excluding the "integration-test".
- **npm run test:integration** will run integration tests (using old NodeJS versions and integrations with webpack, babel and so on)
These tests only work on a Linux-machine with `nvm` installed (for running tests in multiple versions of NodeJS).
## Releasing the latest version
Before attempting the release Handlebars, please make sure that you have the following authorizations:
- Push-access to `handlebars-lang/handlebars.js`
- Publishing rights on npmjs.com for the `handlebars` package
- Publishing rights on gemfury for the `handlebars-source` package
- Push-access to the repo for legacy package managers: `components/handlebars`
- Push-access to the production-repo of the handlebars site: `handlebars-lang/handlebarsjs.com-github-pages`
- Push-access to [handlebars-lang/handlebars.js](https://github.com/handlebars-lang/handlebars.js/)
- Publishing rights on npmjs.com for the [handlebars](https://www.npmjs.com/package/handlebars) package
- Publishing rights on rubygems for the [handlebars-source](https://rubygems.org/gems/handlebars-source) package
- Push-access to the repo for legacy package managers: [components/handlebars.js](https://github.com/components/handlebars.js)
- Push-access to the production-repo of the handlebars site: [handlebars-lang/docs](https://github.com/handlebars-lang/docs)
_When releasing a previous version of Handlebars, please look into the CONTRIBUNG.md in the corresponding branch._
A full release may be completed with the following:
A full release via Docker may be completed with the following:
1. Create a `Dockerfile` in this folder for releasing
```Dockerfile
FROM node:10-slim
ENV EDITOR=vim
# Update stretch repositories
RUN sed -i -e 's/deb.debian.org/archive.debian.org/g' \
-e 's|security.debian.org|archive.debian.org/|g' \
-e '/stretch-updates/d' /etc/apt/sources.list
# Install release dependencies
RUN apt-get update
RUN apt-get install -y git vim
# Work around deprecated npm dependency install via unauthenticated git-protocol:
# https://github.com/kpdecker/generator-release/blob/87aab9b84c9f083635c3fcc822f18acce1f48736/package.json#L31
RUN git config --system url."https://github.com/".insteadOf git://github.com/
# Configure git
RUN git config --system user.email "release@handlebarsjs.com"
RUN git config --system user.name "handlebars-lang"
RUN mkdir /home/node/.config
RUN mkdir /home/node/.ssh
RUN mkdir /home/node/tmp
# Generate config for yo generator-release:
# https://github.com/kpdecker/generator-release#example
# You have to add a valid GitHub access token! (Used for reading issues and pull requests.)
RUN echo "module.exports = {\n auth: 'oauth',\n token: 'GitHub personal access token'\n};" > /home/node/.config/generator-release
RUN chown -R node:node /home/node/.config
RUN chown -R node:node /home/node/.ssh
RUN chown -R node:node /home/node/tmp
# Add the generated key to GitHub: https://github.com/settings/keys
RUN ssh-keygen -q -t ed25519 -N '' -f /home/node/.ssh/id_ed25519 -C "release@handlebarsjs.com"
RUN chmod 0600 /home/node/.ssh/id_ed25519*
RUN chown node:node /home/node/.ssh/id_ed25519*
```
npm ci
npm run build
2. Build and run the Docker image
```bash
docker build --tag handlebars:release .
docker run --rm --interactive --tty \
--volume $PWD:/app \
--workdir /app \
--user $(id -u):$(id -g) \
--env NPM_CONFIG_PREFIX=/home/node/.npm-global \
handlebars:release bash -c 'export PATH=$PATH:/home/node/.npm-global/bin; bash'
```
* Add SSH key to GitHub: `cat /home/node/.ssh/id_ed25519.pub` (https://github.com/settings/keys)
* Add GitHub API token: `vi /home/node/.config/generator-release`
* Execute the following steps:
```bash
npm install
npm install -g yo@1 grunt@1 generator-release
npm run release
# Warning! This step will collect data from GitHub, bump the version,
# create a new commit, create a new tag and push it to GitHub.
# https://github.com/kpdecker/generator-release?tab=readme-ov-file#usage
yo release
npm login
npm publish
yo release:publish components handlebars.js dist/components/
```
6. Publish Ruby `handlebars-source` gem:
```bash
docker run --rm --interactive --tty \
--volume $PWD:/app \
--workdir /app \
ruby:3.2-slim bash
```
* Execute the following steps:
```bash
cd dist/components/
gem build handlebars-source.gemspec
gem push handlebars-source-*.gem
```
### After the release
After the release, you should check that all places have really been updated. Especially verify that the `latest`-tags
in those places still point to the latest version
@@ -120,4 +203,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/4nbwjaqz/4/
[jsfiddle]: https://jsfiddle.net/9D88g/180/
+16 -8
View File
@@ -1,22 +1,22 @@
# Frequently Asked Questions
## How can I file a bug report:
1. 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).
## Why isn't my Mustache template working?
1. Why isn't my Mustache template working?
Handlebars deviates from Mustache slightly on a few behaviors. These variations are documented in our [readme](https://github.com/handlebars-lang/handlebars.js#differences-between-handlebarsjs-and-mustache).
## Why is it slower when compiling?
1. 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.
## Why doesn't this work with Content Security Policy restrictions?
1. 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.
## How can I include script tags in my template?
1. 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:
@@ -29,7 +29,7 @@ If loading the template via an inlined `<script type="text/x-handlebars">` tag t
It's generally recommended that templates are served through external, precompiled, files, which do not suffer from this issue.
## Why are my precompiled scripts throwing exceptions?
1. 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:
@@ -49,6 +49,14 @@ We include the built client libraries in the npm package for those who want to b
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).
## How do I load the runtime library when using AMD?
1. Why doesn't IE like the `default` name in the AMD module?
The `handlebars.runtime.js` file includes a UMD build, which exposes the library as both the module root and the `default` field for compatibility.
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.
+237
View File
@@ -0,0 +1,237 @@
/* 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
}
}
},
shell: {
integrationTests: {
command: './tests/integration/run-integration-tests.sh'
}
},
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-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',
'shell:integrationTests',
'metrics',
'publish-to-aws'
]);
grunt.registerTask('on-file-change', ['build', 'concat:tests', 'test']);
// === Primary tasks ===
grunt.registerTask('dev', ['clean', 'connect', 'watch']);
grunt.registerTask('default', ['clean', 'build', 'test', 'release']);
grunt.registerTask('test', ['test:bin', 'test:cov']);
grunt.registerTask('bench', ['metrics']);
grunt.registerTask('prepare', ['build', 'concat:tests']);
grunt.registerTask(
'build',
'Builds a distributable version of the current project',
['parser', 'node', 'amd', 'globals']
);
grunt.registerTask('integration-tests', [
'default',
'shell:integrationTests'
]);
};
+167
View File
@@ -0,0 +1,167 @@
[![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/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
@@ -1,198 +0,0 @@
[![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
@@ -1,15 +0,0 @@
# 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
@@ -0,0 +1,6 @@
module.exports = {
rules: {
'no-console': 0,
'no-var': 0
}
};
Executable
+176
View File
@@ -0,0 +1,176 @@
#!/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
@@ -1,136 +0,0 @@
#!/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": "5.0.0-alpha.1",
"version": "4.7.9",
"main": "handlebars.js",
"license": "MIT",
"dependencies": {}
+3 -1
View File
@@ -3,5 +3,7 @@
"repo": "components/handlebars.js",
"version": "1.0.0",
"main": "handlebars.js",
"scripts": ["handlebars.js"]
"scripts": [
"handlebars.js"
]
}
+2 -6
View File
@@ -1,7 +1,7 @@
{
"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",
"homepage": "https://handlebarsjs.com",
"license": "MIT",
"type": "component",
"keywords": [
@@ -11,13 +11,9 @@
],
"authors": [
{
"name": "Chris Wanstrath",
"homepage": "http://chriswanstrath.com"
"name": "Chris Wanstrath"
}
],
"require": {
"robloach/component-installer": "*"
},
"extra": {
"component": {
"name": "handlebars",
+1 -1
View File
@@ -2,7 +2,7 @@
<package>
<metadata>
<id>handlebars.js</id>
<version>5.0.0-alpha.1</version>
<version>4.7.9</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": "5.0.0-alpha.1",
"version": "4.7.9",
"license": "MIT",
"jspm": {
"main": "handlebars",
+24 -23
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`.
@@ -132,6 +132,7 @@ 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";
@@ -147,6 +148,7 @@ interface CommentStatement <: Statement {
}
```
```java
interface Decorator <: Statement {
type: "Decorator";
@@ -207,6 +209,7 @@ 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
@@ -239,6 +242,7 @@ interface NullLiteral <: Literal {
}
```
### Miscellaneous
```java
@@ -287,7 +291,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 `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.
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.
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.
@@ -296,7 +300,8 @@ 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 given path component.
Used to generate the code to resolve a give 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`.
@@ -311,6 +316,7 @@ The `Handlebars.JavaScriptCompiler` object has a number of methods that may be c
- `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.
@@ -324,6 +330,7 @@ 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);
@@ -331,35 +338,29 @@ 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) {
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()];
});
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'}
]
}));
```
-2
View File
@@ -1,7 +1,5 @@
# Decorators
**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 setup 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
@@ -1,17 +0,0 @@
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',
},
},
];
+8
View File
@@ -0,0 +1,8 @@
module.exports = {
env: {
// Handlebars should not use node or browser-specific APIs
'shared-node-browser': true,
node: false,
browser: false
}
};
+6 -7
View File
@@ -1,16 +1,15 @@
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';
+2 -5
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,10 +19,7 @@ function create() {
hb.Utils = Utils;
hb.escapeExpression = Utils.escapeExpression;
// 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.VM = runtime;
hb.template = function(spec) {
return runtime.template(spec, hb);
};
+4 -4
View File
@@ -1,11 +1,11 @@
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';
import { resetLoggedProperties } from './internal/proto-access';
export const VERSION = '4.7.7';
export const VERSION = '4.7.9';
export const COMPILER_REVISION = 8;
export const LAST_COMPATIBLE_COMPILER_REVISION = 7;
@@ -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]';
@@ -86,7 +86,7 @@ HandlebarsEnvironment.prototype = {
*/
resetLoggedPropertyAccesses() {
resetLoggedProperties();
},
}
};
export let log = logger.log;
+2 -2
View File
@@ -23,8 +23,8 @@ let AST = {
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
@@ -0,0 +1,34 @@
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);
}
+8 -14
View File
@@ -1,27 +1,21 @@
/* global define */
/* global define, require */
import { isArray } from '../utils';
let SourceNode;
try {
/* v8 ignore next */
/* istanbul 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.
// 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);
let SourceMap = require('source-map');
SourceNode = SourceMap.SourceNode;
}
// oxlint-disable-next-line no-unused-vars -- Babel 5 requires named catch param
} catch (err) {
/* NOP */
}
/* v8 ignore next -- tested but not covered due to dist build */
/* istanbul ignore if: tested but not covered in istanbul due to dist build */
if (!SourceNode) {
SourceNode = function(line, column, srcFile, chunks) {
this.src = '';
@@ -29,7 +23,7 @@ if (!SourceNode) {
this.add(chunks);
}
};
/* v8 ignore next */
/* istanbul ignore next */
SourceNode.prototype = {
add: function(chunks) {
if (isArray(chunks)) {
@@ -48,7 +42,7 @@ if (!SourceNode) {
},
toString: function() {
return this.src;
},
}
};
}
@@ -138,7 +132,7 @@ CodeGen.prototype = {
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]);
@@ -171,7 +165,7 @@ CodeGen.prototype = {
ret.add(']');
return ret;
},
}
};
export default CodeGen;
+132 -63
View File
@@ -1,5 +1,13 @@
import { Exception } from '@handlebars/parser';
import { isArray, indexOf, extend } from '../utils';
/* eslint-disable new-cap */
import Exception from '../exception';
import {
isArray,
indexOf,
extend,
sanitizeDepth,
sanitizeParts
} from '../utils';
import AST from './ast';
const slice = [].slice;
@@ -50,6 +58,8 @@ Compiler.prototype = {
this.opcodes = [];
this.children = [];
this.options = options;
this.stringParams = options.stringParams;
this.trackIds = options.trackIds;
options.blockParams = options.blockParams || [];
@@ -63,7 +73,7 @@ Compiler.prototype = {
unless: true,
with: true,
log: true,
lookup: true,
lookup: true
},
options.knownHelpers
);
@@ -72,7 +82,7 @@ Compiler.prototype = {
},
compileProgram: function(program) {
let childCompiler = new this.compiler(),
let childCompiler = new this.compiler(), // eslint-disable-line new-cap
result = childCompiler.compile(program, this.options),
guid = this.guid++;
@@ -85,7 +95,7 @@ Compiler.prototype = {
},
accept: function(node) {
/* v8 ignore next -- Sanity code */
/* istanbul ignore next: Sanity code */
if (!this[node.type]) {
throw new Exception('Unknown type: ' + node.type, node);
}
@@ -239,7 +249,7 @@ Compiler.prototype = {
name = path.parts[0],
isBlock = program != null || inverse != null;
this.opcode('getContext', path.depth);
this.opcode('getContext', sanitizeDepth(path.depth));
this.opcode('pushProgram', program);
this.opcode('pushProgram', inverse);
@@ -284,29 +294,32 @@ Compiler.prototype = {
},
PathExpression: function(path) {
this.addDepth(path.depth);
this.opcode('getContext', path.depth);
// Sanitize untrusted AST values at the compiler boundary.
// javascript-compiler.js trusts all opcode arguments to be safe.
const depth = sanitizeDepth(path.depth);
const parts = sanitizeParts(path.parts);
let name = path.parts[0],
this.addDepth(depth);
this.opcode('getContext', depth);
let name = parts[0],
scoped = AST.helpers.scopedId(path),
blockParamId = !path.depth && !scoped && this.blockParamIndex(name);
blockParamId = !depth && !scoped && this.blockParamIndex(name);
if (blockParamId) {
this.opcode('lookupBlockParam', blockParamId, path.parts);
this.opcode(
'lookupBlockParam',
[Number(blockParamId[0]), Number(blockParamId[1])],
parts
);
} else if (!name) {
// Context reference, i.e. `{{foo .}}` or `{{foo ..}}`
this.opcode('pushContext');
} else if (path.data) {
this.options.data = true;
this.opcode('lookupData', path.depth, path.parts, path.strict);
this.opcode('lookupData', depth, parts, path.strict);
} else {
this.opcode(
'lookupOnContext',
path.parts,
path.falsy,
path.strict,
scoped
);
this.opcode('lookupOnContext', parts, path.falsy, path.strict, scoped);
}
},
@@ -315,11 +328,11 @@ Compiler.prototype = {
},
NumberLiteral: function(number) {
this.opcode('pushLiteral', number.value);
this.opcode('pushLiteral', Number(number.value));
},
BooleanLiteral: function(bool) {
this.opcode('pushLiteral', bool.value);
this.opcode('pushLiteral', bool.value === true ? 'true' : 'false');
},
UndefinedLiteral: function() {
@@ -351,7 +364,7 @@ Compiler.prototype = {
this.opcodes.push({
opcode: name,
args: slice.call(arguments, 1),
loc: this.sourceNode[0].loc,
loc: this.sourceNode[0].loc
});
},
@@ -405,7 +418,47 @@ Compiler.prototype = {
},
pushParam: function(val) {
let value = val.value != null ? val.value : val.original || '';
let depth = sanitizeDepth(val.depth);
if (this.stringParams) {
if (value.replace) {
value = value.replace(/^(\.?\.\/)*/g, '').replace(/\//g, '.');
}
if (depth) {
this.addDepth(depth);
}
this.opcode('getContext', depth);
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);
}
},
setupFullMustacheParams: function(sexpr, program, inverse, omitEmpty) {
@@ -436,44 +489,34 @@ Compiler.prototype = {
return [depth, param];
}
}
},
}
};
export function precompile(input, options = {}, env) {
validateInput(input, options);
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
);
}
let environment = compileEnvironment(input, options, env);
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);
return new env.JavaScriptCompiler().compile(environment, options);
}
export function compile(input, options = {}, env) {
options = extend({}, options);
validateInput(input, options);
let compiled;
function compileInput() {
let environment = compileEnvironment(input, options, env),
templateSpec = new env.JavaScriptCompiler().compile(
environment,
options,
undefined,
true
);
return env.template(templateSpec);
}
// Template is only compiled on first use and cached after that point.
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')
@@ -484,22 +527,48 @@ function validateInput(input, options) {
);
}
if (options.trackIds || options.stringParams) {
throw new Exception(
'TrackIds and stringParams are no longer supported. See Github #1145'
);
}
options = extend({}, options);
if (!('data' in options)) {
options.data = true;
}
if (options.compat) {
options.useDepths = true;
}
let compiled;
function compileInput() {
let ast = env.parse(input, options),
environment = new env.Compiler().compile(ast, options),
templateSpec = new env.JavaScriptCompiler().compile(
environment,
options,
undefined,
true
);
return env.template(templateSpec);
}
function compileEnvironment(input, options, env) {
let ast = env.parse(input, options);
return new env.Compiler().compile(ast, options);
// Template is only compiled on first use and cached after that point.
function ret(context, execOptions) {
if (!compiled) {
compiled = compileInput();
}
return compiled.call(this, context, execOptions);
}
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;
}
function argEquals(a, b) {
@@ -528,7 +597,7 @@ function transformLiteralToPath(sexpr) {
depth: 0,
parts: [literal.original + ''],
original: literal.original + '',
loc: literal.loc,
loc: literal.loc
};
}
}
+219
View File
@@ -0,0 +1,219 @@
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)
};
}
+152 -38
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';
@@ -20,7 +20,7 @@ JavaScriptCompiler.prototype = {
this.aliasable('container.lookup'),
'(depths, ',
JSON.stringify(name),
')',
')'
];
},
@@ -64,6 +64,8 @@ JavaScriptCompiler.prototype = {
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;
@@ -71,7 +73,7 @@ JavaScriptCompiler.prototype = {
this.context = context || {
decorators: [],
programs: [],
environments: [],
environments: []
};
this.preamble();
@@ -112,7 +114,7 @@ JavaScriptCompiler.prototype = {
this.source.currentLocation = firstLoc;
this.pushSource('');
/* v8 ignore next */
/* istanbul ignore next */
if (this.stackSlot || this.inlineStack.length || this.compileStack.length) {
throw new Exception('Compile completed with content left on stack');
}
@@ -123,12 +125,11 @@ 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',
@@ -137,7 +138,7 @@ JavaScriptCompiler.prototype = {
'data',
'blockParams',
'depths',
this.decorators.merge(),
this.decorators.merge()
]);
} else {
this.decorators.prepend(
@@ -154,11 +155,11 @@ JavaScriptCompiler.prototype = {
if (!this.isChild) {
let ret = {
compiler: this.compilerInfo(),
main: fn,
main: fn
};
if (this.decorators) {
ret.main_d = this.decorators;
ret.main_d = this.decorators; // eslint-disable-line camelcase
ret.useDecorators = true;
}
@@ -171,9 +172,6 @@ JavaScriptCompiler.prototype = {
}
}
// Release AST/compiler references only needed during compilation for dedup
this.context.environments.length = 0;
if (this.environment.usePartial) {
ret.usePartial = true;
}
@@ -235,7 +233,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,14 +260,14 @@ JavaScriptCompiler.prototype = {
if (asObject) {
params.push(source);
return Function.apply(this, params); // eslint-disable-line no-new-func
return Function.apply(this, params);
} else {
return this.source.wrap([
'function(',
params.join(','),
') {\n ',
source,
'}',
'}'
]);
}
},
@@ -280,7 +278,7 @@ JavaScriptCompiler.prototype = {
sourceSeen,
bufferStart,
bufferEnd;
this.source.each((line) => {
this.source.each(line => {
if (line.appendToBuffer) {
if (bufferStart) {
line.prepend(' + ');
@@ -393,7 +391,7 @@ JavaScriptCompiler.prototype = {
current,
' = ',
this.source.functionCall(blockHelperMissing, 'call', params),
'}',
'}'
]);
},
@@ -424,7 +422,7 @@ JavaScriptCompiler.prototype = {
// Otherwise, the empty string is appended
append: function() {
if (this.isInline()) {
this.replaceStack((current) => [' != null ? ', current, ' : ""']);
this.replaceStack(current => [' != null ? ', current, ' : ""']);
this.pushSource(this.appendToBuffer(this.popStack()));
} else {
@@ -434,13 +432,13 @@ JavaScriptCompiler.prototype = {
local,
' != null) { ',
this.appendToBuffer(local, undefined, true),
' }',
' }'
]);
if (this.environment.isSimple) {
this.pushSource([
'else { ',
this.appendToBuffer("''", undefined, true),
' }',
' }'
]);
}
}
@@ -458,7 +456,7 @@ JavaScriptCompiler.prototype = {
this.aliasable('container.escapeExpression'),
'(',
this.popStack(),
')',
')'
])
);
},
@@ -552,7 +550,7 @@ JavaScriptCompiler.prototype = {
let len = parts.length;
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.
@@ -581,23 +579,61 @@ JavaScriptCompiler.prototype = {
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
}
this.pushStackLiteral(omitEmpty ? 'undefined' : '{}');
},
pushHash: function() {
if (this.hash) {
this.hashes.push(this.hash);
}
this.hash = { values: {} };
this.hash = { values: {}, types: [], contexts: [], ids: [] };
},
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));
},
@@ -650,15 +686,24 @@ JavaScriptCompiler.prototype = {
let foundDecorator = this.nameLookup('decorators', name, 'decorator'),
options = this.setupHelperArgs(name, paramSize);
// Store the resolved decorator in a variable and verify it is a function before
// calling it. Without this, unregistered decorators can cause an unhandled TypeError
// (calling undefined), which crashes the process — enabling Denial of Service.
this.decorators.push(['var decorator = ', foundDecorator, ';']);
this.decorators.push([
'if (typeof decorator !== "function") { throw new Error(',
this.quotedString('Missing decorator: "' + name + '"'),
'); }'
]);
this.decorators.push([
'fn = ',
this.decorators.functionCall(foundDecorator, '', [
this.decorators.functionCall('decorator', '', [
'fn',
'props',
'container',
options,
options
]),
' || fn;',
' || fn;'
]);
},
@@ -692,7 +737,7 @@ JavaScriptCompiler.prototype = {
let functionLookupCode = [
'(',
this.itemsSeparatedBy(possibleFunctionCalls, '||'),
')',
')'
];
let functionCall = this.source.functionCall(
functionLookupCode,
@@ -766,7 +811,7 @@ JavaScriptCompiler.prototype = {
this.aliasable('"function"'),
' ? ',
this.source.functionCall('helper', 'call', helper.callParams),
' : helper))',
' : helper))'
]);
},
@@ -815,7 +860,49 @@ JavaScriptCompiler.prototype = {
//
// Pops a value off the stack and assigns it to the current hash
assignToHash: function(key) {
this.hash.values[key] = this.popStack();
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');
}
},
// HELPERS
@@ -829,7 +916,7 @@ JavaScriptCompiler.prototype = {
for (let i = 0, l = children.length; i < l; i++) {
child = children[i];
compiler = new this.compiler();
compiler = new this.compiler(); // eslint-disable-line new-cap
let existing = this.matchExistingProgram(child);
@@ -925,7 +1012,7 @@ JavaScriptCompiler.prototype = {
createdStack,
usedLiteral;
/* v8 ignore next */
/* istanbul ignore next */
if (!this.isInline()) {
throw new Exception('replaceStack on non-inline');
}
@@ -973,7 +1060,7 @@ JavaScriptCompiler.prototype = {
this.inlineStack = [];
for (let i = 0, len = inlineStack.length; i < len; i++) {
let entry = inlineStack[i];
/* v8 ignore next */
/* istanbul ignore if */
if (entry instanceof Literal) {
this.compileStack.push(entry);
} else {
@@ -995,7 +1082,7 @@ JavaScriptCompiler.prototype = {
return item.value;
} else {
if (!inline) {
/* v8 ignore next */
/* istanbul ignore next */
if (!this.stackSlot) {
throw new Exception('Invalid stack pop');
}
@@ -1009,7 +1096,7 @@ JavaScriptCompiler.prototype = {
let stack = this.isInline() ? this.inlineStack : this.compileStack,
item = stack[stack.length - 1];
/* v8 ignore next */
/* istanbul ignore if */
if (item instanceof Literal) {
return item.value;
} else {
@@ -1061,12 +1148,15 @@ JavaScriptCompiler.prototype = {
params: params,
paramsInit: paramsInit,
name: foundHelper,
callParams: [callContext].concat(params),
callParams: [callContext].concat(params)
};
},
setupParams: function(helper, paramSize, params) {
let options = {},
contexts = [],
types = [],
ids = [],
objectArgs = !params,
param;
@@ -1077,6 +1167,14 @@ 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();
@@ -1093,12 +1191,28 @@ 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';
}
@@ -1122,7 +1236,7 @@ JavaScriptCompiler.prototype = {
} else {
return options;
}
},
}
};
(function() {
@@ -1182,7 +1296,7 @@ function strictLookup(requireTerminal, compiler, parts, startPartIndex, type) {
compiler.quotedString(parts[len]),
', ',
JSON.stringify(compiler.source.currentLocation),
' )',
' )'
];
} else {
return stack;
+178
View File
@@ -0,0 +1,178 @@
/* 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
@@ -0,0 +1,136 @@
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;
@@ -0,0 +1,234 @@
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;
+2 -5
View File
@@ -1,9 +1,7 @@
import { extend } from '../utils';
export default function(instance) {
instance.registerDecorator(
'inline',
function (fn, props, container, options) {
instance.registerDecorator('inline', function(fn, props, container, options) {
let ret = fn;
if (!props.partials) {
props.partials = {};
@@ -20,6 +18,5 @@ export default function (instance) {
props.partials[options.args[0]] = options.fn;
return ret;
}
);
});
}
+68
View File
@@ -0,0 +1,68 @@
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;
+14 -1
View File
@@ -1,4 +1,4 @@
import { isArray } from '../utils';
import { appendContextPath, createFrame, isArray } from '../utils';
export default function(instance) {
instance.registerHelper('blockHelperMissing', function(context, options) {
@@ -11,11 +11,24 @@ 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);
}
});
+31 -22
View File
@@ -1,5 +1,11 @@
import { Exception } from '@handlebars/parser';
import { createFrame, isArray, isFunction, isMap, isSet } from '../utils';
import {
appendContextPath,
blockParams,
createFrame,
isArray,
isFunction
} from '../utils';
import Exception from '../exception';
export default function(instance) {
instance.registerHelper('each', function(context, options) {
@@ -11,7 +17,13 @@ export default function (instance) {
inverse = options.inverse,
i = 0,
ret = '',
data;
data,
contextPath;
if (options.data && options.ids) {
contextPath =
appendContextPath(options.data.contextPath, options.ids[0]) + '.';
}
if (isFunction(context)) {
context = context.call(this);
@@ -21,19 +33,26 @@ export default function (instance) {
data = createFrame(options.data);
}
function execIteration(field, value, index, last) {
function execIteration(field, 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(value, {
fn(context[field], {
data: data,
blockParams: [context[field], field],
blockParams: blockParams(
[context[field], field],
[contextPath + field, null]
)
});
}
@@ -41,19 +60,9 @@ export default function (instance) {
if (isArray(context)) {
for (let j = context.length; i < j; i++) {
if (i in context) {
execIteration(i, context[i], i, i === context.length - 1);
execIteration(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]();
@@ -62,23 +71,23 @@ export default function (instance) {
}
context = newContext;
for (let j = context.length; i < j; i++) {
execIteration(i, context[i], i, i === context.length - 1);
execIteration(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 intermediate keys array.
// an itermediate keys array.
if (priorKey !== undefined) {
execIteration(priorKey, context[priorKey], i - 1);
execIteration(priorKey, i - 1);
}
priorKey = key;
i++;
});
if (priorKey !== undefined) {
execIteration(priorKey, context[priorKey], i - 1, true);
execIteration(priorKey, i - 1, true);
}
}
}
+1 -1
View File
@@ -1,4 +1,4 @@
import { Exception } from '@handlebars/parser';
import Exception from '../exception';
export default function(instance) {
instance.registerHelper('helperMissing', function(/* [args, ]options */) {
+3 -3
View File
@@ -1,5 +1,5 @@
import { Exception } from '@handlebars/parser';
import { isEmpty, isFunction } from '../utils';
import Exception from '../exception';
export default function(instance) {
instance.registerHelper('if', function(conditional, options) {
@@ -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 conditional as purely not empty based on the
// The `includeZero` option may be set to treat the condtional 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);
@@ -27,7 +27,7 @@ export default function (instance) {
return instance.helpers['if'].call(this, conditional, {
fn: options.inverse,
inverse: options.fn,
hash: options.hash,
hash: options.hash
});
});
}
+16 -3
View File
@@ -1,5 +1,11 @@
import { Exception } from '@handlebars/parser';
import { isEmpty, isFunction } from '../utils';
import {
appendContextPath,
blockParams,
createFrame,
isEmpty,
isFunction
} from '../utils';
import Exception from '../exception';
export default function(instance) {
instance.registerHelper('with', function(context, options) {
@@ -14,10 +20,17 @@ 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: [context],
blockParams: blockParams([context], [data && data.contextPath])
});
} else {
return options.inverse(this);
+7 -6
View File
@@ -16,17 +16,18 @@ export function createProtoAccessControl(runtimeOptions) {
methodWhiteList['__defineGetter__'] = false;
methodWhiteList['__defineSetter__'] = false;
methodWhiteList['__lookupGetter__'] = false;
methodWhiteList['__lookupSetter__'] = false;
extend(methodWhiteList, runtimeOptions.allowedProtoMethods);
return {
properties: {
whitelist: propertyWhiteList,
defaultValue: runtimeOptions.allowProtoPropertiesByDefault,
defaultValue: runtimeOptions.allowProtoPropertiesByDefault
},
methods: {
whitelist: methodWhiteList,
defaultValue: runtimeOptions.allowProtoMethodsByDefault,
},
defaultValue: runtimeOptions.allowProtoMethodsByDefault
}
};
}
@@ -45,11 +46,11 @@ function checkWhiteList(protoAccessControlForType, propertyName) {
if (protoAccessControlForType.defaultValue !== undefined) {
return protoAccessControlForType.defaultValue;
}
logUnexpectedPropertyAccessOnce(propertyName);
logUnexpecedPropertyAccessOnce(propertyName);
return false;
}
function logUnexpectedPropertyAccessOnce(propertyName) {
function logUnexpecedPropertyAccessOnce(propertyName) {
if (loggedProperties[propertyName] !== true) {
loggedProperties[propertyName] = true;
logger.log(
@@ -62,7 +63,7 @@ function logUnexpectedPropertyAccessOnce(propertyName) {
}
export function resetLoggedProperties() {
Object.keys(loggedProperties).forEach((propertyName) => {
Object.keys(loggedProperties).forEach(propertyName => {
delete loggedProperties[propertyName];
});
}
+1 -1
View File
@@ -1,6 +1,6 @@
export function wrapHelper(helper, transformOptionsFn) {
if (typeof helper !== 'function') {
// This should not happen, but apparently it does in https://github.com/handlebars-lang/handlebars.js/issues/1639
// This should not happen, but apparently it does in https://github.com/wycats/handlebars.js/issues/1639
// We try to make the wrapper least-invasive by not wrapping it, if the helper is not a function.
return helper;
}
+1 -1
View File
@@ -33,7 +33,7 @@ let logger = {
}
console[method](...message); // eslint-disable-line no-console
}
},
}
};
export default logger;
+14 -2
View File
@@ -1,7 +1,19 @@
/* global globalThis */
export default function(Handlebars) {
let $Handlebars = globalThis.Handlebars;
/* istanbul ignore next */
// https://mathiasbynens.be/notes/globalthis
(function() {
if (typeof globalThis === 'object') return;
Object.prototype.__defineGetter__('__magic__', function() {
return this;
});
__magic__.globalThis = __magic__; // eslint-disable-line no-undef
delete Object.prototype.__magic__;
})();
/* v8 ignore next */
const $Handlebars = globalThis.Handlebars;
/* istanbul ignore next */
Handlebars.noConflict = function() {
if (globalThis.Handlebars === Handlebars) {
globalThis.Handlebars = $Handlebars;
+46 -22
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) {
/* v8 ignore next */
/* istanbul ignore next */
if (!env) {
throw new Exception('No environment passed to template');
}
@@ -68,6 +68,9 @@ 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);
@@ -111,16 +114,12 @@ export function template(templateSpec, env) {
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) {
if (Utils.isMap(parent)) {
return parent.get(propertyName);
}
let result = parent[propertyName];
if (result == null) {
return result;
@@ -139,7 +138,7 @@ export function template(templateSpec, env) {
for (let i = 0; i < len; i++) {
let result = depths[i] && container.lookupProperty(depths[i], name);
if (result != null) {
return depths[i][name];
return result;
}
}
},
@@ -195,13 +194,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;
_setup(options);
ret._setup(options);
if (!options.partial && templateSpec.useData) {
data = initData(context, data);
}
@@ -246,7 +245,7 @@ export function template(templateSpec, env) {
ret.isTop = true;
function _setup(options) {
ret._setup = function(options) {
if (!options.partial) {
let mergedHelpers = {};
addHelpers(mergedHelpers, env.helpers, container);
@@ -283,8 +282,26 @@ 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;
}
@@ -332,22 +349,25 @@ export function wrapProgram(
export function resolvePartial(partial, context, options) {
if (!partial) {
if (options.name === '@partial-block') {
partial = options.data['partial-block'];
partial = lookupOwnProperty(options.data, 'partial-block');
} else {
partial = options.partials[options.name];
partial = lookupOwnProperty(options.partials, options.name);
}
} else if (!partial.call && !options.name) {
// This is a dynamic partial that returned a string
options.name = partial;
partial = options.partials[partial];
partial = lookupOwnProperty(options.partials, partial);
}
return partial;
}
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'];
const currentPartialBlock = lookupOwnProperty(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) {
@@ -374,9 +394,7 @@ 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);
}
@@ -386,6 +404,12 @@ export function noop() {
return '';
}
function lookupOwnProperty(obj, name) {
if (obj && Object.prototype.hasOwnProperty.call(obj, name)) {
return obj[name];
}
}
function initData(context, data) {
if (!data || !('root' in data)) {
data = data ? createFrame(data) : {};
@@ -413,7 +437,7 @@ function executeDecorators(fn, prog, container, depths, data, blockParams) {
function addHelpers(mergedHelpers, helpers, container) {
if (!helpers) return;
Object.keys(helpers).forEach((helperName) => {
Object.keys(helpers).forEach(helperName => {
let helper = helpers[helperName];
mergedHelpers[helperName] = passLookupPropertyOption(helper, container);
});
@@ -421,7 +445,7 @@ function addHelpers(mergedHelpers, helpers, container) {
function passLookupPropertyOption(helper, container) {
const lookupProperty = container.lookupProperty;
return wrapHelper(helper, (options) => {
return wrapHelper(helper, options => {
options.lookupProperty = lookupProperty;
return options;
});
+59 -14
View File
@@ -5,7 +5,7 @@ const escape = {
'"': '&quot;',
"'": '&#x27;',
'`': '&#x60;',
'=': '&#x3D;',
'=': '&#x3D;'
};
const badChars = /[&<>"'`=]/g,
@@ -30,23 +30,32 @@ export function extend(obj /* , ...source */) {
export let toString = Object.prototype.toString;
// Sourced from lodash
// https://github.com/lodash/lodash/blob/4.17.21/LICENSE
export function isFunction(value) {
// https://github.com/bestiejs/lodash/blob/master/LICENSE.txt
/* eslint-disable func-style */
let isFunction = function(value) {
return typeof value === 'function';
}
function testTag(name) {
const tag = '[object ' + name + ']';
return function (value) {
return value && typeof value === 'object'
? toString.call(value) === tag
: false;
};
// 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 */
export const isArray = Array.isArray;
export const isMap = testTag('Map');
export const isSet = testTag('Set');
/* istanbul ignore next */
export const isArray =
Array.isArray ||
function(value) {
return value && typeof value === 'object'
? toString.call(value) === '[object Array]'
: false;
};
// Older IE versions do not directly support indexOf so we must implement our own, sadly.
export function indexOf(array, value) {
@@ -96,3 +105,39 @@ 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;
}
/**
* Coerce an untrusted depth value to a safe non-negative integer.
* Returns `0` for any value that is not a finite, non-negative number.
*
* @param {unknown} depth - The depth value to sanitize.
* @returns {number} A non-negative integer.
*/
export function sanitizeDepth(depth) {
let number = Number(depth);
if (!Number.isFinite(number) || number < 0) {
return 0;
}
return Math.floor(number);
}
/**
* Return a sanitized copy of a PathExpression AST node's parts array.
* Coerces each element to a string, or returns an empty array if parts
* is not an array.
*
* @param {unknown} parts - The parts value to sanitize.
* @returns {string[]} A safe string array.
*/
export function sanitizeParts(parts) {
return Array.isArray(parts) ? parts.map(String) : [];
}
+5 -4
View File
@@ -1,14 +1,15 @@
// USAGE:
// var handlebars = require('handlebars');
/* eslint-env node */
/* eslint-disable no-var */
// var local = handlebars.create();
var handlebars = require('../dist/cjs/handlebars')['default'];
var parser = require('@handlebars/parser');
handlebars.PrintVisitor = parser.PrintVisitor;
handlebars.print = parser.print;
var printer = require('../dist/cjs/handlebars/compiler/printer');
handlebars.PrintVisitor = printer.PrintVisitor;
handlebars.print = printer.print;
module.exports = handlebars;
@@ -18,7 +19,7 @@ function extension(module, filename) {
var templateString = fs.readFileSync(filename, 'utf8');
module.exports = handlebars.compile(templateString);
}
/* v8 ignore next 4 */
/* istanbul ignore else */
if (typeof require !== 'undefined' && require.extensions) {
require.extensions['.handlebars'] = extension;
require.extensions['.hbs'] = extension;
+60 -26
View File
@@ -1,7 +1,8 @@
/* eslint-env node */
/* eslint-disable no-console */
import Async from 'neo-async';
import fs from 'fs';
import Handlebars from './handlebars';
import * as Handlebars from './handlebars';
import { basename } from 'path';
import { SourceMapConsumer, SourceNode } from 'source-map';
@@ -56,7 +57,7 @@ function loadStrings(opts, callback) {
strings = strings.map((string, index) => ({
name: names[index],
path: names[index],
source: string,
source: string
}));
callback(err, strings);
}
@@ -74,10 +75,7 @@ function loadFiles(opts, callback) {
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) {
@@ -94,7 +92,7 @@ function loadFiles(opts, callback) {
opts.hasDirectory = true;
fs.readdir(path, function(err, children) {
/* v8 ignore next -- Race condition that being too lazy to test */
/* istanbul ignore next : Race condition that being too lazy to test */
if (err) {
return callback(err);
}
@@ -113,7 +111,7 @@ function loadFiles(opts, callback) {
});
} else {
fs.readFile(path, 'utf8', function(err, data) {
/* v8 ignore next -- Race condition that being too lazy to test */
/* istanbul ignore next : Race condition that being too lazy to test */
if (err) {
return callback(err);
}
@@ -134,7 +132,7 @@ function loadFiles(opts, callback) {
ret.push({
path: path,
name: name,
source: data,
source: data
});
callback();
@@ -152,7 +150,7 @@ function loadFiles(opts, callback) {
);
}
module.exports.cli = async function (opts) {
module.exports.cli = function(opts) {
if (opts.version) {
console.log(Handlebars.VERSION);
return;
@@ -198,16 +196,24 @@ module.exports.cli = async function (opts) {
const objectName = opts.partial ? 'Handlebars.partials' : 'templates';
if (opts.namespace && !isValidNamespace(opts.namespace)) {
throw new Handlebars.Exception('Invalid namespace format');
}
let output = new SourceNode();
if (!opts.simple) {
if (opts.amd) {
const runtimeModulePath =
(opts.handlebarPath || '') + 'handlebars.runtime';
output.add(
"define(['" +
opts.handlebarPath +
'handlebars.runtime\'], function(Handlebars) {\n Handlebars = Handlebars["default"];'
'define([' +
quoteForJavaScript(runtimeModulePath) +
'], function(Handlebars) {\n Handlebars = Handlebars["default"];'
);
} else if (opts.commonjs) {
output.add('var Handlebars = require("' + opts.commonjs + '");');
output.add(
'var Handlebars = require(' + quoteForJavaScript(opts.commonjs) + ');'
);
} else {
output.add('(function() {\n');
}
@@ -221,10 +227,10 @@ module.exports.cli = async function (opts) {
output.add('{};\n');
}
for (const template of opts.templates) {
opts.templates.forEach(function(template) {
let options = {
knownHelpers: known,
knownHelpersOnly: opts.o,
knownHelpersOnly: opts.o
};
if (opts.map) {
@@ -238,12 +244,11 @@ module.exports.cli = async function (opts) {
// If we are generating a source map, we have to reconstruct the SourceNode object
if (opts.map) {
let consumer = await new SourceMapConsumer(precompiled.map);
let consumer = new SourceMapConsumer(precompiled.map);
precompiled = SourceNode.fromStringWithSourceMap(
precompiled.code,
consumer
);
consumer.destroy();
}
if (opts.simple) {
@@ -258,14 +263,14 @@ module.exports.cli = async function (opts) {
}
output.add([
objectName,
"['",
template.name,
"'] = template(",
'[',
quoteForJavaScript(template.name),
'] = template(',
precompiled,
');\n',
');\n'
]);
}
}
});
// Output the content
if (!opts.simple) {
@@ -280,7 +285,9 @@ module.exports.cli = async function (opts) {
}
if (opts.map) {
output.add('\n//# sourceMappingURL=' + opts.map + '\n');
output.add(
'\n//# sourceMappingURL=' + sanitizeSourceMapComment(opts.map) + '\n'
);
}
output = output.toStringWithSourceMap();
@@ -310,6 +317,33 @@ function arrayCast(value) {
return value;
}
/*
* Safely quotes a value for embedding in generated JavaScript strings
*
* Uses JSON.stringify which handles all special characters.
*/
function quoteForJavaScript(value) {
return JSON.stringify(String(value));
}
/**
* Validates that a namespace is a legitimate dotted JavaScript identifier
* (e.g. "App.templates") to prevent arbitrary code injection
*/
function isValidNamespace(namespace) {
return /^[A-Za-z_$][A-Za-z0-9_$]*(\.[A-Za-z_$][A-Za-z0-9_$]*)*$/.test(
namespace
);
}
/**
* Strips line terminators from source map URLs to prevent injection of new
* JavaScript lines via the sourceMappingURL comment
*/
function sanitizeSourceMapComment(value) {
return String(value).replace(/[\r\n\u2028\u2029]/g, '');
}
/**
* Run uglify to minify the compiled template, if uglify exists in the dependencies.
*
@@ -338,7 +372,7 @@ function minify(output, sourceMapFile) {
return require('uglify-js').minify(output.code, {
sourceMap: {
content: output.map,
url: sourceMapFile,
},
url: sourceMapFile
}
});
}
+9
View File
@@ -0,0 +1,9 @@
module.exports = {
'check-coverage': true,
branches: 100,
lines: 100,
functions: 100,
statements: 100,
exclude: ['**/spec/**', '**/handlebars/compiler/parser.js'],
reporter: 'html'
};
+14423 -18353
View File
File diff suppressed because it is too large Load Diff
+100 -95
View File
@@ -1,26 +1,114 @@
{
"name": "handlebars",
"version": "5.0.0-alpha.1",
"barename": "handlebars",
"version": "4.7.9",
"description": "Handlebars provides the power necessary to let you build semantic templates effectively with no frustration",
"homepage": "https://handlebarsjs.com/",
"keywords": [
"handlebars",
"html",
"mustache",
"template"
"template",
"html"
],
"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.44.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",
"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.5.3",
"grunt-babel": "^5.0.0",
"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-shell": "^4.0.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",
"bin": {
"handlebars": "bin/handlebars.mjs"
"handlebars": "bin/handlebars"
},
"scripts": {
"build": "grunt build",
"release": "npm run build && grunt release",
"format": "prettier --write '**/*.js' && eslint --fix .",
"lint": "npm run lint:eslint && npm run lint:prettier && npm run lint:types",
"lint:eslint": "eslint --max-warnings 0 .",
"lint:prettier": "prettier --check '**/*.js'",
"lint:types": "tsc --noEmit --project 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
}
},
"files": [
"bin",
"dist/*.js",
"dist/amd/**/*.js",
"dist/cjs/**/*.js",
"lib",
"release-notes.md",
@@ -28,102 +116,19 @@
"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,md}": [
"oxfmt --write"
"*.{js,css,json}": [
"prettier --write",
"git add"
],
"*.js": [
"oxlint --fix"
"eslint --fix",
"git add"
]
},
"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
@@ -0,0 +1,5 @@
module.exports = {
tabWidth: 2,
semi: true,
singleQuote: true
};
+297 -287
View File
File diff suppressed because it is too large Load Diff
-88
View File
@@ -1,88 +0,0 @@
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
),
];
+4 -2
View File
@@ -1,3 +1,5 @@
import Handlebars = require('handlebars');
import Handlebars = require('handlebars')
declare module 'handlebars/runtime' {}
declare module "handlebars/runtime" {
}
+47
View File
@@ -0,0 +1,47 @@
{
"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
@@ -0,0 +1,108 @@
<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
@@ -0,0 +1,128 @@
<!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>
+317 -66
View File
@@ -7,100 +7,102 @@ describe('ast', function () {
describe('BlockStatement', function() {
it('should throw on mustache mismatch', function() {
expect(function () {
shouldThrow(
function() {
handlebarsEnv.parse('\n {{#foo}}{{/bar}}');
}).toThrow("foo doesn't match bar - 2:5");
},
Handlebars.Exception,
"foo doesn't match bar - 2:5"
);
});
});
describe('helpers', function() {
describe('#helperExpression', function() {
it('should handle mustache statements', function() {
expect(
equals(
AST.helpers.helperExpression({
type: 'MustacheStatement',
params: [],
hash: undefined,
})
).toBe(false);
expect(
hash: undefined
}),
false
);
equals(
AST.helpers.helperExpression({
type: 'MustacheStatement',
params: [1],
hash: undefined,
})
).toBe(true);
expect(
hash: undefined
}),
true
);
equals(
AST.helpers.helperExpression({
type: 'MustacheStatement',
params: [],
hash: {},
})
).toBe(true);
});
it('should handle block statements', function () {
expect(
AST.helpers.helperExpression({
type: 'BlockStatement',
params: [],
hash: undefined,
})
).toBe(false);
expect(
AST.helpers.helperExpression({
type: 'BlockStatement',
params: [1],
hash: undefined,
})
).toBe(true);
expect(
AST.helpers.helperExpression({
type: 'BlockStatement',
params: [],
hash: {},
})
).toBe(true);
});
it('should handle subexpressions', function () {
expect(AST.helpers.helperExpression({ type: 'SubExpression' })).toBe(
hash: {}
}),
true
);
});
it('should handle block statements', function() {
equals(
AST.helpers.helperExpression({
type: 'BlockStatement',
params: [],
hash: undefined
}),
false
);
equals(
AST.helpers.helperExpression({
type: 'BlockStatement',
params: [1],
hash: undefined
}),
true
);
equals(
AST.helpers.helperExpression({
type: 'BlockStatement',
params: [],
hash: {}
}),
true
);
});
it('should handle subexpressions', function() {
equals(AST.helpers.helperExpression({ type: 'SubExpression' }), true);
});
it('should work with non-helper nodes', function() {
expect(AST.helpers.helperExpression({ type: 'Program' })).toBe(false);
equals(AST.helpers.helperExpression({ type: 'Program' }), false);
expect(AST.helpers.helperExpression({ type: 'PartialStatement' })).toBe(
equals(
AST.helpers.helperExpression({ type: 'PartialStatement' }),
false
);
expect(AST.helpers.helperExpression({ type: 'ContentStatement' })).toBe(
equals(
AST.helpers.helperExpression({ type: 'ContentStatement' }),
false
);
expect(AST.helpers.helperExpression({ type: 'CommentStatement' })).toBe(
equals(
AST.helpers.helperExpression({ type: 'CommentStatement' }),
false
);
expect(AST.helpers.helperExpression({ type: 'PathExpression' })).toBe(
false
);
equals(AST.helpers.helperExpression({ type: 'PathExpression' }), 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(
equals(AST.helpers.helperExpression({ type: 'StringLiteral' }), false);
equals(AST.helpers.helperExpression({ type: 'NumberLiteral' }), false);
equals(AST.helpers.helperExpression({ type: 'BooleanLiteral' }), false);
equals(
AST.helpers.helperExpression({ type: 'UndefinedLiteral' }),
false
);
equals(AST.helpers.helperExpression({ type: 'NullLiteral' }), false);
expect(AST.helpers.helperExpression({ type: 'Hash' })).toBe(false);
expect(AST.helpers.helperExpression({ type: 'HashPair' })).toBe(false);
equals(AST.helpers.helperExpression({ type: 'Hash' }), false);
equals(AST.helpers.helperExpression({ type: 'HashPair' }), false);
});
});
});
@@ -109,12 +111,13 @@ describe('ast', function () {
var ast, body;
function testColumns(node, firstLine, lastLine, firstColumn, 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);
equals(node.loc.start.line, firstLine);
equals(node.loc.start.column, firstColumn);
equals(node.loc.end.line, lastLine);
equals(node.loc.end.column, lastColumn);
}
/* eslint-disable no-multi-spaces */
ast = Handlebars.parse(
'line 1 {{line1Token}}\n' + // 1
' line 2 {{line2token}}\n' + // 2
@@ -128,6 +131,7 @@ describe('ast', function () {
'{{else}}\n' + // 10
'{{/open}}'
); // 11
/* eslint-enable no-multi-spaces */
body = ast.body;
it('gets ContentNode line numbers', function() {
@@ -177,4 +181,251 @@ 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);
});
});
});
});
+71 -50
View File
@@ -1,6 +1,14 @@
global.handlebarsEnv = null;
beforeEach(function() {
global.handlebarsEnv = Handlebars.create();
});
describe('basic context', function() {
it('most basic', function() {
expectTemplate('{{foo}}').withInput({ foo: 'foo' }).toCompileTo('foo');
expectTemplate('{{foo}}')
.withInput({ foo: 'foo' })
.toCompileTo('foo');
});
it('escaping', function() {
@@ -29,14 +37,16 @@ describe('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');
expectTemplate('{{.}}{{length}}')
.withInput('bye')
.toCompileTo('bye3');
});
it('compiling with an undefined context', function() {
@@ -53,7 +63,7 @@ describe('basic context', function () {
expectTemplate('{{! Goodbye}}Goodbye\n{{cruel}}\n{{world}}!')
.withInput({
cruel: 'cruel',
world: 'world',
world: 'world'
})
.withMessage('comments are ignored')
.toCompileTo('Goodbye\ncruel\nworld!');
@@ -82,7 +92,7 @@ describe('basic context', function () {
expectTemplate(string)
.withInput({
goodbye: true,
world: 'world',
world: 'world'
})
.withMessage('booleans show the contents when true')
.toCompileTo('GOODBYE cruel world!');
@@ -90,7 +100,7 @@ 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!');
@@ -100,11 +110,13 @@ describe('basic context', 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 } })
@@ -116,11 +128,13 @@ 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');
expectTemplate('val: {{.}}').withInput(false).toCompileTo('val: false');
expectTemplate('val: {{.}}')
.withInput(false)
.toCompileTo('val: false');
expectTemplate('val: {{val1/val2}}')
.withInput({ val1: { val2: false } })
@@ -129,7 +143,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');
@@ -150,7 +164,7 @@ describe('basic context', function () {
' ' +
typeof options
);
},
}
})
.toCompileTo('true true object');
@@ -158,7 +172,7 @@ describe('basic context', function () {
.withInput({
undefined: function() {
return 'undefined!';
},
}
})
.toCompileTo('undefined!');
@@ -166,7 +180,7 @@ describe('basic context', function () {
.withInput({
null: function() {
return 'null!';
},
}
})
.toCompileTo('null!');
});
@@ -229,7 +243,7 @@ describe('basic context', function () {
.withInput({
awesome: function() {
return new Handlebars.SafeString("&'\\<>");
},
}
})
.withMessage("functions returning safestrings aren't escaped")
.toCompileTo("&'\\<>");
@@ -240,7 +254,7 @@ describe('basic context', function () {
.withInput({
awesome: function() {
return 'Awesome';
},
}
})
.withMessage('functions are called and render their output')
.toCompileTo('Awesome');
@@ -250,7 +264,7 @@ describe('basic context', function () {
awesome: function() {
return this.more;
},
more: 'More awesome',
more: 'More awesome'
})
.withMessage('functions are bound to the context')
.toCompileTo('More awesome');
@@ -262,7 +276,7 @@ describe('basic context', function () {
awesome: function(context) {
return context;
},
frank: 'Frank',
frank: 'Frank'
})
.withMessage('functions are called with context arguments')
.toCompileTo('Frank');
@@ -274,9 +288,9 @@ describe('basic context', function () {
bar: {
awesome: function(context) {
return context;
}
},
},
frank: 'Frank',
frank: 'Frank'
})
.withMessage('functions are called with context arguments')
.toCompileTo('Frank');
@@ -288,7 +302,7 @@ describe('basic context', function () {
awesome: function(context) {
return context;
},
frank: 'Frank',
frank: 'Frank'
})
.withMessage('functions are called with context arguments')
.toCompileTo('Frank');
@@ -299,7 +313,7 @@ describe('basic context', function () {
.withInput({
awesome: function(context, options) {
return options.fn(context);
},
}
})
.withMessage('block functions are called with context and options')
.toCompileTo('inner 1');
@@ -313,7 +327,7 @@ describe('basic context', function () {
value: true,
awesome: function(context, options) {
return options.fn(context);
},
}
})
.withMessage('block functions are called with context and options')
.toCompileTo('inner 1');
@@ -324,7 +338,7 @@ describe('basic context', function () {
.withInput({
awesome: function(options) {
return options.fn(this);
},
}
})
.withMessage('block functions are called with options')
.toCompileTo('inner');
@@ -336,8 +350,8 @@ describe('basic context', function () {
foo: {
awesome: function() {
return this;
},
},
}
}
})
.withMessage('block functions are called with options')
.toCompileTo('inner');
@@ -351,7 +365,7 @@ describe('basic context', function () {
value: true,
awesome: function() {
return this;
},
}
})
.withMessage('block functions are called with options')
.toCompileTo('inner');
@@ -381,13 +395,6 @@ describe('basic context', function () {
.toCompileTo('Goodbye beautiful world!');
});
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: '' } })
@@ -445,7 +452,9 @@ describe('basic context', function () {
.withInput({ person: { name: null } })
.toCompileTo('');
expectTemplate('{{person/name}}').withInput({ person: {} }).toCompileTo('');
expectTemplate('{{person/name}}')
.withInput({ person: {} })
.toCompileTo('');
});
it('this keyword in paths', function() {
@@ -456,7 +465,7 @@ 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');
@@ -468,7 +477,9 @@ describe('basic context', function () {
'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' } })
@@ -479,7 +490,7 @@ describe('basic context', function () {
var helpers = {
foo: function(value) {
return 'bar ' + value;
},
}
};
expectTemplate('{{#goodbyes}}{{foo this}}{{/goodbyes}}')
@@ -490,7 +501,7 @@ 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')
@@ -508,7 +519,7 @@ describe('basic context', function () {
foo: function(value) {
return value;
},
this: 'bar',
this: 'bar'
})
.toCompileTo('bar');
@@ -517,7 +528,7 @@ describe('basic context', function () {
foo: function(value) {
return value;
},
text: { this: 'bar' },
text: { this: 'bar' }
})
.toCompileTo('bar');
});
@@ -525,11 +536,13 @@ describe('basic context', 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');
});
@@ -537,17 +550,21 @@ describe('basic context', 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');
});
@@ -555,9 +572,13 @@ describe('basic context', 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() {
@@ -565,7 +586,7 @@ describe('basic context', function () {
.withInput({
false: function() {
return 'bar';
},
}
})
.withHelper('foo', function(arg) {
return arg;
+40 -36
View File
@@ -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,7 +17,7 @@ describe('blocks', function () {
expectTemplate(string)
.withInput({
goodbyes: [],
world: 'world',
world: 'world'
})
.withMessage('Arrays ignore the contents when empty')
.toCompileTo('cruel world!');
@@ -31,9 +31,9 @@ describe('blocks', function () {
goodbyes: [
{ text: 'goodbye' },
{ text: 'Goodbye' },
{ text: 'GOODBYE' },
{ text: 'GOODBYE' }
],
world: 'world',
world: 'world'
})
.withCompileOptions({ compat: false })
.toCompileTo('goodbyeGoodbyeGOODBYE goodbyeGoodbyeGOODBYE');
@@ -47,9 +47,9 @@ 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!');
@@ -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,7 +73,7 @@ describe('blocks', function () {
expectTemplate(string)
.withInput({
goodbyes: [],
world: 'world',
world: 'world'
})
.withMessage('Arrays ignore the contents when empty')
.toCompileTo('cruel world!');
@@ -86,8 +86,8 @@ describe('blocks', function () {
goodbyes: [
{ text: 'goodbye' },
{ text: 'Goodbye' },
{ text: 'GOODBYE' },
],
{ text: 'GOODBYE' }
]
})
.withMessage(
'Templates can access variables in contexts up the stack with relative path syntax'
@@ -104,8 +104,8 @@ describe('blocks', function () {
goodbyes: [
{ text: 'goodbye' },
{ text: 'Goodbye' },
{ text: 'GOODBYE' },
],
{ text: 'GOODBYE' }
]
})
.toCompileTo('AlanAlanAlanAlanAlanAlan');
});
@@ -122,7 +122,7 @@ describe('blocks', function () {
)
.withInput({
omg: 'OMG!',
outer: [{ sibling: 'sad', inner: [{ text: 'goodbye' }] }],
outer: [{ sibling: 'sad', inner: [{ text: 'goodbye' }] }]
})
.toCompileTo('Goodbye cruel sad OMG!');
});
@@ -135,8 +135,8 @@ describe('blocks', function () {
.withInput({
person: [
{ first: 'Alan', last: 'Johnson' },
{ first: 'Alan', last: 'Johnson' },
],
{ first: 'Alan', last: 'Johnson' }
]
})
.toCompileTo('Alan JohnsonAlan Johnson');
});
@@ -202,7 +202,7 @@ describe('blocks', function () {
expectTemplate('{{#people}}{{name}}{{^}}{{none}}{{/people}}')
.withInput({
none: 'No people',
people: [],
people: []
})
.toCompileTo('No people');
});
@@ -252,7 +252,7 @@ describe('blocks', 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.');
});
@@ -274,7 +274,7 @@ describe('blocks', function () {
)
.withInput({
omg: { yes: 'OMG!' },
outer: [{ inner: [{ yes: 'no', text: 'goodbye' }] }],
outer: [{ inner: [{ yes: 'no', text: 'goodbye' }] }]
})
.withCompileOptions({ compat: true })
.toCompileTo('Goodbye cruel OMG!');
@@ -286,7 +286,7 @@ describe('blocks', function () {
)
.withInput({
omg: { no: 'OMG!' },
outer: [{ inner: [{ yes: 'no', text: 'goodbye' }] }],
outer: [{ inner: [{ yes: 'no', text: 'goodbye' }] }]
})
.withCompileOptions({ compat: true })
.toCompileTo('Goodbye cruel ');
@@ -345,7 +345,7 @@ describe('blocks', function () {
},
nested: function(fn, props, container, options) {
props.nested = options.fn();
},
}
})
.toCompileTo('success');
});
@@ -381,26 +381,26 @@ describe('blocks', function () {
var run;
expectTemplate('{{*decorator "success"}}')
.withDecorator('decorator', function(fn, props, container, options) {
expect(options.args[0]).toBe('success');
equals(options.args[0], 'success');
run = true;
return fn;
})
.withInput({ foo: 'success' })
.toCompileTo('');
expect(run).toBe(true);
equals(run, true);
});
it('should fail when accessing variables from root', function() {
var run;
expectTemplate('{{*decorator foo}}')
.withDecorator('decorator', function(fn, props, container, options) {
expect(options.args[0]).toBeUndefined();
equals(options.args[0], undefined);
run = true;
return fn;
})
.withInput({ foo: 'fail' })
.toCompileTo('');
expect(run).toBe(true);
equals(run, true);
});
describe('registration', function() {
@@ -411,9 +411,9 @@ describe('blocks', function () {
return 'fail';
});
expect(handlebarsEnv.decorators.foo).toBeTruthy();
equals(!!handlebarsEnv.decorators.foo, true);
handlebarsEnv.unregisterDecorator('foo');
expect(handlebarsEnv.decorators.foo).toBeUndefined();
equals(handlebarsEnv.decorators.foo, undefined);
});
it('allows multiple globals', function() {
@@ -421,19 +421,20 @@ describe('blocks', function () {
handlebarsEnv.registerDecorator({
foo: function() {},
bar: function () {},
bar: function() {}
});
expect(handlebarsEnv.decorators.foo).toBeTruthy();
expect(handlebarsEnv.decorators.bar).toBeTruthy();
equals(!!handlebarsEnv.decorators.foo, true);
equals(!!handlebarsEnv.decorators.bar, true);
handlebarsEnv.unregisterDecorator('foo');
handlebarsEnv.unregisterDecorator('bar');
expect(handlebarsEnv.decorators.foo).toBeUndefined();
expect(handlebarsEnv.decorators.bar).toBeUndefined();
equals(handlebarsEnv.decorators.foo, undefined);
equals(handlebarsEnv.decorators.bar, undefined);
});
it('fails with multiple and args', function() {
expect(function () {
shouldThrow(
function() {
handlebarsEnv.registerDecorator(
{
world: function() {
@@ -441,11 +442,14 @@ describe('blocks', function () {
},
testHelper: function() {
return 'found it!';
},
}
},
{}
);
}).toThrow('Arg not supported with multiple decorators');
},
Error,
'Arg not supported with multiple decorators'
);
});
});
});
+84 -126
View File
@@ -6,7 +6,7 @@ describe('builtin helpers', function () {
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,7 +63,7 @@ describe('builtin helpers', function () {
)
.withInput({
goodbye: 0,
world: 'world',
world: 'world'
})
.withMessage('if with zero does not show the contents')
.toCompileTo('GOODBYE cruel world!');
@@ -77,7 +77,7 @@ describe('builtin helpers', function () {
goodbye: function() {
return true;
},
world: 'world',
world: 'world'
})
.withMessage(
'if with function shows the contents when function returns true'
@@ -89,7 +89,7 @@ describe('builtin helpers', function () {
goodbye: function() {
return this.world;
},
world: 'world',
world: 'world'
})
.withMessage(
'if with function shows the contents when function returns string'
@@ -101,7 +101,7 @@ describe('builtin helpers', function () {
goodbye: function() {
return false;
},
world: 'world',
world: 'world'
})
.withMessage(
'if with function does not show the contents when returns false'
@@ -113,7 +113,7 @@ describe('builtin helpers', function () {
goodbye: function() {
return this.foo;
},
world: 'world',
world: 'world'
})
.withMessage(
'if with function does not show the contents when returns undefined'
@@ -127,7 +127,7 @@ describe('builtin helpers', function () {
)
.withInput({
foo: { goodbye: true },
world: 'world',
world: 'world'
})
.toCompileTo('GOODBYE cruel world!');
});
@@ -139,8 +139,8 @@ describe('builtin helpers', function () {
.withInput({
person: {
first: 'Alan',
last: 'Johnson',
},
last: 'Johnson'
}
})
.toCompileTo('Alan Johnson');
});
@@ -151,9 +151,9 @@ describe('builtin helpers', function () {
person: function() {
return {
first: 'Alan',
last: 'Johnson',
last: 'Johnson'
};
},
}
})
.toCompileTo('Alan Johnson');
});
@@ -169,8 +169,8 @@ describe('builtin helpers', function () {
.withInput({
person: {
first: 'Alan',
last: 'Johnson',
},
last: 'Johnson'
}
})
.toCompileTo('Alan Johnson');
});
@@ -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,7 +210,7 @@ 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!');
@@ -222,9 +222,9 @@ describe('builtin helpers', function () {
goodbyes: [
{ text: 'goodbye' },
{ text: 'Goodbye' },
{ text: 'GOODBYE' },
{ text: 'GOODBYE' }
],
world: 'world',
world: 'world'
})
.withRuntimeOptions({ data: false })
.withCompileOptions({ data: false })
@@ -257,18 +257,22 @@ 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 = CompilerContext.compile(string)(hash);
var actual = compileWithPartials(string, hash);
var expected1 =
'&lt;b&gt;#1&lt;/b&gt;. goodbye! 2. GOODBYE! cruel world!';
var expected2 =
'2. GOODBYE! &lt;b&gt;#1&lt;/b&gt;. goodbye! cruel world!';
expect([expected1, expected2]).toContain(actual);
equals(
actual === expected1 || actual === expected2,
true,
'each with object argument iterates over the contents when not empty'
);
expectTemplate(string)
.withInput({
goodbyes: {},
world: 'world',
world: 'world'
})
.toCompileTo('cruel world!');
});
@@ -281,9 +285,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('0. goodbye! 1. Goodbye! 2. GOODBYE! cruel world!');
@@ -297,9 +301,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(
@@ -313,7 +317,7 @@ describe('builtin helpers', function () {
)
.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!'
@@ -337,9 +341,9 @@ 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!');
@@ -353,9 +357,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('goodbye! cruel world!');
@@ -369,9 +373,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(
@@ -385,7 +389,7 @@ describe('builtin helpers', function () {
)
.withInput({
goodbyes: { foo: { text: 'goodbye' }, bar: { text: 'Goodbye' } },
world: 'world',
world: 'world'
})
.withMessage('The @first variable is used')
.toCompileTo('goodbye! cruel world!');
@@ -399,9 +403,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('GOODBYE! cruel world!');
@@ -413,7 +417,7 @@ describe('builtin helpers', function () {
)
.withInput({
goodbyes: { foo: { text: 'goodbye' }, bar: { text: 'Goodbye' } },
world: 'world',
world: 'world'
})
.withMessage('The @last variable is used')
.toCompileTo('Goodbye! cruel world!');
@@ -427,9 +431,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(
@@ -446,10 +450,10 @@ describe('builtin helpers', 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'
@@ -459,7 +463,7 @@ describe('builtin helpers', function () {
expectTemplate(string)
.withInput({
goodbyes: [],
world: 'world',
world: 'world'
})
.withMessage(
'each with array function argument ignores the contents when empty'
@@ -475,9 +479,9 @@ 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!');
@@ -491,8 +495,8 @@ describe('builtin helpers', function () {
.withMessage('should output data')
.withRuntimeOptions({
data: {
exclaim: '!',
},
exclaim: '!'
}
})
.toCompileTo('a!b!c!');
});
@@ -504,50 +508,6 @@ describe('builtin helpers', function () {
);
});
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() {
function Iterator(arr) {
@@ -575,9 +535,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'
@@ -587,7 +547,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'
@@ -626,8 +586,8 @@ describe('builtin helpers', function () {
.withInput({ blah: 'whee' })
.withMessage('log should not display')
.toCompileTo('');
expect(levelArg).toBe(1);
expect(logArg).toBe('whee');
equals(1, levelArg, 'should call log with 1');
equals('whee', logArg, "should call log with 'whee'");
});
it('should call logger at data level', function() {
@@ -642,21 +602,21 @@ describe('builtin helpers', function () {
.withRuntimeOptions({ data: { level: '03' } })
.withCompileOptions({ data: true })
.toCompileTo('');
expect(levelArg).toBe('03');
expect(logArg).toBe('whee');
equals('03', levelArg);
equals('whee', logArg);
});
it('should output to info', function() {
var called;
console.info = function(info) {
expect(info).toBe('whee');
equals('whee', info);
called = true;
console.info = $info;
console.log = $log;
};
console.log = function(log) {
expect(log).toBe('whee');
equals('whee', log);
called = true;
console.info = $info;
console.log = $log;
@@ -665,14 +625,14 @@ describe('builtin helpers', function () {
expectTemplate('{{log blah}}')
.withInput({ blah: 'whee' })
.toCompileTo('');
expect(called).toBe(true);
equals(true, called);
});
it('should log at data level', function() {
var called;
console.error = function(log) {
expect(log).toBe('whee');
equals('whee', log);
called = true;
console.error = $error;
};
@@ -682,7 +642,7 @@ describe('builtin helpers', function () {
.withRuntimeOptions({ data: { level: '03' } })
.withCompileOptions({ data: true })
.toCompileTo('');
expect(called).toBe(true);
equals(true, called);
});
it('should handle missing logger', function() {
@@ -690,7 +650,7 @@ describe('builtin helpers', function () {
console.error = undefined;
console.log = function(log) {
expect(log).toBe('whee');
equals('whee', log);
called = true;
console.log = $log;
};
@@ -700,14 +660,14 @@ describe('builtin helpers', function () {
.withRuntimeOptions({ data: { level: '03' } })
.withCompileOptions({ data: true })
.toCompileTo('');
expect(called).toBe(true);
equals(true, called);
});
it('should handle string log levels', function() {
var called;
console.error = function(log) {
expect(log).toBe('whee');
equals('whee', log);
called = true;
};
@@ -716,7 +676,7 @@ describe('builtin helpers', function () {
.withRuntimeOptions({ data: { level: 'error' } })
.withCompileOptions({ data: true })
.toCompileTo('');
expect(called).toBe(true);
equals(true, called);
called = false;
@@ -725,31 +685,27 @@ describe('builtin helpers', function () {
.withRuntimeOptions({ data: { level: 'ERROR' } })
.withCompileOptions({ data: true })
.toCompileTo('');
expect(called).toBe(true);
equals(true, called);
});
it('should handle hash log levels', function() {
var called;
console.error = function(log) {
expect(log).toBe('whee');
equals('whee', log);
called = true;
};
expectTemplate('{{log blah level="error"}}')
.withInput({ blah: 'whee' })
.toCompileTo('');
expect(called).toBe(true);
equals(true, called);
});
it('should handle hash log levels', function() {
var called = false;
console.info =
console.log =
console.error =
console.debug =
function () {
console.info = console.log = console.error = console.debug = function() {
called = true;
console.info = console.log = console.error = console.debug = $log;
};
@@ -757,16 +713,16 @@ describe('builtin helpers', function () {
expectTemplate('{{log blah level="debug"}}')
.withInput({ blah: 'whee' })
.toCompileTo('');
expect(called).toBe(false);
equals(false, called);
});
it('should pass multiple log arguments', function() {
var called;
console.info = console.log = function(log1, log2, log3) {
expect(log1).toBe('whee');
expect(log2).toBe('foo');
expect(log3).toBe(1);
equals('whee', log1);
equals('foo', log2);
equals(1, log3);
called = true;
console.log = $log;
};
@@ -774,20 +730,22 @@ describe('builtin helpers', function () {
expectTemplate('{{log blah "foo" 1}}')
.withInput({ blah: 'whee' })
.toCompileTo('');
expect(called).toBe(true);
equals(true, called);
});
it('should pass zero log arguments', function() {
var called;
console.info = console.log = function() {
expect(arguments.length).toBe(0);
expect(arguments.length).to.equal(0);
called = true;
console.log = $log;
};
expectTemplate('{{log}}').withInput({ blah: 'whee' }).toCompileTo('');
expect(called).toBe(true);
expectTemplate('{{log}}')
.withInput({ blah: 'whee' })
.toCompileTo('');
expect(called).to.be.true();
});
/* eslint-enable no-console */
});
+205 -68
View File
@@ -10,56 +10,69 @@ describe('compiler', function () {
}
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(
equal(compile('foo').equals(compile('foo')), true);
equal(compile('{{foo}}').equals(compile('{{foo}}')), true);
equal(compile('{{foo.bar}}').equals(compile('{{foo.bar}}')), true);
equal(
compile('{{foo.bar baz "foo" true false bat=1}}').equals(
compile('{{foo.bar baz "foo" true false bat=1}}')
)
).toBe(true);
expect(
),
true
);
equal(
compile('{{foo.bar (baz bat=1)}}').equals(
compile('{{foo.bar (baz bat=1)}}')
)
).toBe(true);
expect(
compile('{{#foo}} {{/foo}}').equals(compile('{{#foo}} {{/foo}}'))
).toBe(true);
),
true
);
equal(
compile('{{#foo}} {{/foo}}').equals(compile('{{#foo}} {{/foo}}')),
true
);
});
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(
equal(compile('foo').equals(compile('bar')), false);
equal(compile('{{foo}}').equals(compile('{{bar}}')), false);
equal(compile('{{foo.bar}}').equals(compile('{{bar.bar}}')), false);
equal(
compile('{{foo.bar baz bat=1}}').equals(
compile('{{foo.bar bar bat=1}}')
)
).toBe(false);
expect(
),
false
);
equal(
compile('{{foo.bar (baz bat=1)}}').equals(
compile('{{foo.bar (bar bat=1)}}')
)
).toBe(false);
expect(
compile('{{#foo}} {{/foo}}').equals(compile('{{#bar}} {{/bar}}'))
).toBe(false);
expect(
compile('{{#foo}} {{/foo}}').equals(compile('{{#foo}} {{foo}}{{/foo}}'))
).toBe(false);
),
false
);
equal(
compile('{{#foo}} {{/foo}}').equals(compile('{{#bar}} {{/bar}}')),
false
);
equal(
compile('{{#foo}} {{/foo}}').equals(
compile('{{#foo}} {{foo}}{{/foo}}')
),
false
);
});
});
describe('#compile', function() {
it('should fail with invalid input', function() {
expect(function () {
shouldThrow(
function() {
Handlebars.compile(null);
}).toThrow(
},
Error,
'You must pass a string or Handlebars AST to Handlebars.compile. You passed null'
);
expect(function () {
shouldThrow(
function() {
Handlebars.compile({});
}).toThrow(
},
Error,
'You must pass a string or Handlebars AST to Handlebars.compile. You passed [object Object]'
);
});
@@ -67,94 +80,218 @@ describe('compiler', function () {
it('should include the location in the error (row and column)', function() {
try {
Handlebars.compile(' \n {{#if}}\n{{/def}}')();
expect.unreachable('Statement must throw exception');
equal(
true,
false,
'Statement must throw exception. This line should not be executed.'
);
} catch (err) {
expect(err.message).toBe("if doesn't match def - 2:5");
equal(
err.message,
"if doesn't match def - 2:5",
'Checking error message'
);
if (Object.getOwnPropertyDescriptor(err, 'column').writable) {
// In Safari 8, the column-property is read-only. This means that even if it is set with defineProperty,
// its value won't change (https://github.com/jquery/esprima/issues/1290#issuecomment-132455482)
// Since this was neither working in Handlebars 3 nor in 4.0.5, we only check the column for other browsers.
expect(err.column).toBe(5);
equal(err.column, 5, 'Checking error column');
}
expect(err.lineNumber).toBe(2);
equal(err.lineNumber, 2, 'Checking error row');
}
});
it('should include the location as enumerable property', function() {
try {
Handlebars.compile(' \n {{#if}}\n{{/def}}')();
expect.unreachable('Statement must throw exception');
equal(
true,
false,
'Statement must throw exception. This line should not be executed.'
);
} catch (err) {
expect(Object.prototype.propertyIsEnumerable.call(err, 'column')).toBe(
true
equal(
Object.prototype.propertyIsEnumerable.call(err, 'column'),
true,
'Checking error column'
);
}
});
it('can utilize AST instance', function() {
expect(
equal(
Handlebars.compile({
type: 'Program',
body: [{ type: 'ContentStatement', value: 'Hello' }],
})()
).toBe('Hello');
body: [{ type: 'ContentStatement', value: 'Hello' }]
})(),
'Hello'
);
});
function createPathExpressionAST(depth, parts) {
return {
type: 'Program',
body: [
{
type: 'MustacheStatement',
escaped: true,
strip: { open: false, close: false },
path: {
type: 'PathExpression',
data: false,
depth: depth,
parts: parts,
original: 'this'
},
params: []
}
]
};
}
it('should safely handle AST with non-integer PathExpression depth', function() {
// depth '0' is coerced to 0 via Number(), compiles safely
var result = Handlebars.compile(createPathExpressionAST('0', ['this']))();
expect(result).to.be.a('string');
});
it('should safely handle AST with negative PathExpression depth', function() {
// Negative depth is clamped to 0
var result = Handlebars.compile(createPathExpressionAST(-1, ['this']))();
expect(result).to.be.a('string');
});
it('should safely handle AST with fractional PathExpression depth', function() {
// Fractional depth is floored to an integer
var result = Handlebars.compile(createPathExpressionAST(0.5, ['this']))();
expect(result).to.be.a('string');
});
it('should safely handle AST with non-array PathExpression parts', function() {
// Non-array parts are coerced to empty array, compiles safely
var result = Handlebars.compile(createPathExpressionAST(0, 'this'))();
expect(result).to.be.a('string');
});
it('should safely handle AST with non-string PathExpression part', function() {
// Non-string parts are coerced to strings via String()
var result = Handlebars.compile(createPathExpressionAST(0, [1]))();
expect(result).to.be.a('string');
});
it('should safely handle AST with non-boolean BooleanLiteral value type', function() {
// The compiler coerces BooleanLiteral.value via === true before
// emitting a pushLiteral opcode, so a non-boolean value like the
// string 'true' becomes the literal 'false'.
var loc = {
source: null,
start: { line: 1, column: 0 },
end: { line: 1, column: 10 }
};
var result = Handlebars.compile({
type: 'Program',
body: [
{
type: 'MustacheStatement',
escaped: true,
strip: { open: false, close: false },
loc: loc,
path: {
type: 'BooleanLiteral',
value: 'true',
original: true,
loc: loc
},
params: []
}
]
})();
// 'true' !== true, so the compiler emits pushLiteral('false').
// Handlebars does not render falsy values, so the output is empty.
expect(result).to.equal('');
});
it('should ignore loc metadata in AST nodes', function() {
equal(
Handlebars.compile({
type: 'Program',
meta: null,
loc: { source: 'fake', start: { line: 1, column: 0 } },
body: [{ type: 'ContentStatement', value: 'Hello' }]
})(),
'Hello'
);
});
it('should accept AST with valid NumberLiteral values', function() {
equal(
Handlebars.compile(Handlebars.parse('{{lookup this 1}}'))(['a', 'b']),
'b'
);
});
it('should accept AST with valid BooleanLiteral values', function() {
equal(
Handlebars.compile(Handlebars.parse('{{#if true}}ok{{/if}}'))({}),
'ok'
);
});
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'
);
equal(Handlebars.compile('')(), '');
});
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)();
expect(options).toStrictEqual({ data: [{ a: 'foo' }, { a: 'bar' }] });
equal(
JSON.stringify(options, 0, 2),
JSON.stringify({ data: [{ a: 'foo' }, { a: 'bar' }] }, 0, 2)
);
});
it('should not modify the options.knownHelpers property(GH-1327)', function() {
var options = { knownHelpers: {} };
Handlebars.compile('{{#each data}}{{@index}}:{{a}} {{/each}}', options)();
expect(options).toStrictEqual({ knownHelpers: {} });
equal(
JSON.stringify(options, 0, 2),
JSON.stringify({ knownHelpers: {} }, 0, 2)
);
});
});
describe('#precompile', function() {
it('should fail with invalid input', function() {
expect(function () {
shouldThrow(
function() {
Handlebars.precompile(null);
}).toThrow(
'You must pass a string or Handlebars AST to Handlebars.compile. You passed null'
},
Error,
'You must pass a string or Handlebars AST to Handlebars.precompile. You passed null'
);
expect(function () {
shouldThrow(
function() {
Handlebars.precompile({});
}).toThrow(
'You must pass a string or Handlebars AST to Handlebars.compile. You passed [object Object]'
},
Error,
'You must pass a string or Handlebars AST to Handlebars.precompile. You passed [object Object]'
);
});
it('can utilize AST instance', function() {
expect(
equal(
/return "Hello"/.test(
Handlebars.precompile({
type: 'Program',
body: [{ type: 'ContentStatement', value: 'Hello' }],
body: [{ type: 'ContentStatement', value: 'Hello' }]
})
).toMatch(/return "Hello"/);
),
true
);
});
it('can pass through an empty string', function() {
expect(Handlebars.precompile('')).toMatch(/return ""/);
equal(/return ""/.test(Handlebars.precompile('')), true);
});
});
});
+9 -9
View File
@@ -93,8 +93,8 @@ describe('data', function () {
data: {
hello: function() {
return 'hello';
},
},
}
}
})
.toCompileTo('hello');
});
@@ -105,8 +105,8 @@ describe('data', function () {
data: {
hello: function(arg) {
return arg;
},
},
}
}
})
.toCompileTo('hello');
});
@@ -159,7 +159,7 @@ describe('data', 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) {
return options.fn(this);
@@ -217,7 +217,7 @@ describe('data', function () {
})
.withInput({ exclaim: true, zomg: 'planet' })
.withRuntimeOptions({ data: { adjective: 'happy' } })
.withMessage('Overridden data output by helper')
.withMessage('Overriden data output by helper')
.toCompileTo('sad world?');
});
@@ -232,7 +232,7 @@ describe('data', function () {
})
.withInput({ exclaim: true, zomg: 'world' })
.withRuntimeOptions({ data: { adjective: 'happy' } })
.withMessage('Overridden data output by helper')
.withMessage('Overriden data output by helper')
.toCompileTo('sad world?');
});
@@ -269,8 +269,8 @@ describe('data', function () {
})
.withRuntimeOptions({
data: {
depth: 0,
},
depth: 0
}
})
.toCompileTo('2 1 0');
});
-6
View File
@@ -1,6 +0,0 @@
// 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
@@ -1,27 +0,0 @@
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 */
}
+12 -1
View File
@@ -3,9 +3,20 @@ 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'
@@ -21,7 +32,7 @@ global.CompilerContext = {
},
compileWithPartial: function(template, options) {
return handlebarsEnv.compile(template, options);
},
}
};
function safeEval(templateSpec) {
+129 -23
View File
@@ -1,4 +1,125 @@
var global = globalThis;
var global = (function() {
return this;
})();
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) {
return new HandlebarsTestBench(templateAsString);
@@ -79,29 +200,18 @@ HandlebarsTestBench.prototype.withMessage = function (message) {
};
HandlebarsTestBench.prototype.toCompileTo = function(expectedOutputAsString) {
expect(this._compileAndExecute()).toBe(expectedOutputAsString);
expect(this._compileAndExecute()).to.equal(
expectedOutputAsString,
this.message
);
};
// see chai "to.throw" (https://www.chaijs.com/api/bdd/#method_throw)
HandlebarsTestBench.prototype.toThrow = function(errorLike, errMsgMatcher) {
var self = this;
var caught;
try {
expect(function() {
self._compileAndExecute();
} 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);
}
}).to.throw(errorLike, errMsgMatcher, this.message);
};
HandlebarsTestBench.prototype._compileAndExecute = function() {
@@ -127,7 +237,3 @@ HandlebarsTestBench.prototype._combineRuntimeOptions = function () {
combinedRuntimeOptions.decorators = this.decorators;
return combinedRuntimeOptions;
};
beforeEach(function () {
global.handlebarsEnv = Handlebars.create();
});
+9 -1
View File
@@ -1,5 +1,13 @@
require('./common');
var chai = require('chai');
var dirtyChai = require('dirty-chai');
chai.use(dirtyChai);
global.expect = chai.expect;
global.sinon = require('sinon');
global.Handlebars = require('../../lib');
global.CompilerContext = {
@@ -9,7 +17,7 @@ global.CompilerContext = {
},
compileWithPartial: function(template, options) {
return handlebarsEnv.compile(template, options);
},
}
};
function safeEval(templateSpec) {
+63
View File
@@ -0,0 +1,63 @@
/* 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
@@ -0,0 +1,70 @@
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 */
}
+2 -2
View File
@@ -1,6 +1,6 @@
define(['handlebars.runtime'], function(Handlebars) {
define(["handlebars.runtime"], function(Handlebars) {
Handlebars = Handlebars["default"]; var template = Handlebars.template, templates = Handlebars.templates = Handlebars.templates || {};
return templates['bom'] = template({"compiler":[8,">= 4.3.0"],"main":function(container,depth0,helpers,partials,data) {
return templates["bom"] = template({"compiler":[8,">= 4.3.0"],"main":function(container,depth0,helpers,partials,data) {
return "a";
},"useData":true});
});
+2 -2
View File
@@ -1,6 +1,6 @@
define(['handlebars.runtime'], function(Handlebars) {
define(["handlebars.runtime"], function(Handlebars) {
Handlebars = Handlebars["default"]; var template = Handlebars.template, templates = Handlebars.templates = Handlebars.templates || {};
return templates['empty'] = template({"compiler":[8,">= 4.3.0"],"main":function(container,depth0,helpers,partials,data) {
return templates["empty"] = template({"compiler":[8,">= 4.3.0"],"main":function(container,depth0,helpers,partials,data) {
return "";
},"useData":true});
});
+2 -2
View File
@@ -1,6 +1,6 @@
define(['handlebars.runtime'], function(Handlebars) {
define(["handlebars.runtime"], function(Handlebars) {
Handlebars = Handlebars["default"]; var template = Handlebars.template, templates = CustomNamespace.templates = CustomNamespace.templates || {};
return templates['empty'] = template({"compiler":[8,">= 4.3.0"],"main":function(container,depth0,helpers,partials,data) {
return templates["empty"] = template({"compiler":[8,">= 4.3.0"],"main":function(container,depth0,helpers,partials,data) {
return "";
},"useData":true});
});
+1 -1
View File
@@ -1,6 +1,6 @@
(function() {
var template = Handlebars.template, templates = Handlebars.templates = Handlebars.templates || {};
templates['empty'] = template({"compiler":[8,">= 4.3.0"],"main":function(container,depth0,helpers,partials,data) {
templates["empty"] = template({"compiler":[8,">= 4.3.0"],"main":function(container,depth0,helpers,partials,data) {
return "";
},"useData":true});
})();
+3 -3
View File
@@ -1,9 +1,9 @@
define(['handlebars.runtime'], function(Handlebars) {
define(["handlebars.runtime"], function(Handlebars) {
Handlebars = Handlebars["default"]; var template = Handlebars.template, templates = Handlebars.templates = Handlebars.templates || {};
templates['firstTemplate'] = template({"compiler":[8,">= 4.3.0"],"main":function(container,depth0,helpers,partials,data) {
templates["firstTemplate"] = template({"compiler":[8,">= 4.3.0"],"main":function(container,depth0,helpers,partials,data) {
return "<div>1</div>";
},"useData":true});
templates['secondTemplate'] = template({"compiler":[8,">= 4.3.0"],"main":function(container,depth0,helpers,partials,data) {
templates["secondTemplate"] = template({"compiler":[8,">= 4.3.0"],"main":function(container,depth0,helpers,partials,data) {
return "<div>2</div>";
},"useData":true});
return templates;
+2 -2
View File
@@ -1,6 +1,6 @@
define(['handlebars.runtime'], function(Handlebars) {
define(["handlebars.runtime"], function(Handlebars) {
Handlebars = Handlebars["default"]; var template = Handlebars.template, templates = Handlebars.templates = Handlebars.templates || {};
return templates['artifacts/partial.template'] = template({"compiler":[8,">= 4.3.0"],"main":function(container,depth0,helpers,partials,data) {
return templates["artifacts/partial.template"] = template({"compiler":[8,">= 4.3.0"],"main":function(container,depth0,helpers,partials,data) {
return "<div>Test Partial</div>";
},"useData":true});
});
+2 -2
View File
@@ -1,6 +1,6 @@
define(['some-path/handlebars.runtime'], function(Handlebars) {
define(["some-path/handlebars.runtime"], function(Handlebars) {
Handlebars = Handlebars["default"]; var template = Handlebars.template, templates = Handlebars.templates = Handlebars.templates || {};
return templates['empty'] = template({"compiler":[8,">= 4.3.0"],"main":function(container,depth0,helpers,partials,data) {
return templates["empty"] = template({"compiler":[8,">= 4.3.0"],"main":function(container,depth0,helpers,partials,data) {
return "";
},"useData":true});
});
+1 -1
View File
@@ -1,5 +1,5 @@
Precompile handlebar templates.
Usage: handlebars.mjs [template|directory]...
Usage: handlebars [template|directory]...
Options:
-f, --output Output File [string]
+3 -3
View File
@@ -1,9 +1,9 @@
define(['handlebars.runtime'], function(Handlebars) {
define(["handlebars.runtime"], function(Handlebars) {
Handlebars = Handlebars["default"]; var template = Handlebars.template, templates = someNameSpace = someNameSpace || {};
templates['empty'] = template({"compiler":[8,">= 4.3.0"],"main":function(container,depth0,helpers,partials,data) {
templates["empty"] = template({"compiler":[8,">= 4.3.0"],"main":function(container,depth0,helpers,partials,data) {
return "";
},"useData":true});
templates['empty'] = template({"compiler":[8,">= 4.3.0"],"main":function(container,depth0,helpers,partials,data) {
templates["empty"] = template({"compiler":[8,">= 4.3.0"],"main":function(container,depth0,helpers,partials,data) {
return "";
},"useData":true});
return templates;
+2 -2
View File
@@ -1,6 +1,6 @@
define(['handlebars.runtime'], function(Handlebars) {
define(["handlebars.runtime"], function(Handlebars) {
Handlebars = Handlebars["default"]; var template = Handlebars.template, templates = Handlebars.templates = Handlebars.templates || {};
return templates['non.default.extension'] = template({"compiler":[8,">= 4.3.0"],"main":function(container,depth0,helpers,partials,data) {
return templates["non.default.extension"] = template({"compiler":[8,">= 4.3.0"],"main":function(container,depth0,helpers,partials,data) {
return "<div>This is a test</div>";
},"useData":true});
});
+2 -2
View File
@@ -1,6 +1,6 @@
define(['handlebars.runtime'], function(Handlebars) {
define(["handlebars.runtime"], function(Handlebars) {
Handlebars = Handlebars["default"]; var template = Handlebars.template, templates = Handlebars.templates = Handlebars.templates || {};
return templates['known.helpers'] = template({"0":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];
+2 -2
View File
@@ -1,6 +1,6 @@
define(['handlebars.runtime'], function(Handlebars) {
define(["handlebars.runtime"], function(Handlebars) {
Handlebars = Handlebars["default"]; var template = Handlebars.template, templates = Handlebars.templates = Handlebars.templates || {};
return Handlebars.partials['partial.template'] = template({"compiler":[8,">= 4.3.0"],"main":function(container,depth0,helpers,partials,data) {
return Handlebars.partials["partial.template"] = template({"compiler":[8,">= 4.3.0"],"main":function(container,depth0,helpers,partials,data) {
return "<div>Test Partial</div>";
},"useData":true});
});
+48 -44
View File
@@ -3,7 +3,7 @@ describe('helpers', function () {
expectTemplate('{{#goodbyes}}{{{link ../prefix}}}{{/goodbyes}}')
.withInput({
prefix: '/root',
goodbyes: [{ text: 'Goodbye', url: 'goodbye' }],
goodbyes: [{ text: 'Goodbye', url: 'goodbye' }]
})
.withHelper('link', function(prefix) {
return (
@@ -53,7 +53,7 @@ describe('helpers', function () {
runWithIdentityHelper('{{{{identity}}}}{{{{/identity}}}}', '');
});
it.skip('helper for nested raw block works if nested raw blocks are broken', function () {
xit('helper for nested raw block works if nested raw blocks are broken', function() {
// This test was introduced in 4.4.4, but it was not the actual problem that lead to the patch release
// The test is deactivated, because in 3.x this template cases an exception and it also does not work in 4.4.3
// If anyone can make this template work without breaking everything else, then go for it,
@@ -111,7 +111,7 @@ describe('helpers', function () {
)
.withInput({
prefix: '/root',
goodbyes: [{ text: 'Goodbye', url: 'goodbye' }],
goodbyes: [{ text: 'Goodbye', url: 'goodbye' }]
})
.withHelper('link', function(prefix, options) {
return (
@@ -133,7 +133,7 @@ describe('helpers', function () {
)
.withInput({
prefix: '/root',
goodbyes: [{ text: 'Goodbye', url: 'goodbye' }],
goodbyes: [{ text: 'Goodbye', url: 'goodbye' }]
})
.withHelper('link', function(prefix, options) {
return (
@@ -152,13 +152,13 @@ describe('helpers', function () {
it('helper returning undefined value', function() {
expectTemplate(' {{nothere}}')
.withHelpers({
nothere: function () {},
nothere: function() {}
})
.toCompileTo(' ');
expectTemplate(' {{#nothere}}{{/nothere}}')
.withHelpers({
nothere: function () {},
nothere: function() {}
})
.toCompileTo(' ');
});
@@ -194,8 +194,8 @@ describe('helpers', function () {
.withInput({
people: [
{ name: 'Alan', id: 1 },
{ name: 'Yehuda', id: 2 },
],
{ name: 'Yehuda', id: 2 }
]
})
.withHelper('link', link)
.toCompileTo(
@@ -232,7 +232,7 @@ describe('helpers', function () {
'{{#form yehuda}}<p>{{name}}</p>{{#link}}Hello{{/link}}{{/form}}'
)
.withInput({
yehuda: { name: 'Yehuda' },
yehuda: { name: 'Yehuda' }
})
.withHelper('link', function(options) {
return '<a href="' + this.name + '">' + options.fn(this) + '</a>';
@@ -278,7 +278,7 @@ describe('helpers', function () {
expectTemplate('{{#list people}}Hello{{^}}{{message}}{{/list}}')
.withInput({
people: [],
message: "Nobody's here",
message: "Nobody's here"
})
.withHelpers({ list: list })
.withMessage('the context of an inverse is the parent of the block')
@@ -289,13 +289,13 @@ describe('helpers', function () {
var hash = {
helper: function() {
return 'winning';
},
}
};
hash.hash = hash;
var helpers = {
'./helper': function() {
return 'fail';
},
}
};
expectTemplate('{{./helper 1}}')
@@ -316,7 +316,7 @@ describe('helpers', function () {
.withHelpers({
world: function() {
return 'world';
},
}
})
.withMessage('helpers hash is available')
.toCompileTo('Goodbye cruel world!');
@@ -326,7 +326,7 @@ describe('helpers', function () {
.withHelpers({
world: function() {
return 'world';
},
}
})
.withMessage('helpers hash is available inside other blocks')
.toCompileTo('Goodbye cruel world!');
@@ -338,7 +338,7 @@ describe('helpers', function () {
.withHelpers({
lookup: function() {
return 'helpers';
},
}
})
.withMessage('helpers hash has precedence escaped expansion')
.toCompileTo('helpers');
@@ -348,7 +348,7 @@ describe('helpers', function () {
.withHelpers({
lookup: function() {
return 'helpers';
},
}
})
.withMessage('helpers hash has precedence simple expansion')
.toCompileTo('helpers');
@@ -360,7 +360,7 @@ describe('helpers', function () {
.withHelpers({
helper: function() {
return 'helper';
},
}
})
.withMessage('helpers hash is available in nested contexts.')
.toCompileTo('helper');
@@ -378,7 +378,7 @@ describe('helpers', function () {
.withHelpers({
world: function() {
return 'world!';
},
}
})
.toCompileTo('found it! Goodbye cruel world!!');
});
@@ -392,7 +392,7 @@ describe('helpers', function () {
return 'fail';
});
handlebarsEnv.unregisterHelper('foo');
expect(handlebarsEnv.helpers.foo).toBeUndefined();
equals(handlebarsEnv.helpers.foo, undefined);
});
it('allows multiple globals', function() {
@@ -406,7 +406,7 @@ describe('helpers', function () {
},
testHelper: function() {
return 'found it!';
},
}
});
expectTemplate(
@@ -417,7 +417,8 @@ describe('helpers', function () {
});
it('fails with multiple and args', function() {
expect(function () {
shouldThrow(
function() {
handlebarsEnv.registerHelper(
{
world: function() {
@@ -425,11 +426,14 @@ describe('helpers', function () {
},
testHelper: function() {
return 'found it!';
},
}
},
{}
);
}).toThrow('Arg not supported with multiple helpers');
},
Error,
'Arg not supported with multiple helpers'
);
});
});
@@ -671,7 +675,7 @@ describe('helpers', function () {
it('Known helper should render helper', function() {
expectTemplate('{{hello}}')
.withCompileOptions({
knownHelpers: { hello: true },
knownHelpers: { hello: true }
})
.withHelper('hello', function() {
return 'foo';
@@ -683,7 +687,7 @@ describe('helpers', function () {
expectTemplate('{{typeof hello}}')
.withCompileOptions({
knownHelpers: { typeof: true },
knownHelpersOnly: true,
knownHelpersOnly: true
})
.withHelper('typeof', function(arg) {
return typeof arg;
@@ -697,7 +701,7 @@ describe('helpers', function () {
it('Builtin helpers available in knownHelpers only mode', function() {
expectTemplate('{{#unless foo}}bar{{/unless}}')
.withCompileOptions({
knownHelpersOnly: true,
knownHelpersOnly: true
})
.toCompileTo('bar');
});
@@ -705,7 +709,7 @@ describe('helpers', function () {
it('Field lookup works in knownHelpers only mode', function() {
expectTemplate('{{foo}}')
.withCompileOptions({
knownHelpersOnly: true,
knownHelpersOnly: true
})
.withInput({ foo: 'bar' })
.toCompileTo('bar');
@@ -714,7 +718,7 @@ describe('helpers', function () {
it('Conditional blocks work in knownHelpers only mode', function() {
expectTemplate('{{#foo}}bar{{/foo}}')
.withCompileOptions({
knownHelpersOnly: true,
knownHelpersOnly: true
})
.withInput({ foo: 'baz' })
.toCompileTo('bar');
@@ -723,7 +727,7 @@ describe('helpers', function () {
it('Invert blocks work in knownHelpers only mode', function() {
expectTemplate('{{^foo}}bar{{/foo}}')
.withCompileOptions({
knownHelpersOnly: true,
knownHelpersOnly: true
})
.withInput({ foo: false })
.toCompileTo('bar');
@@ -732,13 +736,13 @@ describe('helpers', function () {
it('Functions are bound to the context in knownHelpers only mode', function() {
expectTemplate('{{foo}}')
.withCompileOptions({
knownHelpersOnly: true,
knownHelpersOnly: true
})
.withInput({
foo: function() {
return this.bar;
},
bar: 'bar',
bar: 'bar'
})
.toCompileTo('bar');
});
@@ -756,7 +760,7 @@ describe('helpers', function () {
.withInput({
truthy: function() {
return true;
},
}
})
.toCompileTo('yep');
});
@@ -769,7 +773,7 @@ describe('helpers', function () {
},
truthiness: function() {
return false;
},
}
})
.toCompileTo('');
});
@@ -785,7 +789,7 @@ describe('helpers', function () {
},
helper: function() {
return 'ran: ' + arguments[arguments.length - 1].name;
},
}
};
it('should include in ambiguous mustache calls', function() {
@@ -822,7 +826,7 @@ describe('helpers', function () {
expectTemplate('{{helper}}')
.withCompileOptions({
knownHelpers: { helper: true },
knownHelpersOnly: true,
knownHelpersOnly: true
})
.withHelpers(helpers)
.toCompileTo('ran: helper');
@@ -854,7 +858,7 @@ describe('helpers', function () {
})
.withInput({
goodbye: 'goodbye',
world: 'world',
world: 'world'
})
.withMessage('Helper executed')
.toCompileTo('GOODBYE cruel WORLD');
@@ -870,7 +874,7 @@ describe('helpers', function () {
})
.withInput({
goodbye: 'goodbye',
world: 'world',
world: 'world'
})
.withMessage('Helper executed')
.toCompileTo('GOODBYE cruel WORLD');
@@ -886,7 +890,7 @@ describe('helpers', function () {
})
.withInput({
goodbye: 'goodbye',
world: 'world',
world: 'world'
})
.withMessage('Helper not executed')
.toCompileTo('goodbye cruel WORLD cruel GOODBYE');
@@ -904,7 +908,7 @@ describe('helpers', function () {
})
.withInput({
goodbye: 'goodbye',
world: 'world',
world: 'world'
})
.withMessage('Helper executed')
.toCompileTo('GOODBYE cruel WORLD goodbye');
@@ -916,7 +920,7 @@ describe('helpers', function () {
expectTemplate('{{#goodbyes as |value|}}{{value}}{{/goodbyes}}{{value}}')
.withInput({ value: 'foo' })
.withHelper('goodbyes', function(options) {
expect(options.fn.blockParams).toBe(1);
equals(options.fn.blockParams, 1);
return options.fn({ value: 'bar' }, { blockParams: [1, 2] });
})
.toCompileTo('1foo');
@@ -928,7 +932,7 @@ describe('helpers', function () {
return 'foo';
})
.withHelper('goodbyes', function(options) {
expect(options.fn.blockParams).toBe(1);
equals(options.fn.blockParams, 1);
return options.fn({}, { blockParams: [1, 2] });
})
.toCompileTo('1foo');
@@ -943,7 +947,7 @@ describe('helpers', function () {
return 'foo';
})
.withHelper('goodbyes', function(options) {
expect(options.fn.blockParams).toBe(1);
equals(options.fn.blockParams, 1);
return options.fn(this, { blockParams: [1, 2] });
})
.toCompileTo('barfoo');
@@ -960,7 +964,7 @@ describe('helpers', function () {
{ value: 'bar' },
{
blockParams:
options.fn.blockParams === 1 ? [value++, value++] : undefined,
options.fn.blockParams === 1 ? [value++, value++] : undefined
}
);
})
@@ -973,7 +977,7 @@ describe('helpers', function () {
)
.withInput({ value: 'foo' })
.withHelper('goodbyes', function(options) {
expect(options.fn.blockParams).toBe(1);
equals(options.fn.blockParams, 1);
return options.fn({ value: 'bar' }, { blockParams: [1, 2] });
})
.toCompileTo('1foo');
+92 -48
View File
@@ -1,55 +1,99 @@
<!doctype html>
<html>
<head>
<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" />
<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>
</head>
<body>
<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>
<div id="mocha"></div>
</body>
</html>
+8 -13
View File
@@ -19,9 +19,11 @@ describe('javascript-compiler api', function () {
) {
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
@@ -32,11 +34,7 @@ describe('javascript-compiler api', function () {
.toCompileTo('food');
});
});
// 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 () {
describe('#compilerInfo', function() {
var $superCheck, $superInfo;
beforeEach(function() {
$superCheck = handlebarsEnv.VM.checkRevision;
@@ -59,8 +57,7 @@ describe('javascript-compiler api', function () {
.withInput({ foo: 'food' })
.toCompileTo('food ');
});
}
);
});
describe('buffer', function() {
var $superAppend, $superCreate;
beforeEach(function() {
@@ -72,13 +69,11 @@ describe('javascript-compiler api', 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 () {
handlebarsEnv.JavaScriptCompiler.prototype.initializeBuffer = function() {
return this.quotedString('foo_');
};
expectTemplate('{{foo}} ')
@@ -108,7 +103,7 @@ describe('javascript-compiler api', function () {
handlebarsEnv.JavaScriptCompiler.isValidJavaScriptVariableName(
validVariableName
)
).toBe(true);
).to.be.true();
});
});
[('123test', 'abc()', 'abc.cde')].forEach(function(invalidVariableName) {
@@ -117,7 +112,7 @@ describe('javascript-compiler api', function () {
handlebarsEnv.JavaScriptCompiler.isValidJavaScriptVariableName(
invalidVariableName
)
).toBe(false);
).to.be.false();
});
});
});
+455
View File
@@ -0,0 +1,455 @@
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 }
})
);
});
});
+64 -61
View File
@@ -5,8 +5,8 @@ describe('partials', function () {
var hash = {
dudes: [
{ name: 'Yehuda', url: 'http://yehuda' },
{ name: 'Alan', url: 'http://alan' },
],
{ name: 'Alan', url: 'http://alan' }
]
};
expectTemplate(string)
@@ -28,13 +28,13 @@ describe('partials', function () {
var hash = {
dudes: [
{ name: 'Yehuda', url: 'http://yehuda' },
{ name: 'Alan', url: 'http://alan' },
],
{ name: 'Alan', url: 'http://alan' }
]
};
var helpers = {
partial: function() {
return 'dude';
},
}
};
expectTemplate(string)
@@ -57,17 +57,14 @@ describe('partials', function () {
.withInput({
dudes: [
{ name: 'Yehuda', url: 'http://yehuda' },
{ name: 'Alan', url: 'http://alan' },
],
{ name: 'Alan', url: 'http://alan' }
]
})
.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() {
@@ -75,8 +72,8 @@ describe('partials', function () {
.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')
@@ -88,8 +85,8 @@ describe('partials', function () {
var hash = {
dudes: [
{ name: 'Yehuda', url: 'http://yehuda' },
{ name: 'Alan', url: 'http://alan' },
],
{ name: 'Alan', url: 'http://alan' }
]
};
expectTemplate('Dudes: {{#dudes}}{{>dude}}{{/dudes}}')
@@ -130,8 +127,8 @@ describe('partials', function () {
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.')
@@ -143,12 +140,12 @@ describe('partials', function () {
.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(
@@ -159,14 +156,17 @@ describe('partials', 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() {
expect(function () {
handlebarsEnv.registerPartial('undefined_test', undefined);
}).toThrow(
shouldThrow(
function() {
var undef;
handlebarsEnv.registerPartial('undefined_test', undef);
},
Handlebars.Exception,
'Attempting to register a partial called "undefined_test" as undefined'
);
});
@@ -174,7 +174,7 @@ describe('partials', 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'
);
});
@@ -186,8 +186,8 @@ 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.')
@@ -228,13 +228,13 @@ describe('partials', function () {
.toCompileTo('Dudes: Jeepers Creepers');
handlebarsEnv.unregisterPartial('globalTest');
expect(handlebarsEnv.partials.globalTest).toBeUndefined();
equals(handlebarsEnv.partials.globalTest, undefined);
});
it('Multiple partial registration', function() {
handlebarsEnv.registerPartial({
'shared/dude': '{{name}}',
globalTest: '{{anotherDude}}',
globalTest: '{{anotherDude}}'
});
expectTemplate('Dudes: {{> shared/dude}} {{> globalTest}}')
@@ -281,8 +281,8 @@ describe('partials', function () {
.withInput({
dudes: [
{ name: 'Yehuda', url: 'http://yehuda' },
{ name: 'Alan', url: 'http://alan' },
],
{ name: 'Alan', url: 'http://alan' }
]
})
.withPartial('dude', '')
.toCompileTo('Dudes: ');
@@ -341,7 +341,7 @@ describe('partials', function () {
expectTemplate('{{#> dude}}{{value}}{{/dude}}')
.withInput({ context: { value: 'success' } })
.withPartials({
dude: '{{#with context}}{{> @partial-block }}{{/with}}',
dude: '{{#with context}}{{> @partial-block }}{{/with}}'
})
.toCompileTo('success');
});
@@ -350,7 +350,8 @@ describe('partials', 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>');
});
@@ -360,10 +361,11 @@ describe('partials', function () {
'<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>'
@@ -374,7 +376,8 @@ describe('partials', 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');
});
@@ -383,7 +386,7 @@ describe('partials', 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');
});
@@ -403,7 +406,7 @@ describe('partials', function () {
.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>'
@@ -416,7 +419,7 @@ describe('partials', function () {
.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>'
@@ -429,7 +432,7 @@ describe('partials', function () {
.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>'
@@ -442,7 +445,7 @@ describe('partials', function () {
.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>' +
@@ -472,7 +475,7 @@ describe('partials', function () {
expectTemplate(
'{{#with .}}{{#*inline "myPartial"}}success{{/inline}}{{/with}}{{> myPartial}}'
).toThrow(Error, /"myPartial" could not/);
).toThrow(Error, /myPartial could not/);
});
it('should override global partials', function() {
@@ -482,7 +485,7 @@ describe('partials', function () {
.withPartials({
myPartial: function() {
return 'fail';
},
}
})
.toCompileTo('success');
});
@@ -553,7 +556,7 @@ describe('partials', function () {
var env = Handlebars.create();
env.registerPartial('partial', '{{foo}}');
var template = env.compile('{{foo}} {{> partial}}', { noEscape: true });
expect(template({ foo: '<' })).toBe('< <');
equal(template({ foo: '<' }), '< <');
}
});
@@ -563,8 +566,8 @@ describe('partials', function () {
.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');
@@ -575,12 +578,12 @@ describe('partials', function () {
.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'
@@ -592,12 +595,12 @@ describe('partials', function () {
.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(
@@ -613,8 +616,8 @@ describe('partials', function () {
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 })
@@ -629,8 +632,8 @@ describe('partials', function () {
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 })
@@ -645,8 +648,8 @@ describe('partials', function () {
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 })
@@ -662,11 +665,11 @@ describe('partials', function () {
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(
+273 -178
View File
@@ -19,7 +19,7 @@ describe('precompiler', function () {
emptyTemplate = {
path: __dirname + '/artifacts/empty.handlebars',
name: 'empty',
source: '',
source: ''
},
file,
content,
@@ -33,7 +33,7 @@ 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.
*/
async function mockRequireUglify(loadError, callback) {
function mockRequireUglify(loadError, callback) {
var Module = require('module');
var _resolveFilename = Module._resolveFilename;
delete require.cache[require.resolve('uglify-js')];
@@ -45,7 +45,7 @@ describe('precompiler', function () {
return _resolveFilename.call(this, request, mod);
};
try {
await callback();
callback();
} finally {
Module._resolveFilename = _resolveFilename;
delete require.cache[require.resolve('uglify-js')];
@@ -83,312 +83,407 @@ describe('precompiler', function () {
console.error = errorLogFunction;
});
it('should output version', async function () {
await Precompiler.cli({ templates: [], version: true });
expect(log).toBe(Handlebars.VERSION);
it('should output version', function() {
Precompiler.cli({ templates: [], version: true });
equals(log, Handlebars.VERSION);
});
it('should throw if lacking templates', async function () {
await expect(Precompiler.cli({ templates: [] })).rejects.toThrow(
it('should throw if lacking templates', function() {
shouldThrow(
function() {
Precompiler.cli({ templates: [] });
},
Handlebars.Exception,
'Must define at least one template or directory.'
);
});
it('should handle empty/filtered directories', async function () {
Handlebars.precompile = function () {
return 'simple';
};
await Precompiler.cli({ hasDirectory: true, templates: [] });
it('should handle empty/filtered directories', function() {
Precompiler.cli({ hasDirectory: true, templates: [] });
// Success is not throwing
});
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 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 multiple templates', async function () {
await expect(
it('should throw when combining simple and multiple templates', function() {
shouldThrow(
function() {
Precompiler.cli({
templates: [
__dirname + '/artifacts/empty.handlebars',
__dirname + '/artifacts/empty.handlebars',
__dirname + '/artifacts/empty.handlebars'
],
simple: true,
})
).rejects.toThrow('Unable to output multiple templates in simple mode');
simple: true
});
it('should throw when missing name', async function () {
await expect(
Precompiler.cli({ templates: [{ source: '' }], amd: true })
).rejects.toThrow('Name missing for template');
},
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 throw when missing name', function() {
shouldThrow(
function() {
Precompiler.cli({ templates: [{ source: '' }], amd: true });
},
Handlebars.Exception,
'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 output simple templates', async function () {
it('should output simple templates', function() {
Handlebars.precompile = function() {
return 'simple';
};
await Precompiler.cli({ templates: [emptyTemplate], simple: true });
expect(log).toBe('simple\n');
Precompiler.cli({ templates: [emptyTemplate], simple: true });
equal(log, 'simple\n');
});
it('should default to simple templates', async function () {
it('should default to simple templates', function() {
Handlebars.precompile = function() {
return 'simple';
};
await Precompiler.cli({ templates: [{ source: '' }] });
expect(log).toBe('simple\n');
Precompiler.cli({ templates: [{ source: '' }] });
equal(log, 'simple\n');
});
it('should output amd templates', async function () {
it('should output amd templates', function() {
Handlebars.precompile = function() {
return 'amd';
};
await Precompiler.cli({ templates: [emptyTemplate], amd: true });
expect(log).toMatch(/template\(amd\)/);
Precompiler.cli({ templates: [emptyTemplate], amd: true });
equal(/template\(amd\)/.test(log), true);
});
it('should output multiple amd', async function () {
it('should output multiple amd', function() {
Handlebars.precompile = function() {
return 'amd';
};
await Precompiler.cli({
Precompiler.cli({
templates: [emptyTemplate, emptyTemplate],
amd: true,
namespace: 'foo',
namespace: 'foo'
});
expect(log).toMatch(/templates = foo = foo \|\|/);
expect(log).toMatch(/return templates/);
expect(log).toMatch(/template\(amd\)/);
equal(/templates = foo = foo \|\|/.test(log), true);
equal(/return templates/.test(log), true);
equal(/template\(amd\)/.test(log), true);
});
it('should output amd partials', async function () {
it('should output amd partials', function() {
Handlebars.precompile = function() {
return 'amd';
};
await Precompiler.cli({
templates: [emptyTemplate],
amd: true,
partial: true,
Precompiler.cli({ templates: [emptyTemplate], amd: true, partial: true });
equal(/return Handlebars\.partials\["empty"\]/.test(log), true);
equal(/template\(amd\)/.test(log), true);
});
expect(log).toMatch(/return Handlebars\.partials\['empty'\]/);
expect(log).toMatch(/template\(amd\)/);
});
it('should output multiple amd partials', async function () {
it('should output multiple amd partials', function() {
Handlebars.precompile = function() {
return 'amd';
};
await Precompiler.cli({
Precompiler.cli({
templates: [emptyTemplate, emptyTemplate],
amd: true,
partial: true,
partial: true
});
expect(log).not.toMatch(/return Handlebars\.partials\[/);
expect(log).toMatch(/template\(amd\)/);
equal(/return Handlebars\.partials\[/.test(log), false);
equal(/template\(amd\)/.test(log), true);
});
it('should output commonjs templates', async function () {
it('should output commonjs templates', function() {
Handlebars.precompile = function() {
return 'commonjs';
};
await Precompiler.cli({ templates: [emptyTemplate], commonjs: true });
expect(log).toMatch(/template\(commonjs\)/);
Precompiler.cli({ templates: [emptyTemplate], commonjs: true });
equal(/template\(commonjs\)/.test(log), true);
});
it('should set data flag', async function () {
it('should set data flag', function() {
Handlebars.precompile = function(data, options) {
expect(options.data).toBe(true);
equal(options.data, true);
return 'simple';
};
await Precompiler.cli({
templates: [emptyTemplate],
simple: true,
data: true,
});
expect(log).toBe('simple\n');
Precompiler.cli({ templates: [emptyTemplate], simple: true, data: true });
equal(log, 'simple\n');
});
it('should set known helpers', async function () {
it('should set known helpers', function() {
Handlebars.precompile = function(data, options) {
expect(options.knownHelpers.foo).toBe(true);
equal(options.knownHelpers.foo, true);
return 'simple';
};
await Precompiler.cli({
templates: [emptyTemplate],
simple: true,
known: 'foo',
Precompiler.cli({ templates: [emptyTemplate], simple: true, known: 'foo' });
equal(log, 'simple\n');
});
expect(log).toBe('simple\n');
});
it('should output to file system', async function () {
it('should output to file system', function() {
Handlebars.precompile = function() {
return 'simple';
};
await Precompiler.cli({
Precompiler.cli({
templates: [emptyTemplate],
simple: true,
output: 'file!',
output: 'file!'
});
expect(file).toBe('file!');
expect(content).toBe('simple\n');
expect(log).toBe('');
equal(file, 'file!');
equal(content, 'simple\n');
equal(log, '');
});
it('should output minimized templates', async function () {
it('should output minimized templates', function() {
Handlebars.precompile = function() {
return 'amd';
};
uglify.minify = function() {
return { code: 'min' };
};
await Precompiler.cli({ templates: [emptyTemplate], min: true });
expect(log).toBe('min');
Precompiler.cli({ templates: [emptyTemplate], min: true });
equal(log, 'min');
});
it('should omit minimization gracefully, if uglify-js is missing', async function () {
it('should omit minimization gracefully, if uglify-js is missing', function() {
var error = new Error("Cannot find module 'uglify-js'");
error.code = 'MODULE_NOT_FOUND';
await mockRequireUglify(error, async function () {
mockRequireUglify(error, function() {
var Precompiler = require('../dist/cjs/precompiler');
Handlebars.precompile = function() {
return 'amd';
};
await Precompiler.cli({ templates: [emptyTemplate], min: true });
expect(log).toMatch(/template\(amd\)/);
expect(log).toMatch(/\n/);
expect(errorLog).toMatch(/Code minimization is disabled/);
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);
});
});
it('should fail on errors (other than missing module) while loading uglify-js', async function () {
await mockRequireUglify(new Error('Mock Error'), async function () {
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';
};
await expect(
Precompiler.cli({ templates: [emptyTemplate], min: true })
).rejects.toThrow('Mock Error');
Precompiler.cli({ templates: [emptyTemplate], min: true });
},
Error,
'Mock Error'
);
});
});
it('should output map', async function () {
await Precompiler.cli({ templates: [emptyTemplate], map: 'foo.js.map' });
it('should output map', function() {
Precompiler.cli({ templates: [emptyTemplate], map: 'foo.js.map' });
expect(file).toBe('foo.js.map');
expect(log.match(/sourceMappingURL=/g).length).toBe(1);
equal(file, 'foo.js.map');
equal(log.match(/sourceMappingURL=/g).length, 1);
});
it('should output map with minification', async function () {
await Precompiler.cli({
it('should output map', function() {
Precompiler.cli({
templates: [emptyTemplate],
min: true,
map: 'foo.js.map',
map: 'foo.js.map'
});
expect(file).toBe('foo.js.map');
expect(log.match(/sourceMappingURL=/g).length).toBe(1);
equal(file, 'foo.js.map');
equal(log.match(/sourceMappingURL=/g).length, 1);
});
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 throw on missing template', function(done) {
Precompiler.loadTemplates({ files: ['foo'] }, function(err) {
equal(err.message, 'Unable to open template file "foo"');
done();
});
});
}
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');
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"');
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 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 regular expression characters in extensions', async function () {
await loadTemplatesAsync({
files: [__dirname + '/artifacts'],
extension: 'hb(s',
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 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 handle BOM', async function () {
var opts = await loadTemplatesAsync({
it('should handle BOM', function(done) {
var opts = {
files: [__dirname + '/artifacts/bom.handlebars'],
extension: 'handlebars',
bom: true,
bom: true
};
Precompiler.loadTemplates(opts, function(err, opts) {
equal(opts.templates[0].source, 'a');
done(err);
});
expect(opts.templates[0].source).toBe('a');
});
it('should handle different root', async function () {
var opts = await loadTemplatesAsync({
it('should handle different root', function(done) {
var opts = {
files: [__dirname + '/artifacts/empty.handlebars'],
simple: true,
root: 'foo/',
root: 'foo/'
};
Precompiler.loadTemplates(opts, function(err, opts) {
equal(opts.templates[0].name, __dirname + '/artifacts/empty');
done(err);
});
expect(opts.templates[0].name).toBe(__dirname + '/artifacts/empty');
});
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 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 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 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 stdin input', async function () {
});
it('should accept stdin input', function(done) {
var stdin = require('mock-stdin').stdin();
var promise = loadTemplatesAsync({ string: '-' });
Precompiler.loadTemplates({ string: '-' }, function(err, opts) {
equal(opts.templates[0].source, 'foo');
done(err);
});
stdin.send('fo');
stdin.send('o');
stdin.end();
var opts = await promise;
expect(opts.templates[0].source).toBe('foo');
});
it('error on name missing', async function () {
try {
await loadTemplatesAsync({ string: ['', 'bar'] });
throw new Error('should have thrown');
} catch (err) {
expect(err.message).toBe(
it('error on name missing', function(done) {
var opts = { string: ['', 'bar'] };
Precompiler.loadTemplates(opts, function(err) {
equal(
err.message,
'Number of names did not match the number of string inputs'
);
}
done();
});
});
it('should complete when no args are passed', async function () {
var opts = await loadTemplatesAsync({});
expect(opts.templates.length).toBe(0);
it('should complete when no args are passed', function(done) {
Precompiler.loadTemplates({}, function(err, opts) {
equal(opts.templates.length, 0);
done(err);
});
});
});
describe('GHSA-xjpj-3mr7-gcpf: precompiler output escaping', function() {
var FullHandlebars = require('../dist/cjs/handlebars')['default'];
function runCliAndCaptureOutput(options) {
var output = '';
var oldLog = console.log;
console.log = function() {
output += Array.prototype.join.call(arguments, '');
};
try {
Precompiler.cli(options);
} finally {
console.log = oldLog;
}
return output;
}
it('should not inject raw template names into generated code', function() {
var output = runCliAndCaptureOutput({
templates: [
{
name: "evil'];global.__xjpjName=1;//",
source: ''
}
],
amd: true
});
expect(output).to.not.match(/\['evil'\];global\.__xjpjName=1/);
});
it('should not inject raw commonjs option values into generated code', function() {
var output = runCliAndCaptureOutput({
templates: [{ name: 'safe', source: '' }],
commonjs: 'handlebars");global.__xjpjCommon=1;//'
});
expect(output).to.not.match(
/require\("handlebars"\);global\.__xjpjCommon=1/
);
});
it('should reject invalid namespace expressions', function() {
expect(function() {
runCliAndCaptureOutput({
templates: [{ name: 'safe', source: '' }],
namespace: 'App.ns;global.__xjpjNamespace=1;//'
});
}).to.throw(/Invalid namespace/);
});
it('should sanitize sourceMappingURL comment values', function() {
var oldPrecompile = FullHandlebars.precompile;
var oldWriteFileSync = fs.writeFileSync;
FullHandlebars.precompile = function() {
return {
code: '""',
map: '{"version":3,"sources":[],"names":[],"mappings":""}'
};
};
fs.writeFileSync = function() {};
var output;
try {
output = runCliAndCaptureOutput({
templates: [{ name: 'safe', source: '' }],
map: 'good.js.map\n;global.__xjpjMap=1;//'
});
} finally {
FullHandlebars.precompile = oldPrecompile;
fs.writeFileSync = oldWriteFileSync;
}
expect(output).to.not.match(
/sourceMappingURL=[^\n]*\n;global\.__xjpjMap=1/
);
});
});
});
+56 -58
View File
@@ -6,13 +6,13 @@ describe('Regressions', function () {
{
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');
@@ -73,11 +73,11 @@ describe('Regressions', function () {
things: [
{ className: 'one', word: '@fat' },
{ className: 'two', word: '@dhg' },
{ className: 'three', word: '@sayrer' },
{ className: 'three', word: '@sayrer' }
],
hasThings: function() {
return true;
},
}
};
var output =
@@ -89,7 +89,9 @@ 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() {
@@ -98,7 +100,7 @@ describe('Regressions', function () {
)
.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');
@@ -116,7 +118,7 @@ describe('Regressions', function () {
},
inverse: function(block) {
return block.inverse('');
},
}
};
expectTemplate(succeedingTemplate)
@@ -129,7 +131,9 @@ describe('Regressions', function () {
});
it('GH-458: Scoped this identifier', function() {
expectTemplate('{{./foo}}').withInput({ foo: 'bar' }).toCompileTo('bar');
expectTemplate('{{./foo}}')
.withInput({ foo: 'bar' })
.toCompileTo('bar');
});
it('GH-375: Unicode line terminators', function() {
@@ -140,7 +144,9 @@ describe('Regressions', 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 */
@@ -168,7 +174,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(
@@ -180,13 +186,15 @@ describe('Regressions', 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('');
expectTemplate('{{foo.bar}}')
.withInput({ foo: 0 })
.toCompileTo('');
});
it('GH-837: undefined values for helpers', function() {
@@ -194,7 +202,7 @@ describe('Regressions', function () {
.withHelpers({
str: function(value) {
return value + '';
},
}
})
.toCompileTo('undefined');
});
@@ -206,7 +214,7 @@ describe('Regressions', function () {
.withInput({
name: 'foo',
data: [1],
notData: [1],
notData: [1]
})
.toCompileTo('foo');
});
@@ -217,8 +225,8 @@ describe('Regressions', function () {
data: {
'': 'foo',
name: 'Chris',
value: 10000,
},
value: 10000
}
})
.toCompileTo('Key: \nKey: name\nKey: value\n');
});
@@ -228,10 +236,10 @@ describe('Regressions', function () {
.withHelpers({
wrap: function(options) {
return new Handlebars.SafeString(options.fn());
},
}
})
.withPartials({
partial: '{{#wrap}}<partial>{{/wrap}}',
partial: '{{#wrap}}<partial>{{/wrap}}'
})
.toCompileTo('<partial>');
});
@@ -259,7 +267,7 @@ 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');
});
@@ -271,7 +279,7 @@ describe('Regressions', function () {
.withPartials({
doctype: 'doctype{{> content}}',
layout:
'{{#> doctype}}{{#*inline "content"}}layout{{> subcontent}}{{/inline}}{{/doctype}}',
'{{#> doctype}}{{#*inline "content"}}layout{{> subcontent}}{{/inline}}{{/doctype}}'
})
.toCompileTo('doctypelayoutsubcontent');
});
@@ -281,7 +289,7 @@ describe('Regressions', function () {
.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');
});
@@ -290,7 +298,7 @@ describe('Regressions', function () {
expectTemplate('{{#> layout}}Outer{{/layout}}')
.withPartials({
layout: '{{#> inner}}Inner{{/inner}}{{> @partial-block }}',
inner: '',
inner: ''
})
.toCompileTo('Outer');
});
@@ -310,7 +318,7 @@ describe('Regressions', function () {
} else {
return options.inverse(this);
}
},
}
})
.toCompileTo(' 1. IF: John--\n' + ' 2. MYIF: John==\n');
});
@@ -323,33 +331,19 @@ describe('Regressions', function () {
)
.withInput({
listOne: ['a'],
listTwo: ['b'],
listTwo: ['b']
})
.withMessage('')
.toCompileTo('ab');
});
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 ');
@@ -360,7 +354,7 @@ describe('Regressions', function () {
.withPartials({
partialWithBlock:
'{{#if @partial-block}} block {{> @partial-block}} block {{/if}}',
partial: '{{#> partialWithBlock}} partial {{/partialWithBlock}}',
partial: '{{#> partialWithBlock}} partial {{/partialWithBlock}}'
})
.toCompileTo('template block partial block template');
});
@@ -374,18 +368,22 @@ describe('Regressions', function () {
return value.toUpperCase();
});
var result = newHandlebarsInstance.templates['test.hbs']({
name: 'yehuda',
name: 'yehuda'
});
expect(result.trim()).toBe('YEHUDA');
equals(result.trim(), 'YEHUDA');
});
it('should call "helperMissing" if a helper is missing', function() {
var newHandlebarsInstance = Handlebars.create();
expect(function () {
shouldThrow(
function() {
registerTemplate(newHandlebarsInstance, compiledTemplateVersion7());
newHandlebarsInstance.templates['test.hbs']({});
}).toThrow('Missing helper: "loud"');
},
Handlebars.Exception,
'Missing helper: "loud"'
);
});
it('should pass "options.lookupProperty" to "lookup"-helper, even with old templates', function() {
@@ -400,9 +398,9 @@ describe('Regressions', function () {
expect(
newHandlebarsInstance.templates['test.hbs']({
property: 'a',
test: { a: 'b' },
test: { a: 'b' }
})
).toBe('b');
).to.equal('b');
});
function registerTemplate(Handlebars, compileTemplate) {
@@ -429,7 +427,7 @@ describe('Regressions', function () {
) + '\n\n'
);
},
useData: true,
useData: true
};
}
@@ -446,12 +444,12 @@ describe('Regressions', function () {
{
name: 'lookup',
hash: {},
data: data,
data: data
}
)
);
},
useData: true,
useData: true
};
}
});
@@ -462,7 +460,7 @@ describe('Regressions', function () {
.withHelpers({
helpa: function(options) {
return options.hash.length;
},
}
})
.toCompileTo('foo');
});
@@ -478,19 +476,19 @@ describe('Regressions', function () {
newHandlebarsInstance = Handlebars.create();
});
afterEach(function() {
vi.restoreAllMocks();
sinon.restore();
});
it('should only compile global partials once', function() {
var templateSpy = vi.spyOn(newHandlebarsInstance, 'template');
var templateSpy = sinon.spy(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
expect(templateSpy).toHaveBeenCalledTimes(3);
vi.restoreAllMocks();
equal(templateSpy.callCount, 3);
sinon.restore();
});
});

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