diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 000000000..b9d48d491
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,11 @@
+root = true
+
+[*.js]
+indent_size = 2
+insert_final_newline = true
+trim_trailing_whitespace = true
+
+[*.yml]
+indent_size = 2
+insert_final_newline = true
+trim_trailing_whitespace = true
diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs
new file mode 100644
index 000000000..38efcc3ad
--- /dev/null
+++ b/.git-blame-ignore-revs
@@ -0,0 +1,6 @@
+# Upgrade to Prettier 2.7
+3d228334530860a6e3f99dc10777c84bf22292c1
+# Format markdown files with Prettier
+dfe2eaaf20f0b679d94e5a799757c4394d80f1cc
+# migrate to oxlint and oxfmt
+0c1d00282ca619c3416ad819bdf53c0852b32415
diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 000000000..eeda54e24
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1,6 @@
+# Handlebars-template fixtures in test cases need deterministic eol
+*.handlebars text eol=lf
+*.hbs text eol=lf
+
+# Lexer files as well
+*.l text eol=lf
diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md
new file mode 100644
index 000000000..05cdeec92
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE.md
@@ -0,0 +1,9 @@
+Before filing issues, please check the following points first:
+
+- [ ] Please don't open issues for security issues. Instead, file a report at https://www.npmjs.com/advisories/report?package=handlebars
+- [ ] Have a look at https://github.com/handlebars-lang/handlebars.js/blob/master/CONTRIBUTING.md
+- [ ] Read the FAQ at https://github.com/handlebars-lang/handlebars.js/blob/master/FAQ.md
+- [ ] Use the jsfiddle-template at https://jsfiddle.net/4nbwjaqz/4/ to reproduce problems or bugs
+
+This will probably help you to get a solution faster.
+For bugs, it would be great to have a PR with a failing test-case.
diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md
new file mode 100644
index 000000000..610889169
--- /dev/null
+++ b/.github/PULL_REQUEST_TEMPLATE.md
@@ -0,0 +1,12 @@
+Before creating a pull-request, please check https://github.com/handlebars-lang/handlebars.js/blob/master/CONTRIBUTING.md first.
+
+Generally we like to see pull requests that
+
+- [ ] Please don't start pull requests for security issues. Instead, file a report at https://www.npmjs.com/advisories/report?package=handlebars
+- [ ] Maintain the existing code style
+- [ ] Are focused on a single change (i.e. avoid large refactoring or style adjustments in untouched code if not the primary goal of the pull request)
+- [ ] Have good commit messages
+- [ ] Have tests
+- [ ] Have the [typings](https://www.typescriptlang.org/docs/handbook/declaration-files/introduction.html) (types/index.d.ts) updated on every API change. If you need help, updating those, please mention that in the PR description.
+- [ ] Don't significantly decrease the current code coverage (see coverage/lcov-report/index.html)
+- [ ] Please target the `master` branch in the PR.
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
new file mode 100644
index 000000000..a78af14d5
--- /dev/null
+++ b/.github/dependabot.yml
@@ -0,0 +1,9 @@
+version: 2
+updates:
+ - package-ecosystem: npm
+ directory: '/'
+ open-pull-requests-limit: 0
+ schedule:
+ interval: weekly
+ allow:
+ - dependency-type: production
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 000000000..36a1b157d
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,91 @@
+name: CI
+
+on:
+ push:
+ branches:
+ - master
+ pull_request: {}
+
+jobs:
+ lint:
+ name: Lint
+ runs-on: 'ubuntu-latest'
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v6
+
+ - name: Setup Node.js
+ uses: actions/setup-node@v6
+ with:
+ node-version: '24'
+
+ - name: Install dependencies
+ run: npm ci
+
+ - name: Lint
+ run: npm run lint
+
+ test:
+ name: Test (Node)
+ runs-on: ${{ matrix.operating-system }}
+ strategy:
+ fail-fast: false
+ matrix:
+ operating-system: ['ubuntu-latest', 'windows-latest']
+ # https://nodejs.org/en/about/releases/
+ node-version: ['20', '22', '24']
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v6
+ with:
+ submodules: true
+
+ - name: Setup Node.js
+ uses: actions/setup-node@v6
+ with:
+ node-version: ${{ matrix.node-version }}
+
+ - name: Install dependencies
+ run: npm ci
+
+ - 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
+
+ browser:
+ name: Test (Browser)
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v6
+ with:
+ submodules: true
+
+ - name: Setup Node.js
+ uses: actions/setup-node@v6
+ with:
+ node-version: '24'
+
+ - name: Install dependencies
+ run: npm ci
+
+ - name: Install Playwright
+ run: |
+ npx playwright install-deps
+ npx playwright install
+
+ - name: Build
+ run: npm run build
+
+ - name: Test
+ run: |
+ npm run test:browser-smoke
+ npm run test:browser
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
new file mode 100644
index 000000000..5b709f6c6
--- /dev/null
+++ b/.github/workflows/release.yml
@@ -0,0 +1,39 @@
+name: Release
+
+on:
+ workflow_dispatch:
+ push:
+ branches:
+ - master
+ tags:
+ - '*'
+
+jobs:
+ publish-aws-s3:
+ name: Publish to AWS S3
+ runs-on: 'ubuntu-latest'
+ environment: 'builds.handlebarsjs.com.s3.amazonaws.com'
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v6
+ with:
+ submodules: true
+
+ - name: Setup Node.js
+ uses: actions/setup-node@v6
+ with:
+ node-version: '24'
+
+ - name: Install dependencies
+ run: npm ci
+
+ - name: Publish
+ run: |
+ git config --global user.email "release@handlebarsjs.com"
+ git config --global user.name "handlebars-lang"
+ npm run publish:aws
+ env:
+ S3_BUCKET_NAME: 'builds.handlebarsjs.com'
+ S3_REGION: 'us-east-1'
+ S3_ACCESS_KEY_ID: ${{ secrets.S3_ACCESS_KEY_ID }}
+ S3_SECRET_ACCESS_KEY: ${{ secrets.S3_SECRET_ACCESS_KEY }}
diff --git a/.gitignore b/.gitignore
index a96d258e3..1e539294e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,6 +1,19 @@
-dist
-vendor
.rvmrc
.DS_Store
-lib/handlebars/compiler/parser.js
+/tmp/
+*.sublime-project
+*.sublime-workspace
+npm-debug.log
+.idea
+/yarn-error.log
+/yarn.lock
node_modules
+/handlebars-release.tgz
+.nyc_output
+
+# Generated files
+/coverage/
+/dist/
+/tests/bench/results/
+/tests/integration/*/dist/
+/spec/tmp/*
diff --git a/.gitmodules b/.gitmodules
new file mode 100644
index 000000000..09cc7fdfd
--- /dev/null
+++ b/.gitmodules
@@ -0,0 +1,3 @@
+[submodule "spec/mustache"]
+ path = spec/mustache
+ url = https://github.com/mustache/spec.git
diff --git a/.jshintrc b/.jshintrc
deleted file mode 100644
index 4fc2003d9..000000000
--- a/.jshintrc
+++ /dev/null
@@ -1,50 +0,0 @@
-{
- "predef": [
- "console",
- "Ember",
- "DS",
- "Handlebars",
- "Metamorph",
- "ember_assert",
- "ember_warn",
- "ember_deprecate",
- "ember_deprecateFunc",
- "require",
- "equal",
- "test",
- "testBoth",
- "raises",
- "deepEqual",
- "start",
- "stop",
- "ok",
- "strictEqual",
- "module"
- ],
-
- "node" : true,
- "es5" : true,
- "browser" : true,
-
- "boss" : true,
- "curly": false,
- "debug": false,
- "devel": false,
- "eqeqeq": true,
- "evil": true,
- "forin": false,
- "immed": false,
- "laxbreak": false,
- "newcap": true,
- "noarg": true,
- "noempty": false,
- "nonew": false,
- "nomen": false,
- "onevar": false,
- "plusplus": false,
- "regexp": false,
- "undef": true,
- "sub": true,
- "strict": false,
- "white": false
-}
diff --git a/.npmignore b/.npmignore
deleted file mode 100644
index 5c49ba1b2..000000000
--- a/.npmignore
+++ /dev/null
@@ -1,11 +0,0 @@
-.DS_Store
-.gitignore
-.rvmrc
-Gemfile
-Gemfile.lock
-Rakefile
-bench/*
-dist/*
-spec/*
-src/*
-vendor/*
diff --git a/.oxfmtrc.json b/.oxfmtrc.json
new file mode 100644
index 000000000..0c5a6b513
--- /dev/null
+++ b/.oxfmtrc.json
@@ -0,0 +1,28 @@
+{
+ "$schema": "https://raw.githubusercontent.com/nicolo-ribaudo/oxfmt-config-schema/refs/heads/main/schema.json",
+ "singleQuote": true,
+ "tabWidth": 2,
+ "semi": true,
+ "trailingComma": "es5",
+ "printWidth": 80,
+ "ignorePatterns": [
+ ".rvmrc",
+ ".DS_Store",
+ "/tmp/",
+ "*.sublime-project",
+ "*.sublime-workspace",
+ "npm-debug.log",
+ "sauce_connect.log*",
+ ".idea",
+ "yarn-error.log",
+ "/coverage/",
+ ".nyc_output/",
+ "/dist/",
+ "/tests/integration/*/dist/",
+ "/spec/expected/",
+ "/spec/mustache",
+ "/spec/vendor",
+ "*.handlebars",
+ "*.hbs"
+ ]
+}
diff --git a/.oxlintrc.json b/.oxlintrc.json
new file mode 100644
index 000000000..64da9b071
--- /dev/null
+++ b/.oxlintrc.json
@@ -0,0 +1,150 @@
+{
+ "$schema": "https://raw.githubusercontent.com/oxc-project/oxc/main/npm/oxlint/configuration_schema.json",
+ "plugins": ["eslint", "typescript", "unicorn", "oxc", "node", "vitest"],
+ "categories": {
+ "correctness": "error"
+ },
+ "rules": {
+ "no-console": "warn",
+ "no-func-assign": "off",
+ "no-sparse-arrays": "off",
+
+ "default-case": "warn",
+ "guard-for-in": "warn",
+ "no-alert": "error",
+ "no-caller": "error",
+ "no-div-regex": "warn",
+ "no-eval": "error",
+ "no-extend-native": "error",
+ "no-extra-bind": "error",
+ "no-implied-eval": "error",
+ "no-iterator": "error",
+ "no-labels": "error",
+ "no-lone-blocks": "error",
+ "no-loop-func": "error",
+ "no-multi-str": "warn",
+ "no-global-assign": "error",
+ "no-new": "error",
+ "no-new-func": "error",
+ "no-new-wrappers": "error",
+ "no-proto": "error",
+ "no-return-assign": "error",
+ "no-script-url": "error",
+ "no-self-compare": "error",
+ "no-sequences": "error",
+ "no-throw-literal": "error",
+ "no-unused-expressions": "error",
+ "no-warning-comments": "warn",
+ "no-with": "error",
+ "radix": "error",
+
+ "no-label-var": "error",
+ "no-use-before-define": ["error", { "functions": false }],
+
+ "no-var": "error",
+
+ "node/no-process-env": "error"
+ },
+ "ignorePatterns": [
+ "tmp/",
+ "dist/",
+ "coverage/",
+ ".nyc_output/",
+ "handlebars-release.tgz",
+ "tests/integration/*/dist/",
+ "spec/expected/",
+ "spec/mustache",
+ "spec/vendor",
+ "node_modules",
+ "types/"
+ ],
+ "overrides": [
+ {
+ "files": ["lib/**/*.js"],
+ "env": {
+ "node": false,
+ "browser": true
+ }
+ },
+ {
+ "files": ["spec/**/*.js"],
+ "globals": {
+ "CompilerContext": "readonly",
+ "Handlebars": "writable",
+ "handlebarsEnv": "readonly",
+ "expectTemplate": "readonly",
+ "suite": "readonly",
+ "test": "readonly",
+ "testBoth": "readonly",
+ "raises": "readonly",
+ "deepEqual": "readonly",
+ "start": "readonly",
+ "stop": "readonly",
+ "ok": "readonly",
+ "vi": "readonly",
+ "strictEqual": "readonly",
+ "define": "readonly",
+ "expect": "readonly",
+ "beforeEach": "readonly",
+ "afterEach": "readonly",
+ "describe": "readonly",
+ "it": "readonly"
+ },
+ "rules": {
+ "no-var": "off",
+ "dot-notation": "off",
+ "vitest/no-conditional-tests": "off"
+ }
+ },
+ {
+ "files": ["tasks/**/*.js"],
+ "rules": {
+ "node/no-process-env": "off",
+ "prefer-const": "warn",
+ "dot-notation": "error"
+ }
+ },
+ {
+ "files": ["tasks/tests/**/*.js"],
+ "globals": {
+ "describe": "readonly",
+ "it": "readonly",
+ "expect": "readonly",
+ "beforeEach": "readonly",
+ "afterEach": "readonly",
+ "vi": "readonly"
+ }
+ },
+ {
+ "files": ["tests/bench/**/*.mjs"],
+ "rules": {
+ "no-console": "off"
+ }
+ },
+ {
+ "files": ["tests/integration/multi-nodejs-test/**/*.js"],
+ "rules": {
+ "no-console": "off",
+ "no-var": "off"
+ }
+ },
+ {
+ "files": ["tests/browser/**/*.js"],
+ "env": {
+ "browser": true
+ }
+ },
+ {
+ "files": [
+ "tests/integration/webpack-babel-test/src/**/*.js",
+ "tests/integration/webpack-test/src/**/*.js"
+ ],
+ "env": {
+ "browser": true
+ },
+ "rules": {
+ "no-var": "off"
+ }
+ }
+ ]
+}
diff --git a/.swcrc b/.swcrc
new file mode 100644
index 000000000..6dbc7b4e4
--- /dev/null
+++ b/.swcrc
@@ -0,0 +1,8 @@
+{
+ "$schema": "https://swc.rs/schema.json",
+ "module": {
+ "type": "commonjs",
+ "importInterop": "swc"
+ },
+ "sourceMaps": "inline"
+}
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 000000000..49330356b
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -0,0 +1,123 @@
+# 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.
+
+Should you run into other issues with the project, please don't hesitate to let us know by filing an [issue][issue]!
+
+In general, we are going to ask for an **example** of the problem failing, which can be as simple as a jsfiddle/jsbin/etc. We've put together a jsfiddle **[template][jsfiddle]** to ease this. (We will keep this link up to date as new releases occur, so feel free to check back here).
+
+Pull requests containing only failing tests demonstrating the issue are welcomed and this also helps ensure that your issue won't regress in the future once it's fixed.
+
+Documentation issues on the [handlebarsjs.com](https://handlebarsjs.com) site should be reported on [handlebars-lang/docs](https://github.com/handlebars-lang/docs).
+
+## 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.
+
+## Pull Requests
+
+We also accept [pull requests][pull-request]!
+
+Generally we like to see pull requests that
+
+- Maintain the existing code style
+- Are focused on a single change (i.e. avoid large refactoring or style adjustments in untouched code if not the primary goal of the pull request)
+- Have [good commit messages](http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html)
+- Have tests
+- Don't significantly decrease the current code coverage (see coverage/lcov-report/index.html)
+
+## Building
+
+To build Handlebars.js you'll need Node.js installed.
+
+Before building, you need to make sure that the Git submodule `spec/mustache` is included (i.e. the directory `spec/mustache` should not be empty). To include it, if using Git version 1.6.5 or newer, use `git clone --recursive` rather than `git clone`. Or, if you already cloned without `--recursive`, use `git submodule update --init`.
+
+Project dependencies may be installed via `npm install`.
+
+To build Handlebars.js from scratch, run `npm run build` in the root of the project. That will compile CJS modules via SWC and bundle UMD distributions via rspack, outputting results to the dist/ folder. To run tests, use `npm test`.
+
+If you notice any problems, please report them to the GitHub issue tracker at
+[http://github.com/handlebars-lang/handlebars.js/issues](http://github.com/handlebars-lang/handlebars.js/issues).
+
+## Running Tests
+
+To run tests locally, first install all dependencies.
+
+```sh
+npm install
+```
+
+Clone the mustache specs into the spec/mustache folder.
+
+```sh
+cd spec
+rm -r mustache
+git clone https://github.com/mustache/spec.git mustache
+```
+
+From the root directory, run the tests.
+
+```sh
+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.
+
+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.
+
+## 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`
+
+_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:
+
+```
+npm ci
+npm run build
+npm publish
+```
+
+After the release, you should check that all places have really been updated. Especially verify that the `latest`-tags
+in those places still point to the latest version
+
+- [The npm-package](https://www.npmjs.com/package/handlebars) (check latest-tag)
+- [The bower package](https://github.com/components/handlebars.js) (check the package.json)
+- [The AWS S3 Bucket](https://s3.amazonaws.com/builds.handlebarsjs.com) (check latest-tag)
+- [RubyGems](https://rubygems.org/gems/handlebars-source)
+
+When everything is OK, the **handlebars site** needs to be updated.
+
+Go to the master branch of the repo [handlebars-lang/docs](https://github.com/handlebars-lang/docs/tree/master)
+and make a minimal change to the README. This will invoke a github-action that redeploys
+the site, fetching the latest version-number from the npm-registry.
+(note that the default-branch of this repo is not the master and regular changes are done
+in the `handlebars-lang/docs`-repo).
+
+[generator-release]: https://github.com/walmartlabs/generator-release
+[pull-request]: https://github.com/handlebars-lang/handlebars.js/pull/new/master
+[issue]: https://github.com/handlebars-lang/handlebars.js/issues/new
+[jsfiddle]: https://jsfiddle.net/4nbwjaqz/4/
diff --git a/FAQ.md b/FAQ.md
new file mode 100644
index 000000000..ddb261541
--- /dev/null
+++ b/FAQ.md
@@ -0,0 +1,54 @@
+# Frequently Asked Questions
+
+## 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?
+
+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?
+
+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?
+
+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?
+
+If loading the template via an inlined `
+```
+
+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?
+
+When using the precompiler, it's important that a supporting version of the Handlebars runtime be loaded on the target page. In version 1.x there were rudimentary checks to compare the version but these did not always work. This is fixed under 2.x but the version checking does not work between these two versions. If you see unexpected errors such as `undefined is not a function` or similar, please verify that the same version is being used for both the precompiler and the client. This can be checked via:
+
+```sh
+handlebars --version
+```
+
+If using the integrated precompiler and
+
+```javascript
+console.log(Handlebars.VERSION);
+```
+
+On the client side.
+
+We include the built client libraries in the npm package for those who want to be certain that they are using the same client libraries as the compiler.
+
+Should these match, please file an issue with us, per our [issue filing guidelines](https://github.com/handlebars-lang/handlebars.js/blob/master/CONTRIBUTING.md#reporting-issues).
+
+## How do I load the runtime library when using AMD?
+
+The `handlebars.runtime.js` file includes a UMD build, which exposes the library as both the module root and the `default` field for compatibility.
diff --git a/Gemfile b/Gemfile
deleted file mode 100644
index b508d20be..000000000
--- a/Gemfile
+++ /dev/null
@@ -1,5 +0,0 @@
-source "http://rubygems.org"
-
-gem "rake"
-gem "therubyracer", ">= 0.9.8"
-gem "rspec"
diff --git a/Gemfile.lock b/Gemfile.lock
deleted file mode 100644
index c6e4eb4af..000000000
--- a/Gemfile.lock
+++ /dev/null
@@ -1,24 +0,0 @@
-GEM
- remote: http://rubygems.org/
- specs:
- diff-lcs (1.1.3)
- libv8 (3.3.10.4)
- rake (0.9.2.2)
- rspec (2.7.0)
- rspec-core (~> 2.7.0)
- rspec-expectations (~> 2.7.0)
- rspec-mocks (~> 2.7.0)
- rspec-core (2.7.1)
- rspec-expectations (2.7.0)
- diff-lcs (~> 1.1.2)
- rspec-mocks (2.7.0)
- therubyracer (0.9.9)
- libv8 (~> 3.3.10)
-
-PLATFORMS
- ruby
-
-DEPENDENCIES
- rake
- rspec
- therubyracer (>= 0.9.8)
diff --git a/LICENSE b/LICENSE
index 237cd0346..4d9d5806f 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,4 +1,4 @@
-Copyright (C) 2011 by Yehuda Katz
+Copyright (C) 2011-2019 by Yehuda Katz
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
@@ -17,4 +17,3 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
-
diff --git a/README.markdown b/README.markdown
deleted file mode 100644
index ed0b022d7..000000000
--- a/README.markdown
+++ /dev/null
@@ -1,316 +0,0 @@
-[](http://travis-ci.org/wycats/handlebars.js)
-
-Handlebars.js
-=============
-
-Handlebars.js is an extension to the [Mustache templating language](http://mustache.github.com/) created by Chris Wanstrath. Handlebars.js and Mustache are both logicless templating languages that keep the view and the code separated like we all know they should be.
-
-Checkout the official Handlebars docs site at [http://www.handlebarsjs.com](http://www.handlebarsjs.com).
-
-
-Installing
-----------
-Installing Handlebars is easy. Simply [download the package from GitHub](https://github.com/wycats/handlebars.js/archives/master) and add it to your web pages (you should usually use the most recent version).
-
-Usage
------
-In general, the syntax of Handlebars.js templates is a superset of Mustache templates. For basic syntax, check out the [Mustache manpage](http://mustache.github.com/mustache.5.html).
-
-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 = "
Hello, my name is {{name}}. I am from {{hometown}}. I have " +
- "{{kids.length}} kids:
Hello, my name is Alan. I am from Somewhere, TX. I have 2 kids:
-//
-//
Jimmy is 12
-//
Sally is 4
-//
-```
-
-
-Registering Helpers
--------------------
-
-You can register helpers that Handlebars will use when evaluating your
-template. Here's an example, which assumes that your objects have a URL
-embedded in them, as well as the text for a link:
-
-```js
-Handlebars.registerHelper('link_to', function(context) {
- return "" + context.body + "";
-});
-
-var context = { posts: [{url: "/hello-world", body: "Hello World!"}] };
-var source = "
-```
-
-Escaping
---------
-
-By default, the `{{expression}}` syntax will escape its contents. This
-helps to protect you against accidental XSS problems caused by malicious
-data passed from the server as JSON.
-
-To explicitly *not* escape the contents, use the triple-mustache
-(`{{{}}}`). You have seen this used in the above example.
-
-
-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.
-
-### Paths
-
-Handlebars.js supports an extended expression syntax that we call paths. Paths are made up of typical expressions and . characters. Expressions allow you to not only display data from the current context, but to display data from contexts that are descendents and ancestors of the current context.
-
-To display data from descendent contexts, use the `.` character. So, for example, if your data were structured like:
-
-```js
-var data = {"person": { "name": "Alan" }, company: {"name": "Rad, Inc." } };
-```
-
-you could display the person's name from the top-level context with the following expression:
-
-```
-{{person.name}}
-```
-
-You can backtrack using `../`. For example, if you've already traversed into the person object you could still display the company's name with an expression like `{{../company.name}}`, so:
-
-```
-{{#person}}{{name}} - {{../company.name}}{{/person}}
-```
-
-would render:
-
-```
-Alan - Rad, Inc.
-```
-
-### Strings
-
-When calling a helper, you can pass paths or Strings as parameters. For
-instance:
-
-```js
-Handlebars.registerHelper('link_to', function(title, context) {
- return "" + title + ""
-});
-
-var context = { posts: [{url: "/hello-world", body: "Hello World!"}] };
-var source = '
-```
-
-When you pass a String as a parameter to a helper, the literal String
-gets passed to the helper function.
-
-
-### Block Helpers
-
-Handlebars.js also adds the ability to define block helpers. Block helpers are functions that can be called from anywhere in the template. Here's an example:
-
-```js
-var source = "
-```
-
-Whenever the block helper is called it is given two parameters, the argument that is passed to the helper, or the current context if no argument is passed and the compiled contents of the block. Inside of the block helper the value of `this` is the current context, wrapped to include a method named `__get__` that helps translate paths into values within the helpers.
-
-### Partials
-
-You can register additional templates as partials, which will be used by
-Handlebars when it encounters a partial (`{{> partialName}}`). Partials
-can either be String templates or compiled template functions. Here's an
-example:
-
-```js
-var source = "
-```
-
-### Comments
-
-You can add comments to your templates with the following syntax:
-
-```js
-{{! This is a comment }}
-```
-
-You can also use real html comments if you want them to end up in the output.
-
-```html
-
- {{! This comment will not end up in the output }}
-
-
-```
-
-
-Precompiling Templates
-----------------------
-
-Handlebars allows templates to be precompiled and included as javascript
-code rather than the handlebars template allowing for faster startup time.
-
-### Installation
-The precompiler script may be installed via npm using the `npm install -g handlebars`
-command.
-
-### Usage
-
-
-Precompile handlebar templates.
-Usage: handlebars template...
-
-Options:
- -f, --output Output File [string]
- -k, --known Known helpers [string]
- -o, --knownOnly Known helpers only [boolean]
- -m, --min Minimize output [boolean]
- -s, --simple Output template function only. [boolean]
- -r, --root Template root. Base value that will be stripped from template names. [string]
-
-
-If using the precompiler's normal mode, the resulting templates will be stored
-to the `Handlebars.templates` object using the relative template name sans the
-extension. These templates may be executed in the same manner as templates.
-
-If using the simple mode the precompiler will generate a single javascript method.
-To execute this method it must be passed to the using the `Handlebars.template`
-method and the resulting object may be as normal.
-
-### Optimizations
-
-- Rather than using the full _handlebars.js_ library, implementations that
- do not need to compile templates at runtime may include _handlebars.runtime.js_
- whose min+gzip size is approximately 1k.
-- If a helper is known to exist in the target environment they may be defined
- using the `--known name` argument may be used to optimize accesses to these
- helpers for size and speed.
-- When all helpers are known in advance the `--knownOnly` argument may be used
- to optimize all block helper references.
-
-
-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, and we will have some benchmarks in the near future.
-
-
-Building
---------
-
-To build handlebars, just run `rake release`, and you will get two files
-in the `dist` directory.
-
-
-Upgrading
----------
-
-When upgrading from the Handlebars 0.9 series, be aware that the
-signature for passing custom helpers or partials to templates has
-changed.
-
-Instead of:
-
-```js
-template(context, helpers, partials, [data])
-```
-
-Use:
-
-```js
-template(context, {helpers: helpers, partials: partials, data: data})
-```
-
-Known Issues
-------------
-* Handlebars.js can be cryptic when there's an error while rendering.
-* Using a variable, helper, or partial named `class` causes errors in IE browsers. (Instead, use `className`)
-
-Handlebars in the Wild
------------------
-* [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.
-* Don Park wrote an Express.js view engine adapter for Handlebars.js called [hbs](http://github.com/donpark/hbs).
-* [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.
-* [Ember.js](http://www.emberjs.com) makes Handlebars.js the primary way to structure your views, also with automatic data binding support.
-* Les Hill (@leshill) wrote a Rails Asset Pipeline gem named [handlebars_assets](http://github.com/leshill/handlebars_assets).
-
-Helping Out
------------
-To build Handlebars.js you'll need a few things installed.
-
-* Node.js
-* Jison, for building the compiler - `npm install jison`
-* Ruby
-* therubyracer, for running tests - `gem install therubyracer`
-* rspec, for running tests - `gem install rspec`
-
-There's a Gemfile in the repo, so you can run `bundle` to install rspec and therubyracer if you've got bundler installed.
-
-To build Handlebars.js from scratch, you'll want to run `rake compile` in the root of the project. That will build Handlebars and output the results to the dist/ folder. To run tests, run `rake spec`. You can also run our set of benchmarks with `rake bench`.
-
-If you notice any problems, please report them to the GitHub issue tracker at [http://github.com/wycats/handlebars.js/issues](http://github.com/wycats/handlebars.js/issues). Feel free to contact commondream or wycats through GitHub with any other questions or feature requests. To submit changes fork the project and send a pull request.
-
-License
--------
-Handlebars.js is released under the MIT license.
diff --git a/README.md b/README.md
new file mode 100644
index 000000000..bfe73fddb
--- /dev/null
+++ b/README.md
@@ -0,0 +1,198 @@
+[](https://github.com/handlebars-lang/handlebars.js/actions/workflows/ci.yml)
+[](https://www.jsdelivr.com/package/npm/handlebars)
+[](https://www.npmjs.com/package/handlebars)
+[](https://www.npmjs.com/package/handlebars)
+[](https://bundlephobia.com/package/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 =
+ '
Hello, my name is {{name}}. I am from {{hometown}}. I have ' +
+ '{{kids.length}} kids:
Hello, my name is Alan. I am from Somewhere, TX. I have 2 kids:
+//
+//
Jimmy is 12
+//
Sally is 4
+//
+```
+
+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
diff --git a/Rakefile b/Rakefile
deleted file mode 100644
index b6043c29d..000000000
--- a/Rakefile
+++ /dev/null
@@ -1,116 +0,0 @@
-require "rubygems"
-require "bundler/setup"
-
-def compile_parser
- system "jison src/handlebars.yy src/handlebars.l"
- if $?.success?
- File.open("lib/handlebars/compiler/parser.js", "w") do |file|
- file.puts File.read("handlebars.js") + ";"
- end
-
- sh "rm handlebars.js"
- else
- puts "Failed to run Jison."
- end
-end
-
-file "lib/handlebars/compiler/parser.js" => ["src/handlebars.yy","src/handlebars.l"] do
- if ENV['PATH'].split(':').any? {|folder| File.exists?(folder+'/jison')}
- compile_parser
- else
- puts "Jison is not installed. Trying `npm install jison`."
- sh "npm install jison -g"
- compile_parser
- end
-end
-
-task :compile => "lib/handlebars/compiler/parser.js"
-
-desc "run the spec suite"
-task :spec => [:release] do
- system "rspec -cfs spec"
-end
-
-task :default => [:compile, :spec]
-
-def remove_exports(string)
- match = string.match(%r{^// BEGIN\(BROWSER\)\n(.*)\n^// END\(BROWSER\)}m)
- match ? match[1] : string
-end
-
-minimal_deps = %w(base compiler/parser compiler/base compiler/ast utils compiler/compiler runtime).map do |file|
- "lib/handlebars/#{file}.js"
-end
-
-runtime_deps = %w(base utils runtime).map do |file|
- "lib/handlebars/#{file}.js"
-end
-
-directory "dist"
-
-minimal_deps.unshift "dist"
-
-def build_for_task(task)
- FileUtils.rm_rf("dist/*") if File.directory?("dist")
- FileUtils.mkdir_p("dist")
-
- contents = []
- task.prerequisites.each do |filename|
- next if filename == "dist"
-
- contents << "// #{filename}\n" + remove_exports(File.read(filename)) + ";"
- end
-
- File.open(task.name, "w") do |file|
- file.puts contents.join("\n")
- end
-end
-
-file "dist/handlebars.js" => minimal_deps do |task|
- build_for_task(task)
-end
-
-file "dist/handlebars.runtime.js" => runtime_deps do |task|
- build_for_task(task)
-end
-
-task :build => [:compile, "dist/handlebars.js"]
-task :runtime => [:compile, "dist/handlebars.runtime.js"]
-
-desc "build the build and runtime version of handlebars"
-task :release => [:build, :runtime]
-
-directory "vendor"
-
-desc "benchmark against dust.js and mustache.js"
-task :bench => "vendor" do
- require "open-uri"
- #File.open("vendor/mustache.js", "w") do |file|
- #file.puts open("https://github.com/janl/mustache.js/raw/master/mustache.js").read
- #file.puts "module.exports = Mustache;"
- #end
-
- File.open("vendor/benchmark.js", "w") do |file|
- file.puts open("https://raw.github.com/bestiejs/benchmark.js/master/benchmark.js").read
- end
-
- #if File.directory?("vendor/dustjs")
- #system "cd vendor/dustjs && git pull"
- #else
- #system "git clone git://github.com/akdubya/dustjs.git vendor/dustjs"
- #end
-
- #if File.directory?("vendor/coffee")
- #system "cd vendor/coffee && git pull"
- #else
- #system "git clone git://github.com/jashkenas/coffee-script.git vendor/coffee"
- #end
-
- #if File.directory?("vendor/eco")
- #system "cd vendor/eco && git pull && npm update"
- #else
- #system "git clone git://github.com/sstephenson/eco.git vendor/eco && cd vendor/eco && npm update"
- #end
-
- system "node bench/handlebars.js"
-end
diff --git a/SECURITY.md b/SECURITY.md
new file mode 100644
index 000000000..5ec5d795b
--- /dev/null
+++ b/SECURITY.md
@@ -0,0 +1,15 @@
+# Security Policy
+
+We recommend always using the latest versions of Handlebars and its official companion libraries to ensure your application remains as secure as possible.
+
+## Supported Versions
+
+| Version | Supported |
+| ------- | ------------------ |
+| 5.0.x | :white_check_mark: |
+| 4.7.x | :white_check_mark: |
+| < 4.7 | :x: |
+
+## Reporting a Vulnerability
+
+To report a vulnerability, please visit https://github.com/handlebars-lang/handlebars.js/security.
diff --git a/bench/benchwarmer.js b/bench/benchwarmer.js
deleted file mode 100644
index 203c7bca7..000000000
--- a/bench/benchwarmer.js
+++ /dev/null
@@ -1,149 +0,0 @@
-
-var Benchmark = require("benchmark");
-
-var BenchWarmer = function(names) {
- this.benchmarks = [];
- this.currentBenches = [];
- this.names = [];
- this.errors = {};
-};
-
-var print = require("sys").print;
-
-BenchWarmer.prototype = {
- winners: function(benches) {
- var result = Benchmark.filter(benches, function(bench) { return bench.cycles; });
-
- if (result.length > 1) {
- result.sort(function(a, b) { return b.compare(a); });
- first = result[0];
- last = result[result.length - 1];
-
- var winners = [];
-
- Benchmark.each(result, function(bench) {
- if (bench.compare(first) === 0) {
- winners.push(bench);
- }
- });
-
- return winners;
- } else {
- return result;
- }
- },
- suite: function(suite, fn) {
- this.suiteName = suite;
- this.first = true;
-
- var self = this;
-
- fn(function(name, benchFn) {
- self.push(name, benchFn);
- });
- },
- push: function(name, fn) {
- if(this.names.indexOf(name) == -1) {
- this.names.push(name);
- }
-
- var first = this.first, suiteName = this.suiteName, self = this;
- this.first = false;
-
- var bench = new Benchmark(function() {
- fn();
- }, {
- name: this.suiteName + ": " + name,
- onComplete: function() {
- if(first) { self.startLine(suiteName); }
- self.writeBench(bench);
- self.currentBenches.push(bench);
- }, onError: function() {
- self.errors[this.name] = this;
- }
- });
-
- this.benchmarks.push(bench);
- },
- bench: function() {
- var benchSize = 0, names = this.names, self = this, i, l;
-
- for(i=0, l=names.length; i< name.length) { benchSize = name.length; }
- }
-
- this.nameSize = benchSize + 2;
- this.benchSize = 20;
- var horSize = 0;
-
- this.startLine("ops/msec");
- horSize = horSize + "ops/msec ".length;
- for(i=0, l=names.length; i<%= @name %>! You have <%= @count %> new messages."
- },
- object: {
- context: { person: { name: "Larry", age: 45 } },
- handlebars: "{{#with person}}{{name}}{{age}}{{/with}}",
- dust: "{#person}{name}{age}{/person}",
- mustache: "{{#person}}{{name}}{{age}}{{/person}}"
- },
- array: {
- context: { names: [{name: "Moe"}, {name: "Larry"}, {name: "Curly"}, {name: "Shemp"}] },
- handlebars: "{{#each names}}{{name}}{{/each}}",
- dust: "{#names}{name}{/names}",
- mustache: "{{#names}}{{name}}{{/names}}",
- eco: "<% for item in @names: %><%= item.name %><% end %>"
- },
- partial: {
- context: { peeps: [{name: "Moe", count: 15}, {name: "Larry", count: 5}, {name: "Curly", count: 1}] },
- partials: {
- mustache: { variables: "Hello {{name}}! You have {{count}} new messages." },
- handlebars: { variables: "Hello {{name}}! You have {{count}} new messages." }
- },
- handlebars: "{{#each peeps}}{{>variables}}{{/each}}",
- dust: "{#peeps}{>variables/}{/peeps}",
- mustache: "{{#peeps}}{{>variables}}{{/peeps}}"
- },
- recursion: {
- context: { name: '1', kids: [{ name: '1.1', kids: [{name: '1.1.1', kids: []}] }] },
- partials: {
- mustache: { recursion: "{{name}}{{#kids}}{{>recursion}}{{/kids}}" },
- handlebars: { recursion: "{{name}}{{#each kids}}{{>recursion}}{{/each}}" }
- },
- handlebars: "{{name}}{{#each kids}}{{>recursion}}{{/each}}",
- dust: "{name}{#kids}{>recursion:./}{/kids}",
- mustache: "{{name}}{{#kids}}{{>recursion}}{{/kids}}"
- },
- complex: {
- handlebars: "
`. In general it's recommended that attributes always be quoted when their values are generated from a mustache to avoid any potential exploit surfaces.
+- AST constructors have been dropped in favor of plain old javascript objects
+- The runtime version has been increased. Precompiled templates will need to use runtime of at least 4.0.0.
+
+[Commits](https://github.com/handlebars-lang/handlebars.js/compare/v3.0.3...v4.0.0)
+
+## v3.0.3 - April 28th, 2015
+
+- [#1004](https://github.com/handlebars-lang/handlebars.js/issues/1004) - Latest version breaks with RequireJS (global is undefined) ([@boskee](https://github.com/boskee))
+
+[Commits](https://github.com/handlebars-lang/handlebars.js/compare/v3.0.2...v3.0.3)
+
+## v3.0.2 - April 20th, 2015
+
+- [#998](https://github.com/handlebars-lang/handlebars.js/pull/998) - Add full support for es6 ([@kpdecker](https://github.com/kpdecker))
+- [#994](https://github.com/handlebars-lang/handlebars.js/issues/994) - Access Handlebars.Visitor in browser ([@tamlyn](https://github.com/tamlyn))
+- [#990](https://github.com/handlebars-lang/handlebars.js/issues/990) - Allow passing null/undefined literals subexpressions ([@blimmer](https://github.com/blimmer))
+- [#989](https://github.com/handlebars-lang/handlebars.js/issues/989) - Source-map error with requirejs ([@SteppeEagle](https://github.com/SteppeEagle))
+- [#967](https://github.com/handlebars-lang/handlebars.js/issues/967) - can't access "this" property ([@75lb](https://github.com/75lb))
+- Use captureStackTrace for error handler - a009a97
+- Ignore branches tested without coverage monitoring - 37a664b
+
+[Commits](https://github.com/handlebars-lang/handlebars.js/compare/v3.0.1...v3.0.2)
+
+## v3.0.1 - March 24th, 2015
+
+- [#984](https://github.com/handlebars-lang/handlebars.js/pull/984) - Adding documentation for passing arguments into partials ([@johneke](https://github.com/johneke))
+- [#973](https://github.com/handlebars-lang/handlebars.js/issues/973) - version 3 is slower than version 2 ([@elover](https://github.com/elover))
+- [#966](https://github.com/handlebars-lang/handlebars.js/issues/966) - "handlebars --version" does not work with v3.0.0 ([@abloomston](https://github.com/abloomston))
+- [#964](https://github.com/handlebars-lang/handlebars.js/pull/964) - default is a reserved word ([@grassick](https://github.com/grassick))
+- [#962](https://github.com/handlebars-lang/handlebars.js/pull/962) - Add dashbars' link on README. ([@pismute](https://github.com/pismute))
+
+[Commits](https://github.com/handlebars-lang/handlebars.js/compare/v3.0.0...v3.0.1)
+
+## v3.0.0 - February 10th, 2015
+
+- [#941](https://github.com/handlebars-lang/handlebars.js/pull/941) - Add support for dynamic partial names ([@kpdecker](https://github.com/kpdecker))
+- [#940](https://github.com/handlebars-lang/handlebars.js/pull/940) - Add missing reserved words so compiler knows to use array syntax: ([@mattflaschen](https://github.com/mattflaschen))
+- [#938](https://github.com/handlebars-lang/handlebars.js/pull/938) - Fix example using #with helper ([@diwo](https://github.com/diwo))
+- [#930](https://github.com/handlebars-lang/handlebars.js/pull/930) - Add parent tracking and mutation to AST visitors ([@kpdecker](https://github.com/kpdecker))
+- [#926](https://github.com/handlebars-lang/handlebars.js/issues/926) - Depthed lookups fail when program duplicator runs ([@kpdecker](https://github.com/kpdecker))
+- [#918](https://github.com/handlebars-lang/handlebars.js/pull/918) - Add instructions for 'spec/mustache' to CONTRIBUTING.md, fix a few typos ([@oneeman](https://github.com/oneeman))
+- [#915](https://github.com/handlebars-lang/handlebars.js/pull/915) - Ast update ([@kpdecker](https://github.com/kpdecker))
+- [#910](https://github.com/handlebars-lang/handlebars.js/issues/910) - Different behavior of {{@last}} when {{#each}} in {{#each}} ([@zordius](https://github.com/zordius))
+- [#907](https://github.com/handlebars-lang/handlebars.js/issues/907) - Implement named helper variable references ([@kpdecker](https://github.com/kpdecker))
+- [#906](https://github.com/handlebars-lang/handlebars.js/pull/906) - Add parser support for block params ([@mmun](https://github.com/mmun))
+- [#903](https://github.com/handlebars-lang/handlebars.js/issues/903) - Only provide aliases for multiple use calls ([@kpdecker](https://github.com/kpdecker))
+- [#902](https://github.com/handlebars-lang/handlebars.js/pull/902) - Generate Source Maps ([@kpdecker](https://github.com/kpdecker))
+- [#901](https://github.com/handlebars-lang/handlebars.js/issues/901) - Still escapes with noEscape enabled on isolated Handlebars environment ([@zedknight](https://github.com/zedknight))
+- [#896](https://github.com/handlebars-lang/handlebars.js/pull/896) - Simplify BlockNode by removing intermediate MustacheNode ([@mmun](https://github.com/mmun))
+- [#892](https://github.com/handlebars-lang/handlebars.js/pull/892) - Implement parser for else chaining of helpers ([@kpdecker](https://github.com/kpdecker))
+- [#889](https://github.com/handlebars-lang/handlebars.js/issues/889) - Consider extensible parser API ([@kpdecker](https://github.com/kpdecker))
+- [#887](https://github.com/handlebars-lang/handlebars.js/issues/887) - Handlebars.noConflict() option? ([@bradvogel](https://github.com/bradvogel))
+- [#886](https://github.com/handlebars-lang/handlebars.js/issues/886) - Add SafeString to context (or use duck-typing) ([@dominicbarnes](https://github.com/dominicbarnes))
+- [#870](https://github.com/handlebars-lang/handlebars.js/pull/870) - Registering undefined partial throws exception. ([@max-b](https://github.com/max-b))
+- [#866](https://github.com/handlebars-lang/handlebars.js/issues/866) - comments don't respect whitespace control ([@75lb](https://github.com/75lb))
+- [#863](https://github.com/handlebars-lang/handlebars.js/pull/863) - + jsDelivr CDN info ([@tomByrer](https://github.com/tomByrer))
+- [#858](https://github.com/handlebars-lang/handlebars.js/issues/858) - Disable new default auto-indent at included partials ([@majodev](https://github.com/majodev))
+- [#856](https://github.com/handlebars-lang/handlebars.js/pull/856) - jspm compatibility ([@MajorBreakfast](https://github.com/MajorBreakfast))
+- [#805](https://github.com/handlebars-lang/handlebars.js/issues/805) - Request: "strict" lookups ([@nzakas](https://github.com/nzakas))
+
+- Export the default object for handlebars/runtime - 5594416
+- Lookup partials when undefined - 617dd57
+
+Compatibility notes:
+
+- Runtime breaking changes. Must match 3.x runtime and precompiler.
+- The AST has been upgraded to a public API.
+ - There are a number of changes to this, but the format is now documented in docs/compiler-api.md
+ - The Visitor API has been expanded to support mutation and provide a base implementation
+- The `JavaScriptCompiler` APIs have been formalized and documented. As part of the sourcemap handling these should be updated to return arrays for concatenation.
+- `JavaScriptCompiler.namespace` has been removed as it was unused.
+- `SafeString` is now duck typed on `toHTML`
+
+New Features:
+
+- noConflict
+- Source Maps
+- Block Params
+- Strict Mode
+- @last and other each changes
+- Chained else blocks
+- @data methods can now have helper parameters passed to them
+- Dynamic partials
+
+[Commits](https://github.com/handlebars-lang/handlebars.js/compare/v2.0.0...v3.0.0)
+
+## v2.0.0 - September 1st, 2014
+
+- Update jsfiddle to 2.0.0-beta.1 - 0670f65
+- Add contrib note regarding handlebarsjs.com docs - 4d17e3c
+- Play nice with gemspec version numbers - 64d5481
+
+[Commits](https://github.com/handlebars-lang/handlebars.js/compare/v2.0.0-beta.1...v2.0.0)
+
+## v2.0.0-beta.1 - August 26th, 2014
+
+- [#787](https://github.com/handlebars-lang/handlebars.js/pull/787) - Remove whitespace surrounding standalone statements ([@kpdecker](https://github.com/kpdecker))
+- [#827](https://github.com/handlebars-lang/handlebars.js/issues/827) - Render false literal as “false” ([@scoot557](https://github.com/scoot557))
+- [#767](https://github.com/handlebars-lang/handlebars.js/issues/767) - Subexpressions bug with hash and context ([@evensoul](https://github.com/evensoul))
+- Changes to 0/undefined handling
+ - [#731](https://github.com/handlebars-lang/handlebars.js/pull/731) - Strange behavior for {{#foo}} {{bar}} {{/foo}} when foo is 0 ([@kpdecker](https://github.com/kpdecker))
+ - [#820](https://github.com/handlebars-lang/handlebars.js/issues/820) - strange behavior for {{foo.bar}} when foo is 0 or null or false ([@zordius](https://github.com/zordius))
+ - [#837](https://github.com/handlebars-lang/handlebars.js/issues/837) - Strange input for custom helper ( foo.bar == false when foo is undefined ) ([@zordius](https://github.com/zordius))
+- [#819](https://github.com/handlebars-lang/handlebars.js/pull/819) - Implement recursive field lookup ([@kpdecker](https://github.com/kpdecker))
+- [#764](https://github.com/handlebars-lang/handlebars.js/issues/764) - This reference not working for helpers ([@kpdecker](https://github.com/kpdecker))
+- [#773](https://github.com/handlebars-lang/handlebars.js/issues/773) - Implicit parameters in {{#each}} introduces a peculiarity in helpers calling convention ([@Bertrand](https://github.com/Bertrand))
+- [#783](https://github.com/handlebars-lang/handlebars.js/issues/783) - helperMissing and consistency for different expression types ([@ErisDS](https://github.com/ErisDS))
+- [#795](https://github.com/handlebars-lang/handlebars.js/pull/795) - Turn the precompile script into a wrapper around a module. ([@jwietelmann](https://github.com/jwietelmann))
+- [#823](https://github.com/handlebars-lang/handlebars.js/pull/823) - Support inverse sections on the with helper ([@dan-manges](https://github.com/dan-manges))
+- [#834](https://github.com/handlebars-lang/handlebars.js/pull/834) - Refactor blocks, programs and inverses ([@mmun](https://github.com/mmun))
+- [#852](https://github.com/handlebars-lang/handlebars.js/issues/852) - {{foo~}} space control behavior is different from older version ([@zordius](https://github.com/zordius))
+- [#835](https://github.com/handlebars-lang/handlebars.js/issues/835) - Templates overwritten if file is loaded twice
+
+- Expose escapeExpression on the root object - 980c38c
+- Remove nested function eval in blockHelperMissing - 6f22ec1
+- Fix compiler program de-duping - 9e3f824
+
+Compatibility notes:
+
+- The default build now outputs a generic UMD wrapper. This should be transparent change but may cause issues in some environments.
+- Runtime compatibility breaks in both directions. Ensure that both compiler and client are upgraded to 2.0.0-beta.1 or higher at the same time.
+ - `programWithDepth` has been removed an instead an array of context values is passed to fields needing depth lookups.
+- `false` values are now printed to output rather than silently dropped
+- Lines containing only block statements and whitespace are now removed. This matches the Mustache spec but may cause issues with code that expects whitespace to exist but would not otherwise.
+- Partials that are standalone will now indent their rendered content
+- `AST.ProgramNode`'s signature has changed.
+- Numerious methods/features removed from pseudo-API classes
+ - `JavaScriptCompiler.register`
+ - `JavaScriptCompiler.replaceStack` no longer supports non-inline replace
+ - `Compiler.disassemble`
+ - `DECLARE` opcode
+ - `strip` opcode
+ - `lookup` opcode
+ - Content nodes may have their `string` values mutated over time. `original` field provides the unmodified value.
+- Removed unused `Handlebars.registerHelper` `inverse` parameter
+- `each` helper requires iterator parameter
+
+[Commits](https://github.com/handlebars-lang/handlebars.js/compare/v2.0.0-alpha.4...v2.0.0-beta.1)
+
+## v2.0.0-alpha.4 - May 19th, 2014
+
+- Expose setup wrappers for compiled templates - 3638874
+
+[Commits](https://github.com/handlebars-lang/handlebars.js/compare/v2.0.0-alpha.3...v2.0.0-alpha.4)
+
+## v2.0.0-alpha.3 - May 19th, 2014
+
+- [#797](https://github.com/handlebars-lang/handlebars.js/pull/797) - Pass full helper ID to helperMissing when options are provided ([@tomdale](https://github.com/tomdale))
+- [#793](https://github.com/handlebars-lang/handlebars.js/pull/793) - Ensure isHelper is coerced to a boolean ([@mmun](https://github.com/mmun))
+- Refactor template init logic - 085e5e1
+
+[Commits](https://github.com/handlebars-lang/handlebars.js/compare/v2.0.0-alpha.2...v2.0.0-alpha.3)
+
+## v2.0.0-alpha.2 - March 6th, 2014
+
+- [#756](https://github.com/handlebars-lang/handlebars.js/pull/756) - fix bug in IE<=8 (no Array::map), closes #751 ([@jenseng](https://github.com/jenseng))
+- [#749](https://github.com/handlebars-lang/handlebars.js/pull/749) - properly handle multiple subexpressions in the same hash, fixes #748 ([@jenseng](https://github.com/jenseng))
+- [#743](https://github.com/handlebars-lang/handlebars.js/issues/743) - subexpression confusion/problem? ([@waynedpj](https://github.com/waynedpj))
+- [#746](https://github.com/handlebars-lang/handlebars.js/issues/746) - [CLI] support `handlebars --version` ([@apfelbox](https://github.com/apfelbox))
+- [#747](https://github.com/handlebars-lang/handlebars.js/pull/747) - updated grunt-saucelabs, failing tests revealed ([@Jonahss](https://github.com/Jonahss))
+- Make JSON a requirement for the compiler. - 058c0fb
+- Temporarily kill the AWS publish CI step - 8347ee2
+
+Compatibility notes:
+
+- A JSON polyfill is required to run the compiler under IE8 and below. It's recommended that the precompiler be used in lieu of running the compiler on these legacy environments.
+
+[Commits](https://github.com/handlebars-lang/handlebars.js/compare/v2.0.0-alpha.1...v2.0.0-alpha.2)
+
+## v2.0.0-alpha.1 - February 10th, 2014
+
+- [#182](https://github.com/handlebars-lang/handlebars.js/pull/182) - Allow passing hash parameters to partials ([@kpdecker](https://github.com/kpdecker))
+- [#392](https://github.com/handlebars-lang/handlebars.js/pull/392) - Access to root context in partials and helpers ([@kpdecker](https://github.com/kpdecker))
+- [#472](https://github.com/handlebars-lang/handlebars.js/issues/472) - Helpers cannot have decimal parameters ([@kayleg](https://github.com/kayleg))
+- [#569](https://github.com/handlebars-lang/handlebars.js/pull/569) - Unable to lookup array values using @index ([@kpdecker](https://github.com/kpdecker))
+- [#491](https://github.com/handlebars-lang/handlebars.js/pull/491) - For nested helpers: get the @ variables of the outer helper from the inner one ([@kpdecker](https://github.com/kpdecker))
+- [#669](https://github.com/handlebars-lang/handlebars.js/issues/669) - Ability to unregister a helper ([@dbachrach](https://github.com/dbachrach))
+- [#730](https://github.com/handlebars-lang/handlebars.js/pull/730) - Raw block helpers ([@kpdecker](https://github.com/kpdecker))
+- [#634](https://github.com/handlebars-lang/handlebars.js/pull/634) - It would be great to have the helper name passed to `blockHelperMissing` ([@kpdecker](https://github.com/kpdecker))
+- [#729](https://github.com/handlebars-lang/handlebars.js/pull/729) - Convert template spec to object literal ([@kpdecker](https://github.com/kpdecker))
+
+- [#658](https://github.com/handlebars-lang/handlebars.js/issues/658) - Depthed helpers do not work after an upgrade from 1.0.0 ([@xibxor](https://github.com/xibxor))
+- [#671](https://github.com/handlebars-lang/handlebars.js/issues/671) - Crashes on no-parameter {{#each}} ([@stepancheg](https://github.com/stepancheg))
+- [#689](https://github.com/handlebars-lang/handlebars.js/issues/689) - broken template precompilation ([@AAS](https://github.com/AAS))
+- [#698](https://github.com/handlebars-lang/handlebars.js/pull/698) - Fix parser generation under windows ([@osiris43](https://github.com/osiris43))
+- [#699](https://github.com/handlebars-lang/handlebars.js/issues/699) - @DATA not compiles to invalid JS in stringParams mode ([@kpdecker](https://github.com/kpdecker))
+- [#705](https://github.com/handlebars-lang/handlebars.js/issues/705) - 1.3.0 can not be wrapped in an IIFE ([@craigteegarden](https://github.com/craigteegarden))
+- [#706](https://github.com/handlebars-lang/handlebars.js/pull/706) - README: Use with helper instead of relying on blockHelperMissing ([@scottgonzalez](https://github.com/scottgonzalez))
+
+- [#700](https://github.com/handlebars-lang/handlebars.js/pull/700) - Remove redundant conditions ([@blakeembrey](https://github.com/blakeembrey))
+- [#704](https://github.com/handlebars-lang/handlebars.js/pull/704) - JavaScript Compiler Cleanup ([@blakeembrey](https://github.com/blakeembrey))
+
+Compatibility notes:
+
+- `helperMissing` helper no longer has the indexed name argument. Helper name is now available via `options.name`.
+- Precompiler output has changed, which breaks compatibility with prior versions of the runtime and precompiled output.
+- `JavaScriptCompiler.compilerInfo` now returns generic objects rather than javascript source.
+- AST changes
+ - INTEGER -> NUMBER
+ - Additional PartialNode hash parameter
+ - New RawBlockNode type
+- Data frames now have a `_parent` field. This is internal but is enumerable for performance/compatibility reasons.
+
+[Commits](https://github.com/handlebars-lang/handlebars.js/compare/v1.3.0...v2.0.0-alpha.1)
+
+## v1.3.0 - January 1st, 2014
+
+- [#690](https://github.com/handlebars-lang/handlebars.js/pull/690) - Added support for subexpressions ([@machty](https://github.com/machty))
+- [#696](https://github.com/handlebars-lang/handlebars.js/pull/696) - Fix for reserved keyword "default" ([@nateirwin](https://github.com/nateirwin))
+- [#692](https://github.com/handlebars-lang/handlebars.js/pull/692) - add line numbers to nodes when parsing ([@fivetanley](https://github.com/fivetanley))
+- [#695](https://github.com/handlebars-lang/handlebars.js/pull/695) - Pull options out from param setup to allow easier extension ([@blakeembrey](https://github.com/blakeembrey))
+- [#694](https://github.com/handlebars-lang/handlebars.js/pull/694) - Make the environment reusable ([@blakeembrey](https://github.com/blakeembrey))
+- [#636](https://github.com/handlebars-lang/handlebars.js/issues/636) - Print line and column of errors ([@sgronblo](https://github.com/sgronblo))
+- Use literal for data lookup - c1a93d3
+- Add stack handling sanity checks - cd885bf
+- Fix stack id "leak" on replaceStack - ddfe457
+- Fix incorrect stack pop when replacing literals - f4d337d
+
+[Commits](https://github.com/handlebars-lang/handlebars.js/compare/v1.2.1...v1.3.0)
+
+## v1.2.1 - December 26th, 2013
+
+- [#684](https://github.com/handlebars-lang/handlebars.js/pull/684) - Allow any number of trailing characters for valid JavaScript variable ([@blakeembrey](https://github.com/blakeembrey))
+- [#686](https://github.com/handlebars-lang/handlebars.js/pull/686) - Falsy AMD module names in version 1.2.0 ([@kpdecker](https://github.com/kpdecker))
+
+[Commits](https://github.com/handlebars-lang/handlebars.js/compare/v1.2.0...v1.2.1)
+
+## v1.2.0 - December 23rd, 2013
+
+- [#675](https://github.com/handlebars-lang/handlebars.js/issues/675) - Cannot compile empty template for partial ([@erwinw](https://github.com/erwinw))
+- [#677](https://github.com/handlebars-lang/handlebars.js/issues/677) - Triple brace statements fail under IE ([@hamzaCM](https://github.com/hamzaCM))
+- [#655](https://github.com/handlebars-lang/handlebars.js/issues/655) - Loading Handlebars using bower ([@niki4810](https://github.com/niki4810))
+- [#657](https://github.com/handlebars-lang/handlebars.js/pull/657) - Fixes issue where cli compiles non handlebars templates ([@chrishoage](https://github.com/chrishoage))
+- [#681](https://github.com/handlebars-lang/handlebars.js/pull/681) - Adds in-browser testing and Saucelabs CI ([@kpdecker](https://github.com/kpdecker))
+- [#661](https://github.com/handlebars-lang/handlebars.js/pull/661) - Add @first and @index to #each object iteration ([@cgp](https://github.com/cgp))
+- [#650](https://github.com/handlebars-lang/handlebars.js/pull/650) - Handlebars is MIT-licensed ([@thomasboyt](https://github.com/thomasboyt))
+- [#641](https://github.com/handlebars-lang/handlebars.js/pull/641) - Document ember testing process ([@kpdecker](https://github.com/kpdecker))
+- [#662](https://github.com/handlebars-lang/handlebars.js/issues/662) - handlebars-source 1.1.2 is missing from RubyGems.
+- [#656](https://github.com/handlebars-lang/handlebars.js/issues/656) - Expose COMPILER_REVISION checks as a hook ([@machty](https://github.com/machty))
+- [#668](https://github.com/handlebars-lang/handlebars.js/issues/668) - Consider publishing handlebars-runtime as a separate module on npm ([@dlmanning](https://github.com/dlmanning))
+- [#679](https://github.com/handlebars-lang/handlebars.js/issues/679) - Unable to override invokePartial ([@mattbrailsford](https://github.com/mattbrailsford))
+- [#646](https://github.com/handlebars-lang/handlebars.js/pull/646) - Fix "\\{{" immediately following "\{{" ([@dmarcotte](https://github.com/dmarcotte))
+- Allow extend to work with non-prototyped objects - eb53f2e
+- Add JavascriptCompiler public API tests - 1a751b2
+- Add AST test coverage for more complex paths - ddea5be
+- Fix handling of boolean escape in MustacheNode - b4968bb
+
+Compatibility notes:
+
+- `@index` and `@first` are now supported for `each` iteration on objects
+- `Handlebars.VM.checkRevision` and `Handlebars.JavaScriptCompiler.prototype.compilerInfo` now available to modify the version checking behavior.
+- Browserify users may link to the runtime library via `require('handlebars/runtime')`
+
+[Commits](https://github.com/handlebars-lang/handlebars.js/compare/v1.1.2...v1.2.0)
+
+## v1.1.2 - November 5th, 2013
+
+- [#645](https://github.com/handlebars-lang/handlebars.js/issues/645) - 1.1.1 fails under IE8 ([@kpdecker](https://github.com/kpdecker))
+- [#644](https://github.com/handlebars-lang/handlebars.js/issues/644) - Using precompiled templates (AMD mode) with handlebars.runtime 1.1.1 ([@fddima](https://github.com/fddima))
+
+- Add simple binary utility tests - 96a45a4
+- Fix empty string compilation - eea708a
+
+[Commits](https://github.com/handlebars-lang/handlebars.js/compare/v1.1.1...v1.1.2)
+
+## v1.1.1 - November 4th, 2013
+
+- [#642](https://github.com/handlebars-lang/handlebars.js/issues/642) - handlebars 1.1.0 are broken with nodejs
+
+- Fix release notes link - 17ba258
+
+[Commits](https://github.com/handlebars-lang/handlebars.js/compare/v1.1.0...v1.1.1)
+
+## v1.1.0 - November 3rd, 2013
+
+- [#628](https://github.com/handlebars-lang/handlebars.js/pull/628) - Convert code to ES6 modules ([@kpdecker](https://github.com/kpdecker))
+- [#336](https://github.com/handlebars-lang/handlebars.js/pull/336) - Add whitespace control syntax ([@kpdecker](https://github.com/kpdecker))
+- [#535](https://github.com/handlebars-lang/handlebars.js/pull/535) - Fix for probable JIT error under Safari ([@sorentwo](https://github.com/sorentwo))
+- [#483](https://github.com/handlebars-lang/handlebars.js/issues/483) - Add first and last @ vars to each helper ([@denniskuczynski](https://github.com/denniskuczynski))
+- [#557](https://github.com/handlebars-lang/handlebars.js/pull/557) - `\\{{foo}}` escaping only works in some situations ([@dmarcotte](https://github.com/dmarcotte))
+- [#552](https://github.com/handlebars-lang/handlebars.js/pull/552) - Added BOM removal flag. ([@blessenm](https://github.com/blessenm))
+- [#543](https://github.com/handlebars-lang/handlebars.js/pull/543) - publish passing master builds to s3 ([@fivetanley](https://github.com/fivetanley))
+
+- [#608](https://github.com/handlebars-lang/handlebars.js/issues/608) - Add `includeZero` flag to `if` conditional
+- [#498](https://github.com/handlebars-lang/handlebars.js/issues/498) - `Handlebars.compile` fails on empty string although a single blank works fine
+- [#599](https://github.com/handlebars-lang/handlebars.js/issues/599) - lambda helpers only receive options if used with arguments
+- [#592](https://github.com/handlebars-lang/handlebars.js/issues/592) - Optimize array and subprogram performance
+- [#571](https://github.com/handlebars-lang/handlebars.js/issues/571) - uglify upgrade breaks compatibility with older versions of node
+- [#587](https://github.com/handlebars-lang/handlebars.js/issues/587) - Partial inside partial breaks?
+
+Compatibility notes:
+
+- The project now includes separate artifacts for AMD, CommonJS, and global objects.
+ - AMD: Users may load the bundled `handlebars.amd.js` or `handlebars.runtime.amd.js` files or load individual modules directly. AMD users should also note that the handlebars object is exposed via the `default` field on the imported object. This [gist](https://gist.github.com/wycats/7417be0dc361a69d5916) provides some discussion of possible compatibility shims.
+ - CommonJS/Node: Node loading occurs as normal via `require`
+ - Globals: The `handlebars.js` and `handlebars.runtime.js` files should behave in the same manner as the v1.0.12 / 1.0.0 release.
+- Build artifacts have been removed from the repository. [npm][npm], [components/handlebars.js][components], [cdnjs][cdnjs], or the [builds page][builds-page] should now be used as the source of built artifacts.
+- Context-stored helpers are now always passed the `options` hash. Previously no-argument helpers did not have this argument.
+
+[Commits](https://github.com/handlebars-lang/handlebars.js/compare/v1.0.12...v1.1.0)
+
+## v1.0.12 / 1.0.0 - May 31 2013
+
+- [#515](https://github.com/handlebars-lang/handlebars.js/issues/515) - Add node require extensions support ([@jjclark1982](https://github.com/jjclark1982))
+- [#517](https://github.com/handlebars-lang/handlebars.js/issues/517) - Fix amd precompiler output with directories ([@blessenm](https://github.com/blessenm))
+- [#433](https://github.com/handlebars-lang/handlebars.js/issues/433) - Add support for unicode ids
+- [#469](https://github.com/handlebars-lang/handlebars.js/issues/469) - Add support for `?` in ids
+- [#534](https://github.com/handlebars-lang/handlebars.js/issues/534) - Protect from object prototype modifications
+- [#519](https://github.com/handlebars-lang/handlebars.js/issues/519) - Fix partials with . name ([@jamesgorrie](https://github.com/jamesgorrie))
+- [#519](https://github.com/handlebars-lang/handlebars.js/issues/519) - Allow ID or strings in partial names
+- [#437](https://github.com/handlebars-lang/handlebars.js/issues/437) - Require matching brace counts in escaped expressions
+- Merge passed partials and helpers with global namespace values
+- Add support for complex ids in @data references
+- Docs updates
+
+Compatibility notes:
+
+- The parser is now stricter on `{{{`, requiring that the end token be `}}}`. Templates that do not
+ follow this convention should add the additional brace value.
+- Code that relies on global the namespace being muted when custom helpers or partials are passed will need to explicitly pass an `undefined` value for any helpers that should not be available.
+- The compiler version has changed. Precompiled templates with 1.0.12 or higher must use the 1.0.0 or higher runtime.
+
+[Commits](https://github.com/handlebars-lang/handlebars.js/compare/v1.0.11...v1.0.12)
+
+## v1.0.11 / 1.0.0-rc4 - May 13 2013
+
+- [#458](https://github.com/handlebars-lang/handlebars.js/issues/458) - Fix `./foo` syntax ([@jpfiset](https://github.com/jpfiset))
+- [#460](https://github.com/handlebars-lang/handlebars.js/issues/460) - Allow `:` in unescaped identifiers ([@jpfiset](https://github.com/jpfiset))
+- [#471](https://github.com/handlebars-lang/handlebars.js/issues/471) - Create release notes (These!)
+- [#456](https://github.com/handlebars-lang/handlebars.js/issues/456) - Allow escaping of `\\`
+- [#211](https://github.com/handlebars-lang/handlebars.js/issues/211) - Fix exception in `escapeExpression`
+- [#375](https://github.com/handlebars-lang/handlebars.js/issues/375) - Escape unicode newlines
+- [#461](https://github.com/handlebars-lang/handlebars.js/issues/461) - Do not fail when compiling `""`
+- [#302](https://github.com/handlebars-lang/handlebars.js/issues/302) - Fix sanity check in knownHelpersOnly mode
+- [#369](https://github.com/handlebars-lang/handlebars.js/issues/369) - Allow registration of multiple helpers and partial by passing definition object
+- Add bower package declaration ([@DevinClark](https://github.com/DevinClark))
+- Add NuSpec package declaration ([@MikeMayer](https://github.com/MikeMayer))
+- Handle empty context in `with` ([@thejohnfreeman](https://github.com/thejohnfreeman))
+- Support custom template extensions in CLI ([@matteoagosti](https://github.com/matteoagosti))
+- Fix Rhino support ([@broady](https://github.com/broady))
+- Include contexts in string mode ([@leshill](https://github.com/leshill))
+- Return precompiled scripts when compiling to AMD ([@JamesMaroney](https://github.com/JamesMaroney))
+- Docs updates ([@iangreenleaf](https://github.com/iangreenleaf), [@gilesbowkett](https://github.com/gilesbowkett), [@utkarsh2012](https://github.com/utkarsh2012))
+- Fix `toString` handling under IE and browserify ([@tommydudebreaux](https://github.com/tommydudebreaux))
+- Add program metadata
+
+[Commits](https://github.com/handlebars-lang/handlebars.js/compare/v1.0.10...v1.0.11)
+
+## v1.0.10 - Node - Feb 27 2013
+
+- [#428](https://github.com/handlebars-lang/handlebars.js/issues/428) - Fix incorrect rendering of nested programs
+- Fix exception message ([@tricknotes](https://github.com/tricknotes))
+- Added negative number literal support
+- Concert library to single IIFE
+- Add handlebars-source gemspec ([@machty](https://github.com/machty))
+
+[Commits](https://github.com/handlebars-lang/handlebars.js/compare/v1.0.9...v1.0.10)
+
+## v1.0.9 - Node - Feb 15 2013
+
+- Added `Handlebars.create` API in node module for sandboxed instances ([@tommydudebreaux](https://github.com/tommydudebreaux))
+
+[Commits](https://github.com/handlebars-lang/handlebars.js/compare/1.0.0-rc.3...v1.0.9)
+
+## 1.0.0-rc3 - Browser - Feb 14 2013
+
+- Prevent use of `this` or `..` in illogical place ([@leshill](https://github.com/leshill))
+- Allow AST passing for `parse`/`compile`/`precompile` ([@machty](https://github.com/machty))
+- Optimize generated output by inlining statements where possible
+- Check compiler version when evaluating templates
+- Package browser dist in npm package
+
+[Commits](https://github.com/handlebars-lang/handlebars.js/compare/v1.0.8...1.0.0-rc.3)
+
+## Prior Versions
+
+When upgrading from the Handlebars 0.9 series, be aware that the
+signature for passing custom helpers or partials to templates has
+changed.
+
+Instead of:
+
+```js
+template(context, helpers, partials, [data]);
+```
+
+Use:
+
+```js
+template(context, { helpers: helpers, partials: partials, data: data });
+```
+
+[builds-page]: http://builds.handlebarsjs.com.s3.amazonaws.com/index.html
+[cdnjs]: http://cdnjs.com/libraries/handlebars.js/
+[components]: https://github.com/components/handlebars.js
+[npm]: https://npmjs.org/package/handlebars
diff --git a/rspack.config.js b/rspack.config.js
new file mode 100644
index 000000000..4c805c2cf
--- /dev/null
+++ b/rspack.config.js
@@ -0,0 +1,88 @@
+const { rspack } = require('@rspack/core');
+const path = require('path');
+const fs = require('fs');
+
+const pkg = require('./package.json');
+const license = fs.readFileSync(path.resolve(__dirname, 'LICENSE'), 'utf8');
+const banner = `/*!
+
+ @license magnet:?xt=urn:btih:d3d9a9a6595521f9666a5e94cc830dab83b65699&dn=expat.txt Expat
+ ${pkg.name} v${pkg.version}
+
+${license}
+*/`;
+
+function createConfig(entry, filename, minimize) {
+ const plugins = [];
+
+ if (!minimize) {
+ // For non-minified builds, use BannerPlugin to add the license header
+ plugins.push(new rspack.BannerPlugin({ banner, raw: true }));
+ }
+
+ return {
+ mode: minimize ? 'production' : 'none',
+ context: __dirname,
+ entry,
+ output: {
+ path: path.resolve(__dirname, 'dist'),
+ filename,
+ library: {
+ name: 'Handlebars',
+ type: 'umd',
+ export: 'default',
+ },
+ globalObject: 'this',
+ clean: false,
+ },
+ module: {
+ rules: [
+ {
+ test: /\.js$/,
+ exclude: /node_modules/,
+ use: {
+ loader: 'builtin:swc-loader',
+ options: {
+ jsc: {
+ parser: { syntax: 'ecmascript' },
+ },
+ },
+ },
+ },
+ ],
+ },
+ optimization: {
+ minimize,
+ minimizer: minimize
+ ? [
+ new rspack.SwcJsMinimizerRspackPlugin({
+ extractComments: false,
+ minimizerOptions: {
+ compress: { passes: 2 },
+ mangle: true,
+ format: {
+ comments: false,
+ // Prepend the license banner in the minified output
+ preamble: banner,
+ },
+ },
+ }),
+ ]
+ : [],
+ },
+ plugins,
+ target: ['web', 'browserslist'],
+ devtool: false,
+ };
+}
+
+module.exports = [
+ createConfig('./lib/handlebars.js', 'handlebars.js', false),
+ createConfig('./lib/handlebars.runtime.js', 'handlebars.runtime.js', false),
+ createConfig('./lib/handlebars.js', 'handlebars.min.js', true),
+ createConfig(
+ './lib/handlebars.runtime.js',
+ 'handlebars.runtime.min.js',
+ true
+ ),
+];
diff --git a/runtime.d.ts b/runtime.d.ts
new file mode 100644
index 000000000..b0feef9a1
--- /dev/null
+++ b/runtime.d.ts
@@ -0,0 +1,3 @@
+import Handlebars = require('handlebars');
+
+declare module 'handlebars/runtime' {}
diff --git a/runtime.js b/runtime.js
new file mode 100644
index 000000000..306207cd2
--- /dev/null
+++ b/runtime.js
@@ -0,0 +1,3 @@
+// Create a simple path alias to allow browserify to resolve
+// the runtime on a supported path.
+module.exports = require('./dist/cjs/handlebars.runtime')['default'];
diff --git a/spec/acceptance_spec.rb b/spec/acceptance_spec.rb
deleted file mode 100644
index d89641777..000000000
--- a/spec/acceptance_spec.rb
+++ /dev/null
@@ -1,101 +0,0 @@
-require "spec_helper"
-
-class TestContext
- class TestModule
- attr_reader :name, :tests
-
- def initialize(name)
- @name = name
- @tests = []
- end
- end
-
- attr_reader :modules
-
- def initialize
- @modules = []
- end
-
- def module(name)
- @modules << TestModule.new(name)
- end
-
- def test(name, function)
- @modules.last.tests << [name, function]
- end
-end
-
-test_context = TestContext.new
-js_context = Handlebars::Spec::CONTEXT
-
-Module.new do
- extend Test::Unit::Assertions
-
- def self.js_backtrace(context)
- begin
- context.eval("throw")
- rescue V8::JSError => e
- return e.backtrace(:javascript)
- end
- end
-
- js_context["p"] = proc do |str|
- p str
- end
-
- js_context["ok"] = proc do |ok, message|
- js_context["$$RSPEC1$$"] = ok
-
- result = js_context.eval("!!$$RSPEC1$$")
-
- message ||= "#{ok} was not truthy"
-
- unless result
- backtrace = js_backtrace(js_context)
- message << "\n#{backtrace.join("\n")}"
- end
-
- assert result, message
- end
-
- js_context["equals"] = proc do |first, second, message|
- js_context["$$RSPEC1$$"] = first
- js_context["$$RSPEC2$$"] = second
-
- result = js_context.eval("$$RSPEC1$$ == $$RSPEC2$$")
-
- additional_message = "#{first.inspect} did not == #{second.inspect}"
- message = message ? "#{message} (#{additional_message})" : additional_message
-
- unless result
- backtrace = js_backtrace(js_context)
- message << "\n#{backtrace.join("\n")}"
- end
-
- assert result, message
- end
-
- js_context["equal"] = js_context["equals"]
-
- js_context["module"] = proc do |name|
- test_context.module(name)
- end
-
- js_context["test"] = proc do |name, function|
- test_context.test(name, function)
- end
-
- local = Regexp.escape(File.expand_path(Dir.pwd))
- qunit_spec = File.expand_path("../qunit_spec.js", __FILE__)
- js_context.load(qunit_spec.sub(/^#{local}\//, ''))
-end
-
-test_context.modules.each do |mod|
- describe mod.name do
- mod.tests.each do |name, function|
- it name do
- function.call
- end
- end
- end
-end
diff --git a/spec/artifacts/bom.handlebars b/spec/artifacts/bom.handlebars
new file mode 100644
index 000000000..548d71419
--- /dev/null
+++ b/spec/artifacts/bom.handlebars
@@ -0,0 +1 @@
+a
\ No newline at end of file
diff --git a/spec/artifacts/empty.handlebars b/spec/artifacts/empty.handlebars
new file mode 100644
index 000000000..e69de29bb
diff --git a/spec/artifacts/example_1.handlebars b/spec/artifacts/example_1.handlebars
new file mode 100644
index 000000000..054e96cb8
--- /dev/null
+++ b/spec/artifacts/example_1.handlebars
@@ -0,0 +1 @@
+{{foo}}
diff --git a/spec/artifacts/example_2.hbs b/spec/artifacts/example_2.hbs
new file mode 100644
index 000000000..963eab972
--- /dev/null
+++ b/spec/artifacts/example_2.hbs
@@ -0,0 +1 @@
+Hello, {{name}}!
diff --git a/spec/artifacts/known.helpers.handlebars b/spec/artifacts/known.helpers.handlebars
new file mode 100644
index 000000000..74ab9d47b
--- /dev/null
+++ b/spec/artifacts/known.helpers.handlebars
@@ -0,0 +1,6 @@
+{{#someHelper true}}
+
\ No newline at end of file
diff --git a/spec/artifacts/partial.template.handlebars b/spec/artifacts/partial.template.handlebars
new file mode 100644
index 000000000..f99ae8ebc
--- /dev/null
+++ b/spec/artifacts/partial.template.handlebars
@@ -0,0 +1 @@
+
Test Partial
\ No newline at end of file
diff --git a/spec/ast.js b/spec/ast.js
new file mode 100644
index 000000000..c70812487
--- /dev/null
+++ b/spec/ast.js
@@ -0,0 +1,180 @@
+describe('ast', function () {
+ if (!Handlebars.AST) {
+ return;
+ }
+
+ var AST = Handlebars.AST;
+
+ describe('BlockStatement', function () {
+ it('should throw on mustache mismatch', function () {
+ expect(function () {
+ handlebarsEnv.parse('\n {{#foo}}{{/bar}}');
+ }).toThrow("foo doesn't match bar - 2:5");
+ });
+ });
+
+ describe('helpers', function () {
+ describe('#helperExpression', function () {
+ it('should handle mustache statements', function () {
+ expect(
+ AST.helpers.helperExpression({
+ type: 'MustacheStatement',
+ params: [],
+ hash: undefined,
+ })
+ ).toBe(false);
+ expect(
+ AST.helpers.helperExpression({
+ type: 'MustacheStatement',
+ params: [1],
+ hash: undefined,
+ })
+ ).toBe(true);
+ expect(
+ 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(
+ true
+ );
+ });
+ it('should work with non-helper nodes', function () {
+ expect(AST.helpers.helperExpression({ type: 'Program' })).toBe(false);
+
+ expect(AST.helpers.helperExpression({ type: 'PartialStatement' })).toBe(
+ false
+ );
+ expect(AST.helpers.helperExpression({ type: 'ContentStatement' })).toBe(
+ false
+ );
+ expect(AST.helpers.helperExpression({ type: 'CommentStatement' })).toBe(
+ false
+ );
+
+ expect(AST.helpers.helperExpression({ type: 'PathExpression' })).toBe(
+ false
+ );
+
+ expect(AST.helpers.helperExpression({ type: 'StringLiteral' })).toBe(
+ false
+ );
+ expect(AST.helpers.helperExpression({ type: 'NumberLiteral' })).toBe(
+ false
+ );
+ expect(AST.helpers.helperExpression({ type: 'BooleanLiteral' })).toBe(
+ false
+ );
+ expect(AST.helpers.helperExpression({ type: 'UndefinedLiteral' })).toBe(
+ false
+ );
+ expect(AST.helpers.helperExpression({ type: 'NullLiteral' })).toBe(
+ false
+ );
+
+ expect(AST.helpers.helperExpression({ type: 'Hash' })).toBe(false);
+ expect(AST.helpers.helperExpression({ type: 'HashPair' })).toBe(false);
+ });
+ });
+ });
+
+ describe('Line Numbers', function () {
+ 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);
+ }
+
+ ast = Handlebars.parse(
+ 'line 1 {{line1Token}}\n' + // 1
+ ' line 2 {{line2token}}\n' + // 2
+ ' line 3 {{#blockHelperOnLine3}}\n' + // 3
+ 'line 4{{line4token}}\n' + // 4
+ 'line5{{else}}\n' + // 5
+ '{{line6Token}}\n' + // 6
+ '{{/blockHelperOnLine3}}\n' + // 7
+ '{{#open}}\n' + // 8
+ '{{else inverse}}\n' + // 9
+ '{{else}}\n' + // 10
+ '{{/open}}'
+ ); // 11
+ body = ast.body;
+
+ it('gets ContentNode line numbers', function () {
+ var contentNode = body[0];
+ testColumns(contentNode, 1, 1, 0, 7);
+ });
+
+ it('gets MustacheStatement line numbers', function () {
+ var mustacheNode = body[1];
+ testColumns(mustacheNode, 1, 1, 7, 21);
+ });
+
+ it('gets line numbers correct when newlines appear', function () {
+ testColumns(body[2], 1, 2, 21, 8);
+ });
+
+ it('gets MustacheStatement line numbers correct across newlines', function () {
+ var secondMustacheStatement = body[3];
+ testColumns(secondMustacheStatement, 2, 2, 8, 22);
+ });
+
+ it('gets the block helper information correct', function () {
+ var blockHelperNode = body[5];
+ testColumns(blockHelperNode, 3, 7, 8, 23);
+ });
+
+ it('correctly records the line numbers the program of a block helper', function () {
+ var blockHelperNode = body[5],
+ program = blockHelperNode.program;
+
+ testColumns(program, 3, 5, 31, 5);
+ });
+
+ it('correctly records the line numbers of an inverse of a block helper', function () {
+ var blockHelperNode = body[5],
+ inverse = blockHelperNode.inverse;
+
+ testColumns(inverse, 5, 7, 13, 0);
+ });
+
+ it('correctly records the line number of chained inverses', function () {
+ var chainInverseNode = body[7];
+
+ testColumns(chainInverseNode.program, 8, 9, 9, 0);
+ testColumns(chainInverseNode.inverse, 9, 10, 16, 0);
+ testColumns(chainInverseNode.inverse.body[0].program, 9, 10, 16, 0);
+ testColumns(chainInverseNode.inverse.body[0].inverse, 10, 11, 8, 0);
+ });
+ });
+});
diff --git a/spec/basic.js b/spec/basic.js
new file mode 100644
index 000000000..1668b7930
--- /dev/null
+++ b/spec/basic.js
@@ -0,0 +1,575 @@
+describe('basic context', function () {
+ it('most basic', function () {
+ expectTemplate('{{foo}}').withInput({ foo: 'foo' }).toCompileTo('foo');
+ });
+
+ it('escaping', function () {
+ expectTemplate('\\{{foo}}')
+ .withInput({ foo: 'food' })
+ .toCompileTo('{{foo}}');
+
+ expectTemplate('content \\{{foo}}')
+ .withInput({ foo: 'food' })
+ .toCompileTo('content {{foo}}');
+
+ expectTemplate('\\\\{{foo}}')
+ .withInput({ foo: 'food' })
+ .toCompileTo('\\food');
+
+ expectTemplate('content \\\\{{foo}}')
+ .withInput({ foo: 'food' })
+ .toCompileTo('content \\food');
+
+ expectTemplate('\\\\ {{foo}}')
+ .withInput({ foo: 'food' })
+ .toCompileTo('\\\\ food');
+ });
+
+ it('compiling with a basic context', function () {
+ expectTemplate('Goodbye\n{{cruel}}\n{{world}}!')
+ .withInput({
+ cruel: 'cruel',
+ world: 'world',
+ })
+ .withMessage('It works if all the required keys are provided')
+ .toCompileTo('Goodbye\ncruel\nworld!');
+ });
+
+ it('compiling with a string context', function () {
+ expectTemplate('{{.}}{{length}}').withInput('bye').toCompileTo('bye3');
+ });
+
+ it('compiling with an undefined context', function () {
+ expectTemplate('Goodbye\n{{cruel}}\n{{world.bar}}!')
+ .withInput(undefined)
+ .toCompileTo('Goodbye\n\n!');
+
+ expectTemplate('{{#unless foo}}Goodbye{{../test}}{{test2}}{{/unless}}')
+ .withInput(undefined)
+ .toCompileTo('Goodbye');
+ });
+
+ it('comments', function () {
+ expectTemplate('{{! Goodbye}}Goodbye\n{{cruel}}\n{{world}}!')
+ .withInput({
+ cruel: 'cruel',
+ world: 'world',
+ })
+ .withMessage('comments are ignored')
+ .toCompileTo('Goodbye\ncruel\nworld!');
+
+ expectTemplate(' {{~! comment ~}} blah').toCompileTo('blah');
+
+ expectTemplate(' {{~!-- long-comment --~}} blah').toCompileTo(
+ 'blah'
+ );
+
+ expectTemplate(' {{! comment ~}} blah').toCompileTo(' blah');
+
+ expectTemplate(' {{!-- long-comment --~}} blah').toCompileTo(
+ ' blah'
+ );
+
+ expectTemplate(' {{~! comment}} blah').toCompileTo(' blah');
+
+ expectTemplate(' {{~!-- long-comment --}} blah').toCompileTo(
+ ' blah'
+ );
+ });
+
+ it('boolean', function () {
+ var string = '{{#goodbye}}GOODBYE {{/goodbye}}cruel {{world}}!';
+ expectTemplate(string)
+ .withInput({
+ goodbye: true,
+ world: 'world',
+ })
+ .withMessage('booleans show the contents when true')
+ .toCompileTo('GOODBYE cruel world!');
+
+ expectTemplate(string)
+ .withInput({
+ goodbye: false,
+ world: 'world',
+ })
+ .withMessage('booleans do not show the contents when false')
+ .toCompileTo('cruel world!');
+ });
+
+ it('zeros', function () {
+ expectTemplate('num1: {{num1}}, num2: {{num2}}')
+ .withInput({
+ num1: 42,
+ num2: 0,
+ })
+ .toCompileTo('num1: 42, num2: 0');
+
+ expectTemplate('num: {{.}}').withInput(0).toCompileTo('num: 0');
+
+ expectTemplate('num: {{num1/num2}}')
+ .withInput({ num1: { num2: 0 } })
+ .toCompileTo('num: 0');
+ });
+
+ it('false', function () {
+ /* eslint-disable no-new-wrappers */
+ expectTemplate('val1: {{val1}}, val2: {{val2}}')
+ .withInput({
+ val1: false,
+ val2: new Boolean(false),
+ })
+ .toCompileTo('val1: false, val2: false');
+
+ expectTemplate('val: {{.}}').withInput(false).toCompileTo('val: false');
+
+ expectTemplate('val: {{val1/val2}}')
+ .withInput({ val1: { val2: false } })
+ .toCompileTo('val: false');
+
+ expectTemplate('val1: {{{val1}}}, val2: {{{val2}}}')
+ .withInput({
+ val1: false,
+ val2: new Boolean(false),
+ })
+ .toCompileTo('val1: false, val2: false');
+
+ expectTemplate('val: {{{val1/val2}}}')
+ .withInput({ val1: { val2: false } })
+ .toCompileTo('val: false');
+ /* eslint-enable */
+ });
+
+ it('should handle undefined and null', function () {
+ expectTemplate('{{awesome undefined null}}')
+ .withInput({
+ awesome: function (_undefined, _null, options) {
+ return (
+ (_undefined === undefined) +
+ ' ' +
+ (_null === null) +
+ ' ' +
+ typeof options
+ );
+ },
+ })
+ .toCompileTo('true true object');
+
+ expectTemplate('{{undefined}}')
+ .withInput({
+ undefined: function () {
+ return 'undefined!';
+ },
+ })
+ .toCompileTo('undefined!');
+
+ expectTemplate('{{null}}')
+ .withInput({
+ null: function () {
+ return 'null!';
+ },
+ })
+ .toCompileTo('null!');
+ });
+
+ it('newlines', function () {
+ expectTemplate("Alan's\nTest").toCompileTo("Alan's\nTest");
+
+ expectTemplate("Alan's\rTest").toCompileTo("Alan's\rTest");
+ });
+
+ it('escaping text', function () {
+ expectTemplate("Awesome's")
+ .withMessage(
+ "text is escaped so that it doesn't get caught on single quotes"
+ )
+ .toCompileTo("Awesome's");
+
+ expectTemplate('Awesome\\')
+ .withMessage("text is escaped so that the closing quote can't be ignored")
+ .toCompileTo('Awesome\\');
+
+ expectTemplate('Awesome\\\\ foo')
+ .withMessage("text is escaped so that it doesn't mess up backslashes")
+ .toCompileTo('Awesome\\\\ foo');
+
+ expectTemplate('Awesome {{foo}}')
+ .withInput({ foo: '\\' })
+ .withMessage("text is escaped so that it doesn't mess up backslashes")
+ .toCompileTo('Awesome \\');
+
+ expectTemplate(" ' ' ")
+ .withMessage('double quotes never produce invalid javascript')
+ .toCompileTo(" ' ' ");
+ });
+
+ it('escaping expressions', function () {
+ expectTemplate('{{{awesome}}}')
+ .withInput({ awesome: "&'\\<>" })
+ .withMessage("expressions with 3 handlebars aren't escaped")
+ .toCompileTo("&'\\<>");
+
+ expectTemplate('{{&awesome}}')
+ .withInput({ awesome: "&'\\<>" })
+ .withMessage("expressions with {{& handlebars aren't escaped")
+ .toCompileTo("&'\\<>");
+
+ expectTemplate('{{awesome}}')
+ .withInput({ awesome: '&"\'`\\<>' })
+ .withMessage('by default expressions should be escaped')
+ .toCompileTo('&"'`\\<>');
+
+ expectTemplate('{{awesome}}')
+ .withInput({ awesome: 'Escaped, looks like: <b>' })
+ .withMessage('escaping should properly handle amperstands')
+ .toCompileTo('Escaped, <b> looks like: <b>');
+ });
+
+ it("functions returning safestrings shouldn't be escaped", function () {
+ expectTemplate('{{awesome}}')
+ .withInput({
+ awesome: function () {
+ return new Handlebars.SafeString("&'\\<>");
+ },
+ })
+ .withMessage("functions returning safestrings aren't escaped")
+ .toCompileTo("&'\\<>");
+ });
+
+ it('functions', function () {
+ expectTemplate('{{awesome}}')
+ .withInput({
+ awesome: function () {
+ return 'Awesome';
+ },
+ })
+ .withMessage('functions are called and render their output')
+ .toCompileTo('Awesome');
+
+ expectTemplate('{{awesome}}')
+ .withInput({
+ awesome: function () {
+ return this.more;
+ },
+ more: 'More awesome',
+ })
+ .withMessage('functions are bound to the context')
+ .toCompileTo('More awesome');
+ });
+
+ it('functions with context argument', function () {
+ expectTemplate('{{awesome frank}}')
+ .withInput({
+ awesome: function (context) {
+ return context;
+ },
+ frank: 'Frank',
+ })
+ .withMessage('functions are called with context arguments')
+ .toCompileTo('Frank');
+ });
+
+ it('pathed functions with context argument', function () {
+ expectTemplate('{{bar.awesome frank}}')
+ .withInput({
+ bar: {
+ awesome: function (context) {
+ return context;
+ },
+ },
+ frank: 'Frank',
+ })
+ .withMessage('functions are called with context arguments')
+ .toCompileTo('Frank');
+ });
+
+ it('depthed functions with context argument', function () {
+ expectTemplate('{{#with frank}}{{../awesome .}}{{/with}}')
+ .withInput({
+ awesome: function (context) {
+ return context;
+ },
+ frank: 'Frank',
+ })
+ .withMessage('functions are called with context arguments')
+ .toCompileTo('Frank');
+ });
+
+ it('block functions with context argument', function () {
+ expectTemplate('{{#awesome 1}}inner {{.}}{{/awesome}}')
+ .withInput({
+ awesome: function (context, options) {
+ return options.fn(context);
+ },
+ })
+ .withMessage('block functions are called with context and options')
+ .toCompileTo('inner 1');
+ });
+
+ it('depthed block functions with context argument', function () {
+ expectTemplate(
+ '{{#with value}}{{#../awesome 1}}inner {{.}}{{/../awesome}}{{/with}}'
+ )
+ .withInput({
+ value: true,
+ awesome: function (context, options) {
+ return options.fn(context);
+ },
+ })
+ .withMessage('block functions are called with context and options')
+ .toCompileTo('inner 1');
+ });
+
+ it('block functions without context argument', function () {
+ expectTemplate('{{#awesome}}inner{{/awesome}}')
+ .withInput({
+ awesome: function (options) {
+ return options.fn(this);
+ },
+ })
+ .withMessage('block functions are called with options')
+ .toCompileTo('inner');
+ });
+
+ it('pathed block functions without context argument', function () {
+ expectTemplate('{{#foo.awesome}}inner{{/foo.awesome}}')
+ .withInput({
+ foo: {
+ awesome: function () {
+ return this;
+ },
+ },
+ })
+ .withMessage('block functions are called with options')
+ .toCompileTo('inner');
+ });
+
+ it('depthed block functions without context argument', function () {
+ expectTemplate(
+ '{{#with value}}{{#../awesome}}inner{{/../awesome}}{{/with}}'
+ )
+ .withInput({
+ value: true,
+ awesome: function () {
+ return this;
+ },
+ })
+ .withMessage('block functions are called with options')
+ .toCompileTo('inner');
+ });
+
+ it('paths with hyphens', function () {
+ expectTemplate('{{foo-bar}}')
+ .withInput({ 'foo-bar': 'baz' })
+ .withMessage('Paths can contain hyphens (-)')
+ .toCompileTo('baz');
+
+ expectTemplate('{{foo.foo-bar}}')
+ .withInput({ foo: { 'foo-bar': 'baz' } })
+ .withMessage('Paths can contain hyphens (-)')
+ .toCompileTo('baz');
+
+ expectTemplate('{{foo/foo-bar}}')
+ .withInput({ foo: { 'foo-bar': 'baz' } })
+ .withMessage('Paths can contain hyphens (-)')
+ .toCompileTo('baz');
+ });
+
+ it('nested paths', function () {
+ expectTemplate('Goodbye {{alan/expression}} world!')
+ .withInput({ alan: { expression: 'beautiful' } })
+ .withMessage('Nested paths access nested objects')
+ .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: '' } })
+ .withMessage('Nested paths access nested objects with empty string')
+ .toCompileTo('Goodbye world!');
+ });
+
+ it('literal paths', function () {
+ expectTemplate('Goodbye {{[@alan]/expression}} world!')
+ .withInput({ '@alan': { expression: 'beautiful' } })
+ .withMessage('Literal paths can be used')
+ .toCompileTo('Goodbye beautiful world!');
+
+ expectTemplate('Goodbye {{[foo bar]/expression}} world!')
+ .withInput({ 'foo bar': { expression: 'beautiful' } })
+ .withMessage('Literal paths can be used')
+ .toCompileTo('Goodbye beautiful world!');
+ });
+
+ it('literal references', function () {
+ expectTemplate('Goodbye {{[foo bar]}} world!')
+ .withInput({ 'foo bar': 'beautiful' })
+ .toCompileTo('Goodbye beautiful world!');
+
+ expectTemplate('Goodbye {{"foo bar"}} world!')
+ .withInput({ 'foo bar': 'beautiful' })
+ .toCompileTo('Goodbye beautiful world!');
+
+ expectTemplate("Goodbye {{'foo bar'}} world!")
+ .withInput({ 'foo bar': 'beautiful' })
+ .toCompileTo('Goodbye beautiful world!');
+
+ expectTemplate('Goodbye {{"foo[bar"}} world!')
+ .withInput({ 'foo[bar': 'beautiful' })
+ .toCompileTo('Goodbye beautiful world!');
+
+ expectTemplate('Goodbye {{"foo\'bar"}} world!')
+ .withInput({ "foo'bar": 'beautiful' })
+ .toCompileTo('Goodbye beautiful world!');
+
+ expectTemplate("Goodbye {{'foo\"bar'}} world!")
+ .withInput({ 'foo"bar': 'beautiful' })
+ .toCompileTo('Goodbye beautiful world!');
+ });
+
+ it("that current context path ({{.}}) doesn't hit helpers", function () {
+ expectTemplate('test: {{.}}')
+ .withInput(null)
+ .withHelpers({ helper: 'awesome' })
+ .toCompileTo('test: ');
+ });
+
+ it('complex but empty paths', function () {
+ expectTemplate('{{person/name}}')
+ .withInput({ person: { name: null } })
+ .toCompileTo('');
+
+ expectTemplate('{{person/name}}').withInput({ person: {} }).toCompileTo('');
+ });
+
+ it('this keyword in paths', function () {
+ expectTemplate('{{#goodbyes}}{{this}}{{/goodbyes}}')
+ .withInput({ goodbyes: ['goodbye', 'Goodbye', 'GOODBYE'] })
+ .withMessage('This keyword in paths evaluates to current context')
+ .toCompileTo('goodbyeGoodbyeGOODBYE');
+
+ expectTemplate('{{#hellos}}{{this/text}}{{/hellos}}')
+ .withInput({
+ hellos: [{ text: 'hello' }, { text: 'Hello' }, { text: 'HELLO' }],
+ })
+ .withMessage('This keyword evaluates in more complex paths')
+ .toCompileTo('helloHelloHELLO');
+ });
+
+ it('this keyword nested inside path', function () {
+ expectTemplate('{{#hellos}}{{text/this/foo}}{{/hellos}}').toThrow(
+ Error,
+ 'Invalid path: text/this - 1:13'
+ );
+
+ expectTemplate('{{[this]}}').withInput({ this: 'bar' }).toCompileTo('bar');
+
+ expectTemplate('{{text/[this]}}')
+ .withInput({ text: { this: 'bar' } })
+ .toCompileTo('bar');
+ });
+
+ it('this keyword in helpers', function () {
+ var helpers = {
+ foo: function (value) {
+ return 'bar ' + value;
+ },
+ };
+
+ expectTemplate('{{#goodbyes}}{{foo this}}{{/goodbyes}}')
+ .withInput({ goodbyes: ['goodbye', 'Goodbye', 'GOODBYE'] })
+ .withHelpers(helpers)
+ .withMessage('This keyword in paths evaluates to current context')
+ .toCompileTo('bar goodbyebar Goodbyebar GOODBYE');
+
+ expectTemplate('{{#hellos}}{{foo this/text}}{{/hellos}}')
+ .withInput({
+ hellos: [{ text: 'hello' }, { text: 'Hello' }, { text: 'HELLO' }],
+ })
+ .withHelpers(helpers)
+ .withMessage('This keyword evaluates in more complex paths')
+ .toCompileTo('bar hellobar Hellobar HELLO');
+ });
+
+ it('this keyword nested inside helpers param', function () {
+ expectTemplate('{{#hellos}}{{foo text/this/foo}}{{/hellos}}').toThrow(
+ Error,
+ 'Invalid path: text/this - 1:17'
+ );
+
+ expectTemplate('{{foo [this]}}')
+ .withInput({
+ foo: function (value) {
+ return value;
+ },
+ this: 'bar',
+ })
+ .toCompileTo('bar');
+
+ expectTemplate('{{foo text/[this]}}')
+ .withInput({
+ foo: function (value) {
+ return value;
+ },
+ text: { this: 'bar' },
+ })
+ .toCompileTo('bar');
+ });
+
+ it('pass string literals', function () {
+ expectTemplate('{{"foo"}}').toCompileTo('');
+
+ expectTemplate('{{"foo"}}').withInput({ foo: 'bar' }).toCompileTo('bar');
+
+ expectTemplate('{{#"foo"}}{{.}}{{/"foo"}}')
+ .withInput({
+ foo: ['bar', 'baz'],
+ })
+ .toCompileTo('barbaz');
+ });
+
+ it('pass number literals', function () {
+ expectTemplate('{{12}}').toCompileTo('');
+
+ expectTemplate('{{12}}').withInput({ 12: 'bar' }).toCompileTo('bar');
+
+ expectTemplate('{{12.34}}').toCompileTo('');
+
+ expectTemplate('{{12.34}}').withInput({ 12.34: 'bar' }).toCompileTo('bar');
+
+ expectTemplate('{{12.34 1}}')
+ .withInput({
+ 12.34: function (arg) {
+ return 'bar' + arg;
+ },
+ })
+ .toCompileTo('bar1');
+ });
+
+ it('pass boolean literals', function () {
+ expectTemplate('{{true}}').toCompileTo('');
+
+ expectTemplate('{{true}}').withInput({ '': 'foo' }).toCompileTo('');
+
+ expectTemplate('{{false}}').withInput({ false: 'foo' }).toCompileTo('foo');
+ });
+
+ it('should handle literals in subexpression', function () {
+ expectTemplate('{{foo (false)}}')
+ .withInput({
+ false: function () {
+ return 'bar';
+ },
+ })
+ .withHelper('foo', function (arg) {
+ return arg;
+ })
+ .toCompileTo('bar');
+ });
+});
diff --git a/spec/blocks.js b/spec/blocks.js
new file mode 100644
index 000000000..0e1746289
--- /dev/null
+++ b/spec/blocks.js
@@ -0,0 +1,452 @@
+describe('blocks', function () {
+ it('array', function () {
+ var string = '{{#goodbyes}}{{text}}! {{/goodbyes}}cruel {{world}}!';
+
+ expectTemplate(string)
+ .withInput({
+ goodbyes: [
+ { text: 'goodbye' },
+ { text: 'Goodbye' },
+ { text: 'GOODBYE' },
+ ],
+ world: 'world',
+ })
+ .withMessage('Arrays iterate over the contents when not empty')
+ .toCompileTo('goodbye! Goodbye! GOODBYE! cruel world!');
+
+ expectTemplate(string)
+ .withInput({
+ goodbyes: [],
+ world: 'world',
+ })
+ .withMessage('Arrays ignore the contents when empty')
+ .toCompileTo('cruel world!');
+ });
+
+ it('array without data', function () {
+ expectTemplate(
+ '{{#goodbyes}}{{text}}{{/goodbyes}} {{#goodbyes}}{{text}}{{/goodbyes}}'
+ )
+ .withInput({
+ goodbyes: [
+ { text: 'goodbye' },
+ { text: 'Goodbye' },
+ { text: 'GOODBYE' },
+ ],
+ world: 'world',
+ })
+ .withCompileOptions({ compat: false })
+ .toCompileTo('goodbyeGoodbyeGOODBYE goodbyeGoodbyeGOODBYE');
+ });
+
+ it('array with @index', function () {
+ expectTemplate(
+ '{{#goodbyes}}{{@index}}. {{text}}! {{/goodbyes}}cruel {{world}}!'
+ )
+ .withInput({
+ goodbyes: [
+ { text: 'goodbye' },
+ { text: 'Goodbye' },
+ { text: 'GOODBYE' },
+ ],
+ world: 'world',
+ })
+ .withMessage('The @index variable is used')
+ .toCompileTo('0. goodbye! 1. Goodbye! 2. GOODBYE! cruel world!');
+ });
+
+ it('empty block', function () {
+ var string = '{{#goodbyes}}{{/goodbyes}}cruel {{world}}!';
+
+ expectTemplate(string)
+ .withInput({
+ goodbyes: [
+ { text: 'goodbye' },
+ { text: 'Goodbye' },
+ { text: 'GOODBYE' },
+ ],
+ world: 'world',
+ })
+ .withMessage('Arrays iterate over the contents when not empty')
+ .toCompileTo('cruel world!');
+
+ expectTemplate(string)
+ .withInput({
+ goodbyes: [],
+ world: 'world',
+ })
+ .withMessage('Arrays ignore the contents when empty')
+ .toCompileTo('cruel world!');
+ });
+
+ it('block with complex lookup', function () {
+ expectTemplate('{{#goodbyes}}{{text}} cruel {{../name}}! {{/goodbyes}}')
+ .withInput({
+ name: 'Alan',
+ goodbyes: [
+ { text: 'goodbye' },
+ { text: 'Goodbye' },
+ { text: 'GOODBYE' },
+ ],
+ })
+ .withMessage(
+ 'Templates can access variables in contexts up the stack with relative path syntax'
+ )
+ .toCompileTo(
+ 'goodbye cruel Alan! Goodbye cruel Alan! GOODBYE cruel Alan! '
+ );
+ });
+
+ it('multiple blocks with complex lookup', function () {
+ expectTemplate('{{#goodbyes}}{{../name}}{{../name}}{{/goodbyes}}')
+ .withInput({
+ name: 'Alan',
+ goodbyes: [
+ { text: 'goodbye' },
+ { text: 'Goodbye' },
+ { text: 'GOODBYE' },
+ ],
+ })
+ .toCompileTo('AlanAlanAlanAlanAlanAlan');
+ });
+
+ it('block with complex lookup using nested context', function () {
+ expectTemplate(
+ '{{#goodbyes}}{{text}} cruel {{foo/../name}}! {{/goodbyes}}'
+ ).toThrow(Error);
+ });
+
+ it('block with deep nested complex lookup', function () {
+ expectTemplate(
+ '{{#outer}}Goodbye {{#inner}}cruel {{../sibling}} {{../../omg}}{{/inner}}{{/outer}}'
+ )
+ .withInput({
+ omg: 'OMG!',
+ outer: [{ sibling: 'sad', inner: [{ text: 'goodbye' }] }],
+ })
+ .toCompileTo('Goodbye cruel sad OMG!');
+ });
+
+ it('works with cached blocks', function () {
+ expectTemplate(
+ '{{#each person}}{{#with .}}{{first}} {{last}}{{/with}}{{/each}}'
+ )
+ .withCompileOptions({ data: false })
+ .withInput({
+ person: [
+ { first: 'Alan', last: 'Johnson' },
+ { first: 'Alan', last: 'Johnson' },
+ ],
+ })
+ .toCompileTo('Alan JohnsonAlan Johnson');
+ });
+
+ describe('inverted sections', function () {
+ it('inverted sections with unset value', function () {
+ expectTemplate(
+ '{{#goodbyes}}{{this}}{{/goodbyes}}{{^goodbyes}}Right On!{{/goodbyes}}'
+ )
+ .withMessage("Inverted section rendered when value isn't set.")
+ .toCompileTo('Right On!');
+ });
+
+ it('inverted section with false value', function () {
+ expectTemplate(
+ '{{#goodbyes}}{{this}}{{/goodbyes}}{{^goodbyes}}Right On!{{/goodbyes}}'
+ )
+ .withInput({ goodbyes: false })
+ .withMessage('Inverted section rendered when value is false.')
+ .toCompileTo('Right On!');
+ });
+
+ it('inverted section with empty set', function () {
+ expectTemplate(
+ '{{#goodbyes}}{{this}}{{/goodbyes}}{{^goodbyes}}Right On!{{/goodbyes}}'
+ )
+ .withInput({ goodbyes: [] })
+ .withMessage('Inverted section rendered when value is empty set.')
+ .toCompileTo('Right On!');
+ });
+
+ it('block inverted sections', function () {
+ expectTemplate('{{#people}}{{name}}{{^}}{{none}}{{/people}}')
+ .withInput({ none: 'No people' })
+ .toCompileTo('No people');
+ });
+
+ it('chained inverted sections', function () {
+ expectTemplate('{{#people}}{{name}}{{else if none}}{{none}}{{/people}}')
+ .withInput({ none: 'No people' })
+ .toCompileTo('No people');
+
+ expectTemplate(
+ '{{#people}}{{name}}{{else if nothere}}fail{{else unless nothere}}{{none}}{{/people}}'
+ )
+ .withInput({ none: 'No people' })
+ .toCompileTo('No people');
+
+ expectTemplate(
+ '{{#people}}{{name}}{{else if none}}{{none}}{{else}}fail{{/people}}'
+ )
+ .withInput({ none: 'No people' })
+ .toCompileTo('No people');
+ });
+
+ it('chained inverted sections with mismatch', function () {
+ expectTemplate(
+ '{{#people}}{{name}}{{else if none}}{{none}}{{/if}}'
+ ).toThrow(Error);
+ });
+
+ it('block inverted sections with empty arrays', function () {
+ expectTemplate('{{#people}}{{name}}{{^}}{{none}}{{/people}}')
+ .withInput({
+ none: 'No people',
+ people: [],
+ })
+ .toCompileTo('No people');
+ });
+ });
+
+ describe('standalone sections', function () {
+ it('block standalone else sections', function () {
+ expectTemplate('{{#people}}\n{{name}}\n{{^}}\n{{none}}\n{{/people}}\n')
+ .withInput({ none: 'No people' })
+ .toCompileTo('No people\n');
+
+ expectTemplate('{{#none}}\n{{.}}\n{{^}}\n{{none}}\n{{/none}}\n')
+ .withInput({ none: 'No people' })
+ .toCompileTo('No people\n');
+
+ expectTemplate('{{#people}}\n{{name}}\n{{^}}\n{{none}}\n{{/people}}\n')
+ .withInput({ none: 'No people' })
+ .toCompileTo('No people\n');
+ });
+
+ it('block standalone else sections can be disabled', function () {
+ expectTemplate('{{#people}}\n{{name}}\n{{^}}\n{{none}}\n{{/people}}\n')
+ .withInput({ none: 'No people' })
+ .withCompileOptions({ ignoreStandalone: true })
+ .toCompileTo('\nNo people\n\n');
+
+ expectTemplate('{{#none}}\n{{.}}\n{{^}}\nFail\n{{/none}}\n')
+ .withInput({ none: 'No people' })
+ .withCompileOptions({ ignoreStandalone: true })
+ .toCompileTo('\nNo people\n\n');
+ });
+
+ it('block standalone chained else sections', function () {
+ expectTemplate(
+ '{{#people}}\n{{name}}\n{{else if none}}\n{{none}}\n{{/people}}\n'
+ )
+ .withInput({ none: 'No people' })
+ .toCompileTo('No people\n');
+
+ expectTemplate(
+ '{{#people}}\n{{name}}\n{{else if none}}\n{{none}}\n{{^}}\n{{/people}}\n'
+ )
+ .withInput({ none: 'No people' })
+ .toCompileTo('No people\n');
+ });
+
+ it('should handle nesting', function () {
+ expectTemplate('{{#data}}\n{{#if true}}\n{{.}}\n{{/if}}\n{{/data}}\nOK.')
+ .withInput({
+ data: [1, 3, 5],
+ })
+ .toCompileTo('1\n3\n5\nOK.');
+ });
+ });
+
+ describe('compat mode', function () {
+ it('block with deep recursive lookup lookup', function () {
+ expectTemplate(
+ '{{#outer}}Goodbye {{#inner}}cruel {{omg}}{{/inner}}{{/outer}}'
+ )
+ .withInput({ omg: 'OMG!', outer: [{ inner: [{ text: 'goodbye' }] }] })
+ .withCompileOptions({ compat: true })
+ .toCompileTo('Goodbye cruel OMG!');
+ });
+
+ it('block with deep recursive pathed lookup', function () {
+ expectTemplate(
+ '{{#outer}}Goodbye {{#inner}}cruel {{omg.yes}}{{/inner}}{{/outer}}'
+ )
+ .withInput({
+ omg: { yes: 'OMG!' },
+ outer: [{ inner: [{ yes: 'no', text: 'goodbye' }] }],
+ })
+ .withCompileOptions({ compat: true })
+ .toCompileTo('Goodbye cruel OMG!');
+ });
+
+ it('block with missed recursive lookup', function () {
+ expectTemplate(
+ '{{#outer}}Goodbye {{#inner}}cruel {{omg.yes}}{{/inner}}{{/outer}}'
+ )
+ .withInput({
+ omg: { no: 'OMG!' },
+ outer: [{ inner: [{ yes: 'no', text: 'goodbye' }] }],
+ })
+ .withCompileOptions({ compat: true })
+ .toCompileTo('Goodbye cruel ');
+ });
+ });
+
+ describe('decorators', function () {
+ it('should apply mustache decorators', function () {
+ expectTemplate('{{#helper}}{{*decorator}}{{/helper}}')
+ .withHelper('helper', function (options) {
+ return options.fn.run;
+ })
+ .withDecorator('decorator', function (fn) {
+ fn.run = 'success';
+ return fn;
+ })
+ .toCompileTo('success');
+ });
+
+ it('should apply allow undefined return', function () {
+ expectTemplate('{{#helper}}{{*decorator}}suc{{/helper}}')
+ .withHelper('helper', function (options) {
+ return options.fn() + options.fn.run;
+ })
+ .withDecorator('decorator', function (fn) {
+ fn.run = 'cess';
+ })
+ .toCompileTo('success');
+ });
+
+ it('should apply block decorators', function () {
+ expectTemplate(
+ '{{#helper}}{{#*decorator}}success{{/decorator}}{{/helper}}'
+ )
+ .withHelper('helper', function (options) {
+ return options.fn.run;
+ })
+ .withDecorator('decorator', function (fn, props, container, options) {
+ fn.run = options.fn();
+ return fn;
+ })
+ .toCompileTo('success');
+ });
+
+ it('should support nested decorators', function () {
+ expectTemplate(
+ '{{#helper}}{{#*decorator}}{{#*nested}}suc{{/nested}}cess{{/decorator}}{{/helper}}'
+ )
+ .withHelper('helper', function (options) {
+ return options.fn.run;
+ })
+ .withDecorators({
+ decorator: function (fn, props, container, options) {
+ fn.run = options.fn.nested + options.fn();
+ return fn;
+ },
+ nested: function (fn, props, container, options) {
+ props.nested = options.fn();
+ },
+ })
+ .toCompileTo('success');
+ });
+
+ it('should apply multiple decorators', function () {
+ expectTemplate(
+ '{{#helper}}{{#*decorator}}suc{{/decorator}}{{#*decorator}}cess{{/decorator}}{{/helper}}'
+ )
+ .withHelper('helper', function (options) {
+ return options.fn.run;
+ })
+ .withDecorator('decorator', function (fn, props, container, options) {
+ fn.run = (fn.run || '') + options.fn();
+ return fn;
+ })
+ .toCompileTo('success');
+ });
+
+ it('should access parent variables', function () {
+ expectTemplate('{{#helper}}{{*decorator foo}}{{/helper}}')
+ .withHelper('helper', function (options) {
+ return options.fn.run;
+ })
+ .withDecorator('decorator', function (fn, props, container, options) {
+ fn.run = options.args;
+ return fn;
+ })
+ .withInput({ foo: 'success' })
+ .toCompileTo('success');
+ });
+
+ it('should work with root program', function () {
+ var run;
+ expectTemplate('{{*decorator "success"}}')
+ .withDecorator('decorator', function (fn, props, container, options) {
+ expect(options.args[0]).toBe('success');
+ run = true;
+ return fn;
+ })
+ .withInput({ foo: 'success' })
+ .toCompileTo('');
+ expect(run).toBe(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();
+ run = true;
+ return fn;
+ })
+ .withInput({ foo: 'fail' })
+ .toCompileTo('');
+ expect(run).toBe(true);
+ });
+
+ describe('registration', function () {
+ it('unregisters', function () {
+ handlebarsEnv.decorators = {};
+
+ handlebarsEnv.registerDecorator('foo', function () {
+ return 'fail';
+ });
+
+ expect(handlebarsEnv.decorators.foo).toBeTruthy();
+ handlebarsEnv.unregisterDecorator('foo');
+ expect(handlebarsEnv.decorators.foo).toBeUndefined();
+ });
+
+ it('allows multiple globals', function () {
+ handlebarsEnv.decorators = {};
+
+ handlebarsEnv.registerDecorator({
+ foo: function () {},
+ bar: function () {},
+ });
+
+ expect(handlebarsEnv.decorators.foo).toBeTruthy();
+ expect(handlebarsEnv.decorators.bar).toBeTruthy();
+ handlebarsEnv.unregisterDecorator('foo');
+ handlebarsEnv.unregisterDecorator('bar');
+ expect(handlebarsEnv.decorators.foo).toBeUndefined();
+ expect(handlebarsEnv.decorators.bar).toBeUndefined();
+ });
+
+ it('fails with multiple and args', function () {
+ expect(function () {
+ handlebarsEnv.registerDecorator(
+ {
+ world: function () {
+ return 'world!';
+ },
+ testHelper: function () {
+ return 'found it!';
+ },
+ },
+ {}
+ );
+ }).toThrow('Arg not supported with multiple decorators');
+ });
+ });
+ });
+});
diff --git a/spec/builtins.js b/spec/builtins.js
new file mode 100644
index 000000000..8ef34af19
--- /dev/null
+++ b/spec/builtins.js
@@ -0,0 +1,808 @@
+describe('builtin helpers', function () {
+ describe('#if', function () {
+ it('if', function () {
+ var string = '{{#if goodbye}}GOODBYE {{/if}}cruel {{world}}!';
+
+ expectTemplate(string)
+ .withInput({
+ goodbye: true,
+ world: 'world',
+ })
+ .withMessage('if with boolean argument shows the contents when true')
+ .toCompileTo('GOODBYE cruel world!');
+
+ expectTemplate(string)
+ .withInput({
+ goodbye: 'dummy',
+ world: 'world',
+ })
+ .withMessage('if with string argument shows the contents')
+ .toCompileTo('GOODBYE cruel world!');
+
+ expectTemplate(string)
+ .withInput({
+ goodbye: false,
+ world: 'world',
+ })
+ .withMessage(
+ 'if with boolean argument does not show the contents when false'
+ )
+ .toCompileTo('cruel world!');
+
+ expectTemplate(string)
+ .withInput({ world: 'world' })
+ .withMessage('if with undefined does not show the contents')
+ .toCompileTo('cruel world!');
+
+ expectTemplate(string)
+ .withInput({
+ goodbye: ['foo'],
+ world: 'world',
+ })
+ .withMessage('if with non-empty array shows the contents')
+ .toCompileTo('GOODBYE cruel world!');
+
+ expectTemplate(string)
+ .withInput({
+ goodbye: [],
+ world: 'world',
+ })
+ .withMessage('if with empty array does not show the contents')
+ .toCompileTo('cruel world!');
+
+ expectTemplate(string)
+ .withInput({
+ goodbye: 0,
+ world: 'world',
+ })
+ .withMessage('if with zero does not show the contents')
+ .toCompileTo('cruel world!');
+
+ expectTemplate(
+ '{{#if goodbye includeZero=true}}GOODBYE {{/if}}cruel {{world}}!'
+ )
+ .withInput({
+ goodbye: 0,
+ world: 'world',
+ })
+ .withMessage('if with zero does not show the contents')
+ .toCompileTo('GOODBYE cruel world!');
+ });
+
+ it('if with function argument', function () {
+ var string = '{{#if goodbye}}GOODBYE {{/if}}cruel {{world}}!';
+
+ expectTemplate(string)
+ .withInput({
+ goodbye: function () {
+ return true;
+ },
+ world: 'world',
+ })
+ .withMessage(
+ 'if with function shows the contents when function returns true'
+ )
+ .toCompileTo('GOODBYE cruel world!');
+
+ expectTemplate(string)
+ .withInput({
+ goodbye: function () {
+ return this.world;
+ },
+ world: 'world',
+ })
+ .withMessage(
+ 'if with function shows the contents when function returns string'
+ )
+ .toCompileTo('GOODBYE cruel world!');
+
+ expectTemplate(string)
+ .withInput({
+ goodbye: function () {
+ return false;
+ },
+ world: 'world',
+ })
+ .withMessage(
+ 'if with function does not show the contents when returns false'
+ )
+ .toCompileTo('cruel world!');
+
+ expectTemplate(string)
+ .withInput({
+ goodbye: function () {
+ return this.foo;
+ },
+ world: 'world',
+ })
+ .withMessage(
+ 'if with function does not show the contents when returns undefined'
+ )
+ .toCompileTo('cruel world!');
+ });
+
+ it('should not change the depth list', function () {
+ expectTemplate(
+ '{{#with foo}}{{#if goodbye}}GOODBYE cruel {{../world}}!{{/if}}{{/with}}'
+ )
+ .withInput({
+ foo: { goodbye: true },
+ world: 'world',
+ })
+ .toCompileTo('GOODBYE cruel world!');
+ });
+ });
+
+ describe('#with', function () {
+ it('with', function () {
+ expectTemplate('{{#with person}}{{first}} {{last}}{{/with}}')
+ .withInput({
+ person: {
+ first: 'Alan',
+ last: 'Johnson',
+ },
+ })
+ .toCompileTo('Alan Johnson');
+ });
+
+ it('with with function argument', function () {
+ expectTemplate('{{#with person}}{{first}} {{last}}{{/with}}')
+ .withInput({
+ person: function () {
+ return {
+ first: 'Alan',
+ last: 'Johnson',
+ };
+ },
+ })
+ .toCompileTo('Alan Johnson');
+ });
+
+ it('with with else', function () {
+ expectTemplate(
+ '{{#with person}}Person is present{{else}}Person is not present{{/with}}'
+ ).toCompileTo('Person is not present');
+ });
+
+ it('with provides block parameter', function () {
+ expectTemplate('{{#with person as |foo|}}{{foo.first}} {{last}}{{/with}}')
+ .withInput({
+ person: {
+ first: 'Alan',
+ last: 'Johnson',
+ },
+ })
+ .toCompileTo('Alan Johnson');
+ });
+
+ it('works when data is disabled', function () {
+ expectTemplate('{{#with person as |foo|}}{{foo.first}} {{last}}{{/with}}')
+ .withInput({ person: { first: 'Alan', last: 'Johnson' } })
+ .withCompileOptions({ data: false })
+ .toCompileTo('Alan Johnson');
+ });
+ });
+
+ describe('#each', function () {
+ beforeEach(function () {
+ handlebarsEnv.registerHelper('detectDataInsideEach', function (options) {
+ return options.data && options.data.exclaim;
+ });
+ });
+
+ it('each', function () {
+ var string = '{{#each goodbyes}}{{text}}! {{/each}}cruel {{world}}!';
+
+ expectTemplate(string)
+ .withInput({
+ goodbyes: [
+ { text: 'goodbye' },
+ { text: 'Goodbye' },
+ { text: 'GOODBYE' },
+ ],
+ world: 'world',
+ })
+ .withMessage(
+ 'each with array argument iterates over the contents when not empty'
+ )
+ .toCompileTo('goodbye! Goodbye! GOODBYE! cruel world!');
+
+ expectTemplate(string)
+ .withInput({
+ goodbyes: [],
+ world: 'world',
+ })
+ .withMessage('each with array argument ignores the contents when empty')
+ .toCompileTo('cruel world!');
+ });
+
+ it('each without data', function () {
+ expectTemplate('{{#each goodbyes}}{{text}}! {{/each}}cruel {{world}}!')
+ .withInput({
+ goodbyes: [
+ { text: 'goodbye' },
+ { text: 'Goodbye' },
+ { text: 'GOODBYE' },
+ ],
+ world: 'world',
+ })
+ .withRuntimeOptions({ data: false })
+ .withCompileOptions({ data: false })
+ .toCompileTo('goodbye! Goodbye! GOODBYE! cruel world!');
+
+ expectTemplate('{{#each .}}{{.}}{{/each}}')
+ .withInput({ goodbyes: 'cruel', world: 'world' })
+ .withRuntimeOptions({ data: false })
+ .withCompileOptions({ data: false })
+ .toCompileTo('cruelworld');
+ });
+
+ it('each without context', function () {
+ expectTemplate('{{#each goodbyes}}{{text}}! {{/each}}cruel {{world}}!')
+ .withInput(undefined)
+ .toCompileTo('cruel !');
+ });
+
+ it('each with an object and @key', function () {
+ var string =
+ '{{#each goodbyes}}{{@key}}. {{text}}! {{/each}}cruel {{world}}!';
+
+ function Clazz() {
+ this['#1'] = { text: 'goodbye' };
+ this[2] = { text: 'GOODBYE' };
+ }
+ Clazz.prototype.foo = 'fail';
+ var hash = { goodbyes: new Clazz(), world: 'world' };
+
+ // Object property iteration order is undefined according to ECMA spec,
+ // so we need to check both possible orders
+ // @see http://stackoverflow.com/questions/280713/elements-order-in-a-for-in-loop
+ var actual = CompilerContext.compile(string)(hash);
+ var expected1 =
+ '<b>#1</b>. goodbye! 2. GOODBYE! cruel world!';
+ var expected2 =
+ '2. GOODBYE! <b>#1</b>. goodbye! cruel world!';
+
+ expect([expected1, expected2]).toContain(actual);
+
+ expectTemplate(string)
+ .withInput({
+ goodbyes: {},
+ world: 'world',
+ })
+ .toCompileTo('cruel world!');
+ });
+
+ it('each with @index', function () {
+ expectTemplate(
+ '{{#each goodbyes}}{{@index}}. {{text}}! {{/each}}cruel {{world}}!'
+ )
+ .withInput({
+ goodbyes: [
+ { text: 'goodbye' },
+ { text: 'Goodbye' },
+ { text: 'GOODBYE' },
+ ],
+ world: 'world',
+ })
+ .withMessage('The @index variable is used')
+ .toCompileTo('0. goodbye! 1. Goodbye! 2. GOODBYE! cruel world!');
+ });
+
+ it('each with nested @index', function () {
+ expectTemplate(
+ '{{#each goodbyes}}{{@index}}. {{text}}! {{#each ../goodbyes}}{{@index}} {{/each}}After {{@index}} {{/each}}{{@index}}cruel {{world}}!'
+ )
+ .withInput({
+ goodbyes: [
+ { text: 'goodbye' },
+ { text: 'Goodbye' },
+ { text: 'GOODBYE' },
+ ],
+ world: 'world',
+ })
+ .withMessage('The @index variable is used')
+ .toCompileTo(
+ '0. goodbye! 0 1 2 After 0 1. Goodbye! 0 1 2 After 1 2. GOODBYE! 0 1 2 After 2 cruel world!'
+ );
+ });
+
+ it('each with block params', function () {
+ expectTemplate(
+ '{{#each goodbyes as |value index|}}{{index}}. {{value.text}}! {{#each ../goodbyes as |childValue childIndex|}} {{index}} {{childIndex}}{{/each}} After {{index}} {{/each}}{{index}}cruel {{world}}!'
+ )
+ .withInput({
+ goodbyes: [{ text: 'goodbye' }, { text: 'Goodbye' }],
+ world: 'world',
+ })
+ .toCompileTo(
+ '0. goodbye! 0 0 0 1 After 0 1. Goodbye! 1 0 1 1 After 1 cruel world!'
+ );
+ });
+
+ it('each with block params and strict compilation', function () {
+ expectTemplate(
+ '{{#each goodbyes as |value index|}}{{index}}. {{value.text}}!{{/each}}'
+ )
+ .withCompileOptions({ strict: true })
+ .withInput({ goodbyes: [{ text: 'goodbye' }, { text: 'Goodbye' }] })
+ .toCompileTo('0. goodbye!1. Goodbye!');
+ });
+
+ it('each object with @index', function () {
+ expectTemplate(
+ '{{#each goodbyes}}{{@index}}. {{text}}! {{/each}}cruel {{world}}!'
+ )
+ .withInput({
+ goodbyes: {
+ a: { text: 'goodbye' },
+ b: { text: 'Goodbye' },
+ c: { text: 'GOODBYE' },
+ },
+ world: 'world',
+ })
+ .withMessage('The @index variable is used')
+ .toCompileTo('0. goodbye! 1. Goodbye! 2. GOODBYE! cruel world!');
+ });
+
+ it('each with @first', function () {
+ expectTemplate(
+ '{{#each goodbyes}}{{#if @first}}{{text}}! {{/if}}{{/each}}cruel {{world}}!'
+ )
+ .withInput({
+ goodbyes: [
+ { text: 'goodbye' },
+ { text: 'Goodbye' },
+ { text: 'GOODBYE' },
+ ],
+ world: 'world',
+ })
+ .withMessage('The @first variable is used')
+ .toCompileTo('goodbye! cruel world!');
+ });
+
+ it('each with nested @first', function () {
+ expectTemplate(
+ '{{#each goodbyes}}({{#if @first}}{{text}}! {{/if}}{{#each ../goodbyes}}{{#if @first}}{{text}}!{{/if}}{{/each}}{{#if @first}} {{text}}!{{/if}}) {{/each}}cruel {{world}}!'
+ )
+ .withInput({
+ goodbyes: [
+ { text: 'goodbye' },
+ { text: 'Goodbye' },
+ { text: 'GOODBYE' },
+ ],
+ world: 'world',
+ })
+ .withMessage('The @first variable is used')
+ .toCompileTo(
+ '(goodbye! goodbye! goodbye!) (goodbye!) (goodbye!) cruel world!'
+ );
+ });
+
+ it('each object with @first', function () {
+ expectTemplate(
+ '{{#each goodbyes}}{{#if @first}}{{text}}! {{/if}}{{/each}}cruel {{world}}!'
+ )
+ .withInput({
+ goodbyes: { foo: { text: 'goodbye' }, bar: { text: 'Goodbye' } },
+ world: 'world',
+ })
+ .withMessage('The @first variable is used')
+ .toCompileTo('goodbye! cruel world!');
+ });
+
+ it('each with @last', function () {
+ expectTemplate(
+ '{{#each goodbyes}}{{#if @last}}{{text}}! {{/if}}{{/each}}cruel {{world}}!'
+ )
+ .withInput({
+ goodbyes: [
+ { text: 'goodbye' },
+ { text: 'Goodbye' },
+ { text: 'GOODBYE' },
+ ],
+ world: 'world',
+ })
+ .withMessage('The @last variable is used')
+ .toCompileTo('GOODBYE! cruel world!');
+ });
+
+ it('each object with @last', function () {
+ expectTemplate(
+ '{{#each goodbyes}}{{#if @last}}{{text}}! {{/if}}{{/each}}cruel {{world}}!'
+ )
+ .withInput({
+ goodbyes: { foo: { text: 'goodbye' }, bar: { text: 'Goodbye' } },
+ world: 'world',
+ })
+ .withMessage('The @last variable is used')
+ .toCompileTo('Goodbye! cruel world!');
+ });
+
+ it('each with nested @last', function () {
+ expectTemplate(
+ '{{#each goodbyes}}({{#if @last}}{{text}}! {{/if}}{{#each ../goodbyes}}{{#if @last}}{{text}}!{{/if}}{{/each}}{{#if @last}} {{text}}!{{/if}}) {{/each}}cruel {{world}}!'
+ )
+ .withInput({
+ goodbyes: [
+ { text: 'goodbye' },
+ { text: 'Goodbye' },
+ { text: 'GOODBYE' },
+ ],
+ world: 'world',
+ })
+ .withMessage('The @last variable is used')
+ .toCompileTo(
+ '(GOODBYE!) (GOODBYE!) (GOODBYE! GOODBYE! GOODBYE!) cruel world!'
+ );
+ });
+
+ it('each with function argument', function () {
+ var string = '{{#each goodbyes}}{{text}}! {{/each}}cruel {{world}}!';
+
+ expectTemplate(string)
+ .withInput({
+ goodbyes: function () {
+ return [
+ { text: 'goodbye' },
+ { text: 'Goodbye' },
+ { text: 'GOODBYE' },
+ ];
+ },
+ world: 'world',
+ })
+ .withMessage(
+ 'each with array function argument iterates over the contents when not empty'
+ )
+ .toCompileTo('goodbye! Goodbye! GOODBYE! cruel world!');
+
+ expectTemplate(string)
+ .withInput({
+ goodbyes: [],
+ world: 'world',
+ })
+ .withMessage(
+ 'each with array function argument ignores the contents when empty'
+ )
+ .toCompileTo('cruel world!');
+ });
+
+ it('each object when last key is an empty string', function () {
+ expectTemplate(
+ '{{#each goodbyes}}{{@index}}. {{text}}! {{/each}}cruel {{world}}!'
+ )
+ .withInput({
+ goodbyes: {
+ a: { text: 'goodbye' },
+ b: { text: 'Goodbye' },
+ '': { text: 'GOODBYE' },
+ },
+ world: 'world',
+ })
+ .withMessage('Empty string key is not skipped')
+ .toCompileTo('0. goodbye! 1. Goodbye! 2. GOODBYE! cruel world!');
+ });
+
+ it('data passed to helpers', function () {
+ expectTemplate(
+ '{{#each letters}}{{this}}{{detectDataInsideEach}}{{/each}}'
+ )
+ .withInput({ letters: ['a', 'b', 'c'] })
+ .withMessage('should output data')
+ .withRuntimeOptions({
+ data: {
+ exclaim: '!',
+ },
+ })
+ .toCompileTo('a!b!c!');
+ });
+
+ it('each on implicit context', function () {
+ expectTemplate('{{#each}}{{text}}! {{/each}}cruel world!').toThrow(
+ handlebarsEnv.Exception,
+ 'Must pass iterator to #each'
+ );
+ });
+
+ it('each on Map', function () {
+ var map = new Map([
+ [1, 'one'],
+ [2, 'two'],
+ [3, 'three'],
+ ]);
+
+ expectTemplate('{{#each map}}{{@key}}(i{{@index}}) {{.}} {{/each}}')
+ .withInput({ map: map })
+ .toCompileTo('1(i0) one 2(i1) two 3(i2) three ');
+
+ expectTemplate('{{#each map}}{{#if @first}}{{.}}{{/if}}{{/each}}')
+ .withInput({ map: map })
+ .toCompileTo('one');
+
+ expectTemplate('{{#each map}}{{#if @last}}{{.}}{{/if}}{{/each}}')
+ .withInput({ map: map })
+ .toCompileTo('three');
+
+ expectTemplate('{{#each map}}{{.}}{{/each}}not-in-each')
+ .withInput({ map: new Map() })
+ .toCompileTo('not-in-each');
+ });
+
+ it('each on Set', function () {
+ var set = new Set([1, 2, 3]);
+
+ expectTemplate('{{#each set}}{{@key}}(i{{@index}}) {{.}} {{/each}}')
+ .withInput({ set: set })
+ .toCompileTo('0(i0) 1 1(i1) 2 2(i2) 3 ');
+
+ expectTemplate('{{#each set}}{{#if @first}}{{.}}{{/if}}{{/each}}')
+ .withInput({ set: set })
+ .toCompileTo('1');
+
+ expectTemplate('{{#each set}}{{#if @last}}{{.}}{{/if}}{{/each}}')
+ .withInput({ set: set })
+ .toCompileTo('3');
+
+ expectTemplate('{{#each set}}{{.}}{{/each}}not-in-each')
+ .withInput({ set: new Set() })
+ .toCompileTo('not-in-each');
+ });
+
+ if (global.Symbol && global.Symbol.iterator) {
+ it('each on iterable', function () {
+ function Iterator(arr) {
+ this.arr = arr;
+ this.index = 0;
+ }
+ Iterator.prototype.next = function () {
+ var value = this.arr[this.index];
+ var done = this.index === this.arr.length;
+ if (!done) {
+ this.index++;
+ }
+ return { value: value, done: done };
+ };
+ function Iterable(arr) {
+ this.arr = arr;
+ }
+ Iterable.prototype[global.Symbol.iterator] = function () {
+ return new Iterator(this.arr);
+ };
+ var string = '{{#each goodbyes}}{{text}}! {{/each}}cruel {{world}}!';
+
+ expectTemplate(string)
+ .withInput({
+ goodbyes: new Iterable([
+ { text: 'goodbye' },
+ { text: 'Goodbye' },
+ { text: 'GOODBYE' },
+ ]),
+ world: 'world',
+ })
+ .withMessage(
+ 'each with array argument iterates over the contents when not empty'
+ )
+ .toCompileTo('goodbye! Goodbye! GOODBYE! cruel world!');
+
+ expectTemplate(string)
+ .withInput({
+ goodbyes: new Iterable([]),
+ world: 'world',
+ })
+ .withMessage(
+ 'each with array argument ignores the contents when empty'
+ )
+ .toCompileTo('cruel world!');
+ });
+ }
+ });
+
+ describe('#log', function () {
+ /* eslint-disable no-console */
+ if (typeof console === 'undefined') {
+ return;
+ }
+
+ var $log, $info, $error;
+ beforeEach(function () {
+ $log = console.log;
+ $info = console.info;
+ $error = console.error;
+ });
+ afterEach(function () {
+ console.log = $log;
+ console.info = $info;
+ console.error = $error;
+ });
+
+ it('should call logger at default level', function () {
+ var levelArg, logArg;
+ handlebarsEnv.log = function (level, arg) {
+ levelArg = level;
+ logArg = arg;
+ };
+
+ expectTemplate('{{log blah}}')
+ .withInput({ blah: 'whee' })
+ .withMessage('log should not display')
+ .toCompileTo('');
+ expect(levelArg).toBe(1);
+ expect(logArg).toBe('whee');
+ });
+
+ it('should call logger at data level', function () {
+ var levelArg, logArg;
+ handlebarsEnv.log = function (level, arg) {
+ levelArg = level;
+ logArg = arg;
+ };
+
+ expectTemplate('{{log blah}}')
+ .withInput({ blah: 'whee' })
+ .withRuntimeOptions({ data: { level: '03' } })
+ .withCompileOptions({ data: true })
+ .toCompileTo('');
+ expect(levelArg).toBe('03');
+ expect(logArg).toBe('whee');
+ });
+
+ it('should output to info', function () {
+ var called;
+
+ console.info = function (info) {
+ expect(info).toBe('whee');
+ called = true;
+ console.info = $info;
+ console.log = $log;
+ };
+ console.log = function (log) {
+ expect(log).toBe('whee');
+ called = true;
+ console.info = $info;
+ console.log = $log;
+ };
+
+ expectTemplate('{{log blah}}')
+ .withInput({ blah: 'whee' })
+ .toCompileTo('');
+ expect(called).toBe(true);
+ });
+
+ it('should log at data level', function () {
+ var called;
+
+ console.error = function (log) {
+ expect(log).toBe('whee');
+ called = true;
+ console.error = $error;
+ };
+
+ expectTemplate('{{log blah}}')
+ .withInput({ blah: 'whee' })
+ .withRuntimeOptions({ data: { level: '03' } })
+ .withCompileOptions({ data: true })
+ .toCompileTo('');
+ expect(called).toBe(true);
+ });
+
+ it('should handle missing logger', function () {
+ var called = false;
+
+ console.error = undefined;
+ console.log = function (log) {
+ expect(log).toBe('whee');
+ called = true;
+ console.log = $log;
+ };
+
+ expectTemplate('{{log blah}}')
+ .withInput({ blah: 'whee' })
+ .withRuntimeOptions({ data: { level: '03' } })
+ .withCompileOptions({ data: true })
+ .toCompileTo('');
+ expect(called).toBe(true);
+ });
+
+ it('should handle string log levels', function () {
+ var called;
+
+ console.error = function (log) {
+ expect(log).toBe('whee');
+ called = true;
+ };
+
+ expectTemplate('{{log blah}}')
+ .withInput({ blah: 'whee' })
+ .withRuntimeOptions({ data: { level: 'error' } })
+ .withCompileOptions({ data: true })
+ .toCompileTo('');
+ expect(called).toBe(true);
+
+ called = false;
+
+ expectTemplate('{{log blah}}')
+ .withInput({ blah: 'whee' })
+ .withRuntimeOptions({ data: { level: 'ERROR' } })
+ .withCompileOptions({ data: true })
+ .toCompileTo('');
+ expect(called).toBe(true);
+ });
+
+ it('should handle hash log levels', function () {
+ var called;
+
+ console.error = function (log) {
+ expect(log).toBe('whee');
+ called = true;
+ };
+
+ expectTemplate('{{log blah level="error"}}')
+ .withInput({ blah: 'whee' })
+ .toCompileTo('');
+ expect(called).toBe(true);
+ });
+
+ it('should handle hash log levels', function () {
+ var called = false;
+
+ console.info =
+ console.log =
+ console.error =
+ console.debug =
+ function () {
+ called = true;
+ console.info = console.log = console.error = console.debug = $log;
+ };
+
+ expectTemplate('{{log blah level="debug"}}')
+ .withInput({ blah: 'whee' })
+ .toCompileTo('');
+ expect(called).toBe(false);
+ });
+
+ 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);
+ called = true;
+ console.log = $log;
+ };
+
+ expectTemplate('{{log blah "foo" 1}}')
+ .withInput({ blah: 'whee' })
+ .toCompileTo('');
+ expect(called).toBe(true);
+ });
+
+ it('should pass zero log arguments', function () {
+ var called;
+
+ console.info = console.log = function () {
+ expect(arguments.length).toBe(0);
+ called = true;
+ console.log = $log;
+ };
+
+ expectTemplate('{{log}}').withInput({ blah: 'whee' }).toCompileTo('');
+ expect(called).toBe(true);
+ });
+ /* eslint-enable no-console */
+ });
+
+ describe('#lookup', function () {
+ it('should lookup arbitrary content', function () {
+ expectTemplate('{{#each goodbyes}}{{lookup ../data .}}{{/each}}')
+ .withInput({ goodbyes: [0, 1], data: ['foo', 'bar'] })
+ .toCompileTo('foobar');
+ });
+
+ it('should not fail on undefined value', function () {
+ expectTemplate('{{#each goodbyes}}{{lookup ../bar .}}{{/each}}')
+ .withInput({ goodbyes: [0, 1], data: ['foo', 'bar'] })
+ .toCompileTo('');
+ });
+ });
+});
diff --git a/spec/compiler.js b/spec/compiler.js
new file mode 100644
index 000000000..b37e67d20
--- /dev/null
+++ b/spec/compiler.js
@@ -0,0 +1,160 @@
+describe('compiler', function () {
+ if (!Handlebars.compile) {
+ return;
+ }
+
+ describe('#equals', function () {
+ function compile(string) {
+ var ast = Handlebars.parse(string);
+ return new Handlebars.Compiler().compile(ast, {});
+ }
+
+ it('should treat as equal', function () {
+ expect(compile('foo').equals(compile('foo'))).toBe(true);
+ expect(compile('{{foo}}').equals(compile('{{foo}}'))).toBe(true);
+ expect(compile('{{foo.bar}}').equals(compile('{{foo.bar}}'))).toBe(true);
+ expect(
+ compile('{{foo.bar baz "foo" true false bat=1}}').equals(
+ compile('{{foo.bar baz "foo" true false bat=1}}')
+ )
+ ).toBe(true);
+ expect(
+ 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);
+ });
+ it('should treat as not equal', function () {
+ expect(compile('foo').equals(compile('bar'))).toBe(false);
+ expect(compile('{{foo}}').equals(compile('{{bar}}'))).toBe(false);
+ expect(compile('{{foo.bar}}').equals(compile('{{bar.bar}}'))).toBe(false);
+ expect(
+ compile('{{foo.bar baz bat=1}}').equals(
+ compile('{{foo.bar bar bat=1}}')
+ )
+ ).toBe(false);
+ expect(
+ 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);
+ });
+ });
+
+ describe('#compile', function () {
+ it('should fail with invalid input', function () {
+ expect(function () {
+ Handlebars.compile(null);
+ }).toThrow(
+ 'You must pass a string or Handlebars AST to Handlebars.compile. You passed null'
+ );
+ expect(function () {
+ Handlebars.compile({});
+ }).toThrow(
+ 'You must pass a string or Handlebars AST to Handlebars.compile. You passed [object Object]'
+ );
+ });
+
+ it('should include the location in the error (row and column)', function () {
+ try {
+ Handlebars.compile(' \n {{#if}}\n{{/def}}')();
+ expect.unreachable('Statement must throw exception');
+ } catch (err) {
+ expect(err.message).toBe("if doesn't match def - 2:5");
+ if (Object.getOwnPropertyDescriptor(err, 'column').writable) {
+ // In Safari 8, the column-property is read-only. This means that even if it is set with defineProperty,
+ // its value won't change (https://github.com/jquery/esprima/issues/1290#issuecomment-132455482)
+ // Since this was neither working in Handlebars 3 nor in 4.0.5, we only check the column for other browsers.
+ expect(err.column).toBe(5);
+ }
+ expect(err.lineNumber).toBe(2);
+ }
+ });
+
+ it('should include the location as enumerable property', function () {
+ try {
+ Handlebars.compile(' \n {{#if}}\n{{/def}}')();
+ expect.unreachable('Statement must throw exception');
+ } catch (err) {
+ expect(Object.prototype.propertyIsEnumerable.call(err, 'column')).toBe(
+ true
+ );
+ }
+ });
+
+ it('can utilize AST instance', function () {
+ expect(
+ Handlebars.compile({
+ type: 'Program',
+ body: [{ type: 'ContentStatement', value: 'Hello' }],
+ })()
+ ).toBe('Hello');
+ });
+
+ it('can pass through an empty string', function () {
+ expect(Handlebars.compile('')()).toBe('');
+ });
+
+ it('throws on desupported options', function () {
+ expect(function () {
+ Handlebars.compile('Dudes', { trackIds: true });
+ }).toThrow(
+ 'TrackIds and stringParams are no longer supported. See Github #1145'
+ );
+ expect(function () {
+ Handlebars.compile('Dudes', { stringParams: true });
+ }).toThrow(
+ 'TrackIds and stringParams are no longer supported. See Github #1145'
+ );
+ });
+
+ it('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' }] });
+ });
+
+ 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: {} });
+ });
+ });
+
+ describe('#precompile', function () {
+ it('should fail with invalid input', function () {
+ expect(function () {
+ Handlebars.precompile(null);
+ }).toThrow(
+ 'You must pass a string or Handlebars AST to Handlebars.compile. You passed null'
+ );
+ expect(function () {
+ Handlebars.precompile({});
+ }).toThrow(
+ 'You must pass a string or Handlebars AST to Handlebars.compile. You passed [object Object]'
+ );
+ });
+
+ it('can utilize AST instance', function () {
+ expect(
+ Handlebars.precompile({
+ type: 'Program',
+ body: [{ type: 'ContentStatement', value: 'Hello' }],
+ })
+ ).toMatch(/return "Hello"/);
+ });
+
+ it('can pass through an empty string', function () {
+ expect(Handlebars.precompile('')).toMatch(/return ""/);
+ });
+ });
+});
diff --git a/spec/data.js b/spec/data.js
new file mode 100644
index 000000000..5402c4f9d
--- /dev/null
+++ b/spec/data.js
@@ -0,0 +1,278 @@
+describe('data', function () {
+ it('passing in data to a compiled function that expects data - works with helpers', function () {
+ expectTemplate('{{hello}}')
+ .withCompileOptions({ data: true })
+ .withHelper('hello', function (options) {
+ return options.data.adjective + ' ' + this.noun;
+ })
+ .withRuntimeOptions({ data: { adjective: 'happy' } })
+ .withInput({ noun: 'cat' })
+ .withMessage('Data output by helper')
+ .toCompileTo('happy cat');
+ });
+
+ it('data can be looked up via @foo', function () {
+ expectTemplate('{{@hello}}')
+ .withRuntimeOptions({ data: { hello: 'hello' } })
+ .withMessage('@foo retrieves template data')
+ .toCompileTo('hello');
+ });
+
+ it('deep @foo triggers automatic top-level data', function () {
+ var helpers = Handlebars.createFrame(handlebarsEnv.helpers);
+
+ helpers.let = function (options) {
+ var frame = Handlebars.createFrame(options.data);
+
+ for (var prop in options.hash) {
+ if (prop in options.hash) {
+ frame[prop] = options.hash[prop];
+ }
+ }
+ return options.fn(this, { data: frame });
+ };
+
+ expectTemplate(
+ '{{#let world="world"}}{{#if foo}}{{#if foo}}Hello {{@world}}{{/if}}{{/if}}{{/let}}'
+ )
+ .withInput({ foo: true })
+ .withHelpers(helpers)
+ .withMessage('Automatic data was triggered')
+ .toCompileTo('Hello world');
+ });
+
+ it('parameter data can be looked up via @foo', function () {
+ expectTemplate('{{hello @world}}')
+ .withRuntimeOptions({ data: { world: 'world' } })
+ .withHelper('hello', function (noun) {
+ return 'Hello ' + noun;
+ })
+ .withMessage('@foo as a parameter retrieves template data')
+ .toCompileTo('Hello world');
+ });
+
+ it('hash values can be looked up via @foo', function () {
+ expectTemplate('{{hello noun=@world}}')
+ .withRuntimeOptions({ data: { world: 'world' } })
+ .withHelper('hello', function (options) {
+ return 'Hello ' + options.hash.noun;
+ })
+ .withMessage('@foo as a parameter retrieves template data')
+ .toCompileTo('Hello world');
+ });
+
+ it('nested parameter data can be looked up via @foo.bar', function () {
+ expectTemplate('{{hello @world.bar}}')
+ .withRuntimeOptions({ data: { world: { bar: 'world' } } })
+ .withHelper('hello', function (noun) {
+ return 'Hello ' + noun;
+ })
+ .withMessage('@foo as a parameter retrieves template data')
+ .toCompileTo('Hello world');
+ });
+
+ it('nested parameter data does not fail with @world.bar', function () {
+ expectTemplate('{{hello @world.bar}}')
+ .withRuntimeOptions({ data: { foo: { bar: 'world' } } })
+ .withHelper('hello', function (noun) {
+ return 'Hello ' + noun;
+ })
+ .withMessage('@foo as a parameter retrieves template data')
+ .toCompileTo('Hello undefined');
+ });
+
+ it('parameter data throws when using complex scope references', function () {
+ expectTemplate(
+ '{{#goodbyes}}{{text}} cruel {{@foo/../name}}! {{/goodbyes}}'
+ ).toThrow(Error);
+ });
+
+ it('data can be functions', function () {
+ expectTemplate('{{@hello}}')
+ .withRuntimeOptions({
+ data: {
+ hello: function () {
+ return 'hello';
+ },
+ },
+ })
+ .toCompileTo('hello');
+ });
+
+ it('data can be functions with params', function () {
+ expectTemplate('{{@hello "hello"}}')
+ .withRuntimeOptions({
+ data: {
+ hello: function (arg) {
+ return arg;
+ },
+ },
+ })
+ .toCompileTo('hello');
+ });
+
+ it('data is inherited downstream', function () {
+ expectTemplate(
+ '{{#let foo=1 bar=2}}{{#let foo=bar.baz}}{{@bar}}{{@foo}}{{/let}}{{@foo}}{{/let}}'
+ )
+ .withInput({ bar: { baz: 'hello world' } })
+ .withCompileOptions({ data: true })
+ .withHelper('let', function (options) {
+ var frame = Handlebars.createFrame(options.data);
+ for (var prop in options.hash) {
+ if (prop in options.hash) {
+ frame[prop] = options.hash[prop];
+ }
+ }
+ return options.fn(this, { data: frame });
+ })
+ .withRuntimeOptions({ data: {} })
+ .withMessage('data variables are inherited downstream')
+ .toCompileTo('2hello world1');
+ });
+
+ it('passing in data to a compiled function that expects data - works with helpers in partials', function () {
+ expectTemplate('{{>myPartial}}')
+ .withCompileOptions({ data: true })
+ .withPartial('myPartial', '{{hello}}')
+ .withHelper('hello', function (options) {
+ return options.data.adjective + ' ' + this.noun;
+ })
+ .withInput({ noun: 'cat' })
+ .withRuntimeOptions({ data: { adjective: 'happy' } })
+ .withMessage('Data output by helper inside partial')
+ .toCompileTo('happy cat');
+ });
+
+ it('passing in data to a compiled function that expects data - works with helpers and parameters', function () {
+ expectTemplate('{{hello world}}')
+ .withCompileOptions({ data: true })
+ .withHelper('hello', function (noun, options) {
+ return options.data.adjective + ' ' + noun + (this.exclaim ? '!' : '');
+ })
+ .withInput({ exclaim: true, world: 'world' })
+ .withRuntimeOptions({ data: { adjective: 'happy' } })
+ .withMessage('Data output by helper')
+ .toCompileTo('happy world!');
+ });
+
+ it('passing in data to a compiled function that expects data - works with block helpers', function () {
+ expectTemplate('{{#hello}}{{world}}{{/hello}}')
+ .withCompileOptions({
+ data: true,
+ })
+ .withHelper('hello', function (options) {
+ return options.fn(this);
+ })
+ .withHelper('world', function (options) {
+ return options.data.adjective + ' world' + (this.exclaim ? '!' : '');
+ })
+ .withInput({ exclaim: true })
+ .withRuntimeOptions({ data: { adjective: 'happy' } })
+ .withMessage('Data output by helper')
+ .toCompileTo('happy world!');
+ });
+
+ it('passing in data to a compiled function that expects data - works with block helpers that use ..', function () {
+ expectTemplate('{{#hello}}{{world ../zomg}}{{/hello}}')
+ .withCompileOptions({ data: true })
+ .withHelper('hello', function (options) {
+ return options.fn({ exclaim: '?' });
+ })
+ .withHelper('world', function (thing, options) {
+ return options.data.adjective + ' ' + thing + (this.exclaim || '');
+ })
+ .withInput({ exclaim: true, zomg: 'world' })
+ .withRuntimeOptions({ data: { adjective: 'happy' } })
+ .withMessage('Data output by helper')
+ .toCompileTo('happy world?');
+ });
+
+ it('passing in data to a compiled function that expects data - data is passed to with block helpers where children use ..', function () {
+ expectTemplate('{{#hello}}{{world ../zomg}}{{/hello}}')
+ .withCompileOptions({ data: true })
+ .withHelper('hello', function (options) {
+ return options.data.accessData + ' ' + options.fn({ exclaim: '?' });
+ })
+ .withHelper('world', function (thing, options) {
+ return options.data.adjective + ' ' + thing + (this.exclaim || '');
+ })
+ .withInput({ exclaim: true, zomg: 'world' })
+ .withRuntimeOptions({ data: { adjective: 'happy', accessData: '#win' } })
+ .withMessage('Data output by helper')
+ .toCompileTo('#win happy world?');
+ });
+
+ it('you can override inherited data when invoking a helper', function () {
+ expectTemplate('{{#hello}}{{world zomg}}{{/hello}}')
+ .withCompileOptions({ data: true })
+ .withHelper('hello', function (options) {
+ return options.fn(
+ { exclaim: '?', zomg: 'world' },
+ { data: { adjective: 'sad' } }
+ );
+ })
+ .withHelper('world', function (thing, options) {
+ return options.data.adjective + ' ' + thing + (this.exclaim || '');
+ })
+ .withInput({ exclaim: true, zomg: 'planet' })
+ .withRuntimeOptions({ data: { adjective: 'happy' } })
+ .withMessage('Overridden data output by helper')
+ .toCompileTo('sad world?');
+ });
+
+ it('you can override inherited data when invoking a helper with depth', function () {
+ expectTemplate('{{#hello}}{{world ../zomg}}{{/hello}}')
+ .withCompileOptions({ data: true })
+ .withHelper('hello', function (options) {
+ return options.fn({ exclaim: '?' }, { data: { adjective: 'sad' } });
+ })
+ .withHelper('world', function (thing, options) {
+ return options.data.adjective + ' ' + thing + (this.exclaim || '');
+ })
+ .withInput({ exclaim: true, zomg: 'world' })
+ .withRuntimeOptions({ data: { adjective: 'happy' } })
+ .withMessage('Overridden data output by helper')
+ .toCompileTo('sad world?');
+ });
+
+ describe('@root', function () {
+ it('the root context can be looked up via @root', function () {
+ expectTemplate('{{@root.foo}}')
+ .withInput({ foo: 'hello' })
+ .withRuntimeOptions({ data: {} })
+ .toCompileTo('hello');
+
+ expectTemplate('{{@root.foo}}')
+ .withInput({ foo: 'hello' })
+ .toCompileTo('hello');
+ });
+
+ it('passed root values take priority', function () {
+ expectTemplate('{{@root.foo}}')
+ .withInput({ foo: 'should not be used' })
+ .withRuntimeOptions({ data: { root: { foo: 'hello' } } })
+ .toCompileTo('hello');
+ });
+ });
+
+ describe('nesting', function () {
+ it('the root context can be looked up via @root', function () {
+ expectTemplate(
+ '{{#helper}}{{#helper}}{{@./depth}} {{@../depth}} {{@../../depth}}{{/helper}}{{/helper}}'
+ )
+ .withInput({ foo: 'hello' })
+ .withHelper('helper', function (options) {
+ var frame = Handlebars.createFrame(options.data);
+ frame.depth = options.data.depth + 1;
+ return options.fn(this, { data: frame });
+ })
+ .withRuntimeOptions({
+ data: {
+ depth: 0,
+ },
+ })
+ .toCompileTo('2 1 0');
+ });
+ });
+});
diff --git a/spec/env/browser-vitest-pre.js b/spec/env/browser-vitest-pre.js
new file mode 100644
index 000000000..e48fcc54a
--- /dev/null
+++ b/spec/env/browser-vitest-pre.js
@@ -0,0 +1,6 @@
+// Pre-setup for browser tests. Must run before the main setup file
+// imports the Handlebars library, so that noConflict() captures this value.
+globalThis.Handlebars = 'no-conflict';
+
+// Polyfill Node.js 'global' for specs that reference it at module level
+globalThis.global = globalThis;
diff --git a/spec/env/browser-vitest.js b/spec/env/browser-vitest.js
new file mode 100644
index 000000000..a9b704609
--- /dev/null
+++ b/spec/env/browser-vitest.js
@@ -0,0 +1,27 @@
+import './common.js';
+import Handlebars from '../../lib/handlebars.js';
+
+globalThis.Handlebars = Handlebars;
+
+globalThis.CompilerContext = {
+ browser: true,
+
+ compile: function (template, options) {
+ var templateSpec = handlebarsEnv.precompile(template, options);
+ return handlebarsEnv.template(safeEval(templateSpec));
+ },
+ compileWithPartial: function (template, options) {
+ return handlebarsEnv.compile(template, options);
+ },
+};
+
+function safeEval(templateSpec) {
+ /* eslint-disable no-eval, no-console */
+ try {
+ return eval('(' + templateSpec + ')');
+ } catch (err) {
+ console.error(templateSpec);
+ throw err;
+ }
+ /* eslint-enable no-eval, no-console */
+}
diff --git a/spec/env/browser.js b/spec/env/browser.js
new file mode 100644
index 000000000..9f17f0c67
--- /dev/null
+++ b/spec/env/browser.js
@@ -0,0 +1,36 @@
+require('./common');
+
+var fs = require('fs'),
+ vm = require('vm');
+
+global.Handlebars = 'no-conflict';
+
+var filename = 'dist/handlebars.js';
+var distHandlebars = fs.readFileSync(
+ require.resolve('../../' + filename),
+ 'utf-8'
+);
+vm.runInThisContext(distHandlebars, filename);
+
+global.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 */
+}
diff --git a/spec/env/common.js b/spec/env/common.js
new file mode 100644
index 000000000..b5ce24f36
--- /dev/null
+++ b/spec/env/common.js
@@ -0,0 +1,133 @@
+var global = globalThis;
+
+global.expectTemplate = function (templateAsString) {
+ return new HandlebarsTestBench(templateAsString);
+};
+
+function HandlebarsTestBench(templateAsString) {
+ this.templateAsString = templateAsString;
+ this.helpers = {};
+ this.partials = {};
+ this.decorators = {};
+ this.input = {};
+ this.message =
+ 'Template' + templateAsString + ' does not evaluate to expected output';
+ this.compileOptions = {};
+ this.runtimeOptions = {};
+}
+
+HandlebarsTestBench.prototype.withInput = function (input) {
+ this.input = input;
+ return this;
+};
+
+HandlebarsTestBench.prototype.withHelper = function (name, helperFunction) {
+ this.helpers[name] = helperFunction;
+ return this;
+};
+
+HandlebarsTestBench.prototype.withHelpers = function (helperFunctions) {
+ var self = this;
+ Object.keys(helperFunctions).forEach(function (name) {
+ self.withHelper(name, helperFunctions[name]);
+ });
+ return this;
+};
+
+HandlebarsTestBench.prototype.withPartial = function (name, partialAsString) {
+ this.partials[name] = partialAsString;
+ return this;
+};
+
+HandlebarsTestBench.prototype.withPartials = function (partials) {
+ var self = this;
+ Object.keys(partials).forEach(function (name) {
+ self.withPartial(name, partials[name]);
+ });
+ return this;
+};
+
+HandlebarsTestBench.prototype.withDecorator = function (
+ name,
+ decoratorFunction
+) {
+ this.decorators[name] = decoratorFunction;
+ return this;
+};
+
+HandlebarsTestBench.prototype.withDecorators = function (decorators) {
+ var self = this;
+ Object.keys(decorators).forEach(function (name) {
+ self.withDecorator(name, decorators[name]);
+ });
+ return this;
+};
+
+HandlebarsTestBench.prototype.withCompileOptions = function (compileOptions) {
+ this.compileOptions = compileOptions;
+ return this;
+};
+
+HandlebarsTestBench.prototype.withRuntimeOptions = function (runtimeOptions) {
+ this.runtimeOptions = runtimeOptions;
+ return this;
+};
+
+HandlebarsTestBench.prototype.withMessage = function (message) {
+ this.message = message;
+ return this;
+};
+
+HandlebarsTestBench.prototype.toCompileTo = function (expectedOutputAsString) {
+ expect(this._compileAndExecute()).toBe(expectedOutputAsString);
+};
+
+HandlebarsTestBench.prototype.toThrow = function (errorLike, errMsgMatcher) {
+ var self = this;
+ var caught;
+ try {
+ 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);
+ }
+};
+
+HandlebarsTestBench.prototype._compileAndExecute = function () {
+ var compile =
+ Object.keys(this.partials).length > 0
+ ? CompilerContext.compileWithPartial
+ : CompilerContext.compile;
+
+ var combinedRuntimeOptions = this._combineRuntimeOptions();
+
+ var template = compile(this.templateAsString, this.compileOptions);
+ return template(this.input, combinedRuntimeOptions);
+};
+
+HandlebarsTestBench.prototype._combineRuntimeOptions = function () {
+ var self = this;
+ var combinedRuntimeOptions = {};
+ Object.keys(this.runtimeOptions).forEach(function (key) {
+ combinedRuntimeOptions[key] = self.runtimeOptions[key];
+ });
+ combinedRuntimeOptions.helpers = this.helpers;
+ combinedRuntimeOptions.partials = this.partials;
+ combinedRuntimeOptions.decorators = this.decorators;
+ return combinedRuntimeOptions;
+};
+
+beforeEach(function () {
+ global.handlebarsEnv = Handlebars.create();
+});
diff --git a/spec/env/node.js b/spec/env/node.js
new file mode 100644
index 000000000..9d2650209
--- /dev/null
+++ b/spec/env/node.js
@@ -0,0 +1,24 @@
+require('./common');
+
+global.Handlebars = require('../../lib');
+
+global.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) {
+ /* eslint-disable no-eval, no-console */
+ try {
+ return eval('(' + templateSpec + ')');
+ } catch (err) {
+ console.error(templateSpec);
+ throw err;
+ }
+ /* eslint-enable no-eval, no-console */
+}
diff --git a/spec/expected/bom.amd.js b/spec/expected/bom.amd.js
new file mode 100644
index 000000000..906989feb
--- /dev/null
+++ b/spec/expected/bom.amd.js
@@ -0,0 +1,6 @@
+define(['handlebars.runtime'], function(Handlebars) {
+ Handlebars = Handlebars["default"]; var template = Handlebars.template, templates = Handlebars.templates = Handlebars.templates || {};
+ return templates['bom'] = template({"compiler":[8,">= 4.3.0"],"main":function(container,depth0,helpers,partials,data) {
+ return "a";
+ },"useData":true});
+ });
\ No newline at end of file
diff --git a/spec/expected/compiled.string.txt b/spec/expected/compiled.string.txt
new file mode 100644
index 000000000..8b1a14d8c
--- /dev/null
+++ b/spec/expected/compiled.string.txt
@@ -0,0 +1,3 @@
+{"compiler":[8,">= 4.3.0"],"main":function(container,depth0,helpers,partials,data) {
+ return "
Test String
";
+},"useData":true}
\ No newline at end of file
diff --git a/spec/expected/empty.amd.js b/spec/expected/empty.amd.js
new file mode 100644
index 000000000..9728609ef
--- /dev/null
+++ b/spec/expected/empty.amd.js
@@ -0,0 +1,6 @@
+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 "";
+},"useData":true});
+});
diff --git a/spec/expected/empty.amd.min.js b/spec/expected/empty.amd.min.js
new file mode 100644
index 000000000..8fc31a610
--- /dev/null
+++ b/spec/expected/empty.amd.min.js
@@ -0,0 +1 @@
+define(["handlebars.runtime"],function(e){var t=(e=e.default).template;return(e.templates=e.templates||{}).empty=t({compiler:[8,">= 4.3.0"],main:function(e,t,a,n,r){return""},useData:!0})});
\ No newline at end of file
diff --git a/spec/expected/empty.amd.namespace.js b/spec/expected/empty.amd.namespace.js
new file mode 100644
index 000000000..1972fb437
--- /dev/null
+++ b/spec/expected/empty.amd.namespace.js
@@ -0,0 +1,6 @@
+define(['handlebars.runtime'], function(Handlebars) {
+ Handlebars = Handlebars["default"]; var template = Handlebars.template, templates = CustomNamespace.templates = CustomNamespace.templates || {};
+return templates['empty'] = template({"compiler":[8,">= 4.3.0"],"main":function(container,depth0,helpers,partials,data) {
+ return "";
+},"useData":true});
+});
diff --git a/spec/expected/empty.amd.simple.js b/spec/expected/empty.amd.simple.js
new file mode 100644
index 000000000..b4014bb83
--- /dev/null
+++ b/spec/expected/empty.amd.simple.js
@@ -0,0 +1,3 @@
+{"compiler":[8,">= 4.3.0"],"main":function(container,depth0,helpers,partials,data) {
+ return "";
+},"useData":true}
\ No newline at end of file
diff --git a/spec/expected/empty.common.js b/spec/expected/empty.common.js
new file mode 100644
index 000000000..099f2f2de
--- /dev/null
+++ b/spec/expected/empty.common.js
@@ -0,0 +1,6 @@
+(function() {
+ var template = Handlebars.template, templates = Handlebars.templates = Handlebars.templates || {};
+templates['empty'] = template({"compiler":[8,">= 4.3.0"],"main":function(container,depth0,helpers,partials,data) {
+ return "";
+},"useData":true});
+})();
\ No newline at end of file
diff --git a/spec/expected/empty.name.amd.js b/spec/expected/empty.name.amd.js
new file mode 100644
index 000000000..13c377f64
--- /dev/null
+++ b/spec/expected/empty.name.amd.js
@@ -0,0 +1,10 @@
+define(['handlebars.runtime'], function(Handlebars) {
+ Handlebars = Handlebars["default"]; var template = Handlebars.template, templates = Handlebars.templates = Handlebars.templates || {};
+templates['firstTemplate'] = template({"compiler":[8,">= 4.3.0"],"main":function(container,depth0,helpers,partials,data) {
+ return "
{{#link}}Hello{{/link}}{{/form}}'
+ )
+ .withInput({
+ yehuda: { name: 'Yehuda' },
+ })
+ .withHelper('link', function (options) {
+ return '' + options.fn(this) + '';
+ })
+ .withHelper('form', function (context, options) {
+ return '';
+ })
+ .withMessage('Both blocks executed')
+ .toCompileTo('');
+ });
+
+ it('block helper inverted sections', function () {
+ var string = "{{#list people}}{{name}}{{^}}Nobody's here{{/list}}";
+ function list(context, options) {
+ if (context.length > 0) {
+ var out = '
';
+ for (var i = 0, j = context.length; i < j; i++) {
+ out += '
';
+ out += options.fn(context[i]);
+ out += '
';
+ }
+ out += '
';
+ return out;
+ } else {
+ return '
' + options.inverse(this) + '
';
+ }
+ }
+
+ // the meaning here may be kind of hard to catch, but list.not is always called,
+ // so we should see the output of both
+ expectTemplate(string)
+ .withInput({ people: [{ name: 'Alan' }, { name: 'Yehuda' }] })
+ .withHelpers({ list: list })
+ .withMessage('an inverse wrapper is passed in as a new context')
+ .toCompileTo('
Alan
Yehuda
');
+
+ expectTemplate(string)
+ .withInput({ people: [] })
+ .withHelpers({ list: list })
+ .withMessage('an inverse wrapper can be optionally called')
+ .toCompileTo("
Nobody's here
");
+
+ expectTemplate('{{#list people}}Hello{{^}}{{message}}{{/list}}')
+ .withInput({
+ people: [],
+ message: "Nobody's here",
+ })
+ .withHelpers({ list: list })
+ .withMessage('the context of an inverse is the parent of the block')
+ .toCompileTo('
Nobody's here
');
+ });
+
+ it('pathed lambas with parameters', function () {
+ var hash = {
+ helper: function () {
+ return 'winning';
+ },
+ };
+ hash.hash = hash;
+ var helpers = {
+ './helper': function () {
+ return 'fail';
+ },
+ };
+
+ expectTemplate('{{./helper 1}}')
+ .withInput(hash)
+ .withHelpers(helpers)
+ .toCompileTo('winning');
+
+ expectTemplate('{{hash/helper 1}}')
+ .withInput(hash)
+ .withHelpers(helpers)
+ .toCompileTo('winning');
+ });
+
+ describe('helpers hash', function () {
+ it('providing a helpers hash', function () {
+ expectTemplate('Goodbye {{cruel}} {{world}}!')
+ .withInput({ cruel: 'cruel' })
+ .withHelpers({
+ world: function () {
+ return 'world';
+ },
+ })
+ .withMessage('helpers hash is available')
+ .toCompileTo('Goodbye cruel world!');
+
+ expectTemplate('Goodbye {{#iter}}{{cruel}} {{world}}{{/iter}}!')
+ .withInput({ iter: [{ cruel: 'cruel' }] })
+ .withHelpers({
+ world: function () {
+ return 'world';
+ },
+ })
+ .withMessage('helpers hash is available inside other blocks')
+ .toCompileTo('Goodbye cruel world!');
+ });
+
+ it('in cases of conflict, helpers win', function () {
+ expectTemplate('{{{lookup}}}')
+ .withInput({ lookup: 'Explicit' })
+ .withHelpers({
+ lookup: function () {
+ return 'helpers';
+ },
+ })
+ .withMessage('helpers hash has precedence escaped expansion')
+ .toCompileTo('helpers');
+
+ expectTemplate('{{lookup}}')
+ .withInput({ lookup: 'Explicit' })
+ .withHelpers({
+ lookup: function () {
+ return 'helpers';
+ },
+ })
+ .withMessage('helpers hash has precedence simple expansion')
+ .toCompileTo('helpers');
+ });
+
+ it('the helpers hash is available is nested contexts', function () {
+ expectTemplate('{{#outer}}{{#inner}}{{helper}}{{/inner}}{{/outer}}')
+ .withInput({ outer: { inner: { unused: [] } } })
+ .withHelpers({
+ helper: function () {
+ return 'helper';
+ },
+ })
+ .withMessage('helpers hash is available in nested contexts.')
+ .toCompileTo('helper');
+ });
+
+ it('the helper hash should augment the global hash', function () {
+ handlebarsEnv.registerHelper('test_helper', function () {
+ return 'found it!';
+ });
+
+ expectTemplate(
+ '{{test_helper}} {{#if cruel}}Goodbye {{cruel}} {{world}}!{{/if}}'
+ )
+ .withInput({ cruel: 'cruel' })
+ .withHelpers({
+ world: function () {
+ return 'world!';
+ },
+ })
+ .toCompileTo('found it! Goodbye cruel world!!');
+ });
+ });
+
+ describe('registration', function () {
+ it('unregisters', function () {
+ handlebarsEnv.helpers = {};
+
+ handlebarsEnv.registerHelper('foo', function () {
+ return 'fail';
+ });
+ handlebarsEnv.unregisterHelper('foo');
+ expect(handlebarsEnv.helpers.foo).toBeUndefined();
+ });
+
+ it('allows multiple globals', function () {
+ var helpers = handlebarsEnv.helpers;
+ handlebarsEnv.helpers = {};
+
+ handlebarsEnv.registerHelper({
+ if: helpers['if'],
+ world: function () {
+ return 'world!';
+ },
+ testHelper: function () {
+ return 'found it!';
+ },
+ });
+
+ expectTemplate(
+ '{{testHelper}} {{#if cruel}}Goodbye {{cruel}} {{world}}!{{/if}}'
+ )
+ .withInput({ cruel: 'cruel' })
+ .toCompileTo('found it! Goodbye cruel world!!');
+ });
+
+ it('fails with multiple and args', function () {
+ expect(function () {
+ handlebarsEnv.registerHelper(
+ {
+ world: function () {
+ return 'world!';
+ },
+ testHelper: function () {
+ return 'found it!';
+ },
+ },
+ {}
+ );
+ }).toThrow('Arg not supported with multiple helpers');
+ });
+ });
+
+ it('decimal number literals work', function () {
+ expectTemplate('Message: {{hello -1.2 1.2}}')
+ .withHelper('hello', function (times, times2) {
+ if (typeof times !== 'number') {
+ times = 'NaN';
+ }
+ if (typeof times2 !== 'number') {
+ times2 = 'NaN';
+ }
+ return 'Hello ' + times + ' ' + times2 + ' times';
+ })
+ .withMessage('template with a negative integer literal')
+ .toCompileTo('Message: Hello -1.2 1.2 times');
+ });
+
+ it('negative number literals work', function () {
+ expectTemplate('Message: {{hello -12}}')
+ .withHelper('hello', function (times) {
+ if (typeof times !== 'number') {
+ times = 'NaN';
+ }
+ return 'Hello ' + times + ' times';
+ })
+ .withMessage('template with a negative integer literal')
+ .toCompileTo('Message: Hello -12 times');
+ });
+
+ describe('String literal parameters', function () {
+ it('simple literals work', function () {
+ expectTemplate('Message: {{hello "world" 12 true false}}')
+ .withHelper('hello', function (param, times, bool1, bool2) {
+ if (typeof times !== 'number') {
+ times = 'NaN';
+ }
+ if (typeof bool1 !== 'boolean') {
+ bool1 = 'NaB';
+ }
+ if (typeof bool2 !== 'boolean') {
+ bool2 = 'NaB';
+ }
+ return (
+ 'Hello ' + param + ' ' + times + ' times: ' + bool1 + ' ' + bool2
+ );
+ })
+ .withMessage('template with a simple String literal')
+ .toCompileTo('Message: Hello world 12 times: true false');
+ });
+
+ it('using a quote in the middle of a parameter raises an error', function () {
+ expectTemplate('Message: {{hello wo"rld"}}').toThrow(Error);
+ });
+
+ it('escaping a String is possible', function () {
+ expectTemplate('Message: {{{hello "\\"world\\""}}}')
+ .withHelper('hello', function (param) {
+ return 'Hello ' + param;
+ })
+ .withMessage('template with an escaped String literal')
+ .toCompileTo('Message: Hello "world"');
+ });
+
+ it("it works with ' marks", function () {
+ expectTemplate('Message: {{{hello "Alan\'s world"}}}')
+ .withHelper('hello', function (param) {
+ return 'Hello ' + param;
+ })
+ .withMessage("template with a ' mark")
+ .toCompileTo("Message: Hello Alan's world");
+ });
+ });
+
+ it('negative number literals work', function () {
+ expectTemplate('Message: {{hello -12}}')
+ .withHelper('hello', function (times) {
+ if (typeof times !== 'number') {
+ times = 'NaN';
+ }
+ return 'Hello ' + times + ' times';
+ })
+ .withMessage('template with a negative integer literal')
+ .toCompileTo('Message: Hello -12 times');
+ });
+
+ describe('multiple parameters', function () {
+ it('simple multi-params work', function () {
+ expectTemplate('Message: {{goodbye cruel world}}')
+ .withInput({ cruel: 'cruel', world: 'world' })
+ .withHelper('goodbye', function (cruel, world) {
+ return 'Goodbye ' + cruel + ' ' + world;
+ })
+ .withMessage('regular helpers with multiple params')
+ .toCompileTo('Message: Goodbye cruel world');
+ });
+
+ it('block multi-params work', function () {
+ expectTemplate(
+ 'Message: {{#goodbye cruel world}}{{greeting}} {{adj}} {{noun}}{{/goodbye}}'
+ )
+ .withInput({ cruel: 'cruel', world: 'world' })
+ .withHelper('goodbye', function (cruel, world, options) {
+ return options.fn({ greeting: 'Goodbye', adj: cruel, noun: world });
+ })
+ .withMessage('block helpers with multiple params')
+ .toCompileTo('Message: Goodbye cruel world');
+ });
+ });
+
+ describe('hash', function () {
+ it('helpers can take an optional hash', function () {
+ expectTemplate('{{goodbye cruel="CRUEL" world="WORLD" times=12}}')
+ .withHelper('goodbye', function (options) {
+ return (
+ 'GOODBYE ' +
+ options.hash.cruel +
+ ' ' +
+ options.hash.world +
+ ' ' +
+ options.hash.times +
+ ' TIMES'
+ );
+ })
+ .withMessage('Helper output hash')
+ .toCompileTo('GOODBYE CRUEL WORLD 12 TIMES');
+ });
+
+ it('helpers can take an optional hash with booleans', function () {
+ function goodbye(options) {
+ if (options.hash.print === true) {
+ return 'GOODBYE ' + options.hash.cruel + ' ' + options.hash.world;
+ } else if (options.hash.print === false) {
+ return 'NOT PRINTING';
+ } else {
+ return 'THIS SHOULD NOT HAPPEN';
+ }
+ }
+
+ expectTemplate('{{goodbye cruel="CRUEL" world="WORLD" print=true}}')
+ .withHelper('goodbye', goodbye)
+ .withMessage('Helper output hash')
+ .toCompileTo('GOODBYE CRUEL WORLD');
+
+ expectTemplate('{{goodbye cruel="CRUEL" world="WORLD" print=false}}')
+ .withHelper('goodbye', goodbye)
+ .withMessage('Boolean helper parameter honored')
+ .toCompileTo('NOT PRINTING');
+ });
+
+ it('block helpers can take an optional hash', function () {
+ expectTemplate('{{#goodbye cruel="CRUEL" times=12}}world{{/goodbye}}')
+ .withHelper('goodbye', function (options) {
+ return (
+ 'GOODBYE ' +
+ options.hash.cruel +
+ ' ' +
+ options.fn(this) +
+ ' ' +
+ options.hash.times +
+ ' TIMES'
+ );
+ })
+ .withMessage('Hash parameters output')
+ .toCompileTo('GOODBYE CRUEL world 12 TIMES');
+ });
+
+ it('block helpers can take an optional hash with single quoted stings', function () {
+ expectTemplate('{{#goodbye cruel="CRUEL" times=12}}world{{/goodbye}}')
+ .withHelper('goodbye', function (options) {
+ return (
+ 'GOODBYE ' +
+ options.hash.cruel +
+ ' ' +
+ options.fn(this) +
+ ' ' +
+ options.hash.times +
+ ' TIMES'
+ );
+ })
+ .withMessage('Hash parameters output')
+ .toCompileTo('GOODBYE CRUEL world 12 TIMES');
+ });
+
+ it('block helpers can take an optional hash with booleans', function () {
+ function goodbye(options) {
+ if (options.hash.print === true) {
+ return 'GOODBYE ' + options.hash.cruel + ' ' + options.fn(this);
+ } else if (options.hash.print === false) {
+ return 'NOT PRINTING';
+ } else {
+ return 'THIS SHOULD NOT HAPPEN';
+ }
+ }
+
+ expectTemplate('{{#goodbye cruel="CRUEL" print=true}}world{{/goodbye}}')
+ .withHelper('goodbye', goodbye)
+ .withMessage('Boolean hash parameter honored')
+ .toCompileTo('GOODBYE CRUEL world');
+
+ expectTemplate('{{#goodbye cruel="CRUEL" print=false}}world{{/goodbye}}')
+ .withHelper('goodbye', goodbye)
+ .withMessage('Boolean hash parameter honored')
+ .toCompileTo('NOT PRINTING');
+ });
+ });
+
+ describe('helperMissing', function () {
+ it('if a context is not found, helperMissing is used', function () {
+ expectTemplate('{{hello}} {{link_to world}}').toThrow(
+ /Missing helper: "link_to"/
+ );
+ });
+
+ it('if a context is not found, custom helperMissing is used', function () {
+ expectTemplate('{{hello}} {{link_to world}}')
+ .withInput({ hello: 'Hello', world: 'world' })
+ .withHelper('helperMissing', function (mesg, options) {
+ if (options.name === 'link_to') {
+ return new Handlebars.SafeString('' + mesg + '');
+ }
+ })
+ .toCompileTo('Hello world');
+ });
+
+ it('if a value is not found, custom helperMissing is used', function () {
+ expectTemplate('{{hello}} {{link_to}}')
+ .withInput({ hello: 'Hello', world: 'world' })
+ .withHelper('helperMissing', function (options) {
+ if (options.name === 'link_to') {
+ return new Handlebars.SafeString('winning');
+ }
+ })
+ .toCompileTo('Hello winning');
+ });
+ });
+
+ describe('knownHelpers', function () {
+ it('Known helper should render helper', function () {
+ expectTemplate('{{hello}}')
+ .withCompileOptions({
+ knownHelpers: { hello: true },
+ })
+ .withHelper('hello', function () {
+ return 'foo';
+ })
+ .toCompileTo('foo');
+ });
+
+ it('Unknown helper in knownHelpers only mode should be passed as undefined', function () {
+ expectTemplate('{{typeof hello}}')
+ .withCompileOptions({
+ knownHelpers: { typeof: true },
+ knownHelpersOnly: true,
+ })
+ .withHelper('typeof', function (arg) {
+ return typeof arg;
+ })
+ .withHelper('hello', function () {
+ return 'foo';
+ })
+ .toCompileTo('undefined');
+ });
+
+ it('Builtin helpers available in knownHelpers only mode', function () {
+ expectTemplate('{{#unless foo}}bar{{/unless}}')
+ .withCompileOptions({
+ knownHelpersOnly: true,
+ })
+ .toCompileTo('bar');
+ });
+
+ it('Field lookup works in knownHelpers only mode', function () {
+ expectTemplate('{{foo}}')
+ .withCompileOptions({
+ knownHelpersOnly: true,
+ })
+ .withInput({ foo: 'bar' })
+ .toCompileTo('bar');
+ });
+
+ it('Conditional blocks work in knownHelpers only mode', function () {
+ expectTemplate('{{#foo}}bar{{/foo}}')
+ .withCompileOptions({
+ knownHelpersOnly: true,
+ })
+ .withInput({ foo: 'baz' })
+ .toCompileTo('bar');
+ });
+
+ it('Invert blocks work in knownHelpers only mode', function () {
+ expectTemplate('{{^foo}}bar{{/foo}}')
+ .withCompileOptions({
+ knownHelpersOnly: true,
+ })
+ .withInput({ foo: false })
+ .toCompileTo('bar');
+ });
+
+ it('Functions are bound to the context in knownHelpers only mode', function () {
+ expectTemplate('{{foo}}')
+ .withCompileOptions({
+ knownHelpersOnly: true,
+ })
+ .withInput({
+ foo: function () {
+ return this.bar;
+ },
+ bar: 'bar',
+ })
+ .toCompileTo('bar');
+ });
+
+ it('Unknown helper call in knownHelpers only mode should throw', function () {
+ expectTemplate('{{typeof hello}}')
+ .withCompileOptions({ knownHelpersOnly: true })
+ .toThrow(Error);
+ });
+ });
+
+ describe('blockHelperMissing', function () {
+ it('lambdas are resolved by blockHelperMissing, not handlebars proper', function () {
+ expectTemplate('{{#truthy}}yep{{/truthy}}')
+ .withInput({
+ truthy: function () {
+ return true;
+ },
+ })
+ .toCompileTo('yep');
+ });
+
+ it('lambdas resolved by blockHelperMissing are bound to the context', function () {
+ expectTemplate('{{#truthy}}yep{{/truthy}}')
+ .withInput({
+ truthy: function () {
+ return this.truthiness();
+ },
+ truthiness: function () {
+ return false;
+ },
+ })
+ .toCompileTo('');
+ });
+ });
+
+ describe('name field', function () {
+ var helpers = {
+ blockHelperMissing: function () {
+ return 'missing: ' + arguments[arguments.length - 1].name;
+ },
+ helperMissing: function () {
+ return 'helper missing: ' + arguments[arguments.length - 1].name;
+ },
+ helper: function () {
+ return 'ran: ' + arguments[arguments.length - 1].name;
+ },
+ };
+
+ it('should include in ambiguous mustache calls', function () {
+ expectTemplate('{{helper}}')
+ .withHelpers(helpers)
+ .toCompileTo('ran: helper');
+ });
+
+ it('should include in helper mustache calls', function () {
+ expectTemplate('{{helper 1}}')
+ .withHelpers(helpers)
+ .toCompileTo('ran: helper');
+ });
+
+ it('should include in ambiguous block calls', function () {
+ expectTemplate('{{#helper}}{{/helper}}')
+ .withHelpers(helpers)
+ .toCompileTo('ran: helper');
+ });
+
+ it('should include in simple block calls', function () {
+ expectTemplate('{{#./helper}}{{/./helper}}')
+ .withHelpers(helpers)
+ .toCompileTo('missing: ./helper');
+ });
+
+ it('should include in helper block calls', function () {
+ expectTemplate('{{#helper 1}}{{/helper}}')
+ .withHelpers(helpers)
+ .toCompileTo('ran: helper');
+ });
+
+ it('should include in known helper calls', function () {
+ expectTemplate('{{helper}}')
+ .withCompileOptions({
+ knownHelpers: { helper: true },
+ knownHelpersOnly: true,
+ })
+ .withHelpers(helpers)
+ .toCompileTo('ran: helper');
+ });
+
+ it('should include full id', function () {
+ expectTemplate('{{#foo.helper}}{{/foo.helper}}')
+ .withInput({ foo: {} })
+ .withHelpers(helpers)
+ .toCompileTo('missing: foo.helper');
+ });
+
+ it('should include full id if a hash is passed', function () {
+ expectTemplate('{{#foo.helper bar=baz}}{{/foo.helper}}')
+ .withInput({ foo: {} })
+ .withHelpers(helpers)
+ .toCompileTo('helper missing: foo.helper');
+ });
+ });
+
+ describe('name conflicts', function () {
+ it('helpers take precedence over same-named context properties', function () {
+ expectTemplate('{{goodbye}} {{cruel world}}')
+ .withHelper('goodbye', function () {
+ return this.goodbye.toUpperCase();
+ })
+ .withHelper('cruel', function (world) {
+ return 'cruel ' + world.toUpperCase();
+ })
+ .withInput({
+ goodbye: 'goodbye',
+ world: 'world',
+ })
+ .withMessage('Helper executed')
+ .toCompileTo('GOODBYE cruel WORLD');
+ });
+
+ it('helpers take precedence over same-named context properties$', function () {
+ expectTemplate('{{#goodbye}} {{cruel world}}{{/goodbye}}')
+ .withHelper('goodbye', function (options) {
+ return this.goodbye.toUpperCase() + options.fn(this);
+ })
+ .withHelper('cruel', function (world) {
+ return 'cruel ' + world.toUpperCase();
+ })
+ .withInput({
+ goodbye: 'goodbye',
+ world: 'world',
+ })
+ .withMessage('Helper executed')
+ .toCompileTo('GOODBYE cruel WORLD');
+ });
+
+ it('Scoped names take precedence over helpers', function () {
+ expectTemplate('{{this.goodbye}} {{cruel world}} {{cruel this.goodbye}}')
+ .withHelper('goodbye', function () {
+ return this.goodbye.toUpperCase();
+ })
+ .withHelper('cruel', function (world) {
+ return 'cruel ' + world.toUpperCase();
+ })
+ .withInput({
+ goodbye: 'goodbye',
+ world: 'world',
+ })
+ .withMessage('Helper not executed')
+ .toCompileTo('goodbye cruel WORLD cruel GOODBYE');
+ });
+
+ it('Scoped names take precedence over block helpers', function () {
+ expectTemplate(
+ '{{#goodbye}} {{cruel world}}{{/goodbye}} {{this.goodbye}}'
+ )
+ .withHelper('goodbye', function (options) {
+ return this.goodbye.toUpperCase() + options.fn(this);
+ })
+ .withHelper('cruel', function (world) {
+ return 'cruel ' + world.toUpperCase();
+ })
+ .withInput({
+ goodbye: 'goodbye',
+ world: 'world',
+ })
+ .withMessage('Helper executed')
+ .toCompileTo('GOODBYE cruel WORLD goodbye');
+ });
+ });
+
+ describe('block params', function () {
+ it('should take presedence over context values', function () {
+ expectTemplate('{{#goodbyes as |value|}}{{value}}{{/goodbyes}}{{value}}')
+ .withInput({ value: 'foo' })
+ .withHelper('goodbyes', function (options) {
+ expect(options.fn.blockParams).toBe(1);
+ return options.fn({ value: 'bar' }, { blockParams: [1, 2] });
+ })
+ .toCompileTo('1foo');
+ });
+
+ it('should take presedence over helper values', function () {
+ expectTemplate('{{#goodbyes as |value|}}{{value}}{{/goodbyes}}{{value}}')
+ .withHelper('value', function () {
+ return 'foo';
+ })
+ .withHelper('goodbyes', function (options) {
+ expect(options.fn.blockParams).toBe(1);
+ return options.fn({}, { blockParams: [1, 2] });
+ })
+ .toCompileTo('1foo');
+ });
+
+ it('should not take presedence over pathed values', function () {
+ expectTemplate(
+ '{{#goodbyes as |value|}}{{./value}}{{/goodbyes}}{{value}}'
+ )
+ .withInput({ value: 'bar' })
+ .withHelper('value', function () {
+ return 'foo';
+ })
+ .withHelper('goodbyes', function (options) {
+ expect(options.fn.blockParams).toBe(1);
+ return options.fn(this, { blockParams: [1, 2] });
+ })
+ .toCompileTo('barfoo');
+ });
+
+ it('should take presednece over parent block params', function () {
+ var value = 1;
+ expectTemplate(
+ '{{#goodbyes as |value|}}{{#goodbyes}}{{value}}{{#goodbyes as |value|}}{{value}}{{/goodbyes}}{{/goodbyes}}{{/goodbyes}}{{value}}'
+ )
+ .withInput({ value: 'foo' })
+ .withHelper('goodbyes', function (options) {
+ return options.fn(
+ { value: 'bar' },
+ {
+ blockParams:
+ options.fn.blockParams === 1 ? [value++, value++] : undefined,
+ }
+ );
+ })
+ .toCompileTo('13foo');
+ });
+
+ it('should allow block params on chained helpers', function () {
+ expectTemplate(
+ '{{#if bar}}{{else goodbyes as |value|}}{{value}}{{/if}}{{value}}'
+ )
+ .withInput({ value: 'foo' })
+ .withHelper('goodbyes', function (options) {
+ expect(options.fn.blockParams).toBe(1);
+ return options.fn({ value: 'bar' }, { blockParams: [1, 2] });
+ })
+ .toCompileTo('1foo');
+ });
+ });
+
+ describe('built-in helpers malformed arguments ', function () {
+ it('if helper - too few arguments', function () {
+ expectTemplate('{{#if}}{{/if}}').toThrow(
+ /#if requires exactly one argument/
+ );
+ });
+
+ it('if helper - too many arguments, string', function () {
+ expectTemplate('{{#if test "string"}}{{/if}}').toThrow(
+ /#if requires exactly one argument/
+ );
+ });
+
+ it('if helper - too many arguments, undefined', function () {
+ expectTemplate('{{#if test undefined}}{{/if}}').toThrow(
+ /#if requires exactly one argument/
+ );
+ });
+
+ it('if helper - too many arguments, null', function () {
+ expectTemplate('{{#if test null}}{{/if}}').toThrow(
+ /#if requires exactly one argument/
+ );
+ });
+
+ it('unless helper - too few arguments', function () {
+ expectTemplate('{{#unless}}{{/unless}}').toThrow(
+ /#unless requires exactly one argument/
+ );
+ });
+
+ it('unless helper - too many arguments', function () {
+ expectTemplate('{{#unless test null}}{{/unless}}').toThrow(
+ /#unless requires exactly one argument/
+ );
+ });
+
+ it('with helper - too few arguments', function () {
+ expectTemplate('{{#with}}{{/with}}').toThrow(
+ /#with requires exactly one argument/
+ );
+ });
+
+ it('with helper - too many arguments', function () {
+ expectTemplate('{{#with test "string"}}{{/with}}').toThrow(
+ /#with requires exactly one argument/
+ );
+ });
+ });
+
+ describe('the lookupProperty-option', function () {
+ it('should be passed to custom helpers', function () {
+ expectTemplate('{{testHelper}}')
+ .withHelper('testHelper', function testHelper(options) {
+ return options.lookupProperty(this, 'testProperty');
+ })
+ .withInput({ testProperty: 'abc' })
+ .toCompileTo('abc');
+ });
+ });
+});
diff --git a/spec/index.html b/spec/index.html
new file mode 100644
index 000000000..69da838fc
--- /dev/null
+++ b/spec/index.html
@@ -0,0 +1,55 @@
+
+
+
+ Handlebars UMD Smoke Test
+
+
+
+
+
Handlebars UMD Smoke Test
+
+
+
+
+
diff --git a/spec/javascript-compiler.js b/spec/javascript-compiler.js
new file mode 100644
index 000000000..1b0920458
--- /dev/null
+++ b/spec/javascript-compiler.js
@@ -0,0 +1,124 @@
+describe('javascript-compiler api', function () {
+ if (!Handlebars.JavaScriptCompiler) {
+ return;
+ }
+
+ describe('#nameLookup', function () {
+ var $superName;
+ beforeEach(function () {
+ $superName = handlebarsEnv.JavaScriptCompiler.prototype.nameLookup;
+ });
+ afterEach(function () {
+ handlebarsEnv.JavaScriptCompiler.prototype.nameLookup = $superName;
+ });
+
+ it('should allow override', function () {
+ handlebarsEnv.JavaScriptCompiler.prototype.nameLookup = function (
+ parent,
+ name
+ ) {
+ return parent + '.bar_' + name;
+ };
+ expectTemplate('{{foo}}')
+ .withInput({ bar_foo: 'food' })
+ .toCompileTo('food');
+ });
+
+ // Tests nameLookup dot vs. bracket behavior. Bracket is required in certain cases
+ // to avoid errors in older browsers.
+ it('should handle reserved words', function () {
+ expectTemplate('{{foo}} {{~null~}}')
+ .withInput({ foo: 'food' })
+ .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 () {
+ var $superCheck, $superInfo;
+ beforeEach(function () {
+ $superCheck = handlebarsEnv.VM.checkRevision;
+ $superInfo = handlebarsEnv.JavaScriptCompiler.prototype.compilerInfo;
+ });
+ afterEach(function () {
+ handlebarsEnv.VM.checkRevision = $superCheck;
+ handlebarsEnv.JavaScriptCompiler.prototype.compilerInfo = $superInfo;
+ });
+ it('should allow compilerInfo override', function () {
+ handlebarsEnv.JavaScriptCompiler.prototype.compilerInfo = function () {
+ return 'crazy';
+ };
+ handlebarsEnv.VM.checkRevision = function (compilerInfo) {
+ if (compilerInfo !== 'crazy') {
+ throw new Error("It didn't work");
+ }
+ };
+ expectTemplate('{{foo}} ')
+ .withInput({ foo: 'food' })
+ .toCompileTo('food ');
+ });
+ }
+ );
+ describe('buffer', function () {
+ var $superAppend, $superCreate;
+ beforeEach(function () {
+ handlebarsEnv.JavaScriptCompiler.prototype.forceBuffer = true;
+ $superAppend = handlebarsEnv.JavaScriptCompiler.prototype.appendToBuffer;
+ $superCreate =
+ handlebarsEnv.JavaScriptCompiler.prototype.initializeBuffer;
+ });
+ afterEach(function () {
+ handlebarsEnv.JavaScriptCompiler.prototype.forceBuffer = false;
+ handlebarsEnv.JavaScriptCompiler.prototype.appendToBuffer = $superAppend;
+ handlebarsEnv.JavaScriptCompiler.prototype.initializeBuffer =
+ $superCreate;
+ });
+
+ it('should allow init buffer override', function () {
+ handlebarsEnv.JavaScriptCompiler.prototype.initializeBuffer =
+ function () {
+ return this.quotedString('foo_');
+ };
+ expectTemplate('{{foo}} ')
+ .withInput({ foo: 'food' })
+ .toCompileTo('foo_food ');
+ });
+ it('should allow append buffer override', function () {
+ handlebarsEnv.JavaScriptCompiler.prototype.appendToBuffer = function (
+ string
+ ) {
+ return $superAppend.call(this, [string, ' + "_foo"']);
+ };
+ expectTemplate('{{foo}}')
+ .withInput({ foo: 'food' })
+ .toCompileTo('food_foo');
+ });
+ });
+
+ describe('#isValidJavaScriptVariableName', function () {
+ // It is there and accessible and could be used by someone. That's why we don't remove it
+ // it 4.x. But if we keep it, we add a test
+ // This test should not encourage you to use the function. It is not needed any more
+ // and might be removed in 5.0
+ ['test', 'abc123', 'abc_123'].forEach(function (validVariableName) {
+ it("should return true for '" + validVariableName + "'", function () {
+ expect(
+ handlebarsEnv.JavaScriptCompiler.isValidJavaScriptVariableName(
+ validVariableName
+ )
+ ).toBe(true);
+ });
+ });
+ [('123test', 'abc()', 'abc.cde')].forEach(function (invalidVariableName) {
+ it("should return true for '" + invalidVariableName + "'", function () {
+ expect(
+ handlebarsEnv.JavaScriptCompiler.isValidJavaScriptVariableName(
+ invalidVariableName
+ )
+ ).toBe(false);
+ });
+ });
+ });
+});
diff --git a/spec/mustache b/spec/mustache
new file mode 160000
index 000000000..83b072161
--- /dev/null
+++ b/spec/mustache
@@ -0,0 +1 @@
+Subproject commit 83b0721610a4e11832e83df19c73ace3289972b9
diff --git a/spec/parser_spec.rb b/spec/parser_spec.rb
deleted file mode 100644
index 8dd13a415..000000000
--- a/spec/parser_spec.rb
+++ /dev/null
@@ -1,262 +0,0 @@
-require "spec_helper"
-
-describe "Parser" do
- let(:handlebars) { @context["Handlebars"] }
-
- before(:all) do
- @compiles = true
- end
-
- def root(&block)
- ASTBuilder.build do
- instance_eval(&block)
- end
- end
-
- def ast_for(string)
- ast = handlebars.parse(string)
- handlebars.print(ast)
- end
-
- class ASTBuilder
- def self.build(&block)
- ret = new
- ret.evaluate(&block)
- ret.out
- end
-
- attr_reader :out
-
- def initialize
- @padding = 0
- @out = ""
- end
-
- def evaluate(&block)
- instance_eval(&block)
- end
-
- def pad(string)
- @out << (" " * @padding) + string + "\n"
- end
-
- def with_padding
- @padding += 1
- ret = yield
- @padding -= 1
- ret
- end
-
- def program
- pad("PROGRAM:")
- with_padding { yield }
- end
-
- def inverse
- pad("{{^}}")
- with_padding { yield }
- end
-
- def block
- pad("BLOCK:")
- with_padding { yield }
- end
-
- def inverted_block
- pad("INVERSE:")
- with_padding { yield }
- end
-
- def mustache(id, params = [], hash = nil)
- hash = " #{hash}" if hash
- pad("{{ #{id} [#{params.join(", ")}]#{hash} }}")
- end
-
- def partial(id, context = nil)
- content = id.dup
- content << " #{context}" if context
- pad("{{> #{content} }}")
- end
-
- def comment(comment)
- pad("{{! '#{comment}' }}")
- end
-
- def multiline_comment(comment)
- pad("{{! '\n#{comment}\n' }}")
- end
-
- def content(string)
- pad("CONTENT[ '#{string}' ]")
- end
-
- def string(string)
- string.inspect
- end
-
- def integer(string)
- "INTEGER{#{string}}"
- end
-
- def boolean(string)
- "BOOLEAN{#{string}}"
- end
-
- def hash(*pairs)
- "HASH{" + pairs.map {|k,v| "#{k}=#{v}" }.join(", ") + "}"
- end
-
- def id(id)
- "ID:#{id}"
- end
-
- def path(*parts)
- "PATH:#{parts.join("/")}"
- end
- end
-
- it "parses simple mustaches" do
- ast_for("{{foo}}").should == root { mustache id("foo") }
- end
-
- it "parses mustaches with paths" do
- ast_for("{{foo/bar}}").should == root { mustache path("foo", "bar") }
- end
-
- it "parses mustaches with this/foo" do
- ast_for("{{this/foo}}").should == root { mustache id("foo") }
- end
-
- it "parses mustaches with - in a path" do
- ast_for("{{foo-bar}}").should == root { mustache id("foo-bar") }
- end
-
- it "parses mustaches with parameters" do
- ast_for("{{foo bar}}").should == root { mustache id("foo"), [id("bar")] }
- end
-
- it "parses mustaches with hash arguments" do
- ast_for("{{foo bar=baz}}").should == root do
- mustache id("foo"), [], hash(["bar", id("baz")])
- end
-
- ast_for("{{foo bar=1}}").should == root do
- mustache id("foo"), [], hash(["bar", integer("1")])
- end
-
- ast_for("{{foo bar=true}}").should == root do
- mustache id("foo"), [], hash(["bar", boolean("true")])
- end
-
- ast_for("{{foo bar=false}}").should == root do
- mustache id("foo"), [], hash(["bar", boolean("false")])
- end
-
- ast_for("{{foo bar=baz bat=bam}}").should == root do
- mustache id("foo"), [], hash(["bar", "ID:baz"], ["bat", "ID:bam"])
- end
-
- ast_for("{{foo bar=baz bat=\"bam\"}}").should == root do
- mustache id("foo"), [], hash(["bar", "ID:baz"], ["bat", "\"bam\""])
- end
-
- ast_for("{{foo omg bar=baz bat=\"bam\"}}").should == root do
- mustache id("foo"), [id("omg")], hash(["bar", id("baz")], ["bat", string("bam")])
- end
-
- ast_for("{{foo omg bar=baz bat=\"bam\" baz=1}}").should == root do
- mustache id("foo"), [id("omg")], hash(["bar", id("baz")], ["bat", string("bam")], ["baz", integer("1")])
- end
-
- ast_for("{{foo omg bar=baz bat=\"bam\" baz=true}}").should == root do
- mustache id("foo"), [id("omg")], hash(["bar", id("baz")], ["bat", string("bam")], ["baz", boolean("true")])
- end
-
- ast_for("{{foo omg bar=baz bat=\"bam\" baz=false}}").should == root do
- mustache id("foo"), [id("omg")], hash(["bar", id("baz")], ["bat", string("bam")], ["baz", boolean("false")])
- end
- end
-
- it "parses mustaches with string parameters" do
- ast_for("{{foo bar \"baz\" }}").should == root { mustache id("foo"), [id("bar"), string("baz")] }
- end
-
- it "parses mustaches with INTEGER parameters" do
- ast_for("{{foo 1}}").should == root { mustache id("foo"), [integer("1")] }
- end
-
- it "parses mustaches with BOOLEAN parameters" do
- ast_for("{{foo true}}").should == root { mustache id("foo"), [boolean("true")] }
- ast_for("{{foo false}}").should == root { mustache id("foo"), [boolean("false")] }
- end
-
- it "parses contents followed by a mustache" do
- ast_for("foo bar {{baz}}").should == root do
- content "foo bar "
- mustache id("baz")
- end
- end
-
- it "parses a partial" do
- ast_for("{{> foo }}").should == root { partial id("foo") }
- end
-
- it "parses a partial with context" do
- ast_for("{{> foo bar}}").should == root { partial id("foo"), id("bar") }
- end
-
- it "parses a comment" do
- ast_for("{{! this is a comment }}").should == root do
- comment " this is a comment "
- end
- end
-
- it "parses a multi-line comment" do
- ast_for("{{!\nthis is a multi-line comment\n}}").should == root do
- multiline_comment "this is a multi-line comment"
- end
- end
-
- it "parses an inverse section" do
- ast_for("{{#foo}} bar {{^}} baz {{/foo}}").should == root do
- block do
- mustache id("foo")
-
- program do
- content " bar "
- end
-
- inverse do
- content " baz "
- end
- end
- end
- end
-
- it "parses a standalone inverse section" do
- ast_for("{{^foo}}bar{{/foo}}").should == root do
- block do
- mustache id("foo")
-
- inverse do
- content "bar"
- end
- end
- end
- end
-
- it "raises if there's a Parse error" do
- lambda { ast_for("{{foo}") }.should raise_error(V8::JSError, /Parse error on line 1/)
- lambda { ast_for("{{foo &}}")}.should raise_error(V8::JSError, /Parse error on line 1/)
- lambda { ast_for("{{#goodbyes}}{{/hellos}}") }.should raise_error(V8::JSError, /goodbyes doesn't match hellos/)
- end
-
- it "knows how to report the correct line number in errors" do
- lambda { ast_for("hello\nmy\n{{foo}") }.should raise_error(V8::JSError, /Parse error on line 3/m)
- lambda { ast_for("hello\n\nmy\n\n{{foo}") }.should raise_error(V8::JSError, /Parse error on line 5/m)
- end
-
- it "knows how to report the correct line number in errors when the first character is a newline" do
- lambda { ast_for("\n\nhello\n\nmy\n\n{{foo}") }.should raise_error(V8::JSError, /Parse error on line 7/m)
- end
-end
diff --git a/spec/partials.js b/spec/partials.js
new file mode 100644
index 000000000..cda837131
--- /dev/null
+++ b/spec/partials.js
@@ -0,0 +1,677 @@
+describe('partials', function () {
+ it('basic partials', function () {
+ var string = 'Dudes: {{#dudes}}{{> dude}}{{/dudes}}';
+ var partial = '{{name}} ({{url}}) ';
+ var hash = {
+ dudes: [
+ { name: 'Yehuda', url: 'http://yehuda' },
+ { name: 'Alan', url: 'http://alan' },
+ ],
+ };
+
+ expectTemplate(string)
+ .withInput(hash)
+ .withPartials({ dude: partial })
+ .toCompileTo('Dudes: Yehuda (http://yehuda) Alan (http://alan) ');
+
+ expectTemplate(string)
+ .withInput(hash)
+ .withPartials({ dude: partial })
+ .withRuntimeOptions({ data: false })
+ .withCompileOptions({ data: false })
+ .toCompileTo('Dudes: Yehuda (http://yehuda) Alan (http://alan) ');
+ });
+
+ it('dynamic partials', function () {
+ var string = 'Dudes: {{#dudes}}{{> (partial)}}{{/dudes}}';
+ var partial = '{{name}} ({{url}}) ';
+ var hash = {
+ dudes: [
+ { name: 'Yehuda', url: 'http://yehuda' },
+ { name: 'Alan', url: 'http://alan' },
+ ],
+ };
+ var helpers = {
+ partial: function () {
+ return 'dude';
+ },
+ };
+
+ expectTemplate(string)
+ .withInput(hash)
+ .withHelpers(helpers)
+ .withPartials({ dude: partial })
+ .toCompileTo('Dudes: Yehuda (http://yehuda) Alan (http://alan) ');
+
+ expectTemplate(string)
+ .withInput(hash)
+ .withHelpers(helpers)
+ .withPartials({ dude: partial })
+ .withRuntimeOptions({ data: false })
+ .withCompileOptions({ data: false })
+ .toCompileTo('Dudes: Yehuda (http://yehuda) Alan (http://alan) ');
+ });
+
+ it('failing dynamic partials', function () {
+ expectTemplate('Dudes: {{#dudes}}{{> (partial)}}{{/dudes}}')
+ .withInput({
+ dudes: [
+ { name: 'Yehuda', url: 'http://yehuda' },
+ { name: 'Alan', url: 'http://alan' },
+ ],
+ })
+ .withHelper('partial', function () {
+ return 'missing';
+ })
+ .withPartial('dude', '{{name}} ({{url}}) ')
+ .toThrow(
+ Handlebars.Exception,
+ 'The partial "missing" could not be found'
+ );
+ });
+
+ it('partials with context', function () {
+ expectTemplate('Dudes: {{>dude dudes}}')
+ .withInput({
+ dudes: [
+ { name: 'Yehuda', url: 'http://yehuda' },
+ { name: 'Alan', url: 'http://alan' },
+ ],
+ })
+ .withPartial('dude', '{{#this}}{{name}} ({{url}}) {{/this}}')
+ .withMessage('Partials can be passed a context')
+ .toCompileTo('Dudes: Yehuda (http://yehuda) Alan (http://alan) ');
+ });
+
+ it('partials with no context', function () {
+ var partial = '{{name}} ({{url}}) ';
+ var hash = {
+ dudes: [
+ { name: 'Yehuda', url: 'http://yehuda' },
+ { name: 'Alan', url: 'http://alan' },
+ ],
+ };
+
+ expectTemplate('Dudes: {{#dudes}}{{>dude}}{{/dudes}}')
+ .withInput(hash)
+ .withPartial('dude', partial)
+ .withCompileOptions({ explicitPartialContext: true })
+ .toCompileTo('Dudes: () () ');
+
+ expectTemplate('Dudes: {{#dudes}}{{>dude name="foo"}}{{/dudes}}')
+ .withInput(hash)
+ .withPartial('dude', partial)
+ .withCompileOptions({ explicitPartialContext: true })
+ .toCompileTo('Dudes: foo () foo () ');
+ });
+
+ it('partials with string context', function () {
+ expectTemplate('Dudes: {{>dude "dudes"}}')
+ .withPartial('dude', '{{.}}')
+ .toCompileTo('Dudes: dudes');
+ });
+
+ it('partials with undefined context', function () {
+ expectTemplate('Dudes: {{>dude dudes}}')
+ .withPartial('dude', '{{foo}} Empty')
+ .toCompileTo('Dudes: Empty');
+ });
+
+ it('partials with duplicate parameters', function () {
+ expectTemplate('Dudes: {{>dude dudes foo bar=baz}}').toThrow(
+ Error,
+ 'Unsupported number of partial arguments: 2 - 1:7'
+ );
+ });
+
+ it('partials with parameters', function () {
+ expectTemplate('Dudes: {{#dudes}}{{> dude others=..}}{{/dudes}}')
+ .withInput({
+ foo: 'bar',
+ dudes: [
+ { name: 'Yehuda', url: 'http://yehuda' },
+ { name: 'Alan', url: 'http://alan' },
+ ],
+ })
+ .withPartial('dude', '{{others.foo}}{{name}} ({{url}}) ')
+ .withMessage('Basic partials output based on current context.')
+ .toCompileTo('Dudes: barYehuda (http://yehuda) barAlan (http://alan) ');
+ });
+
+ it('partial in a partial', function () {
+ expectTemplate('Dudes: {{#dudes}}{{>dude}}{{/dudes}}')
+ .withInput({
+ dudes: [
+ { name: 'Yehuda', url: 'http://yehuda' },
+ { name: 'Alan', url: 'http://alan' },
+ ],
+ })
+ .withPartials({
+ dude: '{{name}} {{> url}} ',
+ url: '{{url}}',
+ })
+ .withMessage('Partials are rendered inside of other partials')
+ .toCompileTo(
+ 'Dudes: Yehuda http://yehuda Alan http://alan '
+ );
+ });
+
+ it('rendering undefined partial throws an exception', function () {
+ expectTemplate('{{> whatever}}').toThrow(
+ Handlebars.Exception,
+ 'The partial "whatever" could not be found'
+ );
+ });
+
+ it('registering undefined partial throws an exception', function () {
+ expect(function () {
+ handlebarsEnv.registerPartial('undefined_test', undefined);
+ }).toThrow(
+ 'Attempting to register a partial called "undefined_test" as undefined'
+ );
+ });
+
+ it('rendering template partial in vm mode throws an exception', function () {
+ expectTemplate('{{> whatever}}').toThrow(
+ Handlebars.Exception,
+ 'The partial "whatever" could not be found'
+ );
+ });
+
+ it('rendering function partial in vm mode', function () {
+ function partial(context) {
+ return context.name + ' (' + context.url + ') ';
+ }
+ expectTemplate('Dudes: {{#dudes}}{{> dude}}{{/dudes}}')
+ .withInput({
+ dudes: [
+ { name: 'Yehuda', url: 'http://yehuda' },
+ { name: 'Alan', url: 'http://alan' },
+ ],
+ })
+ .withPartial('dude', partial)
+ .withMessage('Function partials output based in VM.')
+ .toCompileTo('Dudes: Yehuda (http://yehuda) Alan (http://alan) ');
+ });
+
+ it('GH-14: a partial preceding a selector', function () {
+ expectTemplate('Dudes: {{>dude}} {{anotherDude}}')
+ .withInput({ name: 'Jeepers', anotherDude: 'Creepers' })
+ .withPartial('dude', '{{name}}')
+ .withMessage('Regular selectors can follow a partial')
+ .toCompileTo('Dudes: Jeepers Creepers');
+ });
+
+ it('Partials with slash paths', function () {
+ expectTemplate('Dudes: {{> shared/dude}}')
+ .withInput({ name: 'Jeepers', anotherDude: 'Creepers' })
+ .withPartial('shared/dude', '{{name}}')
+ .withMessage('Partials can use literal paths')
+ .toCompileTo('Dudes: Jeepers');
+ });
+
+ it('Partials with slash and point paths', function () {
+ expectTemplate('Dudes: {{> shared/dude.thing}}')
+ .withInput({ name: 'Jeepers', anotherDude: 'Creepers' })
+ .withPartial('shared/dude.thing', '{{name}}')
+ .withMessage('Partials can use literal with points in paths')
+ .toCompileTo('Dudes: Jeepers');
+ });
+
+ it('Global Partials', function () {
+ handlebarsEnv.registerPartial('globalTest', '{{anotherDude}}');
+
+ expectTemplate('Dudes: {{> shared/dude}} {{> globalTest}}')
+ .withInput({ name: 'Jeepers', anotherDude: 'Creepers' })
+ .withPartial('shared/dude', '{{name}}')
+ .withMessage('Partials can use globals or passed')
+ .toCompileTo('Dudes: Jeepers Creepers');
+
+ handlebarsEnv.unregisterPartial('globalTest');
+ expect(handlebarsEnv.partials.globalTest).toBeUndefined();
+ });
+
+ it('Multiple partial registration', function () {
+ handlebarsEnv.registerPartial({
+ 'shared/dude': '{{name}}',
+ globalTest: '{{anotherDude}}',
+ });
+
+ expectTemplate('Dudes: {{> shared/dude}} {{> globalTest}}')
+ .withInput({ name: 'Jeepers', anotherDude: 'Creepers' })
+ .withPartial('notused', 'notused') // trick the test bench into running with partials enabled
+ .withMessage('Partials can use globals or passed')
+ .toCompileTo('Dudes: Jeepers Creepers');
+ });
+
+ it('Partials with integer path', function () {
+ expectTemplate('Dudes: {{> 404}}')
+ .withInput({ name: 'Jeepers', anotherDude: 'Creepers' })
+ .withPartial(404, '{{name}}')
+ .withMessage('Partials can use literal paths')
+ .toCompileTo('Dudes: Jeepers');
+ });
+
+ it('Partials with complex path', function () {
+ expectTemplate('Dudes: {{> 404/asdf?.bar}}')
+ .withInput({ name: 'Jeepers', anotherDude: 'Creepers' })
+ .withPartial('404/asdf?.bar', '{{name}}')
+ .withMessage('Partials can use literal paths')
+ .toCompileTo('Dudes: Jeepers');
+ });
+
+ it('Partials with escaped', function () {
+ expectTemplate('Dudes: {{> [+404/asdf?.bar]}}')
+ .withInput({ name: 'Jeepers', anotherDude: 'Creepers' })
+ .withPartial('+404/asdf?.bar', '{{name}}')
+ .withMessage('Partials can use literal paths')
+ .toCompileTo('Dudes: Jeepers');
+ });
+
+ it('Partials with string', function () {
+ expectTemplate("Dudes: {{> '+404/asdf?.bar'}}")
+ .withInput({ name: 'Jeepers', anotherDude: 'Creepers' })
+ .withPartial('+404/asdf?.bar', '{{name}}')
+ .withMessage('Partials can use literal paths')
+ .toCompileTo('Dudes: Jeepers');
+ });
+
+ it('should handle empty partial', function () {
+ expectTemplate('Dudes: {{#dudes}}{{> dude}}{{/dudes}}')
+ .withInput({
+ dudes: [
+ { name: 'Yehuda', url: 'http://yehuda' },
+ { name: 'Alan', url: 'http://alan' },
+ ],
+ })
+ .withPartial('dude', '')
+ .toCompileTo('Dudes: ');
+ });
+
+ it('throw on missing partial', function () {
+ var compile = handlebarsEnv.compile;
+ var compileWithPartial = CompilerContext.compileWithPartial;
+ handlebarsEnv.compile = undefined;
+ CompilerContext.compileWithPartial = CompilerContext.compile;
+ expectTemplate('{{> dude}}')
+ .withPartials({ dude: 'fail' })
+ .toThrow(Error, /The partial dude could not be compiled/);
+ handlebarsEnv.compile = compile;
+ CompilerContext.compileWithPartial = compileWithPartial;
+ });
+
+ describe('partial blocks', function () {
+ it('should render partial block as default', function () {
+ expectTemplate('{{#> dude}}success{{/dude}}').toCompileTo('success');
+ });
+
+ it('should execute default block with proper context', function () {
+ expectTemplate('{{#> dude context}}{{value}}{{/dude}}')
+ .withInput({ context: { value: 'success' } })
+ .toCompileTo('success');
+ });
+
+ it('should propagate block parameters to default block', function () {
+ expectTemplate(
+ '{{#with context as |me|}}{{#> dude}}{{me.value}}{{/dude}}{{/with}}'
+ )
+ .withInput({ context: { value: 'success' } })
+ .toCompileTo('success');
+ });
+
+ it('should not use partial block if partial exists', function () {
+ expectTemplate('{{#> dude}}fail{{/dude}}')
+ .withPartials({ dude: 'success' })
+ .toCompileTo('success');
+ });
+
+ it('should render block from partial', function () {
+ expectTemplate('{{#> dude}}success{{/dude}}')
+ .withPartials({ dude: '{{> @partial-block }}' })
+ .toCompileTo('success');
+ });
+
+ it('should be able to render the partial-block twice', function () {
+ expectTemplate('{{#> dude}}success{{/dude}}')
+ .withPartials({ dude: '{{> @partial-block }} {{> @partial-block }}' })
+ .toCompileTo('success success');
+ });
+
+ it('should render block from partial with context', function () {
+ expectTemplate('{{#> dude}}{{value}}{{/dude}}')
+ .withInput({ context: { value: 'success' } })
+ .withPartials({
+ dude: '{{#with context}}{{> @partial-block }}{{/with}}',
+ })
+ .toCompileTo('success');
+ });
+
+ it('should be able to access the @data frame from a partial-block', function () {
+ expectTemplate('{{#> dude}}in-block: {{@root/value}}{{/dude}}')
+ .withInput({ value: 'success' })
+ .withPartials({
+ dude: 'before-block: {{@root/value}} {{> @partial-block }}',
+ })
+ .toCompileTo('before-block: success in-block: success');
+ });
+
+ it('should allow the #each-helper to be used along with partial-blocks', function () {
+ expectTemplate(
+ '{{#> list value}}value = {{.}}{{/list}}'
+ )
+ .withInput({
+ value: ['a', 'b', 'c'],
+ })
+ .withPartials({
+ list: '{{#each .}}{{> @partial-block}}{{/each}}',
+ })
+ .toCompileTo(
+ 'value = avalue = bvalue = c'
+ );
+ });
+
+ it('should render block from partial with context (twice)', function () {
+ expectTemplate('{{#> dude}}{{value}}{{/dude}}')
+ .withInput({ context: { value: 'success' } })
+ .withPartials({
+ dude: '{{#with context}}{{> @partial-block }} {{> @partial-block }}{{/with}}',
+ })
+ .toCompileTo('success success');
+ });
+
+ it('should render block from partial with context', function () {
+ expectTemplate('{{#> dude}}{{../context/value}}{{/dude}}')
+ .withInput({ context: { value: 'success' } })
+ .withPartials({
+ dude: '{{#with context}}{{> @partial-block }}{{/with}}',
+ })
+ .toCompileTo('success');
+ });
+
+ it('should render block from partial with block params', function () {
+ expectTemplate(
+ '{{#with context as |me|}}{{#> dude}}{{me.value}}{{/dude}}{{/with}}'
+ )
+ .withInput({ context: { value: 'success' } })
+ .withPartials({ dude: '{{> @partial-block }}' })
+ .toCompileTo('success');
+ });
+
+ it('should render nested partial blocks', function () {
+ expectTemplate('{{#> outer}}{{value}}{{/outer}}')
+ .withInput({ value: 'success' })
+ .withPartials({
+ outer:
+ '{{#> nested}}{{> @partial-block}}{{/nested}}',
+ nested: '{{> @partial-block}}',
+ })
+ .toCompileTo(
+ 'success'
+ );
+ });
+
+ it('should render nested partial blocks at different nesting levels', function () {
+ expectTemplate('{{#> outer}}{{value}}{{/outer}}')
+ .withInput({ value: 'success' })
+ .withPartials({
+ outer:
+ '{{#> nested}}{{> @partial-block}}{{/nested}}{{> @partial-block}}',
+ nested: '{{> @partial-block}}',
+ })
+ .toCompileTo(
+ 'successsuccess'
+ );
+ });
+
+ it('should render nested partial blocks at different nesting levels (twice)', function () {
+ expectTemplate('{{#> outer}}{{value}}{{/outer}}')
+ .withInput({ value: 'success' })
+ .withPartials({
+ outer:
+ '{{#> nested}}{{> @partial-block}} {{> @partial-block}}{{/nested}}{{> @partial-block}}+{{> @partial-block}}',
+ nested: '{{> @partial-block}}',
+ })
+ .toCompileTo(
+ 'success successsuccess+success'
+ );
+ });
+
+ it('should render nested partial blocks (twice at each level)', function () {
+ expectTemplate('{{#> outer}}{{value}}{{/outer}}')
+ .withInput({ value: 'success' })
+ .withPartials({
+ outer:
+ '{{#> nested}}{{> @partial-block}} {{> @partial-block}}{{/nested}}',
+ nested: '{{> @partial-block}}{{> @partial-block}}',
+ })
+ .toCompileTo(
+ '' +
+ 'success successsuccess success' +
+ ''
+ );
+ });
+ });
+
+ describe('inline partials', function () {
+ it('should define inline partials for template', function () {
+ expectTemplate(
+ '{{#*inline "myPartial"}}success{{/inline}}{{> myPartial}}'
+ ).toCompileTo('success');
+ });
+
+ it('should overwrite multiple partials in the same template', function () {
+ expectTemplate(
+ '{{#*inline "myPartial"}}fail{{/inline}}{{#*inline "myPartial"}}success{{/inline}}{{> myPartial}}'
+ ).toCompileTo('success');
+ });
+
+ it('should define inline partials for block', function () {
+ expectTemplate(
+ '{{#with .}}{{#*inline "myPartial"}}success{{/inline}}{{> myPartial}}{{/with}}'
+ ).toCompileTo('success');
+
+ expectTemplate(
+ '{{#with .}}{{#*inline "myPartial"}}success{{/inline}}{{/with}}{{> myPartial}}'
+ ).toThrow(Error, /"myPartial" could not/);
+ });
+
+ it('should override global partials', function () {
+ expectTemplate(
+ '{{#*inline "myPartial"}}success{{/inline}}{{> myPartial}}'
+ )
+ .withPartials({
+ myPartial: function () {
+ return 'fail';
+ },
+ })
+ .toCompileTo('success');
+ });
+
+ it('should override template partials', function () {
+ expectTemplate(
+ '{{#*inline "myPartial"}}fail{{/inline}}{{#with .}}{{#*inline "myPartial"}}success{{/inline}}{{> myPartial}}{{/with}}'
+ ).toCompileTo('success');
+ });
+
+ it('should override partials down the entire stack', function () {
+ expectTemplate(
+ '{{#with .}}{{#*inline "myPartial"}}success{{/inline}}{{#with .}}{{#with .}}{{> myPartial}}{{/with}}{{/with}}{{/with}}'
+ ).toCompileTo('success');
+ });
+
+ it('should define inline partials for partial call', function () {
+ expectTemplate('{{#*inline "myPartial"}}success{{/inline}}{{> dude}}')
+ .withPartials({ dude: '{{> myPartial }}' })
+ .toCompileTo('success');
+ });
+
+ it('should define inline partials in partial block call', function () {
+ expectTemplate(
+ '{{#> dude}}{{#*inline "myPartial"}}success{{/inline}}{{/dude}}'
+ )
+ .withPartials({ dude: '{{> myPartial }}' })
+ .toCompileTo('success');
+ });
+
+ it('should render nested inline partials', function () {
+ expectTemplate(
+ '{{#*inline "outer"}}{{#>inner}}{{>@partial-block}}{{/inner}}{{/inline}}' +
+ '{{#*inline "inner"}}{{>@partial-block}}{{/inline}}' +
+ '{{#>outer}}{{value}}{{/outer}}'
+ )
+ .withInput({ value: 'success' })
+ .toCompileTo('success');
+ });
+
+ it('should render nested inline partials with partial-blocks on different nesting levels', function () {
+ expectTemplate(
+ '{{#*inline "outer"}}{{#>inner}}{{>@partial-block}}{{/inner}}{{>@partial-block}}{{/inline}}' +
+ '{{#*inline "inner"}}{{>@partial-block}}{{/inline}}' +
+ '{{#>outer}}{{value}}{{/outer}}'
+ )
+ .withInput({ value: 'success' })
+ .toCompileTo(
+ 'successsuccess'
+ );
+ });
+
+ it('should render nested inline partials (twice at each level)', function () {
+ expectTemplate(
+ '{{#*inline "outer"}}{{#>inner}}{{>@partial-block}} {{>@partial-block}}{{/inner}}{{/inline}}' +
+ '{{#*inline "inner"}}{{>@partial-block}}{{>@partial-block}}{{/inline}}' +
+ '{{#>outer}}{{value}}{{/outer}}'
+ )
+ .withInput({ value: 'success' })
+ .toCompileTo(
+ 'success successsuccess success'
+ );
+ });
+ });
+
+ it('should pass compiler flags', function () {
+ if (Handlebars.compile) {
+ var env = Handlebars.create();
+ env.registerPartial('partial', '{{foo}}');
+ var template = env.compile('{{foo}} {{> partial}}', { noEscape: true });
+ expect(template({ foo: '<' })).toBe('< <');
+ }
+ });
+
+ describe('standalone partials', function () {
+ it('indented partials', function () {
+ expectTemplate('Dudes:\n{{#dudes}}\n {{>dude}}\n{{/dudes}}')
+ .withInput({
+ dudes: [
+ { name: 'Yehuda', url: 'http://yehuda' },
+ { name: 'Alan', url: 'http://alan' },
+ ],
+ })
+ .withPartial('dude', '{{name}}\n')
+ .toCompileTo('Dudes:\n Yehuda\n Alan\n');
+ });
+
+ it('nested indented partials', function () {
+ expectTemplate('Dudes:\n{{#dudes}}\n {{>dude}}\n{{/dudes}}')
+ .withInput({
+ dudes: [
+ { name: 'Yehuda', url: 'http://yehuda' },
+ { name: 'Alan', url: 'http://alan' },
+ ],
+ })
+ .withPartials({
+ dude: '{{name}}\n {{> url}}',
+ url: '{{url}}!\n',
+ })
+ .toCompileTo(
+ 'Dudes:\n Yehuda\n http://yehuda!\n Alan\n http://alan!\n'
+ );
+ });
+
+ it('prevent nested indented partials', function () {
+ expectTemplate('Dudes:\n{{#dudes}}\n {{>dude}}\n{{/dudes}}')
+ .withInput({
+ dudes: [
+ { name: 'Yehuda', url: 'http://yehuda' },
+ { name: 'Alan', url: 'http://alan' },
+ ],
+ })
+ .withPartials({
+ dude: '{{name}}\n {{> url}}',
+ url: '{{url}}!\n',
+ })
+ .withCompileOptions({ preventIndent: true })
+ .toCompileTo(
+ 'Dudes:\n Yehuda\n http://yehuda!\n Alan\n http://alan!\n'
+ );
+ });
+ });
+
+ describe('compat mode', function () {
+ it('partials can access parents', function () {
+ expectTemplate('Dudes: {{#dudes}}{{> dude}}{{/dudes}}')
+ .withInput({
+ root: 'yes',
+ dudes: [
+ { name: 'Yehuda', url: 'http://yehuda' },
+ { name: 'Alan', url: 'http://alan' },
+ ],
+ })
+ .withPartials({ dude: '{{name}} ({{url}}) {{root}} ' })
+ .withCompileOptions({ compat: true })
+ .toCompileTo(
+ 'Dudes: Yehuda (http://yehuda) yes Alan (http://alan) yes '
+ );
+ });
+
+ it('partials can access parents with custom context', function () {
+ expectTemplate('Dudes: {{#dudes}}{{> dude "test"}}{{/dudes}}')
+ .withInput({
+ root: 'yes',
+ dudes: [
+ { name: 'Yehuda', url: 'http://yehuda' },
+ { name: 'Alan', url: 'http://alan' },
+ ],
+ })
+ .withPartials({ dude: '{{name}} ({{url}}) {{root}} ' })
+ .withCompileOptions({ compat: true })
+ .toCompileTo(
+ 'Dudes: Yehuda (http://yehuda) yes Alan (http://alan) yes '
+ );
+ });
+
+ it('partials can access parents without data', function () {
+ expectTemplate('Dudes: {{#dudes}}{{> dude}}{{/dudes}}')
+ .withInput({
+ root: 'yes',
+ dudes: [
+ { name: 'Yehuda', url: 'http://yehuda' },
+ { name: 'Alan', url: 'http://alan' },
+ ],
+ })
+ .withPartials({ dude: '{{name}} ({{url}}) {{root}} ' })
+ .withRuntimeOptions({ data: false })
+ .withCompileOptions({ data: false, compat: true })
+ .toCompileTo(
+ 'Dudes: Yehuda (http://yehuda) yes Alan (http://alan) yes '
+ );
+ });
+
+ it('partials inherit compat', function () {
+ expectTemplate('Dudes: {{> dude}}')
+ .withInput({
+ root: 'yes',
+ dudes: [
+ { name: 'Yehuda', url: 'http://yehuda' },
+ { name: 'Alan', url: 'http://alan' },
+ ],
+ })
+ .withPartials({
+ dude: '{{#dudes}}{{name}} ({{url}}) {{root}} {{/dudes}}',
+ })
+ .withCompileOptions({ compat: true })
+ .toCompileTo(
+ 'Dudes: Yehuda (http://yehuda) yes Alan (http://alan) yes '
+ );
+ });
+ });
+});
diff --git a/spec/precompiler.js b/spec/precompiler.js
new file mode 100644
index 000000000..ac4a290e1
--- /dev/null
+++ b/spec/precompiler.js
@@ -0,0 +1,394 @@
+/* eslint-disable no-console */
+describe('precompiler', function () {
+ // NOP Under non-node environments
+ if (typeof process === 'undefined') {
+ return;
+ }
+
+ var Handlebars = require('../lib'),
+ Precompiler = require('../dist/cjs/precompiler'),
+ fs = require('fs'),
+ uglify = require('uglify-js');
+
+ var log,
+ logFunction,
+ errorLog,
+ errorLogFunction,
+ precompile,
+ minify,
+ emptyTemplate = {
+ path: __dirname + '/artifacts/empty.handlebars',
+ name: 'empty',
+ source: '',
+ },
+ file,
+ content,
+ writeFileSync;
+
+ /**
+ * Mock the Module.prototype.require-function such that an error is thrown, when "uglify-js" is loaded.
+ *
+ * The function cleans up its mess when "callback" is finished
+ *
+ * @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) {
+ var Module = require('module');
+ var _resolveFilename = Module._resolveFilename;
+ delete require.cache[require.resolve('uglify-js')];
+ delete require.cache[require.resolve('../dist/cjs/precompiler')];
+ Module._resolveFilename = function (request, mod) {
+ if (request === 'uglify-js') {
+ throw loadError;
+ }
+ return _resolveFilename.call(this, request, mod);
+ };
+ try {
+ await callback();
+ } finally {
+ Module._resolveFilename = _resolveFilename;
+ delete require.cache[require.resolve('uglify-js')];
+ delete require.cache[require.resolve('../dist/cjs/precompiler')];
+ }
+ }
+
+ beforeEach(function () {
+ precompile = Handlebars.precompile;
+ minify = uglify.minify;
+ writeFileSync = fs.writeFileSync;
+
+ // Mock stdout and stderr
+ logFunction = console.log;
+ log = '';
+ console.log = function () {
+ log += Array.prototype.join.call(arguments, '');
+ };
+ errorLogFunction = console.error;
+ errorLog = '';
+ console.error = function () {
+ errorLog += Array.prototype.join.call(arguments, '');
+ };
+
+ fs.writeFileSync = function (_file, _content) {
+ file = _file;
+ content = _content;
+ };
+ });
+ afterEach(function () {
+ Handlebars.precompile = precompile;
+ uglify.minify = minify;
+ fs.writeFileSync = writeFileSync;
+ console.log = logFunction;
+ console.error = errorLogFunction;
+ });
+
+ it('should output version', async function () {
+ await Precompiler.cli({ templates: [], version: true });
+ expect(log).toBe(Handlebars.VERSION);
+ });
+ it('should throw if lacking templates', async function () {
+ await expect(Precompiler.cli({ templates: [] })).rejects.toThrow(
+ 'Must define at least one template or directory.'
+ );
+ });
+ it('should handle empty/filtered directories', async function () {
+ Handlebars.precompile = function () {
+ return 'simple';
+ };
+ await Precompiler.cli({ hasDirectory: true, templates: [] });
+ // Success is not throwing
+ });
+ it('should throw when combining simple and minimized', async function () {
+ await expect(
+ Precompiler.cli({ templates: [__dirname], simple: true, min: true })
+ ).rejects.toThrow('Unable to minimize simple output');
+ });
+ it('should throw when combining simple and multiple templates', async function () {
+ await expect(
+ Precompiler.cli({
+ templates: [
+ __dirname + '/artifacts/empty.handlebars',
+ __dirname + '/artifacts/empty.handlebars',
+ ],
+ simple: true,
+ })
+ ).rejects.toThrow('Unable to output multiple templates in simple mode');
+ });
+ it('should throw when missing name', async function () {
+ await expect(
+ Precompiler.cli({ templates: [{ source: '' }], amd: true })
+ ).rejects.toThrow('Name missing for template');
+ });
+ it('should throw when combining simple and directories', async function () {
+ await expect(
+ Precompiler.cli({ hasDirectory: true, templates: [1], simple: true })
+ ).rejects.toThrow('Unable to output multiple templates in simple mode');
+ });
+
+ it('should output simple templates', async function () {
+ Handlebars.precompile = function () {
+ return 'simple';
+ };
+ await Precompiler.cli({ templates: [emptyTemplate], simple: true });
+ expect(log).toBe('simple\n');
+ });
+ it('should default to simple templates', async function () {
+ Handlebars.precompile = function () {
+ return 'simple';
+ };
+ await Precompiler.cli({ templates: [{ source: '' }] });
+ expect(log).toBe('simple\n');
+ });
+ it('should output amd templates', async function () {
+ Handlebars.precompile = function () {
+ return 'amd';
+ };
+ await Precompiler.cli({ templates: [emptyTemplate], amd: true });
+ expect(log).toMatch(/template\(amd\)/);
+ });
+ it('should output multiple amd', async function () {
+ Handlebars.precompile = function () {
+ return 'amd';
+ };
+ await Precompiler.cli({
+ templates: [emptyTemplate, emptyTemplate],
+ amd: true,
+ namespace: 'foo',
+ });
+ expect(log).toMatch(/templates = foo = foo \|\|/);
+ expect(log).toMatch(/return templates/);
+ expect(log).toMatch(/template\(amd\)/);
+ });
+ it('should output amd partials', async function () {
+ Handlebars.precompile = function () {
+ return 'amd';
+ };
+ await Precompiler.cli({
+ templates: [emptyTemplate],
+ amd: true,
+ partial: true,
+ });
+ expect(log).toMatch(/return Handlebars\.partials\['empty'\]/);
+ expect(log).toMatch(/template\(amd\)/);
+ });
+ it('should output multiple amd partials', async function () {
+ Handlebars.precompile = function () {
+ return 'amd';
+ };
+ await Precompiler.cli({
+ templates: [emptyTemplate, emptyTemplate],
+ amd: true,
+ partial: true,
+ });
+ expect(log).not.toMatch(/return Handlebars\.partials\[/);
+ expect(log).toMatch(/template\(amd\)/);
+ });
+ it('should output commonjs templates', async function () {
+ Handlebars.precompile = function () {
+ return 'commonjs';
+ };
+ await Precompiler.cli({ templates: [emptyTemplate], commonjs: true });
+ expect(log).toMatch(/template\(commonjs\)/);
+ });
+
+ it('should set data flag', async function () {
+ Handlebars.precompile = function (data, options) {
+ expect(options.data).toBe(true);
+ return 'simple';
+ };
+ await Precompiler.cli({
+ templates: [emptyTemplate],
+ simple: true,
+ data: true,
+ });
+ expect(log).toBe('simple\n');
+ });
+
+ it('should set known helpers', async function () {
+ Handlebars.precompile = function (data, options) {
+ expect(options.knownHelpers.foo).toBe(true);
+ return 'simple';
+ };
+ await Precompiler.cli({
+ templates: [emptyTemplate],
+ simple: true,
+ known: 'foo',
+ });
+ expect(log).toBe('simple\n');
+ });
+ it('should output to file system', async function () {
+ Handlebars.precompile = function () {
+ return 'simple';
+ };
+ await Precompiler.cli({
+ templates: [emptyTemplate],
+ simple: true,
+ output: 'file!',
+ });
+ expect(file).toBe('file!');
+ expect(content).toBe('simple\n');
+ expect(log).toBe('');
+ });
+
+ it('should output minimized templates', async function () {
+ Handlebars.precompile = function () {
+ return 'amd';
+ };
+ uglify.minify = function () {
+ return { code: 'min' };
+ };
+ await Precompiler.cli({ templates: [emptyTemplate], min: true });
+ expect(log).toBe('min');
+ });
+
+ it('should omit minimization gracefully, if uglify-js is missing', async function () {
+ var error = new Error("Cannot find module 'uglify-js'");
+ error.code = 'MODULE_NOT_FOUND';
+ await mockRequireUglify(error, async 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/);
+ });
+ });
+
+ it('should fail on errors (other than missing module) while loading uglify-js', async function () {
+ await mockRequireUglify(new Error('Mock Error'), async function () {
+ var Precompiler = require('../dist/cjs/precompiler');
+ Handlebars.precompile = function () {
+ return 'amd';
+ };
+ await expect(
+ Precompiler.cli({ templates: [emptyTemplate], min: true })
+ ).rejects.toThrow('Mock Error');
+ });
+ });
+
+ it('should output map', async function () {
+ await Precompiler.cli({ templates: [emptyTemplate], map: 'foo.js.map' });
+
+ expect(file).toBe('foo.js.map');
+ expect(log.match(/sourceMappingURL=/g).length).toBe(1);
+ });
+
+ it('should output map with minification', async function () {
+ await Precompiler.cli({
+ templates: [emptyTemplate],
+ min: true,
+ map: 'foo.js.map',
+ });
+
+ expect(file).toBe('foo.js.map');
+ expect(log.match(/sourceMappingURL=/g).length).toBe(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', async function () {
+ try {
+ await loadTemplatesAsync({ files: ['foo'] });
+ throw new Error('should have thrown');
+ } catch (err) {
+ expect(err.message).toBe('Unable to open template file "foo"');
+ }
+ });
+ it('should enumerate 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',
+ });
+ // Success is not throwing
+ });
+ it('should handle BOM', async function () {
+ var opts = await loadTemplatesAsync({
+ files: [__dirname + '/artifacts/bom.handlebars'],
+ extension: 'handlebars',
+ bom: true,
+ });
+ expect(opts.templates[0].source).toBe('a');
+ });
+
+ it('should handle different root', async function () {
+ var opts = await loadTemplatesAsync({
+ files: [__dirname + '/artifacts/empty.handlebars'],
+ simple: true,
+ root: 'foo/',
+ });
+ 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 array inputs', async function () {
+ var opts = await loadTemplatesAsync({
+ string: ['', 'bar'],
+ name: ['beep', 'boop'],
+ });
+ expect(opts.templates[0].name).toBe('beep');
+ expect(opts.templates[0].source).toBe('');
+ expect(opts.templates[1].name).toBe('boop');
+ expect(opts.templates[1].source).toBe('bar');
+ });
+ it('should accept stdin input', async function () {
+ var stdin = require('mock-stdin').stdin();
+ var promise = loadTemplatesAsync({ string: '-' });
+ stdin.send('fo');
+ stdin.send('o');
+ stdin.end();
+ var opts = await promise;
+ expect(opts.templates[0].source).toBe('foo');
+ });
+ it('error on name missing', async function () {
+ try {
+ await loadTemplatesAsync({ string: ['', 'bar'] });
+ throw new Error('should have thrown');
+ } catch (err) {
+ expect(err.message).toBe(
+ 'Number of names did not match the number of string inputs'
+ );
+ }
+ });
+
+ it('should complete when no args are passed', async function () {
+ var opts = await loadTemplatesAsync({});
+ expect(opts.templates.length).toBe(0);
+ });
+ });
+});
diff --git a/spec/qunit_spec.js b/spec/qunit_spec.js
deleted file mode 100644
index 5c53e97cb..000000000
--- a/spec/qunit_spec.js
+++ /dev/null
@@ -1,1063 +0,0 @@
-module("basic context");
-
-Handlebars.registerHelper('helperMissing', function(helper, context) {
- if(helper === "link_to") {
- return new Handlebars.SafeString("" + context + "");
- }
-});
-
-var shouldCompileTo = function(string, hashOrArray, expected, message) {
- shouldCompileToWithPartials(string, hashOrArray, false, expected, message);
-};
-var shouldCompileToWithPartials = function(string, hashOrArray, partials, expected, message) {
- var template = CompilerContext[partials ? 'compileWithPartial' : 'compile'](string), ary;
- if(Object.prototype.toString.call(hashOrArray) === "[object Array]") {
- helpers = hashOrArray[1];
-
- if(helpers) {
- for(var prop in Handlebars.helpers) {
- helpers[prop] = Handlebars.helpers[prop];
- }
- }
-
- ary = [];
- ary.push(hashOrArray[0]);
- ary.push({ helpers: hashOrArray[1], partials: hashOrArray[2] });
- } else {
- ary = [hashOrArray];
- }
-
- result = template.apply(this, ary);
- equal(result, expected, "'" + expected + "' should === '" + result + "': " + message);
-};
-
-var shouldThrow = function(fn, exception, message) {
- var caught = false;
- try {
- fn();
- }
- catch (e) {
- if (e instanceof exception) {
- caught = true;
- }
- }
-
- ok(caught, message || null);
-}
-
-test("most basic", function() {
- shouldCompileTo("{{foo}}", { foo: "foo" }, "foo");
-});
-
-test("compiling with a basic context", function() {
- shouldCompileTo("Goodbye\n{{cruel}}\n{{world}}!", {cruel: "cruel", world: "world"}, "Goodbye\ncruel\nworld!",
- "It works if all the required keys are provided");
-});
-
-test("comments", function() {
- shouldCompileTo("{{! Goodbye}}Goodbye\n{{cruel}}\n{{world}}!",
- {cruel: "cruel", world: "world"}, "Goodbye\ncruel\nworld!",
- "comments are ignored");
-});
-
-test("boolean", function() {
- var string = "{{#goodbye}}GOODBYE {{/goodbye}}cruel {{world}}!";
- shouldCompileTo(string, {goodbye: true, world: "world"}, "GOODBYE cruel world!",
- "booleans show the contents when true");
-
- shouldCompileTo(string, {goodbye: false, world: "world"}, "cruel world!",
- "booleans do not show the contents when false");
-});
-
-test("zeros", function() {
- shouldCompileTo("num1: {{num1}}, num2: {{num2}}", {num1: 42, num2: 0},
- "num1: 42, num2: 0");
- shouldCompileTo("num: {{.}}", 0, "num: 0");
- shouldCompileTo("num: {{num1/num2}}", {num1: {num2: 0}}, "num: 0");
-});
-
-test("newlines", function() {
- shouldCompileTo("Alan's\nTest", {}, "Alan's\nTest");
- shouldCompileTo("Alan's\rTest", {}, "Alan's\rTest");
-});
-
-test("escaping text", function() {
- shouldCompileTo("Awesome's", {}, "Awesome's", "text is escaped so that it doesn't get caught on single quotes");
- shouldCompileTo("Awesome\\", {}, "Awesome\\", "text is escaped so that the closing quote can't be ignored");
- shouldCompileTo("Awesome\\\\ foo", {}, "Awesome\\\\ foo", "text is escaped so that it doesn't mess up backslashes");
- shouldCompileTo("Awesome {{foo}}", {foo: '\\'}, "Awesome \\", "text is escaped so that it doesn't mess up backslashes");
- shouldCompileTo(' " " ', {}, ' " " ', "double quotes never produce invalid javascript");
-});
-
-test("escaping expressions", function() {
- shouldCompileTo("{{{awesome}}}", {awesome: "&\"\\<>"}, '&\"\\<>',
- "expressions with 3 handlebars aren't escaped");
-
- shouldCompileTo("{{&awesome}}", {awesome: "&\"\\<>"}, '&\"\\<>',
- "expressions with {{& handlebars aren't escaped");
-
- shouldCompileTo("{{awesome}}", {awesome: "&\"'`\\<>"}, '&"'`\\<>',
- "by default expressions should be escaped");
-
-});
-
-test("functions returning safestrings shouldn't be escaped", function() {
- var hash = {awesome: function() { return new Handlebars.SafeString("&\"\\<>"); }};
- shouldCompileTo("{{awesome}}", hash, '&\"\\<>',
- "functions returning safestrings aren't escaped");
-});
-
-test("functions", function() {
- shouldCompileTo("{{awesome}}", {awesome: function() { return "Awesome"; }}, "Awesome",
- "functions are called and render their output");
-});
-
-test("paths with hyphens", function() {
- shouldCompileTo("{{foo-bar}}", {"foo-bar": "baz"}, "baz", "Paths can contain hyphens (-)");
-});
-
-test("nested paths", function() {
- shouldCompileTo("Goodbye {{alan/expression}} world!", {alan: {expression: "beautiful"}},
- "Goodbye beautiful world!", "Nested paths access nested objects");
-});
-
-test("nested paths with empty string value", function() {
- shouldCompileTo("Goodbye {{alan/expression}} world!", {alan: {expression: ""}},
- "Goodbye world!", "Nested paths access nested objects with empty string");
-});
-
-test("literal paths", function() {
- shouldCompileTo("Goodbye {{[@alan]/expression}} world!", {"@alan": {expression: "beautiful"}},
- "Goodbye beautiful world!", "Literal paths can be used");
-});
-
-test("--- TODO --- bad idea nested paths", function() {
- return;
- var hash = {goodbyes: [{text: "goodbye"}, {text: "Goodbye"}, {text: "GOODBYE"}], world: "world"};
- shouldThrow(function() {
- CompilerContext.compile("{{#goodbyes}}{{../name/../name}}{{/goodbyes}}")(hash);
- }, Handlebars.Exception,
- "Cannot jump (..) into previous context after moving into a context.");
-
- var string = "{{#goodbyes}}{{.././world}} {{/goodbyes}}";
- shouldCompileTo(string, hash, "world world world ", "Same context (.) is ignored in paths");
-});
-
-test("that current context path ({{.}}) doesn't hit helpers", function() {
- shouldCompileTo("test: {{.}}", [null, {helper: "awesome"}], "test: ");
-});
-
-test("complex but empty paths", function() {
- shouldCompileTo("{{person/name}}", {person: {name: null}}, "");
- shouldCompileTo("{{person/name}}", {person: {}}, "");
-});
-
-test("this keyword in paths", function() {
- var string = "{{#goodbyes}}{{this}}{{/goodbyes}}";
- var hash = {goodbyes: ["goodbye", "Goodbye", "GOODBYE"]};
- shouldCompileTo(string, hash, "goodbyeGoodbyeGOODBYE",
- "This keyword in paths evaluates to current context");
-
- string = "{{#hellos}}{{this/text}}{{/hellos}}"
- hash = {hellos: [{text: "hello"}, {text: "Hello"}, {text: "HELLO"}]};
- shouldCompileTo(string, hash, "helloHelloHELLO", "This keyword evaluates in more complex paths");
-});
-
-module("inverted sections");
-
-test("inverted sections with unset value", function() {
- var string = "{{#goodbyes}}{{this}}{{/goodbyes}}{{^goodbyes}}Right On!{{/goodbyes}}";
- var hash = {};
- shouldCompileTo(string, hash, "Right On!", "Inverted section rendered when value isn't set.");
-});
-
-test("inverted section with false value", function() {
- var string = "{{#goodbyes}}{{this}}{{/goodbyes}}{{^goodbyes}}Right On!{{/goodbyes}}";
- var hash = {goodbyes: false};
- shouldCompileTo(string, hash, "Right On!", "Inverted section rendered when value is false.");
-});
-
-test("inverted section with empty set", function() {
- var string = "{{#goodbyes}}{{this}}{{/goodbyes}}{{^goodbyes}}Right On!{{/goodbyes}}";
- var hash = {goodbyes: []};
- shouldCompileTo(string, hash, "Right On!", "Inverted section rendered when value is empty set.");
-});
-
-module("blocks");
-
-test("array", function() {
- var string = "{{#goodbyes}}{{text}}! {{/goodbyes}}cruel {{world}}!"
- var hash = {goodbyes: [{text: "goodbye"}, {text: "Goodbye"}, {text: "GOODBYE"}], world: "world"};
- shouldCompileTo(string, hash, "goodbye! Goodbye! GOODBYE! cruel world!",
- "Arrays iterate over the contents when not empty");
-
- shouldCompileTo(string, {goodbyes: [], world: "world"}, "cruel world!",
- "Arrays ignore the contents when empty");
-
-});
-
-test("empty block", function() {
- var string = "{{#goodbyes}}{{/goodbyes}}cruel {{world}}!"
- var hash = {goodbyes: [{text: "goodbye"}, {text: "Goodbye"}, {text: "GOODBYE"}], world: "world"};
- shouldCompileTo(string, hash, "cruel world!",
- "Arrays iterate over the contents when not empty");
-
- shouldCompileTo(string, {goodbyes: [], world: "world"}, "cruel world!",
- "Arrays ignore the contents when empty");
-});
-
-test("nested iteration", function() {
-
-});
-
-test("block with complex lookup", function() {
- var string = "{{#goodbyes}}{{text}} cruel {{../name}}! {{/goodbyes}}";
- var hash = {name: "Alan", goodbyes: [{text: "goodbye"}, {text: "Goodbye"}, {text: "GOODBYE"}]};
-
- shouldCompileTo(string, hash, "goodbye cruel Alan! Goodbye cruel Alan! GOODBYE cruel Alan! ",
- "Templates can access variables in contexts up the stack with relative path syntax");
-});
-
-test("helper with complex lookup$", function() {
- var string = "{{#goodbyes}}{{{link ../prefix}}}{{/goodbyes}}";
- var hash = {prefix: "/root", goodbyes: [{text: "Goodbye", url: "goodbye"}]};
- var helpers = {link: function(prefix) {
- return "" + this.text + "";
- }};
- shouldCompileTo(string, [hash, helpers], "Goodbye");
-});
-
-test("helper block with complex lookup expression", function() {
- var string = "{{#goodbyes}}{{../name}}{{/goodbyes}}";
- var hash = {name: "Alan"};
- var helpers = {goodbyes: function(options) {
- var out = "";
- var byes = ["Goodbye", "goodbye", "GOODBYE"];
- for (var i = 0,j = byes.length; i < j; i++) {
- out += byes[i] + " " + options.fn(this) + "! ";
- }
- return out;
- }};
- shouldCompileTo(string, [hash, helpers], "Goodbye Alan! goodbye Alan! GOODBYE Alan! ");
-});
-
-test("helper with complex lookup and nested template", function() {
- var string = "{{#goodbyes}}{{#link ../prefix}}{{text}}{{/link}}{{/goodbyes}}";
- var hash = {prefix: '/root', goodbyes: [{text: "Goodbye", url: "goodbye"}]};
- var helpers = {link: function (prefix, options) {
- return "" + options.fn(this) + "";
- }};
- shouldCompileToWithPartials(string, [hash, helpers], false, "Goodbye");
-});
-
-test("helper with complex lookup and nested template in VM+Compiler", function() {
- var string = "{{#goodbyes}}{{#link ../prefix}}{{text}}{{/link}}{{/goodbyes}}";
- var hash = {prefix: '/root', goodbyes: [{text: "Goodbye", url: "goodbye"}]};
- var helpers = {link: function (prefix, options) {
- return "" + options.fn(this) + "";
- }};
- shouldCompileToWithPartials(string, [hash, helpers], true, "Goodbye");
-});
-
-test("block with deep nested complex lookup", function() {
- var string = "{{#outer}}Goodbye {{#inner}}cruel {{../../omg}}{{/inner}}{{/outer}}";
- var hash = {omg: "OMG!", outer: [{ inner: [{ text: "goodbye" }] }] };
-
- shouldCompileTo(string, hash, "Goodbye cruel OMG!");
-});
-
-test("block helper", function() {
- var string = "{{#goodbyes}}{{text}}! {{/goodbyes}}cruel {{world}}!";
- var template = CompilerContext.compile(string);
-
- result = template({world: "world"}, { helpers: {goodbyes: function(options) { return options.fn({text: "GOODBYE"}); }}});
- equal(result, "GOODBYE! cruel world!", "Block helper executed");
-});
-
-test("block helper staying in the same context", function() {
- var string = "{{#form}}
{{name}}
{{/form}}";
- var template = CompilerContext.compile(string);
-
- result = template({name: "Yehuda"}, {helpers: {form: function(options) { return ""; } }});
- equal(result, "", "Block helper executed with current context");
-});
-
-test("block helper should have context in this", function() {
- var source = "
{{#link}}Hello{{/link}}{{/form}}";
- var template = CompilerContext.compile(string);
-
- result = template({
- yehuda: {name: "Yehuda" }
- }, {
- helpers: {
- link: function(options) { return "" + options.fn(this) + ""; },
- form: function(context, options) { return ""; }
- }
- });
- equal(result, "", "Both blocks executed");
-});
-
-test("block inverted sections", function() {
- shouldCompileTo("{{#people}}{{name}}{{^}}{{none}}{{/people}}", {none: "No people"},
- "No people");
-});
-
-test("block inverted sections with empty arrays", function() {
- shouldCompileTo("{{#people}}{{name}}{{^}}{{none}}{{/people}}", {none: "No people", people: []},
- "No people");
-});
-
-test("block helper inverted sections", function() {
- var string = "{{#list people}}{{name}}{{^}}Nobody's here{{/list}}";
- var list = function(context, options) {
- if (context.length > 0) {
- var out = "
";
- for(var i = 0,j=context.length; i < j; i++) {
- out += "
";
- out += options.fn(context[i]);
- out += "
";
- }
- out += "
";
- return out;
- } else {
- return "
" + options.inverse(this) + "
";
- }
- };
-
- var hash = {people: [{name: "Alan"}, {name: "Yehuda"}]};
- var empty = {people: []};
- var rootMessage = {
- people: [],
- message: "Nobody's here"
- };
-
- var messageString = "{{#list people}}Hello{{^}}{{message}}{{/list}}";
-
- // the meaning here may be kind of hard to catch, but list.not is always called,
- // so we should see the output of both
- shouldCompileTo(string, [hash, { list: list }], "
Alan
Yehuda
", "an inverse wrapper is passed in as a new context");
- shouldCompileTo(string, [empty, { list: list }], "
Nobody's here
", "an inverse wrapper can be optionally called");
- shouldCompileTo(messageString, [rootMessage, { list: list }], "
Nobody's here
", "the context of an inverse is the parent of the block");
-});
-
-module("helpers hash");
-
-test("providing a helpers hash", function() {
- shouldCompileTo("Goodbye {{cruel}} {{world}}!", [{cruel: "cruel"}, {world: function() { return "world"; }}], "Goodbye cruel world!",
- "helpers hash is available");
-
- shouldCompileTo("Goodbye {{#iter}}{{cruel}} {{world}}{{/iter}}!", [{iter: [{cruel: "cruel"}]}, {world: function() { return "world"; }}],
- "Goodbye cruel world!", "helpers hash is available inside other blocks");
-});
-
-test("in cases of conflict, the explicit hash wins", function() {
-
-});
-
-test("the helpers hash is available is nested contexts", function() {
-
-});
-
-module("partials");
-
-test("basic partials", function() {
- var string = "Dudes: {{#dudes}}{{> dude}}{{/dudes}}";
- var partial = "{{name}} ({{url}}) ";
- var hash = {dudes: [{name: "Yehuda", url: "http://yehuda"}, {name: "Alan", url: "http://alan"}]};
- shouldCompileToWithPartials(string, [hash, {}, {dude: partial}], true, "Dudes: Yehuda (http://yehuda) Alan (http://alan) ",
- "Basic partials output based on current context.");
-});
-
-test("partials with context", function() {
- var string = "Dudes: {{>dude dudes}}";
- var partial = "{{#this}}{{name}} ({{url}}) {{/this}}";
- var hash = {dudes: [{name: "Yehuda", url: "http://yehuda"}, {name: "Alan", url: "http://alan"}]};
- shouldCompileToWithPartials(string, [hash, {}, {dude: partial}], true, "Dudes: Yehuda (http://yehuda) Alan (http://alan) ",
- "Partials can be passed a context");
-});
-
-test("partial in a partial", function() {
- var string = "Dudes: {{#dudes}}{{>dude}}{{/dudes}}";
- var dude = "{{name}} {{> url}} ";
- var url = "{{url}}";
- var hash = {dudes: [{name: "Yehuda", url: "http://yehuda"}, {name: "Alan", url: "http://alan"}]};
- shouldCompileToWithPartials(string, [hash, {}, {dude: dude, url: url}], true, "Dudes: Yehuda http://yehuda Alan http://alan ", "Partials are rendered inside of other partials");
-});
-
-test("rendering undefined partial throws an exception", function() {
- shouldThrow(function() {
- var template = CompilerContext.compile("{{> whatever}}");
- template();
- }, Handlebars.Exception, "Should throw exception");
-});
-
-test("rendering template partial in vm mode throws an exception", function() {
- shouldThrow(function() {
- var template = CompilerContext.compile("{{> whatever}}");
- var string = "Dudes: {{>dude}} {{another_dude}}";
- var dude = "{{name}}";
- var hash = {name:"Jeepers", another_dude:"Creepers"};
- template();
- }, Handlebars.Exception, "Should throw exception");
-});
-
-test("rendering function partial in vm mode", function() {
- var string = "Dudes: {{#dudes}}{{> dude}}{{/dudes}}";
- var partial = function(context) {
- return context.name + ' (' + context.url + ') ';
- };
- var hash = {dudes: [{name: "Yehuda", url: "http://yehuda"}, {name: "Alan", url: "http://alan"}]};
- shouldCompileTo(string, [hash, {}, {dude: partial}], "Dudes: Yehuda (http://yehuda) Alan (http://alan) ",
- "Function partials output based in VM.");
-});
-
-test("GH-14: a partial preceding a selector", function() {
- var string = "Dudes: {{>dude}} {{another_dude}}";
- var dude = "{{name}}";
- var hash = {name:"Jeepers", another_dude:"Creepers"};
- shouldCompileToWithPartials(string, [hash, {}, {dude:dude}], true, "Dudes: Jeepers Creepers", "Regular selectors can follow a partial");
-});
-
-test("Partials with literal paths", function() {
- var string = "Dudes: {{> [dude]}}";
- var dude = "{{name}}";
- var hash = {name:"Jeepers", another_dude:"Creepers"};
- shouldCompileToWithPartials(string, [hash, {}, {dude:dude}], true, "Dudes: Jeepers", "Partials can use literal paths");
-});
-
-module("String literal parameters");
-
-test("simple literals work", function() {
- var string = 'Message: {{hello "world" 12 true false}}';
- var hash = {};
- var helpers = {hello: function(param, times, bool1, bool2) {
- if(typeof times !== 'number') { times = "NaN"; }
- if(typeof bool1 !== 'boolean') { bool1 = "NaB"; }
- if(typeof bool2 !== 'boolean') { bool2 = "NaB"; }
- return "Hello " + param + " " + times + " times: " + bool1 + " " + bool2;
- }}
- shouldCompileTo(string, [hash, helpers], "Message: Hello world 12 times: true false", "template with a simple String literal");
-});
-
-test("using a quote in the middle of a parameter raises an error", function() {
- shouldThrow(function() {
- var string = 'Message: {{hello wo"rld"}}';
- CompilerContext.compile(string);
- }, Error, "should throw exception");
-});
-
-test("escaping a String is possible", function(){
- var string = 'Message: {{{hello "\\"world\\""}}}';
- var hash = {}
- var helpers = {hello: function(param) { return "Hello " + param; }}
- shouldCompileTo(string, [hash, helpers], "Message: Hello \"world\"", "template with an escaped String literal");
-});
-
-test("it works with ' marks", function() {
- var string = 'Message: {{{hello "Alan\'s world"}}}';
- var hash = {};
- var helpers = {hello: function(param) { return "Hello " + param; }};
- shouldCompileTo(string, [hash, helpers], "Message: Hello Alan's world", "template with a ' mark");
-});
-
-module("multiple parameters");
-
-test("simple multi-params work", function() {
- var string = 'Message: {{goodbye cruel world}}';
- var hash = {cruel: "cruel", world: "world"};
- var helpers = {goodbye: function(cruel, world) { return "Goodbye " + cruel + " " + world; }};
- shouldCompileTo(string, [hash, helpers], "Message: Goodbye cruel world", "regular helpers with multiple params");
-});
-
-test("block multi-params work", function() {
- var string = 'Message: {{#goodbye cruel world}}{{greeting}} {{adj}} {{noun}}{{/goodbye}}';
- var hash = {cruel: "cruel", world: "world"};
- var helpers = {goodbye: function(cruel, world, options) {
- return options.fn({greeting: "Goodbye", adj: cruel, noun: world});
- }};
- shouldCompileTo(string, [hash, helpers], "Message: Goodbye cruel world", "block helpers with multiple params");
-});
-
-module("safestring");
-
-test("constructing a safestring from a string and checking its type", function() {
- var safe = new Handlebars.SafeString("testing 1, 2, 3");
- ok(safe instanceof Handlebars.SafeString, "SafeString is an instance of Handlebars.SafeString");
- equal(safe, "testing 1, 2, 3", "SafeString is equivalent to its underlying string");
-});
-
-module("helperMissing");
-
-test("if a context is not found, helperMissing is used", function() {
- var string = "{{hello}} {{link_to world}}";
- var context = { hello: "Hello", world: "world" };
-
- shouldCompileTo(string, context, "Hello world");
-});
-
-module("knownHelpers");
-
-test("Known helper should render helper", function() {
- var template = CompilerContext.compile("{{hello}}", {knownHelpers: {"hello" : true}})
-
- var result = template({}, {helpers: {hello: function() { return "foo"; }}});
- equal(result, "foo", "'foo' should === '" + result);
-});
-
-test("Unknown helper in knownHelpers only mode should be passed as undefined", function() {
- var template = CompilerContext.compile("{{typeof hello}}", {knownHelpers: {'typeof': true}, knownHelpersOnly: true})
-
- var result = template({}, {helpers: {'typeof': function(arg) { return typeof arg; }, hello: function() { return "foo"; }}});
- equal(result, "undefined", "'undefined' should === '" + result);
-});
-test("Builtin helpers available in knownHelpers only mode", function() {
- var template = CompilerContext.compile("{{#unless foo}}bar{{/unless}}", {knownHelpersOnly: true})
-
- var result = template({});
- equal(result, "bar", "'bar' should === '" + result);
-});
-test("Field lookup works in knownHelpers only mode", function() {
- var template = CompilerContext.compile("{{foo}}", {knownHelpersOnly: true})
-
- var result = template({foo: 'bar'});
- equal(result, "bar", "'bar' should === '" + result);
-});
-test("Conditional blocks work in knownHelpers only mode", function() {
- var template = CompilerContext.compile("{{#foo}}bar{{/foo}}", {knownHelpersOnly: true})
-
- var result = template({foo: 'baz'});
- equal(result, "bar", "'bar' should === '" + result);
-});
-test("Invert blocks work in knownHelpers only mode", function() {
- var template = CompilerContext.compile("{{^foo}}bar{{/foo}}", {knownHelpersOnly: true})
-
- var result = template({foo: false});
- equal(result, "bar", "'bar' should === '" + result);
-});
-
-module("blockHelperMissing");
-
-test("lambdas are resolved by blockHelperMissing, not handlebars proper", function() {
- var string = "{{#truthy}}yep{{/truthy}}";
- var data = { truthy: function() { return true; } };
- shouldCompileTo(string, data, "yep");
-});
-
-var teardown;
-module("built-in helpers", {
- setup: function(){ teardown = null; },
- teardown: function(){ if (teardown) { teardown(); } }
-});
-
-test("with", function() {
- var string = "{{#with person}}{{first}} {{last}}{{/with}}";
- shouldCompileTo(string, {person: {first: "Alan", last: "Johnson"}}, "Alan Johnson");
-});
-
-test("if", function() {
- var string = "{{#if goodbye}}GOODBYE {{/if}}cruel {{world}}!";
- shouldCompileTo(string, {goodbye: true, world: "world"}, "GOODBYE cruel world!",
- "if with boolean argument shows the contents when true");
- shouldCompileTo(string, {goodbye: "dummy", world: "world"}, "GOODBYE cruel world!",
- "if with string argument shows the contents");
- shouldCompileTo(string, {goodbye: false, world: "world"}, "cruel world!",
- "if with boolean argument does not show the contents when false");
- shouldCompileTo(string, {world: "world"}, "cruel world!",
- "if with undefined does not show the contents");
- shouldCompileTo(string, {goodbye: ['foo'], world: "world"}, "GOODBYE cruel world!",
- "if with non-empty array shows the contents");
- shouldCompileTo(string, {goodbye: [], world: "world"}, "cruel world!",
- "if with empty array does not show the contents");
-});
-
-test("if with function argument", function() {
- var string = "{{#if goodbye}}GOODBYE {{/if}}cruel {{world}}!";
- shouldCompileTo(string, {goodbye: function() {return true}, world: "world"}, "GOODBYE cruel world!",
- "if with function shows the contents when function returns true");
- shouldCompileTo(string, {goodbye: function() {return this.world}, world: "world"}, "GOODBYE cruel world!",
- "if with function shows the contents when function returns string");
- shouldCompileTo(string, {goodbye: function() {return false}, world: "world"}, "cruel world!",
- "if with function does not show the contents when returns false");
- shouldCompileTo(string, {goodbye: function() {return this.foo}, world: "world"}, "cruel world!",
- "if with function does not show the contents when returns undefined");
-});
-
-test("each", function() {
- var string = "{{#each goodbyes}}{{text}}! {{/each}}cruel {{world}}!"
- var hash = {goodbyes: [{text: "goodbye"}, {text: "Goodbye"}, {text: "GOODBYE"}], world: "world"};
- shouldCompileTo(string, hash, "goodbye! Goodbye! GOODBYE! cruel world!",
- "each with array argument iterates over the contents when not empty");
- shouldCompileTo(string, {goodbyes: [], world: "world"}, "cruel world!",
- "each with array argument ignores the contents when empty");
-});
-
-test("log", function() {
- var string = "{{log blah}}"
- var hash = { blah: "whee" };
-
- var logArg;
- var originalLog = Handlebars.log;
- Handlebars.log = function(arg){ logArg = arg; }
- teardown = function(){ Handlebars.log = originalLog; }
-
- shouldCompileTo(string, hash, "", "log should not display");
- equals("whee", logArg, "should call log with 'whee'");
-});
-
-test("overriding property lookup", function() {
-
-});
-
-
-test("passing in data to a compiled function that expects data - works with helpers", function() {
- var template = CompilerContext.compile("{{hello}}", {data: true});
-
- var helpers = {
- hello: function(options) {
- return options.data.adjective + " " + this.noun;
- }
- };
-
- var result = template({noun: "cat"}, {helpers: helpers, data: {adjective: "happy"}});
- equals("happy cat", result, "Data output by helper");
-});
-
-test("passing in data to a compiled function that expects data - works with helpers in partials", function() {
- var template = CompilerContext.compile("{{>my_partial}}", {data: true});
-
- var partials = {
- my_partial: CompilerContext.compile("{{hello}}", {data: true})
- };
-
- var helpers = {
- hello: function(options) {
- return options.data.adjective + " " + this.noun;
- }
- };
-
- var result = template({noun: "cat"}, {helpers: helpers, partials: partials, data: {adjective: "happy"}});
- equals("happy cat", result, "Data output by helper inside partial");
-});
-
-test("passing in data to a compiled function that expects data - works with helpers and parameters", function() {
- var template = CompilerContext.compile("{{hello world}}", {data: true});
-
- var helpers = {
- hello: function(noun, options) {
- return options.data.adjective + " " + noun + (this.exclaim ? "!" : "");
- }
- };
-
- var result = template({exclaim: true, world: "world"}, {helpers: helpers, data: {adjective: "happy"}});
- equals("happy world!", result, "Data output by helper");
-});
-
-test("passing in data to a compiled function that expects data - works with block helpers", function() {
- var template = CompilerContext.compile("{{#hello}}{{world}}{{/hello}}", {data: true});
-
- var helpers = {
- hello: function(options) {
- return options.fn(this);
- },
- world: function(options) {
- return options.data.adjective + " world" + (this.exclaim ? "!" : "");
- }
- };
-
- var result = template({exclaim: true}, {helpers: helpers, data: {adjective: "happy"}});
- equals("happy world!", result, "Data output by helper");
-});
-
-test("passing in data to a compiled function that expects data - works with block helpers that use ..", function() {
- var template = CompilerContext.compile("{{#hello}}{{world ../zomg}}{{/hello}}", {data: true});
-
- var helpers = {
- hello: function(options) {
- return options.fn({exclaim: "?"});
- },
- world: function(thing, options) {
- return options.data.adjective + " " + thing + (this.exclaim || "");
- }
- };
-
- var result = template({exclaim: true, zomg: "world"}, {helpers: helpers, data: {adjective: "happy"}});
- equals("happy world?", result, "Data output by helper");
-});
-
-test("passing in data to a compiled function that expects data - data is passed to with block helpers where children use ..", function() {
- var template = CompilerContext.compile("{{#hello}}{{world ../zomg}}{{/hello}}", {data: true});
-
- var helpers = {
- hello: function(options) {
- return options.data.accessData + " " + options.fn({exclaim: "?"});
- },
- world: function(thing, options) {
- return options.data.adjective + " " + thing + (this.exclaim || "");
- }
- };
-
- var result = template({exclaim: true, zomg: "world"}, {helpers: helpers, data: {adjective: "happy", accessData: "#win"}});
- equals("#win happy world?", result, "Data output by helper");
-});
-
-test("you can override inherited data when invoking a helper", function() {
- var template = CompilerContext.compile("{{#hello}}{{world zomg}}{{/hello}}", {data: true});
-
- var helpers = {
- hello: function(options) {
- return options.fn({exclaim: "?", zomg: "world"}, { data: {adjective: "sad"} });
- },
- world: function(thing, options) {
- return options.data.adjective + " " + thing + (this.exclaim || "");
- }
- };
-
- var result = template({exclaim: true, zomg: "planet"}, {helpers: helpers, data: {adjective: "happy"}});
- equals("sad world?", result, "Overriden data output by helper");
-});
-
-
-test("you can override inherited data when invoking a helper with depth", function() {
- var template = CompilerContext.compile("{{#hello}}{{world ../zomg}}{{/hello}}", {data: true});
-
- var helpers = {
- hello: function(options) {
- return options.fn({exclaim: "?"}, { data: {adjective: "sad"} });
- },
- world: function(thing, options) {
- return options.data.adjective + " " + thing + (this.exclaim || "");
- }
- };
-
- var result = template({exclaim: true, zomg: "world"}, {helpers: helpers, data: {adjective: "happy"}});
- equals("sad world?", result, "Overriden data output by helper");
-});
-
-test("helpers take precedence over same-named context properties", function() {
- var template = CompilerContext.compile("{{goodbye}} {{cruel world}}");
-
- var helpers = {
- goodbye: function() {
- return this.goodbye.toUpperCase();
- },
-
- cruel: function(world) {
- return "cruel " + world.toUpperCase();
- }
- };
-
- var context = {
- goodbye: "goodbye",
- world: "world"
- };
-
- var result = template(context, {helpers: helpers});
- equals(result, "GOODBYE cruel WORLD", "Helper executed");
-});
-
-test("helpers take precedence over same-named context properties$", function() {
- var template = CompilerContext.compile("{{#goodbye}} {{cruel world}}{{/goodbye}}");
-
- var helpers = {
- goodbye: function(options) {
- return this.goodbye.toUpperCase() + options.fn(this);
- },
-
- cruel: function(world) {
- return "cruel " + world.toUpperCase();
- }
- };
-
- var context = {
- goodbye: "goodbye",
- world: "world"
- };
-
- var result = template(context, {helpers: helpers});
- equals(result, "GOODBYE cruel WORLD", "Helper executed");
-});
-
-test("Scoped names take precedence over helpers", function() {
- var template = CompilerContext.compile("{{this.goodbye}} {{cruel world}} {{cruel this.goodbye}}");
-
- var helpers = {
- goodbye: function() {
- return this.goodbye.toUpperCase();
- },
-
- cruel: function(world) {
- return "cruel " + world.toUpperCase();
- },
- };
-
- var context = {
- goodbye: "goodbye",
- world: "world"
- };
-
- var result = template(context, {helpers: helpers});
- equals(result, "goodbye cruel WORLD cruel GOODBYE", "Helper not executed");
-});
-
-test("Scoped names take precedence over block helpers", function() {
- var template = CompilerContext.compile("{{#goodbye}} {{cruel world}}{{/goodbye}} {{this.goodbye}}");
-
- var helpers = {
- goodbye: function(options) {
- return this.goodbye.toUpperCase() + options.fn(this);
- },
-
- cruel: function(world) {
- return "cruel " + world.toUpperCase();
- },
- };
-
- var context = {
- goodbye: "goodbye",
- world: "world"
- };
-
- var result = template(context, {helpers: helpers});
- equals(result, "GOODBYE cruel WORLD goodbye", "Helper executed");
-});
-
-test("helpers can take an optional hash", function() {
- var template = CompilerContext.compile('{{goodbye cruel="CRUEL" world="WORLD" times=12}}');
-
- var helpers = {
- goodbye: function(options) {
- return "GOODBYE " + options.hash.cruel + " " + options.hash.world + " " + options.hash.times + " TIMES";
- }
- };
-
- var context = {};
-
- var result = template(context, {helpers: helpers});
- equals(result, "GOODBYE CRUEL WORLD 12 TIMES", "Helper output hash");
-});
-
-test("helpers can take an optional hash with booleans", function() {
- var helpers = {
- goodbye: function(options) {
- if (options.hash.print === true) {
- return "GOODBYE " + options.hash.cruel + " " + options.hash.world;
- } else if (options.hash.print === false) {
- return "NOT PRINTING";
- } else {
- return "THIS SHOULD NOT HAPPEN";
- }
- }
- };
-
- var context = {};
-
- var template = CompilerContext.compile('{{goodbye cruel="CRUEL" world="WORLD" print=true}}');
- var result = template(context, {helpers: helpers});
- equals(result, "GOODBYE CRUEL WORLD", "Helper output hash");
-
- template = CompilerContext.compile('{{goodbye cruel="CRUEL" world="WORLD" print=false}}');
- result = template(context, {helpers: helpers});
- equals(result, "NOT PRINTING", "Boolean helper parameter honored");
-});
-
-test("block helpers can take an optional hash", function() {
- var template = CompilerContext.compile('{{#goodbye cruel="CRUEL" times=12}}world{{/goodbye}}');
-
- var helpers = {
- goodbye: function(options) {
- return "GOODBYE " + options.hash.cruel + " " + options.fn(this) + " " + options.hash.times + " TIMES";
- }
- };
-
- var result = template({}, {helpers: helpers});
- equals(result, "GOODBYE CRUEL world 12 TIMES", "Hash parameters output");
-});
-
-test("block helpers can take an optional hash with booleans", function() {
- var helpers = {
- goodbye: function(options) {
- if (options.hash.print === true) {
- return "GOODBYE " + options.hash.cruel + " " + options.fn(this);
- } else if (options.hash.print === false) {
- return "NOT PRINTING";
- } else {
- return "THIS SHOULD NOT HAPPEN";
- }
- }
- };
-
- var template = CompilerContext.compile('{{#goodbye cruel="CRUEL" print=true}}world{{/goodbye}}');
- var result = template({}, {helpers: helpers});
- equals(result, "GOODBYE CRUEL world", "Boolean hash parameter honored");
-
- var template = CompilerContext.compile('{{#goodbye cruel="CRUEL" print=false}}world{{/goodbye}}');
- var result = template({}, {helpers: helpers});
- equals(result, "NOT PRINTING", "Boolean hash parameter honored");
-});
-
-
-test("arguments to helpers can be retrieved from options hash in string form", function() {
- var template = CompilerContext.compile('{{wycats is.a slave.driver}}', {stringParams: true});
-
- var helpers = {
- wycats: function(passiveVoice, noun, options) {
- return "HELP ME MY BOSS " + passiveVoice + ' ' + noun;
- }
- };
-
- var result = template({}, {helpers: helpers});
-
- equals(result, "HELP ME MY BOSS is.a slave.driver", "String parameters output");
-});
-
-test("when using block form, arguments to helpers can be retrieved from options hash in string form", function() {
- var template = CompilerContext.compile('{{#wycats is.a slave.driver}}help :({{/wycats}}', {stringParams: true});
-
- var helpers = {
- wycats: function(passiveVoice, noun, options) {
- return "HELP ME MY BOSS " + passiveVoice + ' ' +
- noun + ': ' + options.fn(this);
- }
- };
-
- var result = template({}, {helpers: helpers});
-
- equals(result, "HELP ME MY BOSS is.a slave.driver: help :(", "String parameters output");
-});
-
-test("when inside a block in String mode, .. passes the appropriate context in the options hash", function() {
- var template = CompilerContext.compile('{{#with dale}}{{tomdale ../need dad.joke}}{{/with}}', {stringParams: true});
-
- var helpers = {
- tomdale: function(desire, noun, options) {
- return "STOP ME FROM READING HACKER NEWS I " +
- options.contexts[0][desire] + " " + noun;
- },
-
- "with": function(context, options) {
- return options.fn(options.contexts[0][context]);
- }
- };
-
- var result = template({
- dale: {},
-
- need: 'need-a'
- }, {helpers: helpers});
-
- equals(result, "STOP ME FROM READING HACKER NEWS I need-a dad.joke", "Proper context variable output");
-});
-
-test("when inside a block in String mode, .. passes the appropriate context in the options hash to a block helper", function() {
- var template = CompilerContext.compile('{{#with dale}}{{#tomdale ../need dad.joke}}wot{{/tomdale}}{{/with}}', {stringParams: true});
-
- var helpers = {
- tomdale: function(desire, noun, options) {
- return "STOP ME FROM READING HACKER NEWS I " +
- options.contexts[0][desire] + " " + noun + " " +
- options.fn(this);
- },
-
- "with": function(context, options) {
- return options.fn(options.contexts[0][context]);
- }
- };
-
- var result = template({
- dale: {},
-
- need: 'need-a'
- }, {helpers: helpers});
-
- equals(result, "STOP ME FROM READING HACKER NEWS I need-a dad.joke wot", "Proper context variable output");
-});
-
-module("Regressions")
-
-test("GH-94: Cannot read property of undefined", function() {
- var data = {"books":[{"title":"The origin of species","author":{"name":"Charles Darwin"}},{"title":"Lazarillo de Tormes"}]};
- var string = "{{#books}}{{title}}{{author.name}}{{/books}}";
- shouldCompileTo(string, data, "The origin of speciesCharles DarwinLazarillo de Tormes",
- "Renders without an undefined property error");
-});
-
-test("GH-150: Inverted sections print when they shouldn't", function() {
- var string = "{{^set}}not set{{/set}} :: {{#set}}set{{/set}}";
-
- shouldCompileTo(string, {}, "not set :: ", "inverted sections run when property isn't present in context");
- shouldCompileTo(string, {set: undefined}, "not set :: ", "inverted sections run when property is undefined");
- shouldCompileTo(string, {set: false}, "not set :: ", "inverted sections run when property is false");
- shouldCompileTo(string, {set: true}, " :: set", "inverted sections don't run when property is true");
-});
-
-test("Mustache man page", function() {
- var string = "Hello {{name}}. You have just won ${{value}}!{{#in_ca}} Well, ${{taxed_value}}, after taxes.{{/in_ca}}"
- var data = {
- "name": "Chris",
- "value": 10000,
- "taxed_value": 10000 - (10000 * 0.4),
- "in_ca": true
- }
-
- shouldCompileTo(string, data, "Hello Chris. You have just won $10000! Well, $6000, after taxes.", "the hello world mustache example works");
-});
-
-test("GH-158: Using array index twice, breaks the template", function() {
- var string = "{{arr.[0]}}, {{arr.[1]}}";
- var data = { "arr": [1,2] };
-
- shouldCompileTo(string, data, "1, 2", "it works as expected");
-});
-
-test("bug reported by @fat where lambdas weren't being properly resolved", function() {
- var string = "This is a slightly more complicated {{thing}}..\n{{! Just ignore this business. }}\nCheck this out:\n{{#hasThings}}\n
\n{{#things}}\n
{{word}}
\n{{/things}}
.\n{{/hasThings}}\n{{^hasThings}}\n\nNothing to check out...\n{{/hasThings}}";
- var data = {
- thing: function() {
- return "blah";
- },
- things: [
- {className: "one", word: "@fat"},
- {className: "two", word: "@dhg"},
- {className: "three", word:"@sayrer"}
- ],
- hasThings: function() {
- return true;
- }
- };
-
- var output = "This is a slightly more complicated blah..\n\nCheck this out:\n\n
\n\n
@fat
\n\n
@dhg
\n\n
@sayrer
\n
.\n\n";
- shouldCompileTo(string, data, output);
-});
diff --git a/spec/regressions.js b/spec/regressions.js
new file mode 100644
index 000000000..b70235012
--- /dev/null
+++ b/spec/regressions.js
@@ -0,0 +1,505 @@
+describe('Regressions', function () {
+ it('GH-94: Cannot read property of undefined', function () {
+ expectTemplate('{{#books}}{{title}}{{author.name}}{{/books}}')
+ .withInput({
+ books: [
+ {
+ title: 'The origin of species',
+ author: {
+ name: 'Charles Darwin',
+ },
+ },
+ {
+ title: 'Lazarillo de Tormes',
+ },
+ ],
+ })
+ .withMessage('Renders without an undefined property error')
+ .toCompileTo('The origin of speciesCharles DarwinLazarillo de Tormes');
+ });
+
+ it("GH-150: Inverted sections print when they shouldn't", function () {
+ var string = '{{^set}}not set{{/set}} :: {{#set}}set{{/set}}';
+
+ expectTemplate(string)
+ .withMessage(
+ "inverted sections run when property isn't present in context"
+ )
+ .toCompileTo('not set :: ');
+
+ expectTemplate(string)
+ .withInput({ set: undefined })
+ .withMessage('inverted sections run when property is undefined')
+ .toCompileTo('not set :: ');
+
+ expectTemplate(string)
+ .withInput({ set: false })
+ .withMessage('inverted sections run when property is false')
+ .toCompileTo('not set :: ');
+
+ expectTemplate(string)
+ .withInput({ set: true })
+ .withMessage("inverted sections don't run when property is true")
+ .toCompileTo(' :: set');
+ });
+
+ it('GH-158: Using array index twice, breaks the template', function () {
+ expectTemplate('{{arr.[0]}}, {{arr.[1]}}')
+ .withInput({ arr: [1, 2] })
+ .withMessage('it works as expected')
+ .toCompileTo('1, 2');
+ });
+
+ it("bug reported by @fat where lambdas weren't being properly resolved", function () {
+ var string =
+ 'This is a slightly more complicated {{thing}}..\n' +
+ '{{! Just ignore this business. }}\n' +
+ 'Check this out:\n' +
+ '{{#hasThings}}\n' +
+ '
\n' +
+ '{{#things}}\n' +
+ '
{{word}}
\n' +
+ '{{/things}}
.\n' +
+ '{{/hasThings}}\n' +
+ '{{^hasThings}}\n' +
+ '\n' +
+ 'Nothing to check out...\n' +
+ '{{/hasThings}}';
+
+ var data = {
+ thing: function () {
+ return 'blah';
+ },
+ things: [
+ { className: 'one', word: '@fat' },
+ { className: 'two', word: '@dhg' },
+ { className: 'three', word: '@sayrer' },
+ ],
+ hasThings: function () {
+ return true;
+ },
+ };
+
+ var output =
+ 'This is a slightly more complicated blah..\n' +
+ 'Check this out:\n' +
+ '
\n' +
+ '
@fat
\n' +
+ '
@dhg
\n' +
+ '
@sayrer
\n' +
+ '
.\n';
+
+ expectTemplate(string).withInput(data).toCompileTo(output);
+ });
+
+ it('GH-408: Multiple loops fail', function () {
+ expectTemplate(
+ '{{#.}}{{name}}{{/.}}{{#.}}{{name}}{{/.}}{{#.}}{{name}}{{/.}}'
+ )
+ .withInput([
+ { name: 'John Doe', location: { city: 'Chicago' } },
+ { name: 'Jane Doe', location: { city: 'New York' } },
+ ])
+ .withMessage('It should output multiple times')
+ .toCompileTo('John DoeJane DoeJohn DoeJane DoeJohn DoeJane Doe');
+ });
+
+ it('GS-428: Nested if else rendering', function () {
+ var succeedingTemplate =
+ '{{#inverse}} {{#blk}} Unexpected {{/blk}} {{else}} {{#blk}} Expected {{/blk}} {{/inverse}}';
+ var failingTemplate =
+ '{{#inverse}} {{#blk}} Unexpected {{/blk}} {{else}} {{#blk}} Expected {{/blk}} {{/inverse}}';
+
+ var helpers = {
+ blk: function (block) {
+ return block.fn('');
+ },
+ inverse: function (block) {
+ return block.inverse('');
+ },
+ };
+
+ expectTemplate(succeedingTemplate)
+ .withHelpers(helpers)
+ .toCompileTo(' Expected ');
+
+ expectTemplate(failingTemplate)
+ .withHelpers(helpers)
+ .toCompileTo(' Expected ');
+ });
+
+ it('GH-458: Scoped this identifier', function () {
+ expectTemplate('{{./foo}}').withInput({ foo: 'bar' }).toCompileTo('bar');
+ });
+
+ it('GH-375: Unicode line terminators', function () {
+ expectTemplate('\u2028').toCompileTo('\u2028');
+ });
+
+ it('GH-534: Object prototype aliases', function () {
+ /* eslint-disable no-extend-native */
+ Object.prototype[0xd834] = true;
+
+ expectTemplate('{{foo}}').withInput({ foo: 'bar' }).toCompileTo('bar');
+
+ delete Object.prototype[0xd834];
+ /* eslint-enable no-extend-native */
+ });
+
+ it('GH-437: Matching escaping', function () {
+ expectTemplate('{{{a}}').toThrow(Error, /Parse error on/);
+ expectTemplate('{{a}}}').toThrow(Error, /Parse error on/);
+ });
+
+ it('GH-676: Using array in escaping mustache fails', function () {
+ var data = { arr: [1, 2] };
+
+ expectTemplate('{{arr}}')
+ .withInput(data)
+ .withMessage('it works as expected')
+ .toCompileTo(data.arr.toString());
+ });
+
+ it('Mustache man page', function () {
+ expectTemplate(
+ 'Hello {{name}}. You have just won ${{value}}!{{#in_ca}} Well, ${{taxed_value}}, after taxes.{{/in_ca}}'
+ )
+ .withInput({
+ name: 'Chris',
+ value: 10000,
+ taxed_value: 10000 - 10000 * 0.4,
+ in_ca: true,
+ })
+ .withMessage('the hello world mustache example works')
+ .toCompileTo(
+ 'Hello Chris. You have just won $10000! Well, $6000, after taxes.'
+ );
+ });
+
+ it('GH-731: zero context rendering', function () {
+ expectTemplate('{{#foo}} This is {{bar}} ~ {{/foo}}')
+ .withInput({
+ foo: 0,
+ bar: 'OK',
+ })
+ .toCompileTo(' This is ~ ');
+ });
+
+ it('GH-820: zero pathed rendering', function () {
+ expectTemplate('{{foo.bar}}').withInput({ foo: 0 }).toCompileTo('');
+ });
+
+ it('GH-837: undefined values for helpers', function () {
+ expectTemplate('{{str bar.baz}}')
+ .withHelpers({
+ str: function (value) {
+ return value + '';
+ },
+ })
+ .toCompileTo('undefined');
+ });
+
+ it('GH-926: Depths and de-dupe', function () {
+ expectTemplate(
+ '{{#if dater}}{{#each data}}{{../name}}{{/each}}{{else}}{{#each notData}}{{../name}}{{/each}}{{/if}}'
+ )
+ .withInput({
+ name: 'foo',
+ data: [1],
+ notData: [1],
+ })
+ .toCompileTo('foo');
+ });
+
+ it('GH-1021: Each empty string key', function () {
+ expectTemplate('{{#each data}}Key: {{@key}}\n{{/each}}')
+ .withInput({
+ data: {
+ '': 'foo',
+ name: 'Chris',
+ value: 10000,
+ },
+ })
+ .toCompileTo('Key: \nKey: name\nKey: value\n');
+ });
+
+ it('GH-1054: Should handle simple safe string responses', function () {
+ expectTemplate('{{#wrap}}{{>partial}}{{/wrap}}')
+ .withHelpers({
+ wrap: function (options) {
+ return new Handlebars.SafeString(options.fn());
+ },
+ })
+ .withPartials({
+ partial: '{{#wrap}}{{/wrap}}',
+ })
+ .toCompileTo('');
+ });
+
+ it('GH-1065: Sparse arrays', function () {
+ var array = [];
+ array[1] = 'foo';
+ array[3] = 'bar';
+ expectTemplate('{{#each array}}{{@index}}{{.}}{{/each}}')
+ .withInput({ array: array })
+ .toCompileTo('1foo3bar');
+ });
+
+ it('GH-1093: Undefined helper context', function () {
+ expectTemplate('{{#each obj}}{{{helper}}}{{.}}{{/each}}')
+ .withInput({ obj: { foo: undefined, bar: 'bat' } })
+ .withHelpers({
+ helper: function () {
+ // It's valid to execute a block against an undefined context, but
+ // helpers can not do so, so we expect to have an empty object here;
+ for (var name in this) {
+ if (Object.prototype.hasOwnProperty.call(this, name)) {
+ return 'found';
+ }
+ }
+ // And to make IE happy, check for the known string as length is not enumerated.
+ return this === 'bat' ? 'found' : 'not';
+ },
+ })
+ .toCompileTo('notfoundbat');
+ });
+
+ it('should support multiple levels of inline partials', function () {
+ expectTemplate(
+ '{{#> layout}}{{#*inline "subcontent"}}subcontent{{/inline}}{{/layout}}'
+ )
+ .withPartials({
+ doctype: 'doctype{{> content}}',
+ layout:
+ '{{#> doctype}}{{#*inline "content"}}layout{{> subcontent}}{{/inline}}{{/doctype}}',
+ })
+ .toCompileTo('doctypelayoutsubcontent');
+ });
+
+ it('GH-1089: should support failover content in multiple levels of inline partials', function () {
+ expectTemplate('{{#> layout}}{{/layout}}')
+ .withPartials({
+ doctype: 'doctype{{> content}}',
+ layout:
+ '{{#> doctype}}{{#*inline "content"}}layout{{#> subcontent}}subcontent{{/subcontent}}{{/inline}}{{/doctype}}',
+ })
+ .toCompileTo('doctypelayoutsubcontent');
+ });
+
+ it('GH-1099: should support greater than 3 nested levels of inline partials', function () {
+ expectTemplate('{{#> layout}}Outer{{/layout}}')
+ .withPartials({
+ layout: '{{#> inner}}Inner{{/inner}}{{> @partial-block }}',
+ inner: '',
+ })
+ .toCompileTo('Outer');
+ });
+
+ it('GH-1135 : Context handling within each iteration', function () {
+ expectTemplate(
+ '{{#each array}}\n' +
+ ' 1. IF: {{#if true}}{{../name}}-{{../../name}}-{{../../../name}}{{/if}}\n' +
+ ' 2. MYIF: {{#myif true}}{{../name}}={{../../name}}={{../../../name}}{{/myif}}\n' +
+ '{{/each}}'
+ )
+ .withInput({ array: [1], name: 'John' })
+ .withHelpers({
+ myif: function (conditional, options) {
+ if (conditional) {
+ return options.fn(this);
+ } else {
+ return options.inverse(this);
+ }
+ },
+ })
+ .toCompileTo(' 1. IF: John--\n' + ' 2. MYIF: John==\n');
+ });
+
+ it('GH-1186: Support block params for existing programs', function () {
+ expectTemplate(
+ '{{#*inline "test"}}{{> @partial-block }}{{/inline}}' +
+ '{{#>test }}{{#each listOne as |item|}}{{ item }}{{/each}}{{/test}}' +
+ '{{#>test }}{{#each listTwo as |item|}}{{ item }}{{/each}}{{/test}}'
+ )
+ .withInput({
+ listOne: ['a'],
+ 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'],
+ })
+ .withMessage('')
+ .toCompileTo('parent=parent parent=parent ');
+ });
+
+ it('GH-1341: 4.0.7 release breaks {{#if @partial-block}} usage', function () {
+ expectTemplate('template {{>partial}} template')
+ .withPartials({
+ partialWithBlock:
+ '{{#if @partial-block}} block {{> @partial-block}} block {{/if}}',
+ partial: '{{#> partialWithBlock}} partial {{/partialWithBlock}}',
+ })
+ .toCompileTo('template block partial block template');
+ });
+
+ describe('GH-1561: 4.3.x should still work with precompiled templates from 4.0.0 <= x < 4.3.0', function () {
+ it('should compile and execute templates', function () {
+ var newHandlebarsInstance = Handlebars.create();
+
+ registerTemplate(newHandlebarsInstance, compiledTemplateVersion7());
+ newHandlebarsInstance.registerHelper('loud', function (value) {
+ return value.toUpperCase();
+ });
+ var result = newHandlebarsInstance.templates['test.hbs']({
+ name: 'yehuda',
+ });
+ expect(result.trim()).toBe('YEHUDA');
+ });
+
+ it('should call "helperMissing" if a helper is missing', function () {
+ var newHandlebarsInstance = Handlebars.create();
+
+ expect(function () {
+ registerTemplate(newHandlebarsInstance, compiledTemplateVersion7());
+ newHandlebarsInstance.templates['test.hbs']({});
+ }).toThrow('Missing helper: "loud"');
+ });
+
+ it('should pass "options.lookupProperty" to "lookup"-helper, even with old templates', function () {
+ var newHandlebarsInstance = Handlebars.create();
+ registerTemplate(
+ newHandlebarsInstance,
+ compiledTemplateVersion7_usingLookupHelper()
+ );
+
+ newHandlebarsInstance.templates['test.hbs']({});
+
+ expect(
+ newHandlebarsInstance.templates['test.hbs']({
+ property: 'a',
+ test: { a: 'b' },
+ })
+ ).toBe('b');
+ });
+
+ function registerTemplate(Handlebars, compileTemplate) {
+ var template = Handlebars.template,
+ templates = (Handlebars.templates = Handlebars.templates || {});
+ templates['test.hbs'] = template(compileTemplate);
+ }
+
+ function compiledTemplateVersion7() {
+ return {
+ compiler: [7, '>= 4.0.0'],
+ main: function (container, depth0, helpers, partials, data) {
+ return (
+ container.escapeExpression(
+ (
+ helpers.loud ||
+ (depth0 && depth0.loud) ||
+ helpers.helperMissing
+ ).call(
+ depth0 != null ? depth0 : container.nullContext || {},
+ depth0 != null ? depth0.name : depth0,
+ { name: 'loud', hash: {}, data: data }
+ )
+ ) + '\n\n'
+ );
+ },
+ useData: true,
+ };
+ }
+
+ function compiledTemplateVersion7_usingLookupHelper() {
+ // This is the compiled version of "{{lookup test property}}"
+ return {
+ compiler: [7, '>= 4.0.0'],
+ main: function (container, depth0, helpers, partials, data) {
+ return container.escapeExpression(
+ helpers.lookup.call(
+ depth0 != null ? depth0 : container.nullContext || {},
+ depth0 != null ? depth0.test : depth0,
+ depth0 != null ? depth0.property : depth0,
+ {
+ name: 'lookup',
+ hash: {},
+ data: data,
+ }
+ )
+ );
+ },
+ useData: true,
+ };
+ }
+ });
+
+ it('should allow hash with protected array names', function () {
+ expectTemplate('{{helpa length="foo"}}')
+ .withInput({ array: [1], name: 'John' })
+ .withHelpers({
+ helpa: function (options) {
+ return options.hash.length;
+ },
+ })
+ .toCompileTo('foo');
+ });
+
+ describe('GH-1598: Performance degradation for partials since v4.3.0', function () {
+ // Do not run test for runs without compiler
+ if (!Handlebars.compile) {
+ return;
+ }
+
+ var newHandlebarsInstance;
+ beforeEach(function () {
+ newHandlebarsInstance = Handlebars.create();
+ });
+ afterEach(function () {
+ vi.restoreAllMocks();
+ });
+
+ it('should only compile global partials once', function () {
+ var templateSpy = vi.spyOn(newHandlebarsInstance, 'template');
+ newHandlebarsInstance.registerPartial({
+ 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();
+ });
+ });
+
+ describe("GH-1639: TypeError: Cannot read property 'apply' of undefined\" when handlebars version > 4.6.0 (undocumented, deprecated usage)", function () {
+ it('should treat undefined helpers like non-existing helpers', function () {
+ expectTemplate('{{foo}}')
+ .withHelper('foo', undefined)
+ .withInput({ foo: 'bar' })
+ .toCompileTo('bar');
+ });
+ });
+});
diff --git a/spec/require.js b/spec/require.js
new file mode 100644
index 000000000..241f8e86b
--- /dev/null
+++ b/spec/require.js
@@ -0,0 +1,23 @@
+if (typeof require !== 'undefined' && require.extensions['.handlebars']) {
+ describe('Require', function () {
+ it('Load .handlebars files with require()', function () {
+ var template = require('./artifacts/example_1');
+ expect(template).toBe(require('./artifacts/example_1.handlebars'));
+
+ var expected = 'foo\n';
+ var result = template({ foo: 'foo' });
+
+ expect(result).toBe(expected);
+ });
+
+ it('Load .hbs files with require()', function () {
+ var template = require('./artifacts/example_2');
+ expect(template).toBe(require('./artifacts/example_2.hbs'));
+
+ var expected = 'Hello, World!\n';
+ var result = template({ name: 'World' });
+
+ expect(result).toBe(expected);
+ });
+ });
+}
diff --git a/spec/runtime.js b/spec/runtime.js
new file mode 100644
index 000000000..1cb10fc79
--- /dev/null
+++ b/spec/runtime.js
@@ -0,0 +1,57 @@
+describe('runtime', function () {
+ describe('#template', function () {
+ it('should throw on invalid templates', function () {
+ expect(function () {
+ Handlebars.template({});
+ }).toThrow('Unknown template object: object');
+ expect(function () {
+ Handlebars.template();
+ }).toThrow('Unknown template object: undefined');
+ expect(function () {
+ Handlebars.template('');
+ }).toThrow('Unknown template object: string');
+ });
+ it('should throw on version mismatch', function () {
+ expect(function () {
+ Handlebars.template({
+ main: {},
+ compiler: [Handlebars.COMPILER_REVISION + 1],
+ });
+ }).toThrow(
+ /Template was precompiled with a newer version of Handlebars than the current runtime/
+ );
+ expect(function () {
+ Handlebars.template({
+ main: {},
+ compiler: [Handlebars.LAST_COMPATIBLE_COMPILER_REVISION - 1],
+ });
+ }).toThrow(
+ /Template was precompiled with an older version of Handlebars than the current runtime/
+ );
+ expect(function () {
+ Handlebars.template({
+ main: {},
+ });
+ }).toThrow(
+ /Template was precompiled with an older version of Handlebars than the current runtime/
+ );
+ });
+ });
+
+ describe('#noConflict', function () {
+ it('should reset on no conflict', function () {
+ if (!CompilerContext.browser) {
+ return;
+ }
+ var reset = Handlebars;
+ Handlebars.noConflict();
+ expect(Handlebars).toBe('no-conflict');
+
+ Handlebars = 'really, none';
+ reset.noConflict();
+ expect(Handlebars).toBe('really, none');
+
+ Handlebars = reset;
+ });
+ });
+});
diff --git a/spec/security.js b/spec/security.js
new file mode 100644
index 000000000..8187fc611
--- /dev/null
+++ b/spec/security.js
@@ -0,0 +1,457 @@
+describe('security issues', function () {
+ describe('GH-1495: Prevent Remote Code Execution via constructor', function () {
+ it('should not allow constructors to be accessed', function () {
+ expectTemplate('{{lookup (lookup this "constructor") "name"}}')
+ .withInput({})
+ .toCompileTo('');
+
+ expectTemplate('{{constructor.name}}').withInput({}).toCompileTo('');
+ });
+
+ it('GH-1603: should not allow constructors to be accessed (lookup via toString)', function () {
+ expectTemplate('{{lookup (lookup this (list "constructor")) "name"}}')
+ .withInput({})
+ .withHelper('list', function (element) {
+ return [element];
+ })
+ .toCompileTo('');
+ });
+
+ it('should allow the "constructor" property to be accessed if it is an "ownProperty"', function () {
+ expectTemplate('{{constructor.name}}')
+ .withInput({ constructor: { name: 'here we go' } })
+ .toCompileTo('here we go');
+
+ expectTemplate('{{lookup (lookup this "constructor") "name"}}')
+ .withInput({ constructor: { name: 'here we go' } })
+ .toCompileTo('here we go');
+ });
+
+ it('should allow the "constructor" property to be accessed if it is an "own property"', function () {
+ expectTemplate('{{lookup (lookup this "constructor") "name"}}')
+ .withInput({ constructor: { name: 'here we go' } })
+ .toCompileTo('here we go');
+ });
+ });
+
+ describe('GH-1558: Prevent explicit call of helperMissing-helpers', function () {
+ if (!Handlebars.compile) {
+ return;
+ }
+
+ describe('without the option "allowExplicitCallOfHelperMissing"', function () {
+ it('should throw an exception when calling "{{helperMissing}}" ', function () {
+ expectTemplate('{{helperMissing}}').toThrow(Error);
+ });
+
+ it('should throw an exception when calling "{{#helperMissing}}{{/helperMissing}}" ', function () {
+ expectTemplate('{{#helperMissing}}{{/helperMissing}}').toThrow(Error);
+ });
+
+ it('should throw an exception when calling "{{blockHelperMissing "abc" .}}" ', function () {
+ var functionCalls = [];
+ expect(function () {
+ var template = Handlebars.compile('{{blockHelperMissing "abc" .}}');
+ template({
+ fn: function () {
+ functionCalls.push('called');
+ },
+ });
+ }).toThrow();
+ expect(functionCalls.length).toBe(0);
+ });
+
+ it('should throw an exception when calling "{{#blockHelperMissing .}}{{/blockHelperMissing}}"', function () {
+ expectTemplate('{{#blockHelperMissing .}}{{/blockHelperMissing}}')
+ .withInput({
+ fn: function () {
+ return 'functionInData';
+ },
+ })
+ .toThrow(Error);
+ });
+ });
+
+ describe('with the option "allowCallsToHelperMissing" set to true', function () {
+ it('should not throw an exception when calling "{{helperMissing}}" ', function () {
+ var template = Handlebars.compile('{{helperMissing}}');
+ template({}, { allowCallsToHelperMissing: true });
+ });
+
+ it('should not throw an exception when calling "{{#helperMissing}}{{/helperMissing}}" ', function () {
+ var template = Handlebars.compile(
+ '{{#helperMissing}}{{/helperMissing}}'
+ );
+ template({}, { allowCallsToHelperMissing: true });
+ });
+
+ it('should not throw an exception when calling "{{blockHelperMissing "abc" .}}" ', function () {
+ var functionCalls = [];
+ var template = Handlebars.compile('{{blockHelperMissing "abc" .}}');
+ template(
+ {
+ fn: function () {
+ functionCalls.push('called');
+ },
+ },
+ { allowCallsToHelperMissing: true }
+ );
+ expect(functionCalls.length).toBe(1);
+ });
+
+ it('should not throw an exception when calling "{{#blockHelperMissing .}}{{/blockHelperMissing}}"', function () {
+ var template = Handlebars.compile(
+ '{{#blockHelperMissing true}}sdads{{/blockHelperMissing}}'
+ );
+ template({}, { allowCallsToHelperMissing: true });
+ });
+ });
+ });
+
+ describe('GH-1563', function () {
+ var browserSupportsExploit =
+ {}.__defineGetter__ != null && {}.__lookupGetter__ != null;
+
+ it.skipIf(!browserSupportsExploit)(
+ 'should not allow to access constructor after overriding via __defineGetter__',
+ function () {
+ expectTemplate(
+ '{{__defineGetter__ "undefined" valueOf }}' +
+ '{{#with __lookupGetter__ }}' +
+ '{{__defineGetter__ "propertyIsEnumerable" (this.bind (this.bind 1)) }}' +
+ '{{constructor.name}}' +
+ '{{/with}}'
+ )
+ .withInput({})
+ .toThrow(/Missing helper: "__defineGetter__"/);
+ }
+ );
+ });
+
+ describe('GH-1595: dangerous properties', function () {
+ var templates = [
+ '{{constructor}}',
+ '{{__defineGetter__}}',
+ '{{__defineSetter__}}',
+ '{{__lookupGetter__}}',
+ '{{__proto__}}',
+ '{{lookup this "constructor"}}',
+ '{{lookup this "__defineGetter__"}}',
+ '{{lookup this "__defineSetter__"}}',
+ '{{lookup this "__lookupGetter__"}}',
+ '{{lookup this "__proto__"}}',
+ ];
+
+ templates.forEach(function (template) {
+ describe('access should be denied to ' + template, function () {
+ it('by default', function () {
+ expectTemplate(template).withInput({}).toCompileTo('');
+ });
+ it(' with proto-access enabled', function () {
+ expectTemplate(template)
+ .withInput({})
+ .withRuntimeOptions({
+ allowProtoPropertiesByDefault: true,
+ allowProtoMethodsByDefault: true,
+ })
+ .toCompileTo('');
+ });
+ });
+ });
+ });
+ describe('GH-1631: disallow access to prototype functions', function () {
+ function TestClass() {}
+
+ TestClass.prototype.aProperty = 'propertyValue';
+ TestClass.prototype.aMethod = function () {
+ return 'returnValue';
+ };
+
+ beforeEach(function () {
+ handlebarsEnv.resetLoggedPropertyAccesses();
+ });
+
+ afterEach(function () {
+ vi.restoreAllMocks();
+ });
+
+ describe('control access to prototype methods via "allowedProtoMethods"', function () {
+ checkProtoMethodAccess({});
+
+ describe('in compat mode', function () {
+ checkProtoMethodAccess({ compat: true });
+ });
+
+ function checkProtoMethodAccess(compileOptions) {
+ it('should be prohibited by default and log a warning', function () {
+ var spy = vi
+ .spyOn(console, 'error')
+ .mockImplementation(function () {});
+
+ expectTemplate('{{aMethod}}')
+ .withInput(new TestClass())
+ .withCompileOptions(compileOptions)
+ .toCompileTo('');
+
+ expect(spy).toHaveBeenCalledTimes(1);
+ expect(spy.mock.calls[0][0]).toMatch(
+ /Handlebars: Access has been denied/
+ );
+ });
+
+ it('should only log the warning once', function () {
+ var spy = vi
+ .spyOn(console, 'error')
+ .mockImplementation(function () {});
+
+ expectTemplate('{{aMethod}}')
+ .withInput(new TestClass())
+ .withCompileOptions(compileOptions)
+ .toCompileTo('');
+
+ expectTemplate('{{aMethod}}')
+ .withInput(new TestClass())
+ .withCompileOptions(compileOptions)
+ .toCompileTo('');
+
+ expect(spy).toHaveBeenCalledTimes(1);
+ expect(spy.mock.calls[0][0]).toMatch(
+ /Handlebars: Access has been denied/
+ );
+ });
+
+ it('can be allowed, which disables the warning', function () {
+ var spy = vi
+ .spyOn(console, 'error')
+ .mockImplementation(function () {});
+
+ expectTemplate('{{aMethod}}')
+ .withInput(new TestClass())
+ .withCompileOptions(compileOptions)
+ .withRuntimeOptions({
+ allowedProtoMethods: {
+ aMethod: true,
+ },
+ })
+ .toCompileTo('returnValue');
+
+ expect(spy).not.toHaveBeenCalled();
+ });
+
+ it('can be turned on by default, which disables the warning', function () {
+ var spy = vi
+ .spyOn(console, 'error')
+ .mockImplementation(function () {});
+
+ expectTemplate('{{aMethod}}')
+ .withInput(new TestClass())
+ .withCompileOptions(compileOptions)
+ .withRuntimeOptions({
+ allowProtoMethodsByDefault: true,
+ })
+ .toCompileTo('returnValue');
+
+ expect(spy).not.toHaveBeenCalled();
+ });
+
+ it('can be turned off by default, which disables the warning', function () {
+ var spy = vi
+ .spyOn(console, 'error')
+ .mockImplementation(function () {});
+
+ expectTemplate('{{aMethod}}')
+ .withInput(new TestClass())
+ .withCompileOptions(compileOptions)
+ .withRuntimeOptions({
+ allowProtoMethodsByDefault: false,
+ })
+ .toCompileTo('');
+
+ expect(spy).not.toHaveBeenCalled();
+ });
+
+ it('can be turned off, if turned on by default', function () {
+ expectTemplate('{{aMethod}}')
+ .withInput(new TestClass())
+ .withCompileOptions(compileOptions)
+ .withRuntimeOptions({
+ allowProtoMethodsByDefault: true,
+ allowedProtoMethods: {
+ aMethod: false,
+ },
+ })
+ .toCompileTo('');
+ });
+ }
+
+ it('should cause the recursive lookup by default (in "compat" mode)', function () {
+ expectTemplate('{{#aString}}{{trim}}{{/aString}}')
+ .withInput({ aString: ' abc ', trim: 'trim' })
+ .withCompileOptions({ compat: true })
+ .toCompileTo('trim');
+ });
+
+ it('should not cause the recursive lookup if allowed through options(in "compat" mode)', function () {
+ expectTemplate('{{#aString}}{{trim}}{{/aString}}')
+ .withInput({ aString: ' abc ', trim: 'trim' })
+ .withCompileOptions({ compat: true })
+ .withRuntimeOptions({
+ allowedProtoMethods: {
+ trim: true,
+ },
+ })
+ .toCompileTo('abc');
+ });
+ });
+
+ describe('control access to prototype non-methods via "allowedProtoProperties" and "allowProtoPropertiesByDefault', function () {
+ checkProtoPropertyAccess({});
+
+ describe('in compat-mode', function () {
+ checkProtoPropertyAccess({ compat: true });
+ });
+
+ describe('in strict-mode', function () {
+ checkProtoPropertyAccess({ strict: true });
+ });
+
+ function checkProtoPropertyAccess(compileOptions) {
+ it('should be prohibited by default and log a warning', function () {
+ var spy = vi
+ .spyOn(console, 'error')
+ .mockImplementation(function () {});
+
+ expectTemplate('{{aProperty}}')
+ .withInput(new TestClass())
+ .withCompileOptions(compileOptions)
+ .toCompileTo('');
+
+ expect(spy).toHaveBeenCalledTimes(1);
+ expect(spy.mock.calls[0][0]).toMatch(
+ /Handlebars: Access has been denied/
+ );
+ });
+
+ it('can be explicitly prohibited by default, which disables the warning', function () {
+ var spy = vi
+ .spyOn(console, 'error')
+ .mockImplementation(function () {});
+
+ expectTemplate('{{aProperty}}')
+ .withInput(new TestClass())
+ .withCompileOptions(compileOptions)
+ .withRuntimeOptions({
+ allowProtoPropertiesByDefault: false,
+ })
+ .toCompileTo('');
+
+ expect(spy).not.toHaveBeenCalled();
+ });
+
+ it('can be turned on, which disables the warning', function () {
+ var spy = vi
+ .spyOn(console, 'error')
+ .mockImplementation(function () {});
+
+ expectTemplate('{{aProperty}}')
+ .withInput(new TestClass())
+ .withCompileOptions(compileOptions)
+ .withRuntimeOptions({
+ allowedProtoProperties: {
+ aProperty: true,
+ },
+ })
+ .toCompileTo('propertyValue');
+
+ expect(spy).not.toHaveBeenCalled();
+ });
+
+ it('can be turned on by default, which disables the warning', function () {
+ var spy = vi
+ .spyOn(console, 'error')
+ .mockImplementation(function () {});
+
+ expectTemplate('{{aProperty}}')
+ .withInput(new TestClass())
+ .withCompileOptions(compileOptions)
+ .withRuntimeOptions({
+ allowProtoPropertiesByDefault: true,
+ })
+ .toCompileTo('propertyValue');
+
+ expect(spy).not.toHaveBeenCalled();
+ });
+
+ it('can be turned off, if turned on by default', function () {
+ expectTemplate('{{aProperty}}')
+ .withInput(new TestClass())
+ .withCompileOptions(compileOptions)
+ .withRuntimeOptions({
+ allowProtoPropertiesByDefault: true,
+ allowedProtoProperties: {
+ aProperty: false,
+ },
+ })
+ .toCompileTo('');
+ });
+ }
+ });
+
+ describe('compatibility with old runtimes, that do not provide the function "container.lookupProperty"', function () {
+ beforeEach(function simulateRuntimeWithoutLookupProperty() {
+ var oldTemplateMethod = handlebarsEnv.template;
+ vi.spyOn(handlebarsEnv, 'template').mockImplementation(
+ function (templateSpec) {
+ templateSpec.main = wrapToAdjustContainer(templateSpec.main);
+ return oldTemplateMethod.call(this, templateSpec);
+ }
+ );
+ });
+
+ afterEach(function () {
+ vi.restoreAllMocks();
+ });
+
+ it('should work with simple properties', function () {
+ expectTemplate('{{aProperty}}')
+ .withInput({ aProperty: 'propertyValue' })
+ .toCompileTo('propertyValue');
+ });
+
+ it('should work with Array.prototype.length', function () {
+ expectTemplate('{{anArray.length}}')
+ .withInput({ anArray: ['a', 'b', 'c'] })
+ .toCompileTo('3');
+ });
+ });
+ });
+
+ describe('escapes template variables', function () {
+ it('in compat mode', function () {
+ expectTemplate("{{'a\\b'}}")
+ .withCompileOptions({ compat: true })
+ .withInput({ 'a\\b': 'c' })
+ .toCompileTo('c');
+ });
+
+ it('in default mode', function () {
+ expectTemplate("{{'a\\b'}}")
+ .withCompileOptions()
+ .withInput({ 'a\\b': 'c' })
+ .toCompileTo('c');
+ });
+ it('in default mode', function () {
+ expectTemplate("{{'a\\b'}}")
+ .withCompileOptions({ strict: true })
+ .withInput({ 'a\\b': 'c' })
+ .toCompileTo('c');
+ });
+ });
+});
+
+function wrapToAdjustContainer(precompiledTemplateFunction) {
+ return function templateFunctionWrapper(container /*, more args */) {
+ delete container.lookupProperty;
+ return precompiledTemplateFunction.apply(this, arguments);
+ };
+}
diff --git a/spec/source-map.js b/spec/source-map.js
new file mode 100644
index 000000000..4565c3f0e
--- /dev/null
+++ b/spec/source-map.js
@@ -0,0 +1,61 @@
+try {
+ if (typeof define !== 'function' || !define.amd) {
+ var SourceMap = require('source-map'),
+ SourceMapConsumer = SourceMap.SourceMapConsumer;
+ }
+} catch {
+ /* NOP for in browser */
+}
+
+describe('source-map', function () {
+ if (!Handlebars.precompile || !SourceMap) {
+ return;
+ }
+
+ it('should safely include source map info', function () {
+ var template = Handlebars.precompile('{{hello}}', {
+ destName: 'dest.js',
+ srcName: 'src.hbs',
+ });
+
+ expect(template.code).toBeTruthy();
+ if (CompilerContext.browser) {
+ expect(template.map).toBeFalsy();
+ } else {
+ expect(template.map).toBeTruthy();
+ }
+ });
+ it('should map source properly', async function () {
+ var templateSource =
+ ' b{{hello}} \n {{bar}}a {{#block arg hash=(subex 1 subval)}}{{/block}}',
+ template = Handlebars.precompile(templateSource, {
+ destName: 'dest.js',
+ srcName: 'src.hbs',
+ });
+
+ if (template.map) {
+ var consumer = await new SourceMapConsumer(template.map),
+ lines = template.code.split('\n'),
+ srcLines = templateSource.split('\n'),
+ generated = grepLine('" b"', lines),
+ source = grepLine(' b', srcLines);
+
+ var mapped = consumer.originalPositionFor(generated);
+ expect(mapped.line).toBe(source.line);
+ expect(mapped.column).toBe(source.column);
+ consumer.destroy();
+ }
+ });
+});
+
+function grepLine(token, lines) {
+ for (var i = 0; i < lines.length; i++) {
+ var column = lines[i].indexOf(token);
+ if (column >= 0) {
+ return {
+ line: i + 1,
+ column: column,
+ };
+ }
+ }
+}
diff --git a/spec/spec.js b/spec/spec.js
new file mode 100644
index 000000000..3eed33f5b
--- /dev/null
+++ b/spec/spec.js
@@ -0,0 +1,46 @@
+describe('spec', function () {
+ // NOP Under non-node environments
+ if (typeof process === 'undefined') {
+ return;
+ }
+
+ var fs = require('fs');
+
+ var specDir = __dirname + '/mustache/specs/';
+ var specs = fs.readdirSync(specDir).filter((name) => /.*\.json$/.test(name));
+
+ specs.forEach(function (name) {
+ var spec = require(specDir + name);
+ spec.tests.forEach(function (test) {
+ // Our lambda implementation knowingly deviates from the optional Mustache lambda spec
+ // We also do not support alternative delimiters
+ if (
+ name === '~lambdas.json' ||
+ // We also choose to throw if partials are not found
+ (name === 'partials.json' && test.name === 'Failed Lookup') ||
+ // We nest the entire response from partials, not just the literals
+ (name === 'partials.json' && test.name === 'Standalone Indentation') ||
+ /\{\{=/.test(test.template) ||
+ Object.values(test.partials || {}).some((value) => /\{\{=/.test(value))
+ ) {
+ it.skip(name + ' - ' + test.name);
+ return;
+ }
+
+ var data = Object.assign({}, test.data); // Shallow copy
+ if (data.lambda) {
+ // Blergh
+ /* eslint-disable-next-line no-eval */
+ data.lambda = eval('(' + data.lambda.js + ')');
+ }
+ it(name + ' - ' + test.name, function () {
+ expectTemplate(test.template)
+ .withInput(data)
+ .withPartials(test.partials || {})
+ .withCompileOptions({ compat: true })
+ .withMessage(test.desc + ' "' + test.template + '"')
+ .toCompileTo(test.expected);
+ });
+ });
+ });
+});
diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb
deleted file mode 100644
index a83d16b3f..000000000
--- a/spec/spec_helper.rb
+++ /dev/null
@@ -1,157 +0,0 @@
-require "v8"
-
-# Monkey patches due to bugs in RubyRacer
-class V8::JSError
- def initialize(try, to)
- @to = to
- begin
- super(initialize_unsafe(try))
- rescue Exception => e
- # Original code does not make an Array here
- @boundaries = [Boundary.new(:rbframes => e.backtrace)]
- @value = e
- super("BUG! please report. JSError#initialize failed!: #{e.message}")
- end
- end
-
- def parse_js_frames(try)
- raw = @to.rb(try.StackTrace())
- if raw && !raw.empty?
- raw.split("\n")[1..-1].tap do |frames|
- # Original code uses strip!, and the frames are not guaranteed to be strippable
- frames.each {|frame| frame.strip.chomp!(",")}
- end
- else
- []
- end
- end
-end
-
-module Handlebars
- module Spec
- def self.js_backtrace(context)
- begin
- context.eval("throw")
- rescue V8::JSError => e
- return e.backtrace(:javascript)
- end
- end
-
- def self.remove_exports(string)
- match = string.match(%r{\A(.*?)^// BEGIN\(BROWSER\)\n(.*)\n^// END\(BROWSER\)(.*?)\Z}m)
- prelines = match ? match[1].count("\n") + 1 : 0
- ret = match ? match[2] : string
- ("\n" * prelines) + ret
- end
-
- def self.load_helpers(context)
- context["exports"] = nil
-
- context["p"] = proc do |val|
- p val if ENV["DEBUG_JS"]
- end
-
- context["puts"] = proc do |val|
- puts val if ENV["DEBUG_JS"]
- end
-
- context["puts_node"] = proc do |val|
- puts context["Handlebars"]["PrintVisitor"].new.accept(val)
- puts
- end
-
- context["puts_caller"] = proc do
- puts "BACKTRACE:"
- puts Handlebars::Spec.js_backtrace(context)
- puts
- end
- end
-
- def self.js_load(context, file)
- str = File.read(file)
- context.eval(remove_exports(str), file)
- end
-
- CONTEXT = V8::Context.new
- CONTEXT.instance_eval do |context|
- Handlebars::Spec.load_helpers(context);
-
- Handlebars::Spec.js_load(context, 'lib/handlebars/base.js');
- Handlebars::Spec.js_load(context, 'lib/handlebars/utils.js');
- Handlebars::Spec.js_load(context, 'lib/handlebars/runtime.js');
-
- context["CompilerContext"] = {}
- CompilerContext = context["CompilerContext"]
- CompilerContext["compile"] = proc do |*args|
- template, options = args[0], args[1] || nil
- templateSpec = COMPILE_CONTEXT["Handlebars"]["precompile"].call(template, options);
- context["Handlebars"]["template"].call(context.eval("(#{templateSpec})"));
- end
- CompilerContext["compileWithPartial"] = proc do |*args|
- template, options = args[0], args[1] || nil
- FULL_CONTEXT["Handlebars"]["compile"].call(template, options);
- end
- end
-
- COMPILE_CONTEXT = V8::Context.new
- COMPILE_CONTEXT.instance_eval do |context|
- Handlebars::Spec.load_helpers(context);
-
- Handlebars::Spec.js_load(context, 'lib/handlebars/base.js');
- Handlebars::Spec.js_load(context, 'lib/handlebars/utils.js');
- Handlebars::Spec.js_load(context, 'lib/handlebars/compiler/parser.js');
- Handlebars::Spec.js_load(context, 'lib/handlebars/compiler/base.js');
- Handlebars::Spec.js_load(context, 'lib/handlebars/compiler/ast.js');
- Handlebars::Spec.js_load(context, 'lib/handlebars/compiler/visitor.js');
- Handlebars::Spec.js_load(context, 'lib/handlebars/compiler/printer.js');
- Handlebars::Spec.js_load(context, 'lib/handlebars/compiler/compiler.js');
-
- context["Handlebars"]["logger"]["level"] = ENV["DEBUG_JS"] ? context["Handlebars"]["logger"][ENV["DEBUG_JS"]] : 4
-
- context["Handlebars"]["logger"]["log"] = proc do |level, str|
- logger_level = context["Handlebars"]["logger"]["level"].to_i
-
- if logger_level <= level
- puts str
- end
- end
- end
-
- FULL_CONTEXT = V8::Context.new
- FULL_CONTEXT.instance_eval do |context|
- Handlebars::Spec.load_helpers(context);
-
- Handlebars::Spec.js_load(context, 'lib/handlebars/base.js');
- Handlebars::Spec.js_load(context, 'lib/handlebars/utils.js');
- Handlebars::Spec.js_load(context, 'lib/handlebars/compiler/parser.js');
- Handlebars::Spec.js_load(context, 'lib/handlebars/compiler/base.js');
- Handlebars::Spec.js_load(context, 'lib/handlebars/compiler/ast.js');
- Handlebars::Spec.js_load(context, 'lib/handlebars/compiler/visitor.js');
- Handlebars::Spec.js_load(context, 'lib/handlebars/compiler/printer.js');
- Handlebars::Spec.js_load(context, 'lib/handlebars/compiler/compiler.js');
- Handlebars::Spec.js_load(context, 'lib/handlebars/runtime.js');
-
- context["Handlebars"]["logger"]["level"] = ENV["DEBUG_JS"] ? context["Handlebars"]["logger"][ENV["DEBUG_JS"]] : 4
-
- context["Handlebars"]["logger"]["log"] = proc do |level, str|
- logger_level = context["Handlebars"]["logger"]["level"].to_i
-
- if logger_level <= level
- puts str
- end
- end
- end
- end
-end
-
-
-require "test/unit/assertions"
-
-RSpec.configure do |config|
- config.include Test::Unit::Assertions
-
- # Each is required to allow classes to mark themselves as compiler tests
- config.before(:each) do
- @context = @compiles ? Handlebars::Spec::COMPILE_CONTEXT : Handlebars::Spec::CONTEXT
- end
-end
diff --git a/spec/strict.js b/spec/strict.js
new file mode 100644
index 000000000..ad7884635
--- /dev/null
+++ b/spec/strict.js
@@ -0,0 +1,164 @@
+var Exception = Handlebars.Exception;
+
+describe('strict', function () {
+ describe('strict mode', function () {
+ it('should error on missing property lookup', function () {
+ expectTemplate('{{hello}}')
+ .withCompileOptions({ strict: true })
+ .toThrow(Exception, /"hello" not defined in/);
+ });
+
+ it('should error on missing child', function () {
+ expectTemplate('{{hello.bar}}')
+ .withCompileOptions({ strict: true })
+ .withInput({ hello: { bar: 'foo' } })
+ .toCompileTo('foo');
+
+ expectTemplate('{{hello.bar}}')
+ .withCompileOptions({ strict: true })
+ .withInput({ hello: {} })
+ .toThrow(Exception, /"bar" not defined in/);
+ });
+
+ it('should handle explicit undefined', function () {
+ expectTemplate('{{hello.bar}}')
+ .withCompileOptions({ strict: true })
+ .withInput({ hello: { bar: undefined } })
+ .toCompileTo('');
+ });
+
+ it('should error on missing property lookup in known helpers mode', function () {
+ expectTemplate('{{hello}}')
+ .withCompileOptions({
+ strict: true,
+ knownHelpersOnly: true,
+ })
+ .toThrow(Exception, /"hello" not defined in/);
+ });
+
+ it('should error on missing context', function () {
+ expectTemplate('{{hello}}')
+ .withCompileOptions({ strict: true })
+ .toThrow(Error);
+ });
+
+ it('should error on missing data lookup', function () {
+ var xt = expectTemplate('{{@hello}}').withCompileOptions({
+ strict: true,
+ });
+
+ xt.toThrow(Error);
+
+ xt.withRuntimeOptions({ data: { hello: 'foo' } }).toCompileTo('foo');
+ });
+
+ it('should not run helperMissing for helper calls', function () {
+ expectTemplate('{{hello foo}}')
+ .withCompileOptions({ strict: true })
+ .withInput({ foo: true })
+ .toThrow(Exception, /"hello" not defined in/);
+
+ expectTemplate('{{#hello foo}}{{/hello}}')
+ .withCompileOptions({ strict: true })
+ .withInput({ foo: true })
+ .toThrow(Exception, /"hello" not defined in/);
+ });
+
+ it('should throw on ambiguous blocks', function () {
+ expectTemplate('{{#hello}}{{/hello}}')
+ .withCompileOptions({ strict: true })
+ .toThrow(Exception, /"hello" not defined in/);
+
+ expectTemplate('{{^hello}}{{/hello}}')
+ .withCompileOptions({ strict: true })
+ .toThrow(Exception, /"hello" not defined in/);
+
+ expectTemplate('{{#hello.bar}}{{/hello.bar}}')
+ .withCompileOptions({ strict: true })
+ .withInput({ hello: {} })
+ .toThrow(Exception, /"bar" not defined in/);
+ });
+
+ it('should allow undefined parameters when passed to helpers', function () {
+ expectTemplate('{{#unless foo}}success{{/unless}}')
+ .withCompileOptions({ strict: true })
+ .toCompileTo('success');
+ });
+
+ it('should allow undefined hash when passed to helpers', function () {
+ expectTemplate('{{helper value=@foo}}')
+ .withCompileOptions({
+ strict: true,
+ })
+ .withHelpers({
+ helper: function (options) {
+ expect(options.hash).toHaveProperty('value');
+ expect(options.hash.value).toBeUndefined();
+ return 'success';
+ },
+ })
+ .toCompileTo('success');
+ });
+
+ it('should show error location on missing property lookup', function () {
+ expectTemplate('\n\n\n {{hello}}')
+ .withCompileOptions({ strict: true })
+ .toThrow(Exception, '"hello" not defined in [object Object] - 4:5');
+ });
+
+ it('should error contains correct location properties on missing property lookup', function () {
+ try {
+ var template = CompilerContext.compile('\n\n\n {{hello}}', {
+ strict: true,
+ });
+ template({});
+ } catch (error) {
+ expect(error.lineNumber).toBe(4);
+ expect(error.endLineNumber).toBe(4);
+ expect(error.column).toBe(5);
+ expect(error.endColumn).toBe(10);
+ }
+ });
+ });
+
+ describe('assume objects', function () {
+ it('should ignore missing property', function () {
+ expectTemplate('{{hello}}')
+ .withCompileOptions({ assumeObjects: true })
+ .toCompileTo('');
+ });
+
+ it('should ignore missing child', function () {
+ expectTemplate('{{hello.bar}}')
+ .withCompileOptions({ assumeObjects: true })
+ .withInput({ hello: {} })
+ .toCompileTo('');
+ });
+
+ it('should error on missing object', function () {
+ expectTemplate('{{hello.bar}}')
+ .withCompileOptions({ assumeObjects: true })
+ .toThrow(Error);
+ });
+
+ it('should error on missing context', function () {
+ expectTemplate('{{hello}}')
+ .withCompileOptions({ assumeObjects: true })
+ .withInput(undefined)
+ .toThrow(Error);
+ });
+
+ it('should error on missing data lookup', function () {
+ expectTemplate('{{@hello.bar}}')
+ .withCompileOptions({ assumeObjects: true })
+ .withInput(undefined)
+ .toThrow(Error);
+ });
+
+ it('should execute blockHelperMissing', function () {
+ expectTemplate('{{^hello}}foo{{/hello}}')
+ .withCompileOptions({ assumeObjects: true })
+ .toCompileTo('foo');
+ });
+ });
+});
diff --git a/spec/subexpressions.js b/spec/subexpressions.js
new file mode 100644
index 000000000..a5e397fbc
--- /dev/null
+++ b/spec/subexpressions.js
@@ -0,0 +1,218 @@
+describe('subexpressions', function () {
+ it('arg-less helper', function () {
+ expectTemplate('{{foo (bar)}}!')
+ .withHelpers({
+ foo: function (val) {
+ return val + val;
+ },
+ bar: function () {
+ return 'LOL';
+ },
+ })
+ .toCompileTo('LOLLOL!');
+ });
+
+ it('helper w args', function () {
+ expectTemplate('{{blog (equal a b)}}')
+ .withInput({ bar: 'LOL' })
+ .withHelpers({
+ blog: function (val) {
+ return 'val is ' + val;
+ },
+ equal: function (x, y) {
+ return x === y;
+ },
+ })
+ .toCompileTo('val is true');
+ });
+
+ it('mixed paths and helpers', function () {
+ expectTemplate('{{blog baz.bat (equal a b) baz.bar}}')
+ .withInput({ bar: 'LOL', baz: { bat: 'foo!', bar: 'bar!' } })
+ .withHelpers({
+ blog: function (val, that, theOther) {
+ return 'val is ' + val + ', ' + that + ' and ' + theOther;
+ },
+ equal: function (x, y) {
+ return x === y;
+ },
+ })
+ .toCompileTo('val is foo!, true and bar!');
+ });
+
+ it('supports much nesting', function () {
+ expectTemplate('{{blog (equal (equal true true) true)}}')
+ .withInput({ bar: 'LOL' })
+ .withHelpers({
+ blog: function (val) {
+ return 'val is ' + val;
+ },
+ equal: function (x, y) {
+ return x === y;
+ },
+ })
+ .toCompileTo('val is true');
+ });
+
+ it('GH-800 : Complex subexpressions', function () {
+ var context = { a: 'a', b: 'b', c: { c: 'c' }, d: 'd', e: { e: 'e' } };
+ var helpers = {
+ dash: function (a, b) {
+ return a + '-' + b;
+ },
+ concat: function (a, b) {
+ return a + b;
+ },
+ };
+
+ expectTemplate("{{dash 'abc' (concat a b)}}")
+ .withInput(context)
+ .withHelpers(helpers)
+ .toCompileTo('abc-ab');
+
+ expectTemplate('{{dash d (concat a b)}}')
+ .withInput(context)
+ .withHelpers(helpers)
+ .toCompileTo('d-ab');
+
+ expectTemplate('{{dash c.c (concat a b)}}')
+ .withInput(context)
+ .withHelpers(helpers)
+ .toCompileTo('c-ab');
+
+ expectTemplate('{{dash (concat a b) c.c}}')
+ .withInput(context)
+ .withHelpers(helpers)
+ .toCompileTo('ab-c');
+
+ expectTemplate('{{dash (concat a e.e) c.c}}')
+ .withInput(context)
+ .withHelpers(helpers)
+ .toCompileTo('ae-c');
+ });
+
+ it('provides each nested helper invocation its own options hash', function () {
+ var lastOptions = null;
+ var helpers = {
+ equal: function (x, y, options) {
+ if (!options || options === lastOptions) {
+ throw new Error('options hash was reused');
+ }
+ lastOptions = options;
+ return x === y;
+ },
+ };
+ expectTemplate('{{equal (equal true true) true}}')
+ .withHelpers(helpers)
+ .toCompileTo('true');
+ });
+
+ it('with hashes', function () {
+ expectTemplate("{{blog (equal (equal true true) true fun='yes')}}")
+ .withInput({ bar: 'LOL' })
+ .withHelpers({
+ blog: function (val) {
+ return 'val is ' + val;
+ },
+ equal: function (x, y) {
+ return x === y;
+ },
+ })
+ .toCompileTo('val is true');
+ });
+
+ it('as hashes', function () {
+ expectTemplate("{{blog fun=(equal (blog fun=1) 'val is 1')}}")
+ .withHelpers({
+ blog: function (options) {
+ return 'val is ' + options.hash.fun;
+ },
+ equal: function (x, y) {
+ return x === y;
+ },
+ })
+ .toCompileTo('val is true');
+ });
+
+ it('multiple subexpressions in a hash', function () {
+ expectTemplate(
+ '{{input aria-label=(t "Name") placeholder=(t "Example User")}}'
+ )
+ .withHelpers({
+ input: function (options) {
+ var hash = options.hash;
+ var ariaLabel = Handlebars.Utils.escapeExpression(hash['aria-label']);
+ var placeholder = Handlebars.Utils.escapeExpression(hash.placeholder);
+ return new Handlebars.SafeString(
+ ''
+ );
+ },
+ t: function (defaultString) {
+ return new Handlebars.SafeString(defaultString);
+ },
+ })
+ .toCompileTo('');
+ });
+
+ it('multiple subexpressions in a hash with context', function () {
+ expectTemplate(
+ '{{input aria-label=(t item.field) placeholder=(t item.placeholder)}}'
+ )
+ .withInput({
+ item: {
+ field: 'Name',
+ placeholder: 'Example User',
+ },
+ })
+ .withHelpers({
+ input: function (options) {
+ var hash = options.hash;
+ var ariaLabel = Handlebars.Utils.escapeExpression(hash['aria-label']);
+ var placeholder = Handlebars.Utils.escapeExpression(hash.placeholder);
+ return new Handlebars.SafeString(
+ ''
+ );
+ },
+ t: function (defaultString) {
+ return new Handlebars.SafeString(defaultString);
+ },
+ })
+ .toCompileTo('');
+ });
+
+ it('subexpression functions on the context', function () {
+ expectTemplate('{{foo (bar)}}!')
+ .withInput({
+ bar: function () {
+ return 'LOL';
+ },
+ })
+ .withHelpers({
+ foo: function (val) {
+ return val + val;
+ },
+ })
+ .toCompileTo('LOLLOL!');
+ });
+
+ it("subexpressions can't just be property lookups", function () {
+ expectTemplate('{{foo (bar)}}!')
+ .withInput({
+ bar: 'LOL',
+ })
+ .withHelpers({
+ foo: function (val) {
+ return val + val;
+ },
+ })
+ .toThrow();
+ });
+});
diff --git a/spec/tmp/.gitkeep b/spec/tmp/.gitkeep
new file mode 100644
index 000000000..f534490ec
--- /dev/null
+++ b/spec/tmp/.gitkeep
@@ -0,0 +1 @@
+This directory is ignored in .gitignore. It can be used to write temporary files during tests.
\ No newline at end of file
diff --git a/spec/tokenizer.js b/spec/tokenizer.js
new file mode 100644
index 000000000..73200b551
--- /dev/null
+++ b/spec/tokenizer.js
@@ -0,0 +1,796 @@
+function shouldMatchTokens(result, tokens) {
+ for (var index = 0; index < result.length; index++) {
+ expect(result[index].name).toBe(tokens[index]);
+ }
+}
+function shouldBeToken(result, name, text) {
+ expect(result.name).toBe(name);
+ expect(result.text).toBe(text);
+}
+
+describe('Tokenizer', function () {
+ if (!Handlebars.Parser) {
+ return;
+ }
+
+ function tokenize(template) {
+ var parser = Handlebars.Parser,
+ lexer = parser.lexer;
+
+ lexer.setInput(template);
+ var out = [],
+ token;
+
+ while ((token = lexer.lex())) {
+ var result = parser.terminals_[token] || token;
+ if (!result || result === 'EOF' || result === 'INVALID') {
+ break;
+ }
+ out.push({ name: result, text: lexer.yytext });
+ }
+
+ return out;
+ }
+
+ it('tokenizes a simple mustache as "OPEN ID CLOSE"', function () {
+ var result = tokenize('{{foo}}');
+ shouldMatchTokens(result, ['OPEN', 'ID', 'CLOSE']);
+ shouldBeToken(result[1], 'ID', 'foo');
+ });
+
+ it('supports unescaping with &', function () {
+ var result = tokenize('{{&bar}}');
+ shouldMatchTokens(result, ['OPEN', 'ID', 'CLOSE']);
+
+ shouldBeToken(result[0], 'OPEN', '{{&');
+ shouldBeToken(result[1], 'ID', 'bar');
+ });
+
+ it('supports unescaping with {{{', function () {
+ var result = tokenize('{{{bar}}}');
+ shouldMatchTokens(result, ['OPEN_UNESCAPED', 'ID', 'CLOSE_UNESCAPED']);
+
+ shouldBeToken(result[1], 'ID', 'bar');
+ });
+
+ it('supports escaping delimiters', function () {
+ var result = tokenize('{{foo}} \\{{bar}} {{baz}}');
+ shouldMatchTokens(result, [
+ 'OPEN',
+ 'ID',
+ 'CLOSE',
+ 'CONTENT',
+ 'CONTENT',
+ 'OPEN',
+ 'ID',
+ 'CLOSE',
+ ]);
+
+ shouldBeToken(result[3], 'CONTENT', ' ');
+ shouldBeToken(result[4], 'CONTENT', '{{bar}} ');
+ });
+
+ it('supports escaping multiple delimiters', function () {
+ var result = tokenize('{{foo}} \\{{bar}} \\{{baz}}');
+ shouldMatchTokens(result, [
+ 'OPEN',
+ 'ID',
+ 'CLOSE',
+ 'CONTENT',
+ 'CONTENT',
+ 'CONTENT',
+ ]);
+
+ shouldBeToken(result[3], 'CONTENT', ' ');
+ shouldBeToken(result[4], 'CONTENT', '{{bar}} ');
+ shouldBeToken(result[5], 'CONTENT', '{{baz}}');
+ });
+
+ it('supports escaping a triple stash', function () {
+ var result = tokenize('{{foo}} \\{{{bar}}} {{baz}}');
+ shouldMatchTokens(result, [
+ 'OPEN',
+ 'ID',
+ 'CLOSE',
+ 'CONTENT',
+ 'CONTENT',
+ 'OPEN',
+ 'ID',
+ 'CLOSE',
+ ]);
+
+ shouldBeToken(result[4], 'CONTENT', '{{{bar}}} ');
+ });
+
+ it('supports escaping escape character', function () {
+ var result = tokenize('{{foo}} \\\\{{bar}} {{baz}}');
+ shouldMatchTokens(result, [
+ 'OPEN',
+ 'ID',
+ 'CLOSE',
+ 'CONTENT',
+ 'OPEN',
+ 'ID',
+ 'CLOSE',
+ 'CONTENT',
+ 'OPEN',
+ 'ID',
+ 'CLOSE',
+ ]);
+
+ shouldBeToken(result[3], 'CONTENT', ' \\');
+ shouldBeToken(result[5], 'ID', 'bar');
+ });
+
+ it('supports escaping multiple escape characters', function () {
+ var result = tokenize('{{foo}} \\\\{{bar}} \\\\{{baz}}');
+ shouldMatchTokens(result, [
+ 'OPEN',
+ 'ID',
+ 'CLOSE',
+ 'CONTENT',
+ 'OPEN',
+ 'ID',
+ 'CLOSE',
+ 'CONTENT',
+ 'OPEN',
+ 'ID',
+ 'CLOSE',
+ ]);
+
+ shouldBeToken(result[3], 'CONTENT', ' \\');
+ shouldBeToken(result[5], 'ID', 'bar');
+ shouldBeToken(result[7], 'CONTENT', ' \\');
+ shouldBeToken(result[9], 'ID', 'baz');
+ });
+
+ it('supports escaped mustaches after escaped escape characters', function () {
+ var result = tokenize('{{foo}} \\\\{{bar}} \\{{baz}}');
+ shouldMatchTokens(result, [
+ 'OPEN',
+ 'ID',
+ 'CLOSE',
+ 'CONTENT',
+ 'OPEN',
+ 'ID',
+ 'CLOSE',
+ 'CONTENT',
+ 'CONTENT',
+ 'CONTENT',
+ ]);
+
+ shouldBeToken(result[3], 'CONTENT', ' \\');
+ shouldBeToken(result[4], 'OPEN', '{{');
+ shouldBeToken(result[5], 'ID', 'bar');
+ shouldBeToken(result[7], 'CONTENT', ' ');
+ shouldBeToken(result[8], 'CONTENT', '{{baz}}');
+ });
+
+ it('supports escaped escape characters after escaped mustaches', function () {
+ var result = tokenize('{{foo}} \\{{bar}} \\\\{{baz}}');
+ shouldMatchTokens(result, [
+ 'OPEN',
+ 'ID',
+ 'CLOSE',
+ 'CONTENT',
+ 'CONTENT',
+ 'CONTENT',
+ 'OPEN',
+ 'ID',
+ 'CLOSE',
+ ]);
+
+ shouldBeToken(result[4], 'CONTENT', '{{bar}} ');
+ shouldBeToken(result[5], 'CONTENT', '\\');
+ shouldBeToken(result[6], 'OPEN', '{{');
+ shouldBeToken(result[7], 'ID', 'baz');
+ });
+
+ it('supports escaped escape character on a triple stash', function () {
+ var result = tokenize('{{foo}} \\\\{{{bar}}} {{baz}}');
+ shouldMatchTokens(result, [
+ 'OPEN',
+ 'ID',
+ 'CLOSE',
+ 'CONTENT',
+ 'OPEN_UNESCAPED',
+ 'ID',
+ 'CLOSE_UNESCAPED',
+ 'CONTENT',
+ 'OPEN',
+ 'ID',
+ 'CLOSE',
+ ]);
+
+ shouldBeToken(result[3], 'CONTENT', ' \\');
+ shouldBeToken(result[5], 'ID', 'bar');
+ });
+
+ it('tokenizes a simple path', function () {
+ var result = tokenize('{{foo/bar}}');
+ shouldMatchTokens(result, ['OPEN', 'ID', 'SEP', 'ID', 'CLOSE']);
+ });
+
+ it('allows dot notation', function () {
+ var result = tokenize('{{foo.bar}}');
+ shouldMatchTokens(result, ['OPEN', 'ID', 'SEP', 'ID', 'CLOSE']);
+
+ shouldMatchTokens(tokenize('{{foo.bar.baz}}'), [
+ 'OPEN',
+ 'ID',
+ 'SEP',
+ 'ID',
+ 'SEP',
+ 'ID',
+ 'CLOSE',
+ ]);
+ });
+
+ it('allows path literals with []', function () {
+ var result = tokenize('{{foo.[bar]}}');
+ shouldMatchTokens(result, ['OPEN', 'ID', 'SEP', 'ID', 'CLOSE']);
+ });
+
+ it('allows multiple path literals on a line with []', function () {
+ var result = tokenize('{{foo.[bar]}}{{foo.[baz]}}');
+ shouldMatchTokens(result, [
+ 'OPEN',
+ 'ID',
+ 'SEP',
+ 'ID',
+ 'CLOSE',
+ 'OPEN',
+ 'ID',
+ 'SEP',
+ 'ID',
+ 'CLOSE',
+ ]);
+ });
+
+ it('allows escaped literals in []', function () {
+ var result = tokenize('{{foo.[bar\\]]}}');
+ shouldMatchTokens(result, ['OPEN', 'ID', 'SEP', 'ID', 'CLOSE']);
+ });
+
+ it('tokenizes {{.}} as OPEN ID CLOSE', function () {
+ var result = tokenize('{{.}}');
+ shouldMatchTokens(result, ['OPEN', 'ID', 'CLOSE']);
+ });
+
+ it('tokenizes a path as "OPEN (ID SEP)* ID CLOSE"', function () {
+ var result = tokenize('{{../foo/bar}}');
+ shouldMatchTokens(result, [
+ 'OPEN',
+ 'ID',
+ 'SEP',
+ 'ID',
+ 'SEP',
+ 'ID',
+ 'CLOSE',
+ ]);
+ shouldBeToken(result[1], 'ID', '..');
+ });
+
+ it('tokenizes a path with .. as a parent path', function () {
+ var result = tokenize('{{../foo.bar}}');
+ shouldMatchTokens(result, [
+ 'OPEN',
+ 'ID',
+ 'SEP',
+ 'ID',
+ 'SEP',
+ 'ID',
+ 'CLOSE',
+ ]);
+ shouldBeToken(result[1], 'ID', '..');
+ });
+
+ it('tokenizes a path with this/foo as OPEN ID SEP ID CLOSE', function () {
+ var result = tokenize('{{this/foo}}');
+ shouldMatchTokens(result, ['OPEN', 'ID', 'SEP', 'ID', 'CLOSE']);
+ shouldBeToken(result[1], 'ID', 'this');
+ shouldBeToken(result[3], 'ID', 'foo');
+ });
+
+ it('tokenizes a simple mustache with spaces as "OPEN ID CLOSE"', function () {
+ var result = tokenize('{{ foo }}');
+ shouldMatchTokens(result, ['OPEN', 'ID', 'CLOSE']);
+ shouldBeToken(result[1], 'ID', 'foo');
+ });
+
+ it('tokenizes a simple mustache with line breaks as "OPEN ID ID CLOSE"', function () {
+ var result = tokenize('{{ foo \n bar }}');
+ shouldMatchTokens(result, ['OPEN', 'ID', 'ID', 'CLOSE']);
+ shouldBeToken(result[1], 'ID', 'foo');
+ });
+
+ it('tokenizes raw content as "CONTENT"', function () {
+ var result = tokenize('foo {{ bar }} baz');
+ shouldMatchTokens(result, ['CONTENT', 'OPEN', 'ID', 'CLOSE', 'CONTENT']);
+ shouldBeToken(result[0], 'CONTENT', 'foo ');
+ shouldBeToken(result[4], 'CONTENT', ' baz');
+ });
+
+ it('tokenizes a partial as "OPEN_PARTIAL ID CLOSE"', function () {
+ var result = tokenize('{{> foo}}');
+ shouldMatchTokens(result, ['OPEN_PARTIAL', 'ID', 'CLOSE']);
+ });
+
+ it('tokenizes a partial with context as "OPEN_PARTIAL ID ID CLOSE"', function () {
+ var result = tokenize('{{> foo bar }}');
+ shouldMatchTokens(result, ['OPEN_PARTIAL', 'ID', 'ID', 'CLOSE']);
+ });
+
+ it('tokenizes a partial without spaces as "OPEN_PARTIAL ID CLOSE"', function () {
+ var result = tokenize('{{>foo}}');
+ shouldMatchTokens(result, ['OPEN_PARTIAL', 'ID', 'CLOSE']);
+ });
+
+ it('tokenizes a partial space at the }); as "OPEN_PARTIAL ID CLOSE"', function () {
+ var result = tokenize('{{>foo }}');
+ shouldMatchTokens(result, ['OPEN_PARTIAL', 'ID', 'CLOSE']);
+ });
+
+ it('tokenizes a partial space at the }); as "OPEN_PARTIAL ID CLOSE"', function () {
+ var result = tokenize('{{>foo/bar.baz }}');
+ shouldMatchTokens(result, [
+ 'OPEN_PARTIAL',
+ 'ID',
+ 'SEP',
+ 'ID',
+ 'SEP',
+ 'ID',
+ 'CLOSE',
+ ]);
+ });
+
+ it('tokenizes partial block declarations', function () {
+ var result = tokenize('{{#> foo}}');
+ shouldMatchTokens(result, ['OPEN_PARTIAL_BLOCK', 'ID', 'CLOSE']);
+ });
+ it('tokenizes a comment as "COMMENT"', function () {
+ var result = tokenize('foo {{! this is a comment }} bar {{ baz }}');
+ shouldMatchTokens(result, [
+ 'CONTENT',
+ 'COMMENT',
+ 'CONTENT',
+ 'OPEN',
+ 'ID',
+ 'CLOSE',
+ ]);
+ shouldBeToken(result[1], 'COMMENT', '{{! this is a comment }}');
+ });
+
+ it('tokenizes a block comment as "COMMENT"', function () {
+ var result = tokenize('foo {{!-- this is a {{comment}} --}} bar {{ baz }}');
+ shouldMatchTokens(result, [
+ 'CONTENT',
+ 'COMMENT',
+ 'CONTENT',
+ 'OPEN',
+ 'ID',
+ 'CLOSE',
+ ]);
+ shouldBeToken(result[1], 'COMMENT', '{{!-- this is a {{comment}} --}}');
+ });
+
+ it('tokenizes a block comment with whitespace as "COMMENT"', function () {
+ var result = tokenize(
+ 'foo {{!-- this is a\n{{comment}}\n--}} bar {{ baz }}'
+ );
+ shouldMatchTokens(result, [
+ 'CONTENT',
+ 'COMMENT',
+ 'CONTENT',
+ 'OPEN',
+ 'ID',
+ 'CLOSE',
+ ]);
+ shouldBeToken(result[1], 'COMMENT', '{{!-- this is a\n{{comment}}\n--}}');
+ });
+
+ it('tokenizes open and closing blocks as OPEN_BLOCK, ID, CLOSE ..., OPEN_ENDBLOCK ID CLOSE', function () {
+ var result = tokenize('{{#foo}}content{{/foo}}');
+ shouldMatchTokens(result, [
+ 'OPEN_BLOCK',
+ 'ID',
+ 'CLOSE',
+ 'CONTENT',
+ 'OPEN_ENDBLOCK',
+ 'ID',
+ 'CLOSE',
+ ]);
+ });
+
+ it('tokenizes directives', function () {
+ shouldMatchTokens(tokenize('{{#*foo}}content{{/foo}}'), [
+ 'OPEN_BLOCK',
+ 'ID',
+ 'CLOSE',
+ 'CONTENT',
+ 'OPEN_ENDBLOCK',
+ 'ID',
+ 'CLOSE',
+ ]);
+ shouldMatchTokens(tokenize('{{*foo}}'), ['OPEN', 'ID', 'CLOSE']);
+ });
+
+ it('tokenizes inverse sections as "INVERSE"', function () {
+ shouldMatchTokens(tokenize('{{^}}'), ['INVERSE']);
+ shouldMatchTokens(tokenize('{{else}}'), ['INVERSE']);
+ shouldMatchTokens(tokenize('{{ else }}'), ['INVERSE']);
+ });
+
+ it('tokenizes inverse sections with ID as "OPEN_INVERSE ID CLOSE"', function () {
+ var result = tokenize('{{^foo}}');
+ shouldMatchTokens(result, ['OPEN_INVERSE', 'ID', 'CLOSE']);
+ shouldBeToken(result[1], 'ID', 'foo');
+ });
+
+ it('tokenizes inverse sections with ID and spaces as "OPEN_INVERSE ID CLOSE"', function () {
+ var result = tokenize('{{^ foo }}');
+ shouldMatchTokens(result, ['OPEN_INVERSE', 'ID', 'CLOSE']);
+ shouldBeToken(result[1], 'ID', 'foo');
+ });
+
+ it('tokenizes mustaches with params as "OPEN ID ID ID CLOSE"', function () {
+ var result = tokenize('{{ foo bar baz }}');
+ shouldMatchTokens(result, ['OPEN', 'ID', 'ID', 'ID', 'CLOSE']);
+ shouldBeToken(result[1], 'ID', 'foo');
+ shouldBeToken(result[2], 'ID', 'bar');
+ shouldBeToken(result[3], 'ID', 'baz');
+ });
+
+ it('tokenizes mustaches with String params as "OPEN ID ID STRING CLOSE"', function () {
+ var result = tokenize('{{ foo bar "baz" }}');
+ shouldMatchTokens(result, ['OPEN', 'ID', 'ID', 'STRING', 'CLOSE']);
+ shouldBeToken(result[3], 'STRING', 'baz');
+ });
+
+ it('tokenizes mustaches with String params using single quotes as "OPEN ID ID STRING CLOSE"', function () {
+ var result = tokenize("{{ foo bar 'baz' }}");
+ shouldMatchTokens(result, ['OPEN', 'ID', 'ID', 'STRING', 'CLOSE']);
+ shouldBeToken(result[3], 'STRING', 'baz');
+ });
+
+ it('tokenizes String params with spaces inside as "STRING"', function () {
+ var result = tokenize('{{ foo bar "baz bat" }}');
+ shouldMatchTokens(result, ['OPEN', 'ID', 'ID', 'STRING', 'CLOSE']);
+ shouldBeToken(result[3], 'STRING', 'baz bat');
+ });
+
+ it('tokenizes String params with escapes quotes as STRING', function () {
+ var result = tokenize('{{ foo "bar\\"baz" }}');
+ shouldMatchTokens(result, ['OPEN', 'ID', 'STRING', 'CLOSE']);
+ shouldBeToken(result[2], 'STRING', 'bar"baz');
+ });
+
+ it('tokenizes String params using single quotes with escapes quotes as STRING', function () {
+ var result = tokenize("{{ foo 'bar\\'baz' }}");
+ shouldMatchTokens(result, ['OPEN', 'ID', 'STRING', 'CLOSE']);
+ shouldBeToken(result[2], 'STRING', "bar'baz");
+ });
+
+ it('tokenizes numbers', function () {
+ var result = tokenize('{{ foo 1 }}');
+ shouldMatchTokens(result, ['OPEN', 'ID', 'NUMBER', 'CLOSE']);
+ shouldBeToken(result[2], 'NUMBER', '1');
+
+ result = tokenize('{{ foo 1.1 }}');
+ shouldMatchTokens(result, ['OPEN', 'ID', 'NUMBER', 'CLOSE']);
+ shouldBeToken(result[2], 'NUMBER', '1.1');
+
+ result = tokenize('{{ foo -1 }}');
+ shouldMatchTokens(result, ['OPEN', 'ID', 'NUMBER', 'CLOSE']);
+ shouldBeToken(result[2], 'NUMBER', '-1');
+
+ result = tokenize('{{ foo -1.1 }}');
+ shouldMatchTokens(result, ['OPEN', 'ID', 'NUMBER', 'CLOSE']);
+ shouldBeToken(result[2], 'NUMBER', '-1.1');
+ });
+
+ it('tokenizes booleans', function () {
+ var result = tokenize('{{ foo true }}');
+ shouldMatchTokens(result, ['OPEN', 'ID', 'BOOLEAN', 'CLOSE']);
+ shouldBeToken(result[2], 'BOOLEAN', 'true');
+
+ result = tokenize('{{ foo false }}');
+ shouldMatchTokens(result, ['OPEN', 'ID', 'BOOLEAN', 'CLOSE']);
+ shouldBeToken(result[2], 'BOOLEAN', 'false');
+ });
+
+ it('tokenizes undefined and null', function () {
+ var result = tokenize('{{ foo undefined null }}');
+ shouldMatchTokens(result, ['OPEN', 'ID', 'UNDEFINED', 'NULL', 'CLOSE']);
+ shouldBeToken(result[2], 'UNDEFINED', 'undefined');
+ shouldBeToken(result[3], 'NULL', 'null');
+ });
+
+ it('tokenizes hash arguments', function () {
+ var result = tokenize('{{ foo bar=baz }}');
+ shouldMatchTokens(result, ['OPEN', 'ID', 'ID', 'EQUALS', 'ID', 'CLOSE']);
+
+ result = tokenize('{{ foo bar baz=bat }}');
+ shouldMatchTokens(result, [
+ 'OPEN',
+ 'ID',
+ 'ID',
+ 'ID',
+ 'EQUALS',
+ 'ID',
+ 'CLOSE',
+ ]);
+
+ result = tokenize('{{ foo bar baz=1 }}');
+ shouldMatchTokens(result, [
+ 'OPEN',
+ 'ID',
+ 'ID',
+ 'ID',
+ 'EQUALS',
+ 'NUMBER',
+ 'CLOSE',
+ ]);
+
+ result = tokenize('{{ foo bar baz=true }}');
+ shouldMatchTokens(result, [
+ 'OPEN',
+ 'ID',
+ 'ID',
+ 'ID',
+ 'EQUALS',
+ 'BOOLEAN',
+ 'CLOSE',
+ ]);
+
+ result = tokenize('{{ foo bar baz=false }}');
+ shouldMatchTokens(result, [
+ 'OPEN',
+ 'ID',
+ 'ID',
+ 'ID',
+ 'EQUALS',
+ 'BOOLEAN',
+ 'CLOSE',
+ ]);
+
+ result = tokenize('{{ foo bar\n baz=bat }}');
+ shouldMatchTokens(result, [
+ 'OPEN',
+ 'ID',
+ 'ID',
+ 'ID',
+ 'EQUALS',
+ 'ID',
+ 'CLOSE',
+ ]);
+
+ result = tokenize('{{ foo bar baz="bat" }}');
+ shouldMatchTokens(result, [
+ 'OPEN',
+ 'ID',
+ 'ID',
+ 'ID',
+ 'EQUALS',
+ 'STRING',
+ 'CLOSE',
+ ]);
+
+ result = tokenize('{{ foo bar baz="bat" bam=wot }}');
+ shouldMatchTokens(result, [
+ 'OPEN',
+ 'ID',
+ 'ID',
+ 'ID',
+ 'EQUALS',
+ 'STRING',
+ 'ID',
+ 'EQUALS',
+ 'ID',
+ 'CLOSE',
+ ]);
+
+ result = tokenize('{{foo omg bar=baz bat="bam"}}');
+ shouldMatchTokens(result, [
+ 'OPEN',
+ 'ID',
+ 'ID',
+ 'ID',
+ 'EQUALS',
+ 'ID',
+ 'ID',
+ 'EQUALS',
+ 'STRING',
+ 'CLOSE',
+ ]);
+ shouldBeToken(result[2], 'ID', 'omg');
+ });
+
+ it('tokenizes special @ identifiers', function () {
+ var result = tokenize('{{ @foo }}');
+ shouldMatchTokens(result, ['OPEN', 'DATA', 'ID', 'CLOSE']);
+ shouldBeToken(result[2], 'ID', 'foo');
+
+ result = tokenize('{{ foo @bar }}');
+ shouldMatchTokens(result, ['OPEN', 'ID', 'DATA', 'ID', 'CLOSE']);
+ shouldBeToken(result[3], 'ID', 'bar');
+
+ result = tokenize('{{ foo bar=@baz }}');
+ shouldMatchTokens(result, [
+ 'OPEN',
+ 'ID',
+ 'ID',
+ 'EQUALS',
+ 'DATA',
+ 'ID',
+ 'CLOSE',
+ ]);
+ shouldBeToken(result[5], 'ID', 'baz');
+ });
+
+ it('does not time out in a mustache with a single } followed by EOF', function () {
+ shouldMatchTokens(tokenize('{{foo}'), ['OPEN', 'ID']);
+ });
+
+ it('does not time out in a mustache when invalid ID characters are used', function () {
+ shouldMatchTokens(tokenize('{{foo & }}'), ['OPEN', 'ID']);
+ });
+
+ it('tokenizes subexpressions', function () {
+ var result = tokenize('{{foo (bar)}}');
+ shouldMatchTokens(result, [
+ 'OPEN',
+ 'ID',
+ 'OPEN_SEXPR',
+ 'ID',
+ 'CLOSE_SEXPR',
+ 'CLOSE',
+ ]);
+ shouldBeToken(result[1], 'ID', 'foo');
+ shouldBeToken(result[3], 'ID', 'bar');
+
+ result = tokenize('{{foo (a-x b-y)}}');
+ shouldMatchTokens(result, [
+ 'OPEN',
+ 'ID',
+ 'OPEN_SEXPR',
+ 'ID',
+ 'ID',
+ 'CLOSE_SEXPR',
+ 'CLOSE',
+ ]);
+ shouldBeToken(result[1], 'ID', 'foo');
+ shouldBeToken(result[3], 'ID', 'a-x');
+ shouldBeToken(result[4], 'ID', 'b-y');
+ });
+
+ it('tokenizes nested subexpressions', function () {
+ var result = tokenize('{{foo (bar (lol rofl)) (baz)}}');
+ shouldMatchTokens(result, [
+ 'OPEN',
+ 'ID',
+ 'OPEN_SEXPR',
+ 'ID',
+ 'OPEN_SEXPR',
+ 'ID',
+ 'ID',
+ 'CLOSE_SEXPR',
+ 'CLOSE_SEXPR',
+ 'OPEN_SEXPR',
+ 'ID',
+ 'CLOSE_SEXPR',
+ 'CLOSE',
+ ]);
+ shouldBeToken(result[3], 'ID', 'bar');
+ shouldBeToken(result[5], 'ID', 'lol');
+ shouldBeToken(result[6], 'ID', 'rofl');
+ shouldBeToken(result[10], 'ID', 'baz');
+ });
+
+ it('tokenizes nested subexpressions: literals', function () {
+ var result = tokenize(
+ '{{foo (bar (lol true) false) (baz 1) (blah \'b\') (blorg "c")}}'
+ );
+ shouldMatchTokens(result, [
+ 'OPEN',
+ 'ID',
+ 'OPEN_SEXPR',
+ 'ID',
+ 'OPEN_SEXPR',
+ 'ID',
+ 'BOOLEAN',
+ 'CLOSE_SEXPR',
+ 'BOOLEAN',
+ 'CLOSE_SEXPR',
+ 'OPEN_SEXPR',
+ 'ID',
+ 'NUMBER',
+ 'CLOSE_SEXPR',
+ 'OPEN_SEXPR',
+ 'ID',
+ 'STRING',
+ 'CLOSE_SEXPR',
+ 'OPEN_SEXPR',
+ 'ID',
+ 'STRING',
+ 'CLOSE_SEXPR',
+ 'CLOSE',
+ ]);
+ });
+
+ it('tokenizes block params', function () {
+ var result = tokenize('{{#foo as |bar|}}');
+ shouldMatchTokens(result, [
+ 'OPEN_BLOCK',
+ 'ID',
+ 'OPEN_BLOCK_PARAMS',
+ 'ID',
+ 'CLOSE_BLOCK_PARAMS',
+ 'CLOSE',
+ ]);
+
+ result = tokenize('{{#foo as |bar baz|}}');
+ shouldMatchTokens(result, [
+ 'OPEN_BLOCK',
+ 'ID',
+ 'OPEN_BLOCK_PARAMS',
+ 'ID',
+ 'ID',
+ 'CLOSE_BLOCK_PARAMS',
+ 'CLOSE',
+ ]);
+
+ result = tokenize('{{#foo as | bar baz |}}');
+ shouldMatchTokens(result, [
+ 'OPEN_BLOCK',
+ 'ID',
+ 'OPEN_BLOCK_PARAMS',
+ 'ID',
+ 'ID',
+ 'CLOSE_BLOCK_PARAMS',
+ 'CLOSE',
+ ]);
+
+ result = tokenize('{{#foo as as | bar baz |}}');
+ shouldMatchTokens(result, [
+ 'OPEN_BLOCK',
+ 'ID',
+ 'ID',
+ 'OPEN_BLOCK_PARAMS',
+ 'ID',
+ 'ID',
+ 'CLOSE_BLOCK_PARAMS',
+ 'CLOSE',
+ ]);
+
+ result = tokenize('{{else foo as |bar baz|}}');
+ shouldMatchTokens(result, [
+ 'OPEN_INVERSE_CHAIN',
+ 'ID',
+ 'OPEN_BLOCK_PARAMS',
+ 'ID',
+ 'ID',
+ 'CLOSE_BLOCK_PARAMS',
+ 'CLOSE',
+ ]);
+ });
+
+ it('tokenizes raw blocks', function () {
+ var result = tokenize(
+ '{{{{a}}}} abc {{{{/a}}}} aaa {{{{a}}}} abc {{{{/a}}}}'
+ );
+ shouldMatchTokens(result, [
+ 'OPEN_RAW_BLOCK',
+ 'ID',
+ 'CLOSE_RAW_BLOCK',
+ 'CONTENT',
+ 'END_RAW_BLOCK',
+ 'CONTENT',
+ 'OPEN_RAW_BLOCK',
+ 'ID',
+ 'CLOSE_RAW_BLOCK',
+ 'CONTENT',
+ 'END_RAW_BLOCK',
+ ]);
+ });
+});
diff --git a/spec/tokenizer_spec.rb b/spec/tokenizer_spec.rb
deleted file mode 100644
index a8bb94d89..000000000
--- a/spec/tokenizer_spec.rb
+++ /dev/null
@@ -1,254 +0,0 @@
-require "spec_helper"
-require "timeout"
-
-describe "Tokenizer" do
- let(:parser) { @context["handlebars"] }
- let(:lexer) { @context["handlebars"]["lexer"] }
-
- before(:all) do
- @compiles = true
- end
- Token = Struct.new(:name, :text)
-
- def tokenize(string)
- lexer.setInput(string)
- out = []
-
- while token = lexer.lex
- # p token
- result = parser.terminals_[token] || token
- # p result
- break if !result || result == "EOF" || result == "INVALID"
- out << Token.new(result, lexer.yytext)
- end
-
- out
- end
-
- RSpec::Matchers.define :match_tokens do |tokens|
- match do |result|
- result.map(&:name).should == tokens
- end
- end
-
- RSpec::Matchers.define :be_token do |name, string|
- match do |token|
- token.name.should == name
- token.text.should == string
- end
- end
-
- it "tokenizes a simple mustache as 'OPEN ID CLOSE'" do
- result = tokenize("{{foo}}")
- result.should match_tokens(%w(OPEN ID CLOSE))
- result[1].should be_token("ID", "foo")
- end
-
- it "supports escaping delimiters" do
- result = tokenize("{{foo}} \\{{bar}} {{baz}}")
- result.should match_tokens(%w(OPEN ID CLOSE CONTENT CONTENT OPEN ID CLOSE))
-
- result[4].should be_token("CONTENT", "{{bar}} ")
- end
-
- it "supports escaping a triple stash" do
- result = tokenize("{{foo}} \\{{{bar}}} {{baz}}")
- result.should match_tokens(%w(OPEN ID CLOSE CONTENT CONTENT OPEN ID CLOSE))
-
- result[4].should be_token("CONTENT", "{{{bar}}} ")
- end
-
- it "tokenizes a simple path" do
- result = tokenize("{{foo/bar}}")
- result.should match_tokens(%w(OPEN ID SEP ID CLOSE))
- end
-
- it "allows dot notation" do
- result = tokenize("{{foo.bar}}")
- result.should match_tokens(%w(OPEN ID SEP ID CLOSE))
-
- tokenize("{{foo.bar.baz}}").should match_tokens(%w(OPEN ID SEP ID SEP ID CLOSE))
- end
-
- it "allows path literals with []" do
- result = tokenize("{{foo.[bar]}}")
- result.should match_tokens(%w(OPEN ID SEP ID CLOSE))
- end
-
- it "allows multiple path literals on a line with []" do
- result = tokenize("{{foo.[bar]}}{{foo.[baz]}}")
- result.should match_tokens(%w(OPEN ID SEP ID CLOSE OPEN ID SEP ID CLOSE))
- end
-
- it "tokenizes {{.}} as OPEN ID CLOSE" do
- result = tokenize("{{.}}")
- result.should match_tokens(%w(OPEN ID CLOSE))
- end
-
- it "tokenizes a path as 'OPEN (ID SEP)* ID CLOSE'" do
- result = tokenize("{{../foo/bar}}")
- result.should match_tokens(%w(OPEN ID SEP ID SEP ID CLOSE))
- result[1].should be_token("ID", "..")
- end
-
- it "tokenizes a path with .. as a parent path" do
- result = tokenize("{{../foo.bar}}")
- result.should match_tokens(%w(OPEN ID SEP ID SEP ID CLOSE))
- result[1].should be_token("ID", "..")
- end
-
- it "tokenizes a path with this/foo as OPEN ID SEP ID CLOSE" do
- result = tokenize("{{this/foo}}")
- result.should match_tokens(%w(OPEN ID SEP ID CLOSE))
- result[1].should be_token("ID", "this")
- result[3].should be_token("ID", "foo")
- end
-
- it "tokenizes a simple mustache with spaces as 'OPEN ID CLOSE'" do
- result = tokenize("{{ foo }}")
- result.should match_tokens(%w(OPEN ID CLOSE))
- result[1].should be_token("ID", "foo")
- end
-
- it "tokenizes a simple mustache with line breaks as 'OPEN ID ID CLOSE'" do
- result = tokenize("{{ foo \n bar }}")
- result.should match_tokens(%w(OPEN ID ID CLOSE))
- result[1].should be_token("ID", "foo")
- end
-
- it "tokenizes raw content as 'CONTENT'" do
- result = tokenize("foo {{ bar }} baz")
- result.should match_tokens(%w(CONTENT OPEN ID CLOSE CONTENT))
- result[0].should be_token("CONTENT", "foo ")
- result[4].should be_token("CONTENT", " baz")
- end
-
- it "tokenizes a partial as 'OPEN_PARTIAL ID CLOSE'" do
- result = tokenize("{{> foo}}")
- result.should match_tokens(%w(OPEN_PARTIAL ID CLOSE))
- end
-
- it "tokenizes a partial with context as 'OPEN_PARTIAL ID ID CLOSE'" do
- result = tokenize("{{> foo bar }}")
- result.should match_tokens(%w(OPEN_PARTIAL ID ID CLOSE))
- end
-
- it "tokenizes a partial without spaces as 'OPEN_PARTIAL ID CLOSE'" do
- result = tokenize("{{>foo}}")
- result.should match_tokens(%w(OPEN_PARTIAL ID CLOSE))
- end
-
- it "tokenizes a partial space at the end as 'OPEN_PARTIAL ID CLOSE'" do
- result = tokenize("{{>foo }}")
- result.should match_tokens(%w(OPEN_PARTIAL ID CLOSE))
- end
-
- it "tokenizes a comment as 'COMMENT'" do
- result = tokenize("foo {{! this is a comment }} bar {{ baz }}")
- result.should match_tokens(%w(CONTENT COMMENT CONTENT OPEN ID CLOSE))
- result[1].should be_token("COMMENT", " this is a comment ")
- end
-
- it "tokenizes open and closing blocks as 'OPEN_BLOCK ID CLOSE ... OPEN_ENDBLOCK ID CLOSE'" do
- result = tokenize("{{#foo}}content{{/foo}}")
- result.should match_tokens(%w(OPEN_BLOCK ID CLOSE CONTENT OPEN_ENDBLOCK ID CLOSE))
- end
-
- it "tokenizes inverse sections as 'OPEN_INVERSE CLOSE'" do
- tokenize("{{^}}").should match_tokens(%w(OPEN_INVERSE CLOSE))
- tokenize("{{else}}").should match_tokens(%w(OPEN_INVERSE CLOSE))
- tokenize("{{ else }}").should match_tokens(%w(OPEN_INVERSE CLOSE))
- end
-
- it "tokenizes inverse sections with ID as 'OPEN_INVERSE ID CLOSE'" do
- result = tokenize("{{^foo}}")
- result.should match_tokens(%w(OPEN_INVERSE ID CLOSE))
- result[1].should be_token("ID", "foo")
- end
-
- it "tokenizes inverse sections with ID and spaces as 'OPEN_INVERSE ID CLOSE'" do
- result = tokenize("{{^ foo }}")
- result.should match_tokens(%w(OPEN_INVERSE ID CLOSE))
- result[1].should be_token("ID", "foo")
- end
-
- it "tokenizes mustaches with params as 'OPEN ID ID ID CLOSE'" do
- result = tokenize("{{ foo bar baz }}")
- result.should match_tokens(%w(OPEN ID ID ID CLOSE))
- result[1].should be_token("ID", "foo")
- result[2].should be_token("ID", "bar")
- result[3].should be_token("ID", "baz")
- end
-
- it "tokenizes mustaches with String params as 'OPEN ID ID STRING CLOSE'" do
- result = tokenize("{{ foo bar \"baz\" }}")
- result.should match_tokens(%w(OPEN ID ID STRING CLOSE))
- result[3].should be_token("STRING", "baz")
- end
-
- it "tokenizes String params with spaces inside as 'STRING'" do
- result = tokenize("{{ foo bar \"baz bat\" }}")
- result.should match_tokens(%w(OPEN ID ID STRING CLOSE))
- result[3].should be_token("STRING", "baz bat")
- end
-
- it "tokenizes String params with escapes quotes as 'STRING'" do
- result = tokenize(%|{{ foo "bar\\"baz" }}|)
- result.should match_tokens(%w(OPEN ID STRING CLOSE))
- result[2].should be_token("STRING", %{bar"baz})
- end
-
- it "tokenizes numbers" do
- result = tokenize(%|{{ foo 1 }}|)
- result.should match_tokens(%w(OPEN ID INTEGER CLOSE))
- result[2].should be_token("INTEGER", "1")
- end
-
- it "tokenizes booleans" do
- result = tokenize(%|{{ foo true }}|)
- result.should match_tokens(%w(OPEN ID BOOLEAN CLOSE))
- result[2].should be_token("BOOLEAN", "true")
-
- result = tokenize(%|{{ foo false }}|)
- result.should match_tokens(%w(OPEN ID BOOLEAN CLOSE))
- result[2].should be_token("BOOLEAN", "false")
- end
-
- it "tokenizes hash arguments" do
- result = tokenize("{{ foo bar=baz }}")
- result.should match_tokens %w(OPEN ID ID EQUALS ID CLOSE)
-
- result = tokenize("{{ foo bar baz=bat }}")
- result.should match_tokens %w(OPEN ID ID ID EQUALS ID CLOSE)
-
- result = tokenize("{{ foo bar baz=1 }}")
- result.should match_tokens %w(OPEN ID ID ID EQUALS INTEGER CLOSE)
-
- result = tokenize("{{ foo bar baz=true }}")
- result.should match_tokens %w(OPEN ID ID ID EQUALS BOOLEAN CLOSE)
-
- result = tokenize("{{ foo bar baz=false }}")
- result.should match_tokens %w(OPEN ID ID ID EQUALS BOOLEAN CLOSE)
-
- result = tokenize("{{ foo bar\n baz=bat }}")
- result.should match_tokens %w(OPEN ID ID ID EQUALS ID CLOSE)
-
- result = tokenize("{{ foo bar baz=\"bat\" }}")
- result.should match_tokens %w(OPEN ID ID ID EQUALS STRING CLOSE)
-
- result = tokenize("{{ foo bar baz=\"bat\" bam=wot }}")
- result.should match_tokens %w(OPEN ID ID ID EQUALS STRING ID EQUALS ID CLOSE)
-
- result = tokenize("{{foo omg bar=baz bat=\"bam\"}}")
- result.should match_tokens %w(OPEN ID ID ID EQUALS ID ID EQUALS STRING CLOSE)
- result[2].should be_token("ID", "omg")
- end
-
- it "does not time out in a mustache with a single } followed by EOF" do
- Timeout.timeout(1) { tokenize("{{foo}").should match_tokens(%w(OPEN ID)) }
- end
-
- it "does not time out in a mustache when invalid ID characters are used" do
- Timeout.timeout(1) { tokenize("{{foo & }}").should match_tokens(%w(OPEN ID)) }
- end
-end
diff --git a/spec/umd-runtime.html b/spec/umd-runtime.html
new file mode 100644
index 000000000..74ede4695
--- /dev/null
+++ b/spec/umd-runtime.html
@@ -0,0 +1,58 @@
+
+
+
+ Handlebars Runtime UMD Smoke Test
+
+
+
+
+
+
+
+
+
+
diff --git a/spec/utils.js b/spec/utils.js
new file mode 100644
index 000000000..c0250d06c
--- /dev/null
+++ b/spec/utils.js
@@ -0,0 +1,101 @@
+describe('utils', function () {
+ describe('#SafeString', function () {
+ it('constructing a safestring from a string and checking its type', function () {
+ var safe = new Handlebars.SafeString('testing 1, 2, 3');
+ if (!(safe instanceof Handlebars.SafeString)) {
+ throw new Error('Must be instance of SafeString');
+ }
+ expect(safe.toString()).toBe('testing 1, 2, 3');
+ });
+
+ it('it should not escape SafeString properties', function () {
+ var name = new Handlebars.SafeString('Sean O'Malley');
+
+ expectTemplate('{{name}}')
+ .withInput({ name: name })
+ .toCompileTo('Sean O'Malley');
+ });
+ });
+
+ describe('#escapeExpression', function () {
+ it('should escape html', function () {
+ expect(Handlebars.Utils.escapeExpression('foo<&"\'>')).toBe(
+ 'foo<&"'>'
+ );
+ expect(Handlebars.Utils.escapeExpression('foo=')).toBe('foo=');
+ });
+ it('should not escape SafeString', function () {
+ var string = new Handlebars.SafeString('foo<&"\'>');
+ expect(Handlebars.Utils.escapeExpression(string)).toBe('foo<&"\'>');
+
+ var obj = {
+ toHTML: function () {
+ return 'foo<&"\'>';
+ },
+ };
+ expect(Handlebars.Utils.escapeExpression(obj)).toBe('foo<&"\'>');
+ });
+ it('should handle falsy', function () {
+ expect(Handlebars.Utils.escapeExpression('')).toBe('');
+ expect(Handlebars.Utils.escapeExpression(undefined)).toBe('');
+ expect(Handlebars.Utils.escapeExpression(null)).toBe('');
+
+ expect(Handlebars.Utils.escapeExpression(false)).toBe('false');
+ expect(Handlebars.Utils.escapeExpression(0)).toBe('0');
+ });
+ it('should handle empty objects', function () {
+ expect(Handlebars.Utils.escapeExpression({})).toBe({}.toString());
+ expect(Handlebars.Utils.escapeExpression([])).toBe([].toString());
+ });
+ });
+
+ describe('#isEmpty', function () {
+ it('should not be empty', function () {
+ expect(Handlebars.Utils.isEmpty(undefined)).toBe(true);
+ expect(Handlebars.Utils.isEmpty(null)).toBe(true);
+ expect(Handlebars.Utils.isEmpty(false)).toBe(true);
+ expect(Handlebars.Utils.isEmpty('')).toBe(true);
+ expect(Handlebars.Utils.isEmpty([])).toBe(true);
+ });
+
+ it('should be empty', function () {
+ expect(Handlebars.Utils.isEmpty(0)).toBe(false);
+ expect(Handlebars.Utils.isEmpty([1])).toBe(false);
+ expect(Handlebars.Utils.isEmpty('foo')).toBe(false);
+ expect(Handlebars.Utils.isEmpty({ bar: 1 })).toBe(false);
+ });
+ });
+
+ describe('#extend', function () {
+ it('should ignore prototype values', function () {
+ function A() {
+ this.a = 1;
+ }
+ A.prototype.b = 4;
+
+ var b = { b: 2 };
+
+ Handlebars.Utils.extend(b, new A());
+
+ expect(b.a).toBe(1);
+ expect(b.b).toBe(2);
+ });
+ });
+
+ describe('#isType', function () {
+ it('should check if variable is type Array', function () {
+ expect(Handlebars.Utils.isArray('string')).toBe(false);
+ expect(Handlebars.Utils.isArray([])).toBe(true);
+ });
+
+ it('should check if variable is type Map', function () {
+ expect(Handlebars.Utils.isMap('string')).toBe(false);
+ expect(Handlebars.Utils.isMap(new Map())).toBe(true);
+ });
+
+ it('should check if variable is type Set', function () {
+ expect(Handlebars.Utils.isSet('string')).toBe(false);
+ expect(Handlebars.Utils.isSet(new Set())).toBe(true);
+ });
+ });
+});
diff --git a/spec/vendor/require.js b/spec/vendor/require.js
new file mode 100644
index 000000000..05dc42fc9
--- /dev/null
+++ b/spec/vendor/require.js
@@ -0,0 +1,2054 @@
+/** vim: et:ts=4:sw=4:sts=4
+ * @license RequireJS 2.1.9 Copyright (c) 2010-2012, The Dojo Foundation All Rights Reserved.
+ * Available via the MIT or new BSD license.
+ * see: http://github.com/jrburke/requirejs for details
+ */
+//Not using strict: uneven strict support in browsers, #392, and causes
+//problems with requirejs.exec()/transpiler plugins that may not be strict.
+/*jslint regexp: true, nomen: true, sloppy: true */
+/*global window, navigator, document, importScripts, setTimeout, opera */
+
+var requirejs, require, define;
+(function (global) {
+ var req, s, head, baseElement, dataMain, src,
+ interactiveScript, currentlyAddingScript, mainScript, subPath,
+ version = '2.1.9',
+ commentRegExp = /(\/\*([\s\S]*?)\*\/|([^:]|^)\/\/(.*)$)/mg,
+ cjsRequireRegExp = /[^.]\s*require\s*\(\s*["']([^'"\s]+)["']\s*\)/g,
+ jsSuffixRegExp = /\.js$/,
+ currDirRegExp = /^\.\//,
+ op = Object.prototype,
+ ostring = op.toString,
+ hasOwn = op.hasOwnProperty,
+ ap = Array.prototype,
+ apsp = ap.splice,
+ isBrowser = !!(typeof window !== 'undefined' && typeof navigator !== 'undefined' && window.document),
+ isWebWorker = !isBrowser && typeof importScripts !== 'undefined',
+ //PS3 indicates loaded and complete, but need to wait for complete
+ //specifically. Sequence is 'loading', 'loaded', execution,
+ // then 'complete'. The UA check is unfortunate, but not sure how
+ //to feature test w/o causing perf issues.
+ readyRegExp = isBrowser && navigator.platform === 'PLAYSTATION 3' ?
+ /^complete$/ : /^(complete|loaded)$/,
+ defContextName = '_',
+ //Oh the tragedy, detecting opera. See the usage of isOpera for reason.
+ isOpera = typeof opera !== 'undefined' && opera.toString() === '[object Opera]',
+ contexts = {},
+ cfg = {},
+ globalDefQueue = [],
+ useInteractive = false;
+
+ function isFunction(it) {
+ return ostring.call(it) === '[object Function]';
+ }
+
+ function isArray(it) {
+ return ostring.call(it) === '[object Array]';
+ }
+
+ /**
+ * Helper function for iterating over an array. If the func returns
+ * a true value, it will break out of the loop.
+ */
+ function each(ary, func) {
+ if (ary) {
+ var i;
+ for (i = 0; i < ary.length; i += 1) {
+ if (ary[i] && func(ary[i], i, ary)) {
+ break;
+ }
+ }
+ }
+ }
+
+ /**
+ * Helper function for iterating over an array backwards. If the func
+ * returns a true value, it will break out of the loop.
+ */
+ function eachReverse(ary, func) {
+ if (ary) {
+ var i;
+ for (i = ary.length - 1; i > -1; i -= 1) {
+ if (ary[i] && func(ary[i], i, ary)) {
+ break;
+ }
+ }
+ }
+ }
+
+ function hasProp(obj, prop) {
+ return hasOwn.call(obj, prop);
+ }
+
+ function getOwn(obj, prop) {
+ return hasProp(obj, prop) && obj[prop];
+ }
+
+ /**
+ * Cycles over properties in an object and calls a function for each
+ * property value. If the function returns a truthy value, then the
+ * iteration is stopped.
+ */
+ function eachProp(obj, func) {
+ var prop;
+ for (prop in obj) {
+ if (hasProp(obj, prop)) {
+ if (func(obj[prop], prop)) {
+ break;
+ }
+ }
+ }
+ }
+
+ /**
+ * Simple function to mix in properties from source into target,
+ * but only if target does not already have a property of the same name.
+ */
+ function mixin(target, source, force, deepStringMixin) {
+ if (source) {
+ eachProp(source, function (value, prop) {
+ if (force || !hasProp(target, prop)) {
+ if (deepStringMixin && typeof value !== 'string') {
+ if (!target[prop]) {
+ target[prop] = {};
+ }
+ mixin(target[prop], value, force, deepStringMixin);
+ } else {
+ target[prop] = value;
+ }
+ }
+ });
+ }
+ return target;
+ }
+
+ //Similar to Function.prototype.bind, but the 'this' object is specified
+ //first, since it is easier to read/figure out what 'this' will be.
+ function bind(obj, fn) {
+ return function () {
+ return fn.apply(obj, arguments);
+ };
+ }
+
+ function scripts() {
+ return document.getElementsByTagName('script');
+ }
+
+ function defaultOnError(err) {
+ throw err;
+ }
+
+ //Allow getting a global that expressed in
+ //dot notation, like 'a.b.c'.
+ function getGlobal(value) {
+ if (!value) {
+ return value;
+ }
+ var g = global;
+ each(value.split('.'), function (part) {
+ g = g[part];
+ });
+ return g;
+ }
+
+ /**
+ * Constructs an error with a pointer to an URL with more information.
+ * @param {String} id the error ID that maps to an ID on a web page.
+ * @param {String} message human readable error.
+ * @param {Error} [err] the original error, if there is one.
+ *
+ * @returns {Error}
+ */
+ function makeError(id, msg, err, requireModules) {
+ var e = new Error(msg + '\nhttp://requirejs.org/docs/errors.html#' + id);
+ e.requireType = id;
+ e.requireModules = requireModules;
+ if (err) {
+ e.originalError = err;
+ }
+ return e;
+ }
+
+ if (typeof define !== 'undefined') {
+ //If a define is already in play via another AMD loader,
+ //do not overwrite.
+ return;
+ }
+
+ if (typeof requirejs !== 'undefined') {
+ if (isFunction(requirejs)) {
+ //Do not overwrite and existing requirejs instance.
+ return;
+ }
+ cfg = requirejs;
+ requirejs = undefined;
+ }
+
+ //Allow for a require config object
+ if (typeof require !== 'undefined' && !isFunction(require)) {
+ //assume it is a config object.
+ cfg = require;
+ require = undefined;
+ }
+
+ function newContext(contextName) {
+ var inCheckLoaded, Module, context, handlers,
+ checkLoadedTimeoutId,
+ config = {
+ //Defaults. Do not set a default for map
+ //config to speed up normalize(), which
+ //will run faster if there is no default.
+ waitSeconds: 7,
+ baseUrl: './',
+ paths: {},
+ pkgs: {},
+ shim: {},
+ config: {}
+ },
+ registry = {},
+ //registry of just enabled modules, to speed
+ //cycle breaking code when lots of modules
+ //are registered, but not activated.
+ enabledRegistry = {},
+ undefEvents = {},
+ defQueue = [],
+ defined = {},
+ urlFetched = {},
+ requireCounter = 1,
+ unnormalizedCounter = 1;
+
+ /**
+ * Trims the . and .. from an array of path segments.
+ * It will keep a leading path segment if a .. will become
+ * the first path segment, to help with module name lookups,
+ * which act like paths, but can be remapped. But the end result,
+ * all paths that use this function should look normalized.
+ * NOTE: this method MODIFIES the input array.
+ * @param {Array} ary the array of path segments.
+ */
+ function trimDots(ary) {
+ var i, part;
+ for (i = 0; ary[i]; i += 1) {
+ part = ary[i];
+ if (part === '.') {
+ ary.splice(i, 1);
+ i -= 1;
+ } else if (part === '..') {
+ if (i === 1 && (ary[2] === '..' || ary[0] === '..')) {
+ //End of the line. Keep at least one non-dot
+ //path segment at the front so it can be mapped
+ //correctly to disk. Otherwise, there is likely
+ //no path mapping for a path starting with '..'.
+ //This can still fail, but catches the most reasonable
+ //uses of ..
+ break;
+ } else if (i > 0) {
+ ary.splice(i - 1, 2);
+ i -= 2;
+ }
+ }
+ }
+ }
+
+ /**
+ * Given a relative module name, like ./something, normalize it to
+ * a real name that can be mapped to a path.
+ * @param {String} name the relative name
+ * @param {String} baseName a real name that the name arg is relative
+ * to.
+ * @param {Boolean} applyMap apply the map config to the value. Should
+ * only be done if this normalization is for a dependency ID.
+ * @returns {String} normalized name
+ */
+ function normalize(name, baseName, applyMap) {
+ var pkgName, pkgConfig, mapValue, nameParts, i, j, nameSegment,
+ foundMap, foundI, foundStarMap, starI,
+ baseParts = baseName && baseName.split('/'),
+ normalizedBaseParts = baseParts,
+ map = config.map,
+ starMap = map && map['*'];
+
+ //Adjust any relative paths.
+ if (name && name.charAt(0) === '.') {
+ //If have a base name, try to normalize against it,
+ //otherwise, assume it is a top-level require that will
+ //be relative to baseUrl in the end.
+ if (baseName) {
+ if (getOwn(config.pkgs, baseName)) {
+ //If the baseName is a package name, then just treat it as one
+ //name to concat the name with.
+ normalizedBaseParts = baseParts = [baseName];
+ } else {
+ //Convert baseName to array, and lop off the last part,
+ //so that . matches that 'directory' and not name of the baseName's
+ //module. For instance, baseName of 'one/two/three', maps to
+ //'one/two/three.js', but we want the directory, 'one/two' for
+ //this normalization.
+ normalizedBaseParts = baseParts.slice(0, baseParts.length - 1);
+ }
+
+ name = normalizedBaseParts.concat(name.split('/'));
+ trimDots(name);
+
+ //Some use of packages may use a . path to reference the
+ //'main' module name, so normalize for that.
+ pkgConfig = getOwn(config.pkgs, (pkgName = name[0]));
+ name = name.join('/');
+ if (pkgConfig && name === pkgName + '/' + pkgConfig.main) {
+ name = pkgName;
+ }
+ } else if (name.indexOf('./') === 0) {
+ // No baseName, so this is ID is resolved relative
+ // to baseUrl, pull off the leading dot.
+ name = name.substring(2);
+ }
+ }
+
+ //Apply map config if available.
+ if (applyMap && map && (baseParts || starMap)) {
+ nameParts = name.split('/');
+
+ for (i = nameParts.length; i > 0; i -= 1) {
+ nameSegment = nameParts.slice(0, i).join('/');
+
+ if (baseParts) {
+ //Find the longest baseName segment match in the config.
+ //So, do joins on the biggest to smallest lengths of baseParts.
+ for (j = baseParts.length; j > 0; j -= 1) {
+ mapValue = getOwn(map, baseParts.slice(0, j).join('/'));
+
+ //baseName segment has config, find if it has one for
+ //this name.
+ if (mapValue) {
+ mapValue = getOwn(mapValue, nameSegment);
+ if (mapValue) {
+ //Match, update name to the new value.
+ foundMap = mapValue;
+ foundI = i;
+ break;
+ }
+ }
+ }
+ }
+
+ if (foundMap) {
+ break;
+ }
+
+ //Check for a star map match, but just hold on to it,
+ //if there is a shorter segment match later in a matching
+ //config, then favor over this star map.
+ if (!foundStarMap && starMap && getOwn(starMap, nameSegment)) {
+ foundStarMap = getOwn(starMap, nameSegment);
+ starI = i;
+ }
+ }
+
+ if (!foundMap && foundStarMap) {
+ foundMap = foundStarMap;
+ foundI = starI;
+ }
+
+ if (foundMap) {
+ nameParts.splice(0, foundI, foundMap);
+ name = nameParts.join('/');
+ }
+ }
+
+ return name;
+ }
+
+ function removeScript(name) {
+ if (isBrowser) {
+ each(scripts(), function (scriptNode) {
+ if (scriptNode.getAttribute('data-requiremodule') === name &&
+ scriptNode.getAttribute('data-requirecontext') === context.contextName) {
+ scriptNode.parentNode.removeChild(scriptNode);
+ return true;
+ }
+ });
+ }
+ }
+
+ function hasPathFallback(id) {
+ var pathConfig = getOwn(config.paths, id);
+ if (pathConfig && isArray(pathConfig) && pathConfig.length > 1) {
+ //Pop off the first array value, since it failed, and
+ //retry
+ pathConfig.shift();
+ context.require.undef(id);
+ context.require([id]);
+ return true;
+ }
+ }
+
+ //Turns a plugin!resource to [plugin, resource]
+ //with the plugin being undefined if the name
+ //did not have a plugin prefix.
+ function splitPrefix(name) {
+ var prefix,
+ index = name ? name.indexOf('!') : -1;
+ if (index > -1) {
+ prefix = name.substring(0, index);
+ name = name.substring(index + 1, name.length);
+ }
+ return [prefix, name];
+ }
+
+ /**
+ * Creates a module mapping that includes plugin prefix, module
+ * name, and path. If parentModuleMap is provided it will
+ * also normalize the name via require.normalize()
+ *
+ * @param {String} name the module name
+ * @param {String} [parentModuleMap] parent module map
+ * for the module name, used to resolve relative names.
+ * @param {Boolean} isNormalized: is the ID already normalized.
+ * This is true if this call is done for a define() module ID.
+ * @param {Boolean} applyMap: apply the map config to the ID.
+ * Should only be true if this map is for a dependency.
+ *
+ * @returns {Object}
+ */
+ function makeModuleMap(name, parentModuleMap, isNormalized, applyMap) {
+ var url, pluginModule, suffix, nameParts,
+ prefix = null,
+ parentName = parentModuleMap ? parentModuleMap.name : null,
+ originalName = name,
+ isDefine = true,
+ normalizedName = '';
+
+ //If no name, then it means it is a require call, generate an
+ //internal name.
+ if (!name) {
+ isDefine = false;
+ name = '_@r' + (requireCounter += 1);
+ }
+
+ nameParts = splitPrefix(name);
+ prefix = nameParts[0];
+ name = nameParts[1];
+
+ if (prefix) {
+ prefix = normalize(prefix, parentName, applyMap);
+ pluginModule = getOwn(defined, prefix);
+ }
+
+ //Account for relative paths if there is a base name.
+ if (name) {
+ if (prefix) {
+ if (pluginModule && pluginModule.normalize) {
+ //Plugin is loaded, use its normalize method.
+ normalizedName = pluginModule.normalize(name, function (name) {
+ return normalize(name, parentName, applyMap);
+ });
+ } else {
+ normalizedName = normalize(name, parentName, applyMap);
+ }
+ } else {
+ //A regular module.
+ normalizedName = normalize(name, parentName, applyMap);
+
+ //Normalized name may be a plugin ID due to map config
+ //application in normalize. The map config values must
+ //already be normalized, so do not need to redo that part.
+ nameParts = splitPrefix(normalizedName);
+ prefix = nameParts[0];
+ normalizedName = nameParts[1];
+ isNormalized = true;
+
+ url = context.nameToUrl(normalizedName);
+ }
+ }
+
+ //If the id is a plugin id that cannot be determined if it needs
+ //normalization, stamp it with a unique ID so two matching relative
+ //ids that may conflict can be separate.
+ suffix = prefix && !pluginModule && !isNormalized ?
+ '_unnormalized' + (unnormalizedCounter += 1) :
+ '';
+
+ return {
+ prefix: prefix,
+ name: normalizedName,
+ parentMap: parentModuleMap,
+ unnormalized: !!suffix,
+ url: url,
+ originalName: originalName,
+ isDefine: isDefine,
+ id: (prefix ?
+ prefix + '!' + normalizedName :
+ normalizedName) + suffix
+ };
+ }
+
+ function getModule(depMap) {
+ var id = depMap.id,
+ mod = getOwn(registry, id);
+
+ if (!mod) {
+ mod = registry[id] = new context.Module(depMap);
+ }
+
+ return mod;
+ }
+
+ function on(depMap, name, fn) {
+ var id = depMap.id,
+ mod = getOwn(registry, id);
+
+ if (hasProp(defined, id) &&
+ (!mod || mod.defineEmitComplete)) {
+ if (name === 'defined') {
+ fn(defined[id]);
+ }
+ } else {
+ mod = getModule(depMap);
+ if (mod.error && name === 'error') {
+ fn(mod.error);
+ } else {
+ mod.on(name, fn);
+ }
+ }
+ }
+
+ function onError(err, errback) {
+ var ids = err.requireModules,
+ notified = false;
+
+ if (errback) {
+ errback(err);
+ } else {
+ each(ids, function (id) {
+ var mod = getOwn(registry, id);
+ if (mod) {
+ //Set error on module, so it skips timeout checks.
+ mod.error = err;
+ if (mod.events.error) {
+ notified = true;
+ mod.emit('error', err);
+ }
+ }
+ });
+
+ if (!notified) {
+ req.onError(err);
+ }
+ }
+ }
+
+ /**
+ * Internal method to transfer globalQueue items to this context's
+ * defQueue.
+ */
+ function takeGlobalQueue() {
+ //Push all the globalDefQueue items into the context's defQueue
+ if (globalDefQueue.length) {
+ //Array splice in the values since the context code has a
+ //local var ref to defQueue, so cannot just reassign the one
+ //on context.
+ apsp.apply(defQueue,
+ [defQueue.length - 1, 0].concat(globalDefQueue));
+ globalDefQueue = [];
+ }
+ }
+
+ handlers = {
+ 'require': function (mod) {
+ if (mod.require) {
+ return mod.require;
+ } else {
+ return (mod.require = context.makeRequire(mod.map));
+ }
+ },
+ 'exports': function (mod) {
+ mod.usingExports = true;
+ if (mod.map.isDefine) {
+ if (mod.exports) {
+ return mod.exports;
+ } else {
+ return (mod.exports = defined[mod.map.id] = {});
+ }
+ }
+ },
+ 'module': function (mod) {
+ if (mod.module) {
+ return mod.module;
+ } else {
+ return (mod.module = {
+ id: mod.map.id,
+ uri: mod.map.url,
+ config: function () {
+ var c,
+ pkg = getOwn(config.pkgs, mod.map.id);
+ // For packages, only support config targeted
+ // at the main module.
+ c = pkg ? getOwn(config.config, mod.map.id + '/' + pkg.main) :
+ getOwn(config.config, mod.map.id);
+ return c || {};
+ },
+ exports: defined[mod.map.id]
+ });
+ }
+ }
+ };
+
+ function cleanRegistry(id) {
+ //Clean up machinery used for waiting modules.
+ delete registry[id];
+ delete enabledRegistry[id];
+ }
+
+ function breakCycle(mod, traced, processed) {
+ var id = mod.map.id;
+
+ if (mod.error) {
+ mod.emit('error', mod.error);
+ } else {
+ traced[id] = true;
+ each(mod.depMaps, function (depMap, i) {
+ var depId = depMap.id,
+ dep = getOwn(registry, depId);
+
+ //Only force things that have not completed
+ //being defined, so still in the registry,
+ //and only if it has not been matched up
+ //in the module already.
+ if (dep && !mod.depMatched[i] && !processed[depId]) {
+ if (getOwn(traced, depId)) {
+ mod.defineDep(i, defined[depId]);
+ mod.check(); //pass false?
+ } else {
+ breakCycle(dep, traced, processed);
+ }
+ }
+ });
+ processed[id] = true;
+ }
+ }
+
+ function checkLoaded() {
+ var map, modId, err, usingPathFallback,
+ waitInterval = config.waitSeconds * 1000,
+ //It is possible to disable the wait interval by using waitSeconds of 0.
+ expired = waitInterval && (context.startTime + waitInterval) < new Date().getTime(),
+ noLoads = [],
+ reqCalls = [],
+ stillLoading = false,
+ needCycleCheck = true;
+
+ //Do not bother if this call was a result of a cycle break.
+ if (inCheckLoaded) {
+ return;
+ }
+
+ inCheckLoaded = true;
+
+ //Figure out the state of all the modules.
+ eachProp(enabledRegistry, function (mod) {
+ map = mod.map;
+ modId = map.id;
+
+ //Skip things that are not enabled or in error state.
+ if (!mod.enabled) {
+ return;
+ }
+
+ if (!map.isDefine) {
+ reqCalls.push(mod);
+ }
+
+ if (!mod.error) {
+ //If the module should be executed, and it has not
+ //been inited and time is up, remember it.
+ if (!mod.inited && expired) {
+ if (hasPathFallback(modId)) {
+ usingPathFallback = true;
+ stillLoading = true;
+ } else {
+ noLoads.push(modId);
+ removeScript(modId);
+ }
+ } else if (!mod.inited && mod.fetched && map.isDefine) {
+ stillLoading = true;
+ if (!map.prefix) {
+ //No reason to keep looking for unfinished
+ //loading. If the only stillLoading is a
+ //plugin resource though, keep going,
+ //because it may be that a plugin resource
+ //is waiting on a non-plugin cycle.
+ return (needCycleCheck = false);
+ }
+ }
+ }
+ });
+
+ if (expired && noLoads.length) {
+ //If wait time expired, throw error of unloaded modules.
+ err = makeError('timeout', 'Load timeout for modules: ' + noLoads, null, noLoads);
+ err.contextName = context.contextName;
+ return onError(err);
+ }
+
+ //Not expired, check for a cycle.
+ if (needCycleCheck) {
+ each(reqCalls, function (mod) {
+ breakCycle(mod, {}, {});
+ });
+ }
+
+ //If still waiting on loads, and the waiting load is something
+ //other than a plugin resource, or there are still outstanding
+ //scripts, then just try back later.
+ if ((!expired || usingPathFallback) && stillLoading) {
+ //Something is still waiting to load. Wait for it, but only
+ //if a timeout is not already in effect.
+ if ((isBrowser || isWebWorker) && !checkLoadedTimeoutId) {
+ checkLoadedTimeoutId = setTimeout(function () {
+ checkLoadedTimeoutId = 0;
+ checkLoaded();
+ }, 50);
+ }
+ }
+
+ inCheckLoaded = false;
+ }
+
+ Module = function (map) {
+ this.events = getOwn(undefEvents, map.id) || {};
+ this.map = map;
+ this.shim = getOwn(config.shim, map.id);
+ this.depExports = [];
+ this.depMaps = [];
+ this.depMatched = [];
+ this.pluginMaps = {};
+ this.depCount = 0;
+
+ /* this.exports this.factory
+ this.depMaps = [],
+ this.enabled, this.fetched
+ */
+ };
+
+ Module.prototype = {
+ init: function (depMaps, factory, errback, options) {
+ options = options || {};
+
+ //Do not do more inits if already done. Can happen if there
+ //are multiple define calls for the same module. That is not
+ //a normal, common case, but it is also not unexpected.
+ if (this.inited) {
+ return;
+ }
+
+ this.factory = factory;
+
+ if (errback) {
+ //Register for errors on this module.
+ this.on('error', errback);
+ } else if (this.events.error) {
+ //If no errback already, but there are error listeners
+ //on this module, set up an errback to pass to the deps.
+ errback = bind(this, function (err) {
+ this.emit('error', err);
+ });
+ }
+
+ //Do a copy of the dependency array, so that
+ //source inputs are not modified. For example
+ //"shim" deps are passed in here directly, and
+ //doing a direct modification of the depMaps array
+ //would affect that config.
+ this.depMaps = depMaps && depMaps.slice(0);
+
+ this.errback = errback;
+
+ //Indicate this module has be initialized
+ this.inited = true;
+
+ this.ignore = options.ignore;
+
+ //Could have option to init this module in enabled mode,
+ //or could have been previously marked as enabled. However,
+ //the dependencies are not known until init is called. So
+ //if enabled previously, now trigger dependencies as enabled.
+ if (options.enabled || this.enabled) {
+ //Enable this module and dependencies.
+ //Will call this.check()
+ this.enable();
+ } else {
+ this.check();
+ }
+ },
+
+ defineDep: function (i, depExports) {
+ //Because of cycles, defined callback for a given
+ //export can be called more than once.
+ if (!this.depMatched[i]) {
+ this.depMatched[i] = true;
+ this.depCount -= 1;
+ this.depExports[i] = depExports;
+ }
+ },
+
+ fetch: function () {
+ if (this.fetched) {
+ return;
+ }
+ this.fetched = true;
+
+ context.startTime = (new Date()).getTime();
+
+ var map = this.map;
+
+ //If the manager is for a plugin managed resource,
+ //ask the plugin to load it now.
+ if (this.shim) {
+ context.makeRequire(this.map, {
+ enableBuildCallback: true
+ })(this.shim.deps || [], bind(this, function () {
+ return map.prefix ? this.callPlugin() : this.load();
+ }));
+ } else {
+ //Regular dependency.
+ return map.prefix ? this.callPlugin() : this.load();
+ }
+ },
+
+ load: function () {
+ var url = this.map.url;
+
+ //Regular dependency.
+ if (!urlFetched[url]) {
+ urlFetched[url] = true;
+ context.load(this.map.id, url);
+ }
+ },
+
+ /**
+ * Checks if the module is ready to define itself, and if so,
+ * define it.
+ */
+ check: function () {
+ if (!this.enabled || this.enabling) {
+ return;
+ }
+
+ var err, cjsModule,
+ id = this.map.id,
+ depExports = this.depExports,
+ exports = this.exports,
+ factory = this.factory;
+
+ if (!this.inited) {
+ this.fetch();
+ } else if (this.error) {
+ this.emit('error', this.error);
+ } else if (!this.defining) {
+ //The factory could trigger another require call
+ //that would result in checking this module to
+ //define itself again. If already in the process
+ //of doing that, skip this work.
+ this.defining = true;
+
+ if (this.depCount < 1 && !this.defined) {
+ if (isFunction(factory)) {
+ //If there is an error listener, favor passing
+ //to that instead of throwing an error. However,
+ //only do it for define()'d modules. require
+ //errbacks should not be called for failures in
+ //their callbacks (#699). However if a global
+ //onError is set, use that.
+ if ((this.events.error && this.map.isDefine) ||
+ req.onError !== defaultOnError) {
+ try {
+ exports = context.execCb(id, factory, depExports, exports);
+ } catch (e) {
+ err = e;
+ }
+ } else {
+ exports = context.execCb(id, factory, depExports, exports);
+ }
+
+ if (this.map.isDefine) {
+ //If setting exports via 'module' is in play,
+ //favor that over return value and exports. After that,
+ //favor a non-undefined return value over exports use.
+ cjsModule = this.module;
+ if (cjsModule &&
+ cjsModule.exports !== undefined &&
+ //Make sure it is not already the exports value
+ cjsModule.exports !== this.exports) {
+ exports = cjsModule.exports;
+ } else if (exports === undefined && this.usingExports) {
+ //exports already set the defined value.
+ exports = this.exports;
+ }
+ }
+
+ if (err) {
+ err.requireMap = this.map;
+ err.requireModules = this.map.isDefine ? [this.map.id] : null;
+ err.requireType = this.map.isDefine ? 'define' : 'require';
+ return onError((this.error = err));
+ }
+
+ } else {
+ //Just a literal value
+ exports = factory;
+ }
+
+ this.exports = exports;
+
+ if (this.map.isDefine && !this.ignore) {
+ defined[id] = exports;
+
+ if (req.onResourceLoad) {
+ req.onResourceLoad(context, this.map, this.depMaps);
+ }
+ }
+
+ //Clean up
+ cleanRegistry(id);
+
+ this.defined = true;
+ }
+
+ //Finished the define stage. Allow calling check again
+ //to allow define notifications below in the case of a
+ //cycle.
+ this.defining = false;
+
+ if (this.defined && !this.defineEmitted) {
+ this.defineEmitted = true;
+ this.emit('defined', this.exports);
+ this.defineEmitComplete = true;
+ }
+
+ }
+ },
+
+ callPlugin: function () {
+ var map = this.map,
+ id = map.id,
+ //Map already normalized the prefix.
+ pluginMap = makeModuleMap(map.prefix);
+
+ //Mark this as a dependency for this plugin, so it
+ //can be traced for cycles.
+ this.depMaps.push(pluginMap);
+
+ on(pluginMap, 'defined', bind(this, function (plugin) {
+ var load, normalizedMap, normalizedMod,
+ name = this.map.name,
+ parentName = this.map.parentMap ? this.map.parentMap.name : null,
+ localRequire = context.makeRequire(map.parentMap, {
+ enableBuildCallback: true
+ });
+
+ //If current map is not normalized, wait for that
+ //normalized name to load instead of continuing.
+ if (this.map.unnormalized) {
+ //Normalize the ID if the plugin allows it.
+ if (plugin.normalize) {
+ name = plugin.normalize(name, function (name) {
+ return normalize(name, parentName, true);
+ }) || '';
+ }
+
+ //prefix and name should already be normalized, no need
+ //for applying map config again either.
+ normalizedMap = makeModuleMap(map.prefix + '!' + name,
+ this.map.parentMap);
+ on(normalizedMap,
+ 'defined', bind(this, function (value) {
+ this.init([], function () { return value; }, null, {
+ enabled: true,
+ ignore: true
+ });
+ }));
+
+ normalizedMod = getOwn(registry, normalizedMap.id);
+ if (normalizedMod) {
+ //Mark this as a dependency for this plugin, so it
+ //can be traced for cycles.
+ this.depMaps.push(normalizedMap);
+
+ if (this.events.error) {
+ normalizedMod.on('error', bind(this, function (err) {
+ this.emit('error', err);
+ }));
+ }
+ normalizedMod.enable();
+ }
+
+ return;
+ }
+
+ load = bind(this, function (value) {
+ this.init([], function () { return value; }, null, {
+ enabled: true
+ });
+ });
+
+ load.error = bind(this, function (err) {
+ this.inited = true;
+ this.error = err;
+ err.requireModules = [id];
+
+ //Remove temp unnormalized modules for this module,
+ //since they will never be resolved otherwise now.
+ eachProp(registry, function (mod) {
+ if (mod.map.id.indexOf(id + '_unnormalized') === 0) {
+ cleanRegistry(mod.map.id);
+ }
+ });
+
+ onError(err);
+ });
+
+ //Allow plugins to load other code without having to know the
+ //context or how to 'complete' the load.
+ load.fromText = bind(this, function (text, textAlt) {
+ /*jslint evil: true */
+ var moduleName = map.name,
+ moduleMap = makeModuleMap(moduleName),
+ hasInteractive = useInteractive;
+
+ //As of 2.1.0, support just passing the text, to reinforce
+ //fromText only being called once per resource. Still
+ //support old style of passing moduleName but discard
+ //that moduleName in favor of the internal ref.
+ if (textAlt) {
+ text = textAlt;
+ }
+
+ //Turn off interactive script matching for IE for any define
+ //calls in the text, then turn it back on at the end.
+ if (hasInteractive) {
+ useInteractive = false;
+ }
+
+ //Prime the system by creating a module instance for
+ //it.
+ getModule(moduleMap);
+
+ //Transfer any config to this other module.
+ if (hasProp(config.config, id)) {
+ config.config[moduleName] = config.config[id];
+ }
+
+ try {
+ req.exec(text);
+ } catch (e) {
+ return onError(makeError('fromtexteval',
+ 'fromText eval for ' + id +
+ ' failed: ' + e,
+ e,
+ [id]));
+ }
+
+ if (hasInteractive) {
+ useInteractive = true;
+ }
+
+ //Mark this as a dependency for the plugin
+ //resource
+ this.depMaps.push(moduleMap);
+
+ //Support anonymous modules.
+ context.completeLoad(moduleName);
+
+ //Bind the value of that module to the value for this
+ //resource ID.
+ localRequire([moduleName], load);
+ });
+
+ //Use parentName here since the plugin's name is not reliable,
+ //could be some weird string with no path that actually wants to
+ //reference the parentName's path.
+ plugin.load(map.name, localRequire, load, config);
+ }));
+
+ context.enable(pluginMap, this);
+ this.pluginMaps[pluginMap.id] = pluginMap;
+ },
+
+ enable: function () {
+ enabledRegistry[this.map.id] = this;
+ this.enabled = true;
+
+ //Set flag mentioning that the module is enabling,
+ //so that immediate calls to the defined callbacks
+ //for dependencies do not trigger inadvertent load
+ //with the depCount still being zero.
+ this.enabling = true;
+
+ //Enable each dependency
+ each(this.depMaps, bind(this, function (depMap, i) {
+ var id, mod, handler;
+
+ if (typeof depMap === 'string') {
+ //Dependency needs to be converted to a depMap
+ //and wired up to this module.
+ depMap = makeModuleMap(depMap,
+ (this.map.isDefine ? this.map : this.map.parentMap),
+ false,
+ !this.skipMap);
+ this.depMaps[i] = depMap;
+
+ handler = getOwn(handlers, depMap.id);
+
+ if (handler) {
+ this.depExports[i] = handler(this);
+ return;
+ }
+
+ this.depCount += 1;
+
+ on(depMap, 'defined', bind(this, function (depExports) {
+ this.defineDep(i, depExports);
+ this.check();
+ }));
+
+ if (this.errback) {
+ on(depMap, 'error', bind(this, this.errback));
+ }
+ }
+
+ id = depMap.id;
+ mod = registry[id];
+
+ //Skip special modules like 'require', 'exports', 'module'
+ //Also, don't call enable if it is already enabled,
+ //important in circular dependency cases.
+ if (!hasProp(handlers, id) && mod && !mod.enabled) {
+ context.enable(depMap, this);
+ }
+ }));
+
+ //Enable each plugin that is used in
+ //a dependency
+ eachProp(this.pluginMaps, bind(this, function (pluginMap) {
+ var mod = getOwn(registry, pluginMap.id);
+ if (mod && !mod.enabled) {
+ context.enable(pluginMap, this);
+ }
+ }));
+
+ this.enabling = false;
+
+ this.check();
+ },
+
+ on: function (name, cb) {
+ var cbs = this.events[name];
+ if (!cbs) {
+ cbs = this.events[name] = [];
+ }
+ cbs.push(cb);
+ },
+
+ emit: function (name, evt) {
+ each(this.events[name], function (cb) {
+ cb(evt);
+ });
+ if (name === 'error') {
+ //Now that the error handler was triggered, remove
+ //the listeners, since this broken Module instance
+ //can stay around for a while in the registry.
+ delete this.events[name];
+ }
+ }
+ };
+
+ function callGetModule(args) {
+ //Skip modules already defined.
+ if (!hasProp(defined, args[0])) {
+ getModule(makeModuleMap(args[0], null, true)).init(args[1], args[2]);
+ }
+ }
+
+ function removeListener(node, func, name, ieName) {
+ //Favor detachEvent because of IE9
+ //issue, see attachEvent/addEventListener comment elsewhere
+ //in this file.
+ if (node.detachEvent && !isOpera) {
+ //Probably IE. If not it will throw an error, which will be
+ //useful to know.
+ if (ieName) {
+ node.detachEvent(ieName, func);
+ }
+ } else {
+ node.removeEventListener(name, func, false);
+ }
+ }
+
+ /**
+ * Given an event from a script node, get the requirejs info from it,
+ * and then removes the event listeners on the node.
+ * @param {Event} evt
+ * @returns {Object}
+ */
+ function getScriptData(evt) {
+ //Using currentTarget instead of target for Firefox 2.0's sake. Not
+ //all old browsers will be supported, but this one was easy enough
+ //to support and still makes sense.
+ var node = evt.currentTarget || evt.srcElement;
+
+ //Remove the listeners once here.
+ removeListener(node, context.onScriptLoad, 'load', 'onreadystatechange');
+ removeListener(node, context.onScriptError, 'error');
+
+ return {
+ node: node,
+ id: node && node.getAttribute('data-requiremodule')
+ };
+ }
+
+ function intakeDefines() {
+ var args;
+
+ //Any defined modules in the global queue, intake them now.
+ takeGlobalQueue();
+
+ //Make sure any remaining defQueue items get properly processed.
+ while (defQueue.length) {
+ args = defQueue.shift();
+ if (args[0] === null) {
+ return onError(makeError('mismatch', 'Mismatched anonymous define() module: ' + args[args.length - 1]));
+ } else {
+ //args are id, deps, factory. Should be normalized by the
+ //define() function.
+ callGetModule(args);
+ }
+ }
+ }
+
+ context = {
+ config: config,
+ contextName: contextName,
+ registry: registry,
+ defined: defined,
+ urlFetched: urlFetched,
+ defQueue: defQueue,
+ Module: Module,
+ makeModuleMap: makeModuleMap,
+ nextTick: req.nextTick,
+ onError: onError,
+
+ /**
+ * Set a configuration for the context.
+ * @param {Object} cfg config object to integrate.
+ */
+ configure: function (cfg) {
+ //Make sure the baseUrl ends in a slash.
+ if (cfg.baseUrl) {
+ if (cfg.baseUrl.charAt(cfg.baseUrl.length - 1) !== '/') {
+ cfg.baseUrl += '/';
+ }
+ }
+
+ //Save off the paths and packages since they require special processing,
+ //they are additive.
+ var pkgs = config.pkgs,
+ shim = config.shim,
+ objs = {
+ paths: true,
+ config: true,
+ map: true
+ };
+
+ eachProp(cfg, function (value, prop) {
+ if (objs[prop]) {
+ if (prop === 'map') {
+ if (!config.map) {
+ config.map = {};
+ }
+ mixin(config[prop], value, true, true);
+ } else {
+ mixin(config[prop], value, true);
+ }
+ } else {
+ config[prop] = value;
+ }
+ });
+
+ //Merge shim
+ if (cfg.shim) {
+ eachProp(cfg.shim, function (value, id) {
+ //Normalize the structure
+ if (isArray(value)) {
+ value = {
+ deps: value
+ };
+ }
+ if ((value.exports || value.init) && !value.exportsFn) {
+ value.exportsFn = context.makeShimExports(value);
+ }
+ shim[id] = value;
+ });
+ config.shim = shim;
+ }
+
+ //Adjust packages if necessary.
+ if (cfg.packages) {
+ each(cfg.packages, function (pkgObj) {
+ var location;
+
+ pkgObj = typeof pkgObj === 'string' ? { name: pkgObj } : pkgObj;
+ location = pkgObj.location;
+
+ //Create a brand new object on pkgs, since currentPackages can
+ //be passed in again, and config.pkgs is the internal transformed
+ //state for all package configs.
+ pkgs[pkgObj.name] = {
+ name: pkgObj.name,
+ location: location || pkgObj.name,
+ //Remove leading dot in main, so main paths are normalized,
+ //and remove any trailing .js, since different package
+ //envs have different conventions: some use a module name,
+ //some use a file name.
+ main: (pkgObj.main || 'main')
+ .replace(currDirRegExp, '')
+ .replace(jsSuffixRegExp, '')
+ };
+ });
+
+ //Done with modifications, assing packages back to context config
+ config.pkgs = pkgs;
+ }
+
+ //If there are any "waiting to execute" modules in the registry,
+ //update the maps for them, since their info, like URLs to load,
+ //may have changed.
+ eachProp(registry, function (mod, id) {
+ //If module already has init called, since it is too
+ //late to modify them, and ignore unnormalized ones
+ //since they are transient.
+ if (!mod.inited && !mod.map.unnormalized) {
+ mod.map = makeModuleMap(id);
+ }
+ });
+
+ //If a deps array or a config callback is specified, then call
+ //require with those args. This is useful when require is defined as a
+ //config object before require.js is loaded.
+ if (cfg.deps || cfg.callback) {
+ context.require(cfg.deps || [], cfg.callback);
+ }
+ },
+
+ makeShimExports: function (value) {
+ function fn() {
+ var ret;
+ if (value.init) {
+ ret = value.init.apply(global, arguments);
+ }
+ return ret || (value.exports && getGlobal(value.exports));
+ }
+ return fn;
+ },
+
+ makeRequire: function (relMap, options) {
+ options = options || {};
+
+ function localRequire(deps, callback, errback) {
+ var id, map, requireMod;
+
+ if (options.enableBuildCallback && callback && isFunction(callback)) {
+ callback.__requireJsBuild = true;
+ }
+
+ if (typeof deps === 'string') {
+ if (isFunction(callback)) {
+ //Invalid call
+ return onError(makeError('requireargs', 'Invalid require call'), errback);
+ }
+
+ //If require|exports|module are requested, get the
+ //value for them from the special handlers. Caveat:
+ //this only works while module is being defined.
+ if (relMap && hasProp(handlers, deps)) {
+ return handlers[deps](registry[relMap.id]);
+ }
+
+ //Synchronous access to one module. If require.get is
+ //available (as in the Node adapter), prefer that.
+ if (req.get) {
+ return req.get(context, deps, relMap, localRequire);
+ }
+
+ //Normalize module name, if it contains . or ..
+ map = makeModuleMap(deps, relMap, false, true);
+ id = map.id;
+
+ if (!hasProp(defined, id)) {
+ return onError(makeError('notloaded', 'Module name "' +
+ id +
+ '" has not been loaded yet for context: ' +
+ contextName +
+ (relMap ? '' : '. Use require([])')));
+ }
+ return defined[id];
+ }
+
+ //Grab defines waiting in the global queue.
+ intakeDefines();
+
+ //Mark all the dependencies as needing to be loaded.
+ context.nextTick(function () {
+ //Some defines could have been added since the
+ //require call, collect them.
+ intakeDefines();
+
+ requireMod = getModule(makeModuleMap(null, relMap));
+
+ //Store if map config should be applied to this require
+ //call for dependencies.
+ requireMod.skipMap = options.skipMap;
+
+ requireMod.init(deps, callback, errback, {
+ enabled: true
+ });
+
+ checkLoaded();
+ });
+
+ return localRequire;
+ }
+
+ mixin(localRequire, {
+ isBrowser: isBrowser,
+
+ /**
+ * Converts a module name + .extension into an URL path.
+ * *Requires* the use of a module name. It does not support using
+ * plain URLs like nameToUrl.
+ */
+ toUrl: function (moduleNamePlusExt) {
+ var ext,
+ index = moduleNamePlusExt.lastIndexOf('.'),
+ segment = moduleNamePlusExt.split('/')[0],
+ isRelative = segment === '.' || segment === '..';
+
+ //Have a file extension alias, and it is not the
+ //dots from a relative path.
+ if (index !== -1 && (!isRelative || index > 1)) {
+ ext = moduleNamePlusExt.substring(index, moduleNamePlusExt.length);
+ moduleNamePlusExt = moduleNamePlusExt.substring(0, index);
+ }
+
+ return context.nameToUrl(normalize(moduleNamePlusExt,
+ relMap && relMap.id, true), ext, true);
+ },
+
+ defined: function (id) {
+ return hasProp(defined, makeModuleMap(id, relMap, false, true).id);
+ },
+
+ specified: function (id) {
+ id = makeModuleMap(id, relMap, false, true).id;
+ return hasProp(defined, id) || hasProp(registry, id);
+ }
+ });
+
+ //Only allow undef on top level require calls
+ if (!relMap) {
+ localRequire.undef = function (id) {
+ //Bind any waiting define() calls to this context,
+ //fix for #408
+ takeGlobalQueue();
+
+ var map = makeModuleMap(id, relMap, true),
+ mod = getOwn(registry, id);
+
+ removeScript(id);
+
+ delete defined[id];
+ delete urlFetched[map.url];
+ delete undefEvents[id];
+
+ if (mod) {
+ //Hold on to listeners in case the
+ //module will be attempted to be reloaded
+ //using a different config.
+ if (mod.events.defined) {
+ undefEvents[id] = mod.events;
+ }
+
+ cleanRegistry(id);
+ }
+ };
+ }
+
+ return localRequire;
+ },
+
+ /**
+ * Called to enable a module if it is still in the registry
+ * awaiting enablement. A second arg, parent, the parent module,
+ * is passed in for context, when this method is overridden by
+ * the optimizer. Not shown here to keep code compact.
+ */
+ enable: function (depMap) {
+ var mod = getOwn(registry, depMap.id);
+ if (mod) {
+ getModule(depMap).enable();
+ }
+ },
+
+ /**
+ * Internal method used by environment adapters to complete a load event.
+ * A load event could be a script load or just a load pass from a synchronous
+ * load call.
+ * @param {String} moduleName the name of the module to potentially complete.
+ */
+ completeLoad: function (moduleName) {
+ var found, args, mod,
+ shim = getOwn(config.shim, moduleName) || {},
+ shExports = shim.exports;
+
+ takeGlobalQueue();
+
+ while (defQueue.length) {
+ args = defQueue.shift();
+ if (args[0] === null) {
+ args[0] = moduleName;
+ //If already found an anonymous module and bound it
+ //to this name, then this is some other anon module
+ //waiting for its completeLoad to fire.
+ if (found) {
+ break;
+ }
+ found = true;
+ } else if (args[0] === moduleName) {
+ //Found matching define call for this script!
+ found = true;
+ }
+
+ callGetModule(args);
+ }
+
+ //Do this after the cycle of callGetModule in case the result
+ //of those calls/init calls changes the registry.
+ mod = getOwn(registry, moduleName);
+
+ if (!found && !hasProp(defined, moduleName) && mod && !mod.inited) {
+ if (config.enforceDefine && (!shExports || !getGlobal(shExports))) {
+ if (hasPathFallback(moduleName)) {
+ return;
+ } else {
+ return onError(makeError('nodefine',
+ 'No define call for ' + moduleName,
+ null,
+ [moduleName]));
+ }
+ } else {
+ //A script that does not call define(), so just simulate
+ //the call for it.
+ callGetModule([moduleName, (shim.deps || []), shim.exportsFn]);
+ }
+ }
+
+ checkLoaded();
+ },
+
+ /**
+ * Converts a module name to a file path. Supports cases where
+ * moduleName may actually be just an URL.
+ * Note that it **does not** call normalize on the moduleName,
+ * it is assumed to have already been normalized. This is an
+ * internal API, not a public one. Use toUrl for the public API.
+ */
+ nameToUrl: function (moduleName, ext, skipExt) {
+ var paths, pkgs, pkg, pkgPath, syms, i, parentModule, url,
+ parentPath;
+
+ //If a colon is in the URL, it indicates a protocol is used and it is just
+ //an URL to a file, or if it starts with a slash, contains a query arg (i.e. ?)
+ //or ends with .js, then assume the user meant to use an url and not a module id.
+ //The slash is important for protocol-less URLs as well as full paths.
+ if (req.jsExtRegExp.test(moduleName)) {
+ //Just a plain path, not module name lookup, so just return it.
+ //Add extension if it is included. This is a bit wonky, only non-.js things pass
+ //an extension, this method probably needs to be reworked.
+ url = moduleName + (ext || '');
+ } else {
+ //A module that needs to be converted to a path.
+ paths = config.paths;
+ pkgs = config.pkgs;
+
+ syms = moduleName.split('/');
+ //For each module name segment, see if there is a path
+ //registered for it. Start with most specific name
+ //and work up from it.
+ for (i = syms.length; i > 0; i -= 1) {
+ parentModule = syms.slice(0, i).join('/');
+ pkg = getOwn(pkgs, parentModule);
+ parentPath = getOwn(paths, parentModule);
+ if (parentPath) {
+ //If an array, it means there are a few choices,
+ //Choose the one that is desired
+ if (isArray(parentPath)) {
+ parentPath = parentPath[0];
+ }
+ syms.splice(0, i, parentPath);
+ break;
+ } else if (pkg) {
+ //If module name is just the package name, then looking
+ //for the main module.
+ if (moduleName === pkg.name) {
+ pkgPath = pkg.location + '/' + pkg.main;
+ } else {
+ pkgPath = pkg.location;
+ }
+ syms.splice(0, i, pkgPath);
+ break;
+ }
+ }
+
+ //Join the path parts together, then figure out if baseUrl is needed.
+ url = syms.join('/');
+ url += (ext || (/^data\:|\?/.test(url) || skipExt ? '' : '.js'));
+ url = (url.charAt(0) === '/' || url.match(/^[\w\+\.\-]+:/) ? '' : config.baseUrl) + url;
+ }
+
+ return config.urlArgs ? url +
+ ((url.indexOf('?') === -1 ? '?' : '&') +
+ config.urlArgs) : url;
+ },
+
+ //Delegates to req.load. Broken out as a separate function to
+ //allow overriding in the optimizer.
+ load: function (id, url) {
+ req.load(context, id, url);
+ },
+
+ /**
+ * Executes a module callback function. Broken out as a separate function
+ * solely to allow the build system to sequence the files in the built
+ * layer in the right sequence.
+ *
+ * @private
+ */
+ execCb: function (name, callback, args, exports) {
+ return callback.apply(exports, args);
+ },
+
+ /**
+ * callback for script loads, used to check status of loading.
+ *
+ * @param {Event} evt the event from the browser for the script
+ * that was loaded.
+ */
+ onScriptLoad: function (evt) {
+ //Using currentTarget instead of target for Firefox 2.0's sake. Not
+ //all old browsers will be supported, but this one was easy enough
+ //to support and still makes sense.
+ if (evt.type === 'load' ||
+ (readyRegExp.test((evt.currentTarget || evt.srcElement).readyState))) {
+ //Reset interactive script so a script node is not held onto for
+ //to long.
+ interactiveScript = null;
+
+ //Pull out the name of the module and the context.
+ var data = getScriptData(evt);
+ context.completeLoad(data.id);
+ }
+ },
+
+ /**
+ * Callback for script errors.
+ */
+ onScriptError: function (evt) {
+ var data = getScriptData(evt);
+ if (!hasPathFallback(data.id)) {
+ return onError(makeError('scripterror', 'Script error for: ' + data.id, evt, [data.id]));
+ }
+ }
+ };
+
+ context.require = context.makeRequire();
+ return context;
+ }
+
+ /**
+ * Main entry point.
+ *
+ * If the only argument to require is a string, then the module that
+ * is represented by that string is fetched for the appropriate context.
+ *
+ * If the first argument is an array, then it will be treated as an array
+ * of dependency string names to fetch. An optional function callback can
+ * be specified to execute when all of those dependencies are available.
+ *
+ * Make a local req variable to help Caja compliance (it assumes things
+ * on a require that are not standardized), and to give a short
+ * name for minification/local scope use.
+ */
+ req = requirejs = function (deps, callback, errback, optional) {
+
+ //Find the right context, use default
+ var context, config,
+ contextName = defContextName;
+
+ // Determine if have config object in the call.
+ if (!isArray(deps) && typeof deps !== 'string') {
+ // deps is a config object
+ config = deps;
+ if (isArray(callback)) {
+ // Adjust args if there are dependencies
+ deps = callback;
+ callback = errback;
+ errback = optional;
+ } else {
+ deps = [];
+ }
+ }
+
+ if (config && config.context) {
+ contextName = config.context;
+ }
+
+ context = getOwn(contexts, contextName);
+ if (!context) {
+ context = contexts[contextName] = req.s.newContext(contextName);
+ }
+
+ if (config) {
+ context.configure(config);
+ }
+
+ return context.require(deps, callback, errback);
+ };
+
+ /**
+ * Support require.config() to make it easier to cooperate with other
+ * AMD loaders on globally agreed names.
+ */
+ req.config = function (config) {
+ return req(config);
+ };
+
+ /**
+ * Execute something after the current tick
+ * of the event loop. Override for other envs
+ * that have a better solution than setTimeout.
+ * @param {Function} fn function to execute later.
+ */
+ req.nextTick = typeof setTimeout !== 'undefined' ? function (fn) {
+ setTimeout(fn, 4);
+ } : function (fn) { fn(); };
+
+ /**
+ * Export require as a global, but only if it does not already exist.
+ */
+ if (!require) {
+ require = req;
+ }
+
+ req.version = version;
+
+ //Used to filter out dependencies that are already paths.
+ req.jsExtRegExp = /^\/|:|\?|\.js$/;
+ req.isBrowser = isBrowser;
+ s = req.s = {
+ contexts: contexts,
+ newContext: newContext
+ };
+
+ //Create default context.
+ req({});
+
+ //Exports some context-sensitive methods on global require.
+ each([
+ 'toUrl',
+ 'undef',
+ 'defined',
+ 'specified'
+ ], function (prop) {
+ //Reference from contexts instead of early binding to default context,
+ //so that during builds, the latest instance of the default context
+ //with its config gets used.
+ req[prop] = function () {
+ var ctx = contexts[defContextName];
+ return ctx.require[prop].apply(ctx, arguments);
+ };
+ });
+
+ if (isBrowser) {
+ head = s.head = document.getElementsByTagName('head')[0];
+ //If BASE tag is in play, using appendChild is a problem for IE6.
+ //When that browser dies, this can be removed. Details in this jQuery bug:
+ //http://dev.jquery.com/ticket/2709
+ baseElement = document.getElementsByTagName('base')[0];
+ if (baseElement) {
+ head = s.head = baseElement.parentNode;
+ }
+ }
+
+ /**
+ * Any errors that require explicitly generates will be passed to this
+ * function. Intercept/override it if you want custom error handling.
+ * @param {Error} err the error object.
+ */
+ req.onError = defaultOnError;
+
+ /**
+ * Creates the node for the load command. Only used in browser envs.
+ */
+ req.createNode = function (config, moduleName, url) {
+ var node = config.xhtml ?
+ document.createElementNS('http://www.w3.org/1999/xhtml', 'html:script') :
+ document.createElement('script');
+ node.type = config.scriptType || 'text/javascript';
+ node.charset = 'utf-8';
+ node.async = true;
+ return node;
+ };
+
+ /**
+ * Does the request to load a module for the browser case.
+ * Make this a separate function to allow other environments
+ * to override it.
+ *
+ * @param {Object} context the require context to find state.
+ * @param {String} moduleName the name of the module.
+ * @param {Object} url the URL to the module.
+ */
+ req.load = function (context, moduleName, url) {
+ var config = (context && context.config) || {},
+ node;
+ if (isBrowser) {
+ //In the browser so use a script tag
+ node = req.createNode(config, moduleName, url);
+
+ node.setAttribute('data-requirecontext', context.contextName);
+ node.setAttribute('data-requiremodule', moduleName);
+
+ //Set up load listener. Test attachEvent first because IE9 has
+ //a subtle issue in its addEventListener and script onload firings
+ //that do not match the behavior of all other browsers with
+ //addEventListener support, which fire the onload event for a
+ //script right after the script execution. See:
+ //https://connect.microsoft.com/IE/feedback/details/648057/script-onload-event-is-not-fired-immediately-after-script-execution
+ //UNFORTUNATELY Opera implements attachEvent but does not follow the script
+ //script execution mode.
+ if (node.attachEvent &&
+ //Check if node.attachEvent is artificially added by custom script or
+ //natively supported by browser
+ //read https://github.com/jrburke/requirejs/issues/187
+ //if we can NOT find [native code] then it must NOT natively supported.
+ //in IE8, node.attachEvent does not have toString()
+ //Note the test for "[native code" with no closing brace, see:
+ //https://github.com/jrburke/requirejs/issues/273
+ !(node.attachEvent.toString && node.attachEvent.toString().indexOf('[native code') < 0) &&
+ !isOpera) {
+ //Probably IE. IE (at least 6-8) do not fire
+ //script onload right after executing the script, so
+ //we cannot tie the anonymous define call to a name.
+ //However, IE reports the script as being in 'interactive'
+ //readyState at the time of the define call.
+ useInteractive = true;
+
+ node.attachEvent('onreadystatechange', context.onScriptLoad);
+ //It would be great to add an error handler here to catch
+ //404s in IE9+. However, onreadystatechange will fire before
+ //the error handler, so that does not help. If addEventListener
+ //is used, then IE will fire error before load, but we cannot
+ //use that pathway given the connect.microsoft.com issue
+ //mentioned above about not doing the 'script execute,
+ //then fire the script load event listener before execute
+ //next script' that other browsers do.
+ //Best hope: IE10 fixes the issues,
+ //and then destroys all installs of IE 6-9.
+ //node.attachEvent('onerror', context.onScriptError);
+ } else {
+ node.addEventListener('load', context.onScriptLoad, false);
+ node.addEventListener('error', context.onScriptError, false);
+ }
+ node.src = url;
+
+ //For some cache cases in IE 6-8, the script executes before the end
+ //of the appendChild execution, so to tie an anonymous define
+ //call to the module name (which is stored on the node), hold on
+ //to a reference to this node, but clear after the DOM insertion.
+ currentlyAddingScript = node;
+ if (baseElement) {
+ head.insertBefore(node, baseElement);
+ } else {
+ head.appendChild(node);
+ }
+ currentlyAddingScript = null;
+
+ return node;
+ } else if (isWebWorker) {
+ try {
+ //In a web worker, use importScripts. This is not a very
+ //efficient use of importScripts, importScripts will block until
+ //its script is downloaded and evaluated. However, if web workers
+ //are in play, the expectation that a build has been done so that
+ //only one script needs to be loaded anyway. This may need to be
+ //reevaluated if other use cases become common.
+ importScripts(url);
+
+ //Account for anonymous modules
+ context.completeLoad(moduleName);
+ } catch (e) {
+ context.onError(makeError('importscripts',
+ 'importScripts failed for ' +
+ moduleName + ' at ' + url,
+ e,
+ [moduleName]));
+ }
+ }
+ };
+
+ function getInteractiveScript() {
+ if (interactiveScript && interactiveScript.readyState === 'interactive') {
+ return interactiveScript;
+ }
+
+ eachReverse(scripts(), function (script) {
+ if (script.readyState === 'interactive') {
+ return (interactiveScript = script);
+ }
+ });
+ return interactiveScript;
+ }
+
+ //Look for a data-main script attribute, which could also adjust the baseUrl.
+ if (isBrowser && !cfg.skipDataMain) {
+ //Figure out baseUrl. Get it from the script tag with require.js in it.
+ eachReverse(scripts(), function (script) {
+ //Set the 'head' where we can append children by
+ //using the script's parent.
+ if (!head) {
+ head = script.parentNode;
+ }
+
+ //Look for a data-main attribute to set main script for the page
+ //to load. If it is there, the path to data main becomes the
+ //baseUrl, if it is not already set.
+ dataMain = script.getAttribute('data-main');
+ if (dataMain) {
+ //Preserve dataMain in case it is a path (i.e. contains '?')
+ mainScript = dataMain;
+
+ //Set final baseUrl if there is not already an explicit one.
+ if (!cfg.baseUrl) {
+ //Pull off the directory of data-main for use as the
+ //baseUrl.
+ src = mainScript.split('/');
+ mainScript = src.pop();
+ subPath = src.length ? src.join('/') + '/' : './';
+
+ cfg.baseUrl = subPath;
+ }
+
+ //Strip off any trailing .js since mainScript is now
+ //like a module name.
+ mainScript = mainScript.replace(jsSuffixRegExp, '');
+
+ //If mainScript is still a path, fall back to dataMain
+ if (req.jsExtRegExp.test(mainScript)) {
+ mainScript = dataMain;
+ }
+
+ //Put the data-main script in the files to load.
+ cfg.deps = cfg.deps ? cfg.deps.concat(mainScript) : [mainScript];
+
+ return true;
+ }
+ });
+ }
+
+ /**
+ * The function that handles definitions of modules. Differs from
+ * require() in that a string for the module should be the first argument,
+ * and the function to execute after dependencies are loaded should
+ * return a value to define the module corresponding to the first argument's
+ * name.
+ */
+ define = function (name, deps, callback) {
+ var node, context;
+
+ //Allow for anonymous modules
+ if (typeof name !== 'string') {
+ //Adjust args appropriately
+ callback = deps;
+ deps = name;
+ name = null;
+ }
+
+ //This module may not have dependencies
+ if (!isArray(deps)) {
+ callback = deps;
+ deps = null;
+ }
+
+ //If no name, and callback is a function, then figure out if it a
+ //CommonJS thing with dependencies.
+ if (!deps && isFunction(callback)) {
+ deps = [];
+ //Remove comments from the callback string,
+ //look for require calls, and pull them into the dependencies,
+ //but only if there are function args.
+ if (callback.length) {
+ callback
+ .toString()
+ .replace(commentRegExp, '')
+ .replace(cjsRequireRegExp, function (match, dep) {
+ deps.push(dep);
+ });
+
+ //May be a CommonJS thing even without require calls, but still
+ //could use exports, and module. Avoid doing exports and module
+ //work though if it just needs require.
+ //REQUIRES the function to expect the CommonJS variables in the
+ //order listed below.
+ deps = (callback.length === 1 ? ['require'] : ['require', 'exports', 'module']).concat(deps);
+ }
+ }
+
+ //If in IE 6-8 and hit an anonymous define() call, do the interactive
+ //work.
+ if (useInteractive) {
+ node = currentlyAddingScript || getInteractiveScript();
+ if (node) {
+ if (!name) {
+ name = node.getAttribute('data-requiremodule');
+ }
+ context = contexts[node.getAttribute('data-requirecontext')];
+ }
+ }
+
+ //Always save off evaluating the def call until the script onload handler.
+ //This allows multiple modules to be in a file without prematurely
+ //tracing dependencies, and allows for anonymous module support,
+ //where the module name is not known until the script onload event
+ //occurs. If no context, use the global queue, and get it processed
+ //in the onscript load callback.
+ (context ? context.defQueue : globalDefQueue).push([name, deps, callback]);
+ };
+
+ define.amd = {
+ jQuery: true
+ };
+
+
+ /**
+ * Executes the text. Normally just uses eval, but can be modified
+ * to use a better, environment-specific call. Only used for transpiling
+ * loader plugins, not for plain JS modules.
+ * @param {String} text the text to execute/evaluate.
+ */
+ req.exec = function (text) {
+ /*jslint evil: true */
+ return eval(text);
+ };
+
+ //Set up with config info.
+ req(cfg);
+}(this));
diff --git a/spec/whitespace-control.js b/spec/whitespace-control.js
new file mode 100644
index 000000000..cea4249b5
--- /dev/null
+++ b/spec/whitespace-control.js
@@ -0,0 +1,147 @@
+describe('whitespace control', function () {
+ it('should strip whitespace around mustache calls', function () {
+ var hash = { foo: 'bar<' };
+
+ expectTemplate(' {{~foo~}} ').withInput(hash).toCompileTo('bar<');
+
+ expectTemplate(' {{~foo}} ').withInput(hash).toCompileTo('bar< ');
+
+ expectTemplate(' {{foo~}} ').withInput(hash).toCompileTo(' bar<');
+
+ expectTemplate(' {{~&foo~}} ').withInput(hash).toCompileTo('bar<');
+
+ expectTemplate(' {{~{foo}~}} ').withInput(hash).toCompileTo('bar<');
+
+ expectTemplate('1\n{{foo~}} \n\n 23\n{{bar}}4').toCompileTo('1\n23\n4');
+ });
+
+ describe('blocks', function () {
+ it('should strip whitespace around simple block calls', function () {
+ var hash = { foo: 'bar<' };
+
+ expectTemplate(' {{~#if foo~}} bar {{~/if~}} ')
+ .withInput(hash)
+ .toCompileTo('bar');
+
+ expectTemplate(' {{#if foo~}} bar {{/if~}} ')
+ .withInput(hash)
+ .toCompileTo(' bar ');
+
+ expectTemplate(' {{~#if foo}} bar {{~/if}} ')
+ .withInput(hash)
+ .toCompileTo(' bar ');
+
+ expectTemplate(' {{#if foo}} bar {{/if}} ')
+ .withInput(hash)
+ .toCompileTo(' bar ');
+
+ expectTemplate(' \n\n{{~#if foo~}} \n\nbar \n\n{{~/if~}}\n\n ')
+ .withInput(hash)
+ .toCompileTo('bar');
+
+ expectTemplate(' a\n\n{{~#if foo~}} \n\nbar \n\n{{~/if~}}\n\na ')
+ .withInput(hash)
+ .toCompileTo(' abara ');
+ });
+
+ it('should strip whitespace around inverse block calls', function () {
+ expectTemplate(' {{~^if foo~}} bar {{~/if~}} ').toCompileTo('bar');
+
+ expectTemplate(' {{^if foo~}} bar {{/if~}} ').toCompileTo(' bar ');
+
+ expectTemplate(' {{~^if foo}} bar {{~/if}} ').toCompileTo(' bar ');
+
+ expectTemplate(' {{^if foo}} bar {{/if}} ').toCompileTo(' bar ');
+
+ expectTemplate(
+ ' \n\n{{~^if foo~}} \n\nbar \n\n{{~/if~}}\n\n '
+ ).toCompileTo('bar');
+ });
+
+ it('should strip whitespace around complex block calls', function () {
+ var hash = { foo: 'bar<' };
+
+ expectTemplate('{{#if foo~}} bar {{~^~}} baz {{~/if}}')
+ .withInput(hash)
+ .toCompileTo('bar');
+
+ expectTemplate('{{#if foo~}} bar {{^~}} baz {{/if}}')
+ .withInput(hash)
+ .toCompileTo('bar ');
+
+ expectTemplate('{{#if foo}} bar {{~^~}} baz {{~/if}}')
+ .withInput(hash)
+ .toCompileTo(' bar');
+
+ expectTemplate('{{#if foo}} bar {{^~}} baz {{/if}}')
+ .withInput(hash)
+ .toCompileTo(' bar ');
+
+ expectTemplate('{{#if foo~}} bar {{~else~}} baz {{~/if}}')
+ .withInput(hash)
+ .toCompileTo('bar');
+
+ expectTemplate(
+ '\n\n{{~#if foo~}} \n\nbar \n\n{{~^~}} \n\nbaz \n\n{{~/if~}}\n\n'
+ )
+ .withInput(hash)
+ .toCompileTo('bar');
+
+ expectTemplate(
+ '\n\n{{~#if foo~}} \n\n{{{foo}}} \n\n{{~^~}} \n\nbaz \n\n{{~/if~}}\n\n'
+ )
+ .withInput(hash)
+ .toCompileTo('bar<');
+
+ expectTemplate('{{#if foo~}} bar {{~^~}} baz {{~/if}}').toCompileTo(
+ 'baz'
+ );
+
+ expectTemplate('{{#if foo}} bar {{~^~}} baz {{/if}}').toCompileTo('baz ');
+
+ expectTemplate('{{#if foo~}} bar {{~^}} baz {{~/if}}').toCompileTo(
+ ' baz'
+ );
+
+ expectTemplate('{{#if foo~}} bar {{~^}} baz {{/if}}').toCompileTo(
+ ' baz '
+ );
+
+ expectTemplate('{{#if foo~}} bar {{~else~}} baz {{~/if}}').toCompileTo(
+ 'baz'
+ );
+
+ expectTemplate(
+ '\n\n{{~#if foo~}} \n\nbar \n\n{{~^~}} \n\nbaz \n\n{{~/if~}}\n\n'
+ ).toCompileTo('baz');
+ });
+ });
+
+ it('should strip whitespace around partials', function () {
+ expectTemplate('foo {{~> dude~}} ')
+ .withPartials({ dude: 'bar' })
+ .toCompileTo('foobar');
+
+ expectTemplate('foo {{> dude~}} ')
+ .withPartials({ dude: 'bar' })
+ .toCompileTo('foo bar');
+
+ expectTemplate('foo {{> dude}} ')
+ .withPartials({ dude: 'bar' })
+ .toCompileTo('foo bar ');
+
+ expectTemplate('foo\n {{~> dude}} ')
+ .withPartials({ dude: 'bar' })
+ .toCompileTo('foobar');
+
+ expectTemplate('foo\n {{> dude}} ')
+ .withPartials({ dude: 'bar' })
+ .toCompileTo('foo\n bar');
+ });
+
+ it('should only strip whitespace once', function () {
+ expectTemplate(' {{~foo~}} {{foo}} {{foo}} ')
+ .withInput({ foo: 'bar' })
+ .toCompileTo('barbar bar ');
+ });
+});
diff --git a/src/handlebars.l b/src/handlebars.l
deleted file mode 100644
index 592fd5c7a..000000000
--- a/src/handlebars.l
+++ /dev/null
@@ -1,42 +0,0 @@
-
-%x mu emu
-
-%%
-
-[^\x00]*?/("{{") {
- if(yytext.slice(-1) !== "\\") this.begin("mu");
- if(yytext.slice(-1) === "\\") yytext = yytext.substr(0,yyleng-1), this.begin("emu");
- if(yytext) return 'CONTENT';
- }
-
-[^\x00]+ { return 'CONTENT'; }
-
-[^\x00]{2,}?/("{{") { this.popState(); return 'CONTENT'; }
-
-"{{>" { return 'OPEN_PARTIAL'; }
-"{{#" { return 'OPEN_BLOCK'; }
-"{{/" { return 'OPEN_ENDBLOCK'; }
-"{{^" { return 'OPEN_INVERSE'; }
-"{{"\s*"else" { return 'OPEN_INVERSE'; }
-"{{{" { return 'OPEN_UNESCAPED'; }
-"{{&" { return 'OPEN_UNESCAPED'; }
-"{{!"[\s\S]*?"}}" { yytext = yytext.substr(3,yyleng-5); this.popState(); return 'COMMENT'; }
-"{{" { return 'OPEN'; }
-
-"=" { return 'EQUALS'; }
-"."/[} ] { return 'ID'; }
-".." { return 'ID'; }
-[\/.] { return 'SEP'; }
-\s+ { /*ignore whitespace*/ }
-"}}}" { this.popState(); return 'CLOSE'; }
-"}}" { this.popState(); return 'CLOSE'; }
-'"'("\\"["]|[^"])*'"' { yytext = yytext.substr(1,yyleng-2).replace(/\\"/g,'"'); return 'STRING'; }
-"true"/[}\s] { return 'BOOLEAN'; }
-"false"/[}\s] { return 'BOOLEAN'; }
-[0-9]+/[}\s] { return 'INTEGER'; }
-[a-zA-Z0-9_$-]+/[=}\s\/.] { return 'ID'; }
-'['[^\]]*']' { yytext = yytext.substr(1, yyleng-2); return 'ID'; }
-. { return 'INVALID'; }
-
-<> { return 'EOF'; }
-
diff --git a/src/handlebars.yy b/src/handlebars.yy
deleted file mode 100644
index ec4fbe186..000000000
--- a/src/handlebars.yy
+++ /dev/null
@@ -1,99 +0,0 @@
-%start root
-
-%%
-
-root
- : program EOF { return $1; }
- ;
-
-program
- : statements simpleInverse statements { $$ = new yy.ProgramNode($1, $3); }
- | statements { $$ = new yy.ProgramNode($1); }
- | "" { $$ = new yy.ProgramNode([]); }
- ;
-
-statements
- : statement { $$ = [$1]; }
- | statements statement { $1.push($2); $$ = $1; }
- ;
-
-statement
- : openInverse program closeBlock { $$ = new yy.BlockNode($1, $2.inverse, $2, $3); }
- | openBlock program closeBlock { $$ = new yy.BlockNode($1, $2, $2.inverse, $3); }
- | mustache { $$ = $1; }
- | partial { $$ = $1; }
- | CONTENT { $$ = new yy.ContentNode($1); }
- | COMMENT { $$ = new yy.CommentNode($1); }
- ;
-
-openBlock
- : OPEN_BLOCK inMustache CLOSE { $$ = new yy.MustacheNode($2[0], $2[1]); }
- ;
-
-openInverse
- : OPEN_INVERSE inMustache CLOSE { $$ = new yy.MustacheNode($2[0], $2[1]); }
- ;
-
-closeBlock
- : OPEN_ENDBLOCK path CLOSE { $$ = $2; }
- ;
-
-mustache
- : OPEN inMustache CLOSE { $$ = new yy.MustacheNode($2[0], $2[1]); }
- | OPEN_UNESCAPED inMustache CLOSE { $$ = new yy.MustacheNode($2[0], $2[1], true); }
- ;
-
-
-partial
- : OPEN_PARTIAL path CLOSE { $$ = new yy.PartialNode($2); }
- | OPEN_PARTIAL path path CLOSE { $$ = new yy.PartialNode($2, $3); }
- ;
-
-simpleInverse
- : OPEN_INVERSE CLOSE { }
- ;
-
-inMustache
- : path params hash { $$ = [[$1].concat($2), $3]; }
- | path params { $$ = [[$1].concat($2), null]; }
- | path hash { $$ = [[$1], $2]; }
- | path { $$ = [[$1], null]; }
- ;
-
-params
- : params param { $1.push($2); $$ = $1; }
- | param { $$ = [$1]; }
- ;
-
-param
- : path { $$ = $1; }
- | STRING { $$ = new yy.StringNode($1); }
- | INTEGER { $$ = new yy.IntegerNode($1); }
- | BOOLEAN { $$ = new yy.BooleanNode($1); }
- ;
-
-hash
- : hashSegments { $$ = new yy.HashNode($1); }
- ;
-
-hashSegments
- : hashSegments hashSegment { $1.push($2); $$ = $1; }
- | hashSegment { $$ = [$1]; }
- ;
-
-hashSegment
- : ID EQUALS path { $$ = [$1, $3]; }
- | ID EQUALS STRING { $$ = [$1, new yy.StringNode($3)]; }
- | ID EQUALS INTEGER { $$ = [$1, new yy.IntegerNode($3)]; }
- | ID EQUALS BOOLEAN { $$ = [$1, new yy.BooleanNode($3)]; }
- ;
-
-path
- : pathSegments { $$ = new yy.IdNode($1); }
- ;
-
-pathSegments
- : pathSegments SEP ID { $1.push($3); $$ = $1; }
- | ID { $$ = [$1]; }
- ;
-
diff --git a/tasks/publish-to-aws.js b/tasks/publish-to-aws.js
new file mode 100644
index 000000000..80b2b5120
--- /dev/null
+++ b/tasks/publish-to-aws.js
@@ -0,0 +1,123 @@
+/* eslint-disable no-console */
+const fs = require('fs');
+const { S3, PutObjectCommand } = require('@aws-sdk/client-s3');
+const git = require('./util/git');
+const semver = require('semver');
+
+const PUBLISHED_FILES = [
+ 'handlebars.js',
+ 'handlebars.min.js',
+ 'handlebars.runtime.js',
+ 'handlebars.runtime.min.js',
+];
+
+let s3Client;
+
+async function main() {
+ console.log('remotes: ' + (await git.remotes()));
+ console.log('branches: ' + (await git.branches()));
+
+ const commitInfo = await git.commitInfo();
+ console.log('tag: ', commitInfo.tagName);
+
+ const suffixes = buildSuffixes(commitInfo);
+
+ if (suffixes.length > 0) {
+ validateS3Env();
+ console.log('publishing file-suffixes: ' + JSON.stringify(suffixes));
+ await publish(suffixes);
+ }
+}
+
+function buildSuffixes(commitInfo) {
+ const suffixes = [];
+
+ if (commitInfo.isMaster) {
+ suffixes.push('-latest');
+ suffixes.push('-' + commitInfo.headSha);
+ }
+
+ if (commitInfo.tagName != null && semver.valid(commitInfo.tagName)) {
+ suffixes.push('-' + commitInfo.tagName);
+ }
+
+ return suffixes;
+}
+
+function validateS3Env() {
+ const bucket = process.env.S3_BUCKET_NAME,
+ region = process.env.S3_REGION,
+ key = process.env.S3_ACCESS_KEY_ID,
+ secret = process.env.S3_SECRET_ACCESS_KEY;
+
+ if (!bucket || !region || !key || !secret) {
+ throw new Error('Missing S3 config values');
+ }
+}
+
+async function publish(suffixes, overrides) {
+ const publishPromises = suffixes.map((suffix) =>
+ publishSuffix(suffix, overrides)
+ );
+ return Promise.all(publishPromises);
+}
+
+async function publishSuffix(suffix, overrides) {
+ const publishPromises = PUBLISHED_FILES.map(async (filename) => {
+ const nameInBucket = getNameInBucket(filename, suffix);
+ const localFile = getLocalFile(filename);
+ await uploadToBucket(localFile, nameInBucket, overrides);
+ console.log(`Published ${localFile} to build server (${nameInBucket})`);
+ });
+ return Promise.all(publishPromises);
+}
+
+async function uploadToBucket(localFile, nameInBucket, overrides) {
+ const s3 = overrides?.s3Client ?? getS3Client();
+ const bucket = overrides?.bucket ?? process.env.S3_BUCKET_NAME;
+
+ return s3.send(
+ new PutObjectCommand({
+ Bucket: bucket,
+ Key: nameInBucket,
+ Body: fs.readFileSync(localFile, 'utf8'),
+ })
+ );
+}
+
+function getS3Client() {
+ if (!s3Client) {
+ s3Client = new S3({
+ region: process.env.S3_REGION,
+ credentials: {
+ accessKeyId: process.env.S3_ACCESS_KEY_ID,
+ secretAccessKey: process.env.S3_SECRET_ACCESS_KEY,
+ },
+ });
+ }
+ return s3Client;
+}
+
+function getNameInBucket(filename, suffix) {
+ return filename.replace(/\.js$/, suffix + '.js');
+}
+
+function getLocalFile(filename) {
+ return 'dist/' + filename;
+}
+
+module.exports = {
+ PUBLISHED_FILES,
+ buildSuffixes,
+ validateS3Env,
+ publish,
+ getNameInBucket,
+ getLocalFile,
+};
+
+if (require.main === module) {
+ main().catch((err) => {
+ console.error(err);
+ process.exit(1);
+ });
+}
diff --git a/tasks/tests/README.md b/tasks/tests/README.md
new file mode 100644
index 000000000..3c4051cdf
--- /dev/null
+++ b/tasks/tests/README.md
@@ -0,0 +1 @@
+Use `mocha tasks/tests` to run these tests
diff --git a/tasks/tests/cli.test.js b/tasks/tests/cli.test.js
new file mode 100644
index 000000000..0b7edb5a4
--- /dev/null
+++ b/tasks/tests/cli.test.js
@@ -0,0 +1,274 @@
+const fs = require('fs');
+const { exec } = require('child_process');
+const { execCommand, FileTestHelper } = require('cli-testlab');
+const Handlebars = require('../../lib');
+
+const cli = 'node ./bin/handlebars.mjs';
+
+expect.extend({
+ toEqualWithRelaxedSpace(received, expected) {
+ const normalize = (str) =>
+ typeof str === 'string'
+ ? str
+ .replace(/\r\n/g, '\n')
+ .split('\n')
+ .map((line) => line.replace(/\s+/g, ' ').trim())
+ .filter((line) => line.length > 0)
+ .join('\n')
+ .trim()
+ : str;
+
+ const normalizedReceived = normalize(received);
+ const normalizedExpected = normalize(expected);
+ const pass = normalizedReceived === normalizedExpected;
+
+ return {
+ pass,
+ message: () =>
+ `Expected output to match with relaxed whitespace.\n\n` +
+ `Expected:\n${normalizedExpected}\n\nReceived:\n${normalizedReceived}`,
+ };
+ },
+});
+
+function expectedFile(specPath) {
+ return fs.readFileSync(specPath, 'utf-8');
+}
+
+describe('bin/handlebars', function () {
+ describe('help and version', function () {
+ it('--help displays help menu', async function () {
+ const result = await execCommand(`${cli} --help`);
+ expect(result.stdout).toEqualWithRelaxedSpace(
+ expectedFile('./spec/expected/help.menu.txt')
+ );
+ });
+
+ it('no arguments displays help menu', async function () {
+ const result = await execCommand(`${cli}`);
+ expect(result.stdout).toEqualWithRelaxedSpace(
+ expectedFile('./spec/expected/help.menu.txt')
+ );
+ });
+
+ it('-v prints the compiler version', async function () {
+ await execCommand(`${cli} -v`, {
+ expectedOutput: Handlebars.VERSION,
+ });
+ });
+ });
+
+ describe('AMD output', function () {
+ it('-a produces AMD output', async function () {
+ const result = await execCommand(
+ `${cli} -a spec/artifacts/empty.handlebars`
+ );
+ expect(result.stdout).toEqualWithRelaxedSpace(
+ expectedFile('./spec/expected/empty.amd.js')
+ );
+ });
+
+ it('-a -s produces simple AMD output', async function () {
+ const result = await execCommand(
+ `${cli} -a -s spec/artifacts/empty.handlebars`
+ );
+ expect(result.stdout).toEqualWithRelaxedSpace(
+ expectedFile('./spec/expected/empty.amd.simple.js')
+ );
+ });
+
+ it('-a -m produces minified AMD output', async function () {
+ const result = await execCommand(
+ `${cli} -a -m spec/artifacts/empty.handlebars`
+ );
+ expect(result.stdout).toEqualWithRelaxedSpace(
+ expectedFile('./spec/expected/empty.amd.min.js')
+ );
+ });
+ });
+
+ describe('CommonJS output', function () {
+ it('-c produces CommonJS output', async function () {
+ const result = await execCommand(
+ `${cli} spec/artifacts/empty.handlebars -c`
+ );
+ expect(result.stdout).toEqualWithRelaxedSpace(
+ expectedFile('./spec/expected/empty.common.js')
+ );
+ });
+ });
+
+ describe('namespace', function () {
+ it('-n sets custom namespace', async function () {
+ const result = await execCommand(
+ `${cli} -a -n CustomNamespace.templates spec/artifacts/empty.handlebars`
+ );
+ expect(result.stdout).toEqualWithRelaxedSpace(
+ expectedFile('./spec/expected/empty.amd.namespace.js')
+ );
+ });
+
+ it('--namespace sets custom namespace', async function () {
+ const result = await execCommand(
+ `${cli} -a --namespace CustomNamespace.templates spec/artifacts/empty.handlebars`
+ );
+ expect(result.stdout).toEqualWithRelaxedSpace(
+ expectedFile('./spec/expected/empty.amd.namespace.js')
+ );
+ });
+
+ it('multiple files share a namespace', async function () {
+ const result = await execCommand(
+ `${cli} spec/artifacts/empty.handlebars spec/artifacts/empty.handlebars -a -n someNameSpace`
+ );
+ expect(result.stdout).toEqualWithRelaxedSpace(
+ expectedFile('./spec/expected/namespace.amd.js')
+ );
+ });
+ });
+
+ describe('file output', function () {
+ let files;
+
+ beforeEach(function () {
+ files = new FileTestHelper({ basePath: '.' });
+ files.createDir('tmp');
+ });
+
+ afterEach(function () {
+ files.cleanup();
+ });
+
+ it('-f writes output to a file', async function () {
+ const outputFile = 'tmp/cli-test-output.js';
+ files.registerForCleanup(outputFile);
+
+ await execCommand(
+ `${cli} -a -f ${outputFile} spec/artifacts/empty.handlebars`
+ );
+
+ expect(files.fileExists(outputFile)).toBe(true);
+ const content = files.getFileTextContent(outputFile);
+ expect(content).toEqualWithRelaxedSpace(
+ expectedFile('./spec/expected/empty.amd.js')
+ );
+ });
+
+ it('--map writes source map and appends sourceMappingURL', async function () {
+ const mapFile = 'tmp/cli-test-source.map';
+ files.registerForCleanup(mapFile);
+
+ const result = await execCommand(
+ `${cli} -i "