Compare commits

...

203 Commits

Author SHA1 Message Date
Nils Knappmeier ee913e28bd Added tests for multiple partial-block calls with inline partials
- nested inline partials with partial-blocks on different nesting levels
- nested inline partials (twice at each level)
2017-01-02 10:13:49 +01:00
Nils Knappmeier 7a77f61c44 Add more tests for different scenarios of using partial-blocks
- Multiple partial-blocks at different nesting levels
- Calling partial-blocks twice with nested partial-blocks
- Calling the partial-block from within the #each-helper
2017-01-02 10:05:21 +01:00
Nils Knappmeier 72753bcaa3 Possible fix for #1252: Refactoring for nested partial-block calls
This fix treats partial-blocks more like closures and uses the closure-context of
the "invokePartial"-function to store the @partial-block for the partial.
2017-01-01 08:45:59 +01:00
Nils Knappmeier f3d266a66e Test-case for #1252: Using @partial-block twice in a template not possible 2016-12-31 00:32:03 +01:00
Nils Knappmeier a023cb4dd9 Make "column"-property of Errors enumerable
Fixes #1284

Appearently, there is a use-case of stringifying the error in order to
evaluated its properties on another system. There was a regression
from 4.0.5  to 4.0.6 that the column-property of compilation errors
was not  enumerable anymore in 4.0.6 (due to  commit 20c965c) and
thus was not included in the output of "JSON.stringify".
2016-12-30 16:33:32 +01:00
Nils Knappmeier c7dc353956 Testcase to verify that compile-errors have a column-property
Related to #1284

The test ensures that the property is there, because it is important to
some people.
2016-12-30 16:33:32 +01:00
Lon Ingram ad3037cf54 v4.0.6 (again)
Missed some versions in the repo that needed bumping.
2016-11-12 12:32:45 -06:00
Lon Ingram 44ce5f40aa Update release notes 2016-11-12 10:40:57 -06:00
Lon Ingram 3385de0921 v4.0.6 2016-11-11 12:04:25 -06:00
Lon Ingram abc160ae8e Use XML-like tags in test instead of bizarre dot delimiters 2016-11-11 12:01:16 -06:00
Lon Ingram 5ee91cb58a Add test reproducing #1185 2016-11-11 12:01:16 -06:00
Lon Ingram ef9e0dc2ab Walk up data frames for nested @partial-block
The root cause of #1218 is that `invokePartial` creates a stack of data frames
for nested partial blocks, but `resolvePartial` always uses the value at top of
the stack without "popping" it. The result is an infinite recursive loop, as
references to `@partial-block` in the partial at the top of the stack resolve to
itself.

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

Fix #1218
2016-11-11 12:01:16 -06:00
Kabir 0d0c4d1bbd Add a new lightweight package based on handlebars in the README
Add a new package [just-handlebars-helpers](https://github.com/leapfrogtechnology/just-handlebars-helpers) on the list. 

This is a lightweight package that offers a collection of common handlebars helpers. And it is [fully tested](https://codecov.io/github/leapfrogtechnology/just-handlebars-helpers)
2016-11-11 12:01:16 -06:00
Charles O'Farrell 8ff49cef52 Ensure that existing blockParams and depths are respected on dupe programs
Fixes #1186
2016-11-11 12:01:16 -06:00
Mr. Leo 1b885d496a Added cory 2016-11-11 12:01:16 -06:00
Gennadiy Litvinyuk 45e14b7b45 Preserve License info in Closure Compiler
To preserve license info in Closure Compiler the license has to be JSDoc-comment (not simple comment) and have @license before license text
2016-11-11 12:01:16 -06:00
Paul Falgout 1379e8f50c Contributing doc fix: failing thats -> failing tests 2016-11-11 12:01:16 -06:00
Tim Wang 6272c15074 Update license date 2016-11-11 12:01:16 -06:00
kpdecker 959ee557b0 Update jsfiddle to point to latest 2016-11-11 12:01:16 -06:00
Anders D. Johnson 577b760b50 Fix typos on decorators-api.md. 2016-11-11 12:01:16 -06:00
kpdecker 8c19874497 Drop extra Error params
This was causing a difficult to diagnose failure under IE and doesn’t give us enough value to justify the change.
2016-11-11 12:01:16 -06:00
Ryan Lewis 8fc16367b0 Add documentation for running tests to contributing.md
lawnsea: this contains the changes from 8a9c79b
2016-11-11 12:01:16 -06:00
kpdecker 400916c225 Avoid error in older browsers in test
The tests are run through the transpiler and just reverting the user of template literal is easier than adding transpiler to the test stack.
2016-11-11 12:01:16 -06:00
kpdecker fee23342b1 Update target browser test versions 2016-11-11 12:01:16 -06:00
kpdecker 32d6363841 Exclude coverage check in exception conditional 2016-11-11 12:01:16 -06:00
kpdecker 20c965cd65 Fix throw when creating exception object in Safari
https://github.com/jquery/esprima/issues/1290
2016-11-11 12:01:16 -06:00
kpdecker 6c9f98c025 Update build for modern node versions 2016-11-11 12:01:16 -06:00
kpdecker c393c81561 Relax depth check for context push
Fixes #1135
2016-11-11 12:01:16 -06:00
kpdecker 205c61cfb1 v4.0.5 2015-11-19 23:06:54 -06:00
kpdecker ddaff8ea29 Update release notes 2015-11-19 23:06:29 -06:00
kpdecker 685cf92bcb Return current handlebars instance from noConflict
Fixes wycats/handlebars-site#131
2015-11-19 22:58:26 -06:00
kpdecker 7a6c228592 Add webpack to dev dependency to support npm 3 2015-11-19 22:55:03 -06:00
kpdecker 0a3b3c2831 Further relax uglify dependency 2015-11-19 22:54:39 -06:00
kpdecker 251ec3b128 Work around uglify preserveComments some issue
Root cause: https://github.com/gruntjs/grunt-contrib-uglify/issues/366

Fixes #1129
2015-11-19 22:54:20 -06:00
kpdecker c21118d04b Include tests for minimized artifacts 2015-11-19 22:53:31 -06:00
Kevin Decker 0f00f31f6b Merge pull request #1132 from plynchnlm/master
Update uglify-js to avoid vulnerability
2015-11-18 01:13:47 -06:00
Paul Lynch 1dc417f1a5 Update uglify-js to avoid vulnerability 2015-11-16 18:23:25 -05:00
kpdecker 9f59de9657 Fix lint errors under latest eslint 2015-10-31 13:32:43 -05:00
Kevin Decker 3406b95cfd Merge pull request #1127 from victorwpbastos/master
Tag DOMBars as deprecated
2015-10-31 12:25:38 -05:00
Victor Bastos 7b48a284e0 Tag DOMBars as deprecated 2015-10-31 13:51:14 -02:00
kpdecker 98a6717e9e Add print-script helper script 2015-10-30 12:21:15 -05:00
kpdecker b7c95e9feb v4.0.4 2015-10-29 01:54:17 -05:00
kpdecker b1e561ee26 Update release notes 2015-10-29 01:53:40 -05:00
Kevin Decker 690ae2edc7 Merge pull request #1125 from nknapp/patch-2
Add promised-handlebars to "in-the-wild"-list
2015-10-29 01:50:17 -05:00
Nils Knappmeier 496ed8a25f Add promised-handlebars to "in-the-wild"-list 2015-10-28 18:00:16 +01:00
Kevin Decker d20f02d10d Merge pull request #1121 from shinypb/undefined_partial_error_message
Include partial name in 'undefined partial' exception message
2015-10-19 12:01:39 -05:00
Mark Christian 33b53ef989 Use template string for error message and double-quotes for quoting partial name 2015-10-19 09:44:53 -07:00
Mark Christian f08d48764d Include partial name in 'undefined partial' exception message 2015-10-19 09:22:54 -07:00
kpdecker 9365b82900 v4.0.3 2015-09-23 22:41:14 -05:00
kpdecker bf091777a9 Update release notes 2015-09-23 22:40:45 -05:00
kpdecker 94c840b6ed Create data frame for @partial-block
Fixes #1099
2015-09-23 22:08:59 -05:00
kpdecker 861d6f7b8d Fix tests under IE 2015-09-23 21:59:58 -05:00
kpdecker fffb5a985f Fix iteration over undefined values
Allow for iteration on undefined values, but special case undefined and null to prevent rendering errors when not running in strict mode.

Fixes #1093
2015-09-23 21:17:57 -05:00
kpdecker 08781798f5 Allow for escaped ] characters in [] IDs
Allows for ] literal characters to be used within [] IDs by prefixing them with the \ character. `\` literal at the end of the  may be referenced by the `\\` sequence if conflicting. Under most circumstances the `\\` sequence will continue to work.

Potentially breaking change for users of [] ids that have `\\` anywhere in the id or `\` at the end of the id.

Fixes #1092
2015-09-16 16:15:01 -05:00
kpdecker 641fe335df Update travis to test node 0.12 and 4.0.0 2015-09-16 16:15:01 -05:00
Kevin Decker 733b7c8752 Merge pull request #1091 from nikolas/patch-2
fix typo in release notes
2015-09-04 15:18:48 -05:00
Kevin Decker 9a86e40970 Merge pull request #1090 from nikolas/patch-1
grammar fixes in 4.0.0 release notes
2015-09-04 15:18:25 -05:00
Nik Nyby efad390ffc fix typo in release notes 2015-09-04 15:53:31 -04:00
Nik Nyby 3696d23752 grammar fixes in 4.0.0 release notes 2015-09-04 15:47:52 -04:00
kpdecker a5a3ab01d3 v4.0.2 2015-09-04 09:13:16 -05:00
kpdecker c83354101e Update release notes 2015-09-04 09:12:48 -05:00
kpdecker 7b1fdf814c Fix use of decorators within partials
If a decorator is used within a partial but not in the calling template, the hash is not passed in. For now error on the side of always including as just assigning values has minimal overhead.

Fixes #1089
2015-09-04 09:09:17 -05:00
kpdecker c7b28a65da v4.0.1 2015-09-02 21:21:36 -05:00
kpdecker 9a27693fae Update release notes 2015-09-02 21:21:06 -05:00
kpdecker 2d9a5151e8 Add source map flag for istanbul fork 2015-09-02 21:20:03 -05:00
kpdecker 3934b3ab92 Remove duplicate dependency 2015-09-02 21:19:53 -05:00
kpdecker 05b82a203e Fix failure when using decorators in partials 2015-09-02 20:43:54 -05:00
kpdecker f71f4e759e Better assert message in shouldThrow 2015-09-02 20:42:49 -05:00
kpdecker 6e9198b207 Remove duplicated release notes 2015-09-02 20:42:23 -05:00
kpdecker cc8ed7a9e7 Update subset of npm packages 2015-09-02 11:40:59 -05:00
kpdecker 454d20e3b2 Remove keen code
This hasn’t been running and is horribly out of date.
2015-09-02 11:40:35 -05:00
kpdecker 0aef72cb8e Update to latest eslint 2015-09-01 17:56:32 -05:00
kpdecker bff5fab8f9 v4.0.0 2015-09-01 08:19:14 -05:00
kpdecker f97b99e0cd Update release notes 2015-09-01 08:18:41 -05:00
kpdecker 6c07367cd6 Update release notes 2015-09-01 08:15:48 -05:00
kpdecker 83b8e846a3 Escape = in HTML content
There was a potential XSS exploit when using unquoted attributes that this should help reduce.

Fixes #1083
2015-09-01 01:44:35 -05:00
kpdecker b0d217e13d Rev runtime compiler revision 2015-09-01 01:43:00 -05:00
kpdecker c13c7df73d Add basic decorators docs
Fixes #1088
2015-09-01 01:29:25 -05:00
kpdecker b63d74a7b3 Add explicitPartialContext compiler flag
Fixes #1032
2015-09-01 01:05:00 -05:00
kpdecker e7a64f018c Merge branch 'decorators' 2015-09-01 00:48:20 -05:00
Kevin Decker 0f5061e445 Merge pull request #1087 from denniskuczynski/each_object_empty_string_key
Fix #each when last object entry has empty key
2015-08-30 17:27:52 -05:00
Dennis Kuczynski 7c896a074d Fix #each when last object entry has empty key 2015-08-30 10:43:24 -04:00
Kevin Decker e70430dd2e Merge pull request #1084 from blendlabs/master
Bump uglify version to avoid vulnerability flag.
2015-08-25 12:38:11 -05:00
John Steidley 9d4353c35c Bump uglify version to fix vulnerability 2015-08-24 17:06:40 -07:00
Kevin Decker c727e1f97c Merge pull request #1076 from wycats/partial-block
Implement partial blocks
2015-08-23 23:45:17 -05:00
kpdecker 6c45f49b24 Implement decorator helper method 2015-08-22 11:13:08 -07:00
kpdecker 495cd05a7e Implement inline partials
Allows for partials to be defined within the current template to allow for localized code reuse as well as for conditional behavior within nested partials.

Fixes #1018
2015-08-22 11:13:08 -07:00
kpdecker 452afbf2ff Implement block decorators
These allow for a given block to be wrapped in helper methods or metadata and allow for more control over the current container and method before the code is run.
2015-08-22 10:59:34 -07:00
kpdecker 408192ba9f Add decorator parsing 2015-08-22 10:59:34 -07:00
kpdecker 2a4a5447f5 Implement decorator environment and registration 2015-08-22 10:59:34 -07:00
kpdecker 1c274088c1 Update AST doc for partial block 2015-08-22 10:59:08 -07:00
kpdecker 0b5e82e1c3 Add whitespace control to partial block statements 2015-08-22 10:59:08 -07:00
kpdecker 233caf3f7c Create validateClose helper method
Avoid duplicating the logic needed to check for close block mismatches.
2015-08-22 10:59:08 -07:00
kpdecker 91ffd32cad Implement partial blocks
This allows for failover for missing partials as well as limited templating ability through the `{{> @partial-block }}` partial special case.

Partial fix for #1018
2015-08-22 10:59:08 -07:00
kpdecker 2571dd8e8e Improve sanity checks in compiler and visitor 2015-08-22 10:58:44 -07:00
Kevin Decker 729d9fdffe Merge pull request #1081 from hashchange/patch-1
Add Marionette.Handlebars to "in the Wild" list
2015-08-21 18:17:56 -07:00
hashchange a634cd3219 Add Marionette.Handlebars to "in the Wild" list 2015-08-21 13:32:20 +02:00
kpdecker 95d84badca Drop AST constructors in favor of JSON
These were little more than object literal statements that were less clear due to their use of index-based arguments.

Fixes #1077
2015-08-18 23:57:27 -07:00
kpdecker 9a2d1d6009 Pass container rather than exec as context
There is no real need for us to do `.call(container` other than for backwards compatibility with legacy versions. Using the 4.x release as a chance to optimize this behavior.
2015-08-18 23:54:04 -07:00
kpdecker 08093d72f0 Remove unused parameters 2015-08-18 23:14:09 -07:00
kpdecker 958273c2e6 Add object option to test runner 2015-08-18 23:13:15 -07:00
kpdecker 1c2b74e537 Run node tests last
This covers all of the test cases and generally have better stack traces so we want to have these featured more prominently.
2015-08-18 23:06:23 -07:00
kpdecker b85cad8df6 Fix travis config typo 2015-08-14 18:53:08 -05:00
kpdecker d21de5d569 Increase travis git window to avoid test failures
https://twitter.com/travisci/status/288390896339267584
2015-08-14 15:45:41 -05:00
kpdecker 519fac5081 Link to try handlebars from readme 2015-08-14 15:20:45 -05:00
kpdecker 624a4c71aa Cleanup stack traces for test assertions 2015-08-14 15:20:09 -05:00
kpdecker 3ce04ddd23 Include inline source maps in babel build 2015-08-14 15:19:37 -05:00
kpdecker ea3a5a1eb6 Add ignoreStandalone compiler option
Fixes #1072
2015-08-13 01:51:17 -05:00
kpdecker 269dd492bb Link to installation docs 2015-08-12 22:37:26 -05:00
kpdecker 2ae0ef6eef Add license information to bower.json
Fixes #1074
2015-08-12 02:45:51 -05:00
kpdecker 06d515a89d Ignore empty when iterating on sparse arrays
Fixes #1065
2015-08-04 12:40:33 -05:00
kpdecker 0de8dac702 Add support for string and stdin precompilation
Fixes #1071
2015-08-04 12:34:28 -05:00
kpdecker d716fd01c5 Remove no-shadow rule 2015-08-04 12:33:19 -05:00
kpdecker 77e6bfc5a1 Simplify object assignment generation logic 2015-08-04 12:33:05 -05:00
kpdecker 00f74420f9 Convert precompiler template loading to async 2015-08-04 10:55:51 -05:00
kpdecker a62cbad95a Refactor precompiler API into two phase
Load templates and then parse them in a distinct operation. This will allow us to use other input sources such as stdin and strings.
2015-08-04 09:26:19 -05:00
kpdecker ba31ef8ae4 Increase sauce test concurrency 2015-08-03 23:43:13 -05:00
kpdecker a44fe470b4 Include doctype in amd harness 2015-08-03 23:43:00 -05:00
kpdecker bb9831b73e Specify platform for firefox in sauce tests
Works around what appears to be an init issue within Sauce.
2015-08-03 23:42:50 -05:00
kpdecker c532b89af6 Fix parser declaration under amd builds 2015-08-03 22:45:39 -05:00
kpdecker 72eb10ca4b Revert "Pull sauce tests out of CI"
This reverts commit e2ba22eaad.
2015-08-03 22:27:25 -05:00
kpdecker bd643ce12b Pull out duplicated documentation from readme
Fixes #1006
2015-08-03 21:13:47 -05:00
kpdecker 93b07605cd Bulletproof AST.helpers.helperExpression
Avoid undefined values and potential false positives from other type values such as partials.

Fixes #1055
2015-08-03 20:31:57 -05:00
kpdecker 85750f8d0a Use += in printer 2015-08-03 18:51:46 -05:00
kpdecker fac71ce700 Fix incorrect variable removal 2015-08-03 17:59:23 -05:00
kpdecker 324d615726 Enforce 100% code coverage 2015-08-03 17:49:47 -05:00
kpdecker c3cbaa25a4 Fix partial handling with different context 2015-08-03 17:27:38 -05:00
kpdecker 9b1f9c7e44 Style updates 2015-08-03 17:03:37 -05:00
kpdecker 8e868ab225 Always return string responses
Certain optimizations for simple templates could result in objects returned by helpers returned rather than their string representation, resulting in some odd edge cases. This ensures that strings are always returned from the API for consistency.

Fixes #1054.
2015-08-03 16:08:23 -05:00
kpdecker 5d4b8da344 Pass undefined fields to helpers in strict mode
This allows for `{{helper foo}}` to still operate under strict mode when `foo` is not defined on the context. This allows helpers to perform whatever existence checks they please so patterns like `{{#if foo}}{{foo}}{{/if}}` can be used to protect against missing values.

Fixes #1063
2015-08-03 15:59:53 -05:00
kpdecker 279e038ba7 Avoid depth creation when context remains the same
Creating a new depth value seems to confuse users as they don’t expect things like `if` to require multiple `..` to break out of. With the change, we avoid pushing a context to the depth list if it’s already on the top of the stack, effectively removing cases where `.` and `..` are the same object and multiple `..` references are required.

This is a breaking change and all templates that utilize `..` will have to check their usage and confirm that this does not break desired behavior. Helper authors now need to take care to return the same context value whenever it is conceptually the same and to avoid behaviors that may execute children under the current context in some situations and under different contexts under other situations.

Fixes #1028
2015-08-03 15:15:09 -05:00
kpdecker 1c08771215 Fix track id handling in partials
Fixes #914
2015-08-03 12:16:02 -05:00
kpdecker 9f265b9761 Handle this references properly in track id mode 2015-08-03 12:13:24 -05:00
kpdecker 0aa54f49de Avoid log output in test 2015-08-03 12:13:00 -05:00
kpdecker 1f11cc0186 Remove out of date TODO 2015-08-03 12:12:30 -05:00
kpdecker 9a49d35023 Improve logging API
Adds multiple variable support and the ability to set statement level logging semantics.

This breaks that logger API, cleaning up the manner in which enums are set, but the other behaviors are backwards compatible.

Fixes #956
2015-08-03 11:37:49 -05:00
kpdecker b664997dc3 Move logger into separate module 2015-08-01 22:03:11 -05:00
kpdecker ac82842cb6 Add rest params to es6 supported list 2015-08-01 22:01:41 -05:00
kpdecker efddc3c09c Increase code coverage 2015-08-01 22:01:16 -05:00
kpdecker f3e8b18925 Add istanbul ignore to babel boilerplate 2015-08-01 21:46:45 -05:00
kpdecker 15b55a307b Move helpers into separate modules 2015-08-01 17:54:47 -05:00
kpdecker 231a8d7256 Fix with operator in no @data mode 2015-08-01 16:29:45 -05:00
kpdecker e2ba22eaad Pull sauce tests out of CI
Disabling these until #1069 can resolve whatever the root issue is.
2015-08-01 16:08:57 -05:00
kpdecker 1bb640be41 Allow empty key name in each iteration
Fixes #1021
2015-08-01 16:04:40 -05:00
kpdecker 2a851067b9 Add with block parameter support
Fixes #1042
2015-08-01 15:48:28 -05:00
kpdecker 410141c31e Fix escaping of non-javascript identifiers
The ‘ character would cause invalid javascript to be generated as it was not properly escaped. Switching to JSON.stringify safely handles all potential unescaped cases.
2015-08-01 15:48:28 -05:00
Kevin Decker 060c087e62 Merge pull request #1068 from 0xack13/master
Fix typo
2015-07-30 09:53:25 -05:00
Saleh Batati 5acf77a99a Merge pull request #1 from 0xack13/0xack13-patch-1-fix-typo
Fix typo
2015-07-30 06:08:11 +03:00
Saleh Batati fe057168ae Fix typo 2015-07-30 06:02:51 +03:00
Kevin Decker c83c363a58 Merge pull request #1060 from ericbn/issue-1056
#1056 Fixed grammar for nested raw blocks
2015-07-20 10:49:14 +03:00
Eric Nielsen aa7a45b443 Added comment about Jison's topState() 2015-07-19 12:34:44 -03:00
kpdecker a458fe26f4 Update jsfiddle link to 3.0.3 2015-07-19 11:28:43 +03:00
Eric Nielsen 2f9495c1df Added spec message 2015-07-16 15:15:06 -03:00
Eric Nielsen b9fe7ce618 #1056 Added spec for nested raw block 2015-07-15 14:45:53 -03:00
Eric Nielsen 868ef4b309 #1056 Fixed grammar for nested raw blocks 2015-07-15 14:45:43 -03:00
Kevin Decker 8c09b3c896 Merge pull request #1052 from maqnouch/patch-1
Updated year in License
2015-07-02 23:40:03 +02:00
AQNOUCH Mohammed 9bbc177fd5 Updated year in License 2015-07-02 15:53:10 +00:00
kpdecker 93faffa549 Fix location information for programs
There appears to be a bug in our use of jison causing the parent location information to be reported to programs. I wasn’t able to work through what might be causing this so instead using the location information of the statements collection to generate the proper location information.

This is a bit of a hack but we are very far behind on the Jison release train and upgrading will likely be a less than pleasant task that doesn’t provide us much benefit.

Fixes #1024
2015-06-26 16:58:34 -05:00
Kevin Decker d2fb3a4906 Merge pull request #1037 from tomxtobin/minor-readme-typo-fixes
Fix minor typos in README
2015-06-09 06:07:35 -04:00
Tom X. Tobin d0805e9cfa Fix minor typos in README 2015-06-08 18:37:41 -04:00
Kevin Decker 2c1d509c6c Merge pull request #1019 from aymerick/aymerick-patch-typo-test
Fixes typo in tests
2015-05-06 11:13:43 -05:00
kpdecker 569f288551 Add tests for string contexts
Fixes #1013
2015-05-06 10:48:23 -05:00
Aymerick d0d2168ec2 Fixes typo in tests
mutache => mustache
2015-05-06 14:03:14 +02:00
Kevin Decker 8cba84df11 Merge pull request #1015 from aymerick/fix-test
Fix tokenizer test for double quoted strings
2015-05-05 10:23:16 -05:00
kpdecker 00bfdd782f Fix VERSION update script
This broke after updating to es6 formatted code.

Fixes #1016
2015-05-05 10:20:56 -05:00
Aymerick afd595bd6e Fix tokenizer test for double quoted strings
There is two consecutive tests with the same input data: "{{ foo bar \'baz\' }}"

I suppose the first test should be about testing double quoted string.
2015-05-01 11:35:25 +02:00
kpdecker 891f48b7e9 v3.0.3 2015-04-28 14:52:04 -05:00
kpdecker 0bf79ea5c0 Update release notes 2015-04-28 14:51:32 -05:00
kpdecker 972f521718 Move noConflict implementation to module
DRYs the code to avoid escapes like #1004

Fixes #1004
2015-04-27 10:19:50 -05:00
kpdecker 4a40fc8e2f NPM ignore log files 2015-04-27 10:19:50 -05:00
kpdecker fc13400b6f Remove jshint completely 2015-04-27 10:19:49 -05:00
Kevin Decker 6ecf509385 Merge pull request #1008 from shannonmoeller/patch-1
Added handlebars-layouts to the in-the-wild list
2015-04-25 09:54:25 -05:00
Shannon Moeller d945089565 Added handlebars-layouts to the in-the-wild list
I've written a set of helpers which implement layout blocks similar to Jade, Jinja, Swig, Twig, and others. It seems to be gaining some popularity (around 10k npm downloads per month at present) and I would be honored to have it included in the in-the-wild list in case it may prove useful to others.
2015-04-24 22:29:40 -04:00
kpdecker 2f0b86665f v3.0.2 2015-04-20 03:11:12 -05:00
kpdecker d48e840513 Update release notes 2015-04-20 03:10:49 -05:00
kpdecker 4bed826d0e Update for let and optional parameters 2015-04-20 02:38:28 -05:00
kpdecker 0263aa48bc Run the precompiler module through es6 toolchain 2015-04-20 02:25:53 -05:00
kpdecker ecf60ab1bf Enable additional es6 features in eslint 2015-04-20 02:24:49 -05:00
Kevin Decker c6ab044fa8 Merge pull request #998 from wycats/babel
Add full support for es6
2015-04-16 17:12:40 -05:00
kpdecker fdc94207a3 Fix UMD build target generation 2015-04-16 16:54:30 -05:00
kpdecker e3d3eda2e1 Add full support for es6
Converts the tool chain to use babel, eslint, and webpack vs. the previous proprietary solutions.

Additionally begins enforcing additional linting concerns as well as updates the code to reflect these rules.

Fixes #855
Fixes #993
2015-04-16 16:43:01 -05:00
kpdecker 2a02261a5b Fix IE test freakout 2015-04-14 22:57:15 -05:00
kpdecker a009a97a2e Use captureStackTrace for error handler
Provides more stable error stack reporting when running under v8 environments.
2015-04-14 01:14:18 -05:00
kpdecker 54e743a09a Allow this references in literal statements
Fixes #967
2015-04-14 01:12:39 -05:00
kpdecker a7160a8d27 Expose Visitor in full browser build
Fixes #994
2015-04-13 21:25:53 -05:00
kpdecker dfd141cd35 Allow undefined and null in helper names 2015-04-13 21:18:53 -05:00
kpdecker 37a664bf64 Ignore branches tested without coverage monitoring 2015-04-07 23:38:05 -05:00
kpdecker c699d0b82f Remove vestigial code 2015-04-07 23:37:46 -05:00
kpdecker 2d149e7797 Add undefined and null literal support
This adds the UndefinedLiteral and NullLiteral to AST.

Fixes #990
2015-04-07 23:37:38 -05:00
kpdecker 81a4d50955 Avoid source-map import under AMD
Fixes #989
2015-04-07 22:35:14 -05:00
kpdecker e15af4c849 Add require.js error reporting in tests
Adds test coverage for #989
2015-04-07 21:51:50 -05:00
kpdecker afe730e059 v3.0.1 2015-03-24 14:22:38 -05:00
kpdecker 40894e619b Update release notes 2015-03-24 14:22:22 -05:00
Kevin Decker 1d401a2abc Merge pull request #984 from johneke/master
Adding documentation for passing arguments into partials
2015-03-24 14:20:22 -05:00
johneke a10d907a65 Update README.markdown 2015-03-18 22:55:26 -04:00
johneke fed86c01ad Adding documentation for parameters in partials 2015-03-18 20:23:15 -04:00
kpdecker ab96073c6b Optimize hot path in escapeExpression
Avoid deoptimizations in v8 due to the duct type check on string instances.

Partial fix for #973
2015-03-16 22:06:01 -05:00
kpdecker 64ab232d72 Fix --version flag
Fixes #966
2015-02-28 19:58:23 -06:00
Kevin Decker 8c357b9243 Merge pull request #964 from grassick/patch-1
default is a reserved word
2015-02-28 16:26:39 -06:00
grassick 71690ae39d default is a reserved word
Quoting it to fix in older browsers, especially Android 2.3.x.
2015-02-18 10:57:02 -05:00
Kevin Decker 3c9ff77e68 Merge pull request #962 from pismute/master
Add dashbars' link on README.
2015-02-16 09:01:07 -06:00
Changwoo Park 5ede36b8fd Add dashbars' link on README. 2015-02-16 19:29:13 +09:00
kpdecker c15f90e513 Fix typo 2015-02-10 20:40:53 -06:00
115 changed files with 4769 additions and 2945 deletions
+200
View File
@@ -0,0 +1,200 @@
{
"globals": {
"self": false
},
"env": {
"node": true
},
"ecmaFeatures": {
// Enabling features that can be implemented without polyfills. Want to avoid polyfills at this time.
"arrowFunctions": true,
"blockBindings": true,
"defaultParams": true,
"destructuring": true,
"modules": true,
"objectLiteralComputedProperties": true,
"objectLiteralDuplicateProperties": true,
"objectLiteralShorthandMethods": true,
"objectLiteralShorthandProperties": true,
"restParams": true,
"spread": true,
"templateStrings": true
},
"rules": {
// Possible Errors //
//-----------------//
"comma-dangle": [2, "never"],
"no-cond-assign": [2, "except-parens"],
// Allow for debugging
"no-console": 1,
"no-constant-condition": 2,
"no-control-regex": 2,
// Allow for debugging
"no-debugger": 1,
"no-dupe-args": 2,
"no-dupe-keys": 2,
"no-duplicate-case": 2,
"no-empty": 2,
"no-empty-character-class": 2,
"no-ex-assign": 2,
"no-extra-boolean-cast": 2,
"no-extra-parens": 0,
"no-extra-semi": 2,
"no-func-assign": 0,
// Stylistic... might consider disallowing in the future
"no-inner-declarations": 0,
"no-invalid-regexp": 2,
"no-irregular-whitespace": 2,
"no-negated-in-lhs": 2,
"no-obj-calls": 2,
"no-regex-spaces": 2,
"quote-props": [2, "as-needed", {"keywords": true}],
"no-sparse-arrays": 0,
// Optimizer and coverage will handle/highlight this and can be useful for debugging
"no-unreachable": 1,
"use-isnan": 2,
"valid-jsdoc": 0,
"valid-typeof": 2,
// Best Practices //
//----------------//
"block-scoped-var": 0,
"complexity": 0,
"consistent-return": 0,
"curly": 2,
"default-case": 1,
"dot-notation": [2, {"allowKeywords": false}],
"eqeqeq": 0,
"guard-for-in": 1,
"no-alert": 2,
"no-caller": 2,
"no-div-regex": 1,
"no-else-return": 0,
"no-empty-label": 2,
"no-eq-null": 0,
"no-eval": 2,
"no-extend-native": 2,
"no-extra-bind": 2,
"no-fallthrough": 2,
"no-floating-decimal": 2,
"no-implied-eval": 2,
"no-iterator": 2,
"no-labels": 2,
"no-lone-blocks": 2,
"no-loop-func": 2,
"no-multi-spaces": 2,
"no-multi-str": 1,
"no-native-reassign": 2,
"no-new": 2,
"no-new-func": 2,
"no-new-wrappers": 2,
"no-octal": 2,
"no-octal-escape": 2,
"no-param-reassign": 0,
"no-process-env": 2,
"no-proto": 2,
"no-redeclare": 2,
"no-return-assign": 2,
"no-script-url": 2,
"no-self-compare": 2,
"no-sequences": 2,
"no-throw-literal": 2,
"no-unused-expressions": 2,
"no-void": 0,
"no-warning-comments": 1,
"no-with": 2,
"radix": 2,
"vars-on-top": 0,
"wrap-iife": 2,
"yoda": 0,
// Strict //
//--------//
"strict": 0,
// Variables //
//-----------//
"no-catch-shadow": 2,
"no-delete-var": 2,
"no-label-var": 2,
"no-shadow": 0,
"no-shadow-restricted-names": 2,
"no-undef": 2,
"no-undef-init": 2,
"no-undefined": 0,
"no-unused-vars": [2, {"vars": "all", "args": "after-used"}],
"no-use-before-define": [2, "nofunc"],
// Node.js //
//---------//
// Others left to environment defaults
"no-mixed-requires": 0,
// Stylistic //
//-----------//
"indent": 0,
"brace-style": [2, "1tbs", {"allowSingleLine": true}],
"camelcase": 2,
"comma-spacing": [2, {"before": false, "after": true}],
"comma-style": [2, "last"],
"consistent-this": [1, "self"],
"eol-last": 2,
"func-names": 0,
"func-style": [2, "declaration"],
"key-spacing": [2, {
"beforeColon": false,
"afterColon": true
}],
"max-nested-callbacks": 0,
"new-cap": 2,
"new-parens": 2,
"newline-after-var": 0,
"no-array-constructor": 2,
"no-continue": 0,
"no-inline-comments": 0,
"no-lonely-if": 2,
"no-mixed-spaces-and-tabs": 2,
"no-multiple-empty-lines": 0,
"no-nested-ternary": 1,
"no-new-object": 2,
"no-spaced-func": 2,
"no-ternary": 0,
"no-trailing-spaces": 2,
"no-underscore-dangle": 0,
"no-extra-parens": [2, "functions"],
"one-var": 0,
"operator-assignment": 0,
"padded-blocks": 0,
"quote-props": 0,
"quotes": [2, "single", "avoid-escape"],
"semi": 2,
"semi-spacing": [2, {"before": false, "after": true}],
"sort-vars": 0,
"space-after-keywords": [2, "always"],
"space-before-blocks": [2, "always"],
"space-before-function-paren": [2, {"anonymous": "never", "named": "never"}],
"space-in-brackets": 0,
"space-in-parens": [2, "never"],
"space-infix-ops": 2,
"space-return-throw-case": 2,
"space-unary-ops": 2,
"spaced-comment": [2, "always", {"markers": [","]}],
"wrap-regex": 1,
"no-var": 1
}
}
-52
View File
@@ -1,52 +0,0 @@
{
"predef": [
"console",
"require",
"suite",
"equal",
"equals",
"test",
"testBoth",
"raises",
"deepEqual",
"start",
"stop",
"ok",
"strictEqual",
"module",
"define",
"describe",
"it",
"afterEach",
"beforeEach"
],
"node" : true,
"browser" : true,
"esnext": true,
"boss" : true,
"curly": false,
"debug": false,
"devel": false,
"eqeqeq": false,
"eqnull": true,
"evil": true,
"forin": false,
"immed": false,
"laxbreak": true,
"newcap": true,
"noarg": true,
"noempty": false,
"nonew": false,
"nomen": false,
"onevar": false,
"plusplus": false,
"regexp": false,
"undef": true,
"unused": true,
"sub": true,
"strict": false,
"white": false
}
+2 -1
View File
@@ -1,7 +1,7 @@
.DS_Store
.gitignore
.rvmrc
.jshintrc
.eslintrc
.travis.yml
.rspec
Gemfile
@@ -10,6 +10,7 @@ Rakefile
Gruntfile.js
*.gemspec
*.nuspec
*.log
bench/*
configurations/*
components/*
+5 -1
View File
@@ -13,11 +13,15 @@ env:
- secure: Nm4AgSfsgNB21kgKrF9Tl7qVZU8YYREhouQunFracTcZZh2NZ2XH5aHuSiXCj88B13Cr/jGbJKsZ4T3QS3wWYtz6lkyVOx3H3iI+TMtqhD9RM3a7A4O+4vVN8IioB2YjhEu0OKjwgX5gp+0uF+pLEi7Hpj6fupD3AbbL5uYcKg8=
matrix:
include:
- node_js: '0.10'
- node_js: '5'
env:
- PUBLISH=true
- secure: pLTzghtVll9yGKJI0AaB0uI8GypfWxLTaIB0ZL8//yN3nAEIKMhf/RRilYTsn/rKj2NUa7vt2edYILi3lttOUlCBOwTc9amiRms1W8Lwr/3IdWPeBLvLuH1zNJRm2lBAwU4LBSqaOwhGaxOQr6KHTnWudhNhgOucxpZfvfI/dFw=
- secure: yERYCf7AwL11D9uMtacly/THGV8BlzsMmrt+iQVvGA3GaY6QMmfYqf6P6cCH98sH5etd1Y+1e6YrPeMjqI6lyRllT7FptoyOdHulazQe86VQN4sc0EpqMlH088kB7gGjTut9Z+X9ViooT5XEh9WA5jXEI9pXhQJNoIHkWPuwGuY=
- node_js: '4'
cache:
directories:
- node_modules
git:
depth: 100
+21 -2
View File
@@ -6,7 +6,7 @@ Please see our [FAQ](https://github.com/wycats/handlebars.js/blob/master/FAQ.md)
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 thats demonstrating the issue are welcomed and this also helps ensure that your issue won't regress in the future once it's fixed.
Pull requests containing only failing tests demonstrating the issue are welcomed and this also helps ensure that your issue won't regress in the future once it's fixed.
Documentation issues on the handlebarsjs.com site should be reported on [handlebars-site](https://github.com/wycats/handlebars-site).
@@ -42,6 +42,25 @@ The `grunt dev` implements watching for tests and allows for in browser testing
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).
##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
```
## Ember testing
The current ember distribution should be tested as part of the handlebars release process. This requires building the `handlebars-source` gem locally and then executing the ember test script.
@@ -77,4 +96,4 @@ After this point the handlebars site needs to be updated to point to the new ver
[generator-release]: https://github.com/walmartlabs/generator-release
[pull-request]: https://github.com/wycats/handlebars.js/pull/new/master
[issue]: https://github.com/wycats/handlebars.js/issues/new
[jsfiddle]: http://jsfiddle.net/9D88g/26/
[jsfiddle]: https://jsfiddle.net/9D88g/47/
+72 -44
View File
@@ -1,16 +1,18 @@
var childProcess = require('child_process');
/* eslint-disable no-process-env */
module.exports = function(grunt) {
grunt.initConfig({
pkg: grunt.file.readJSON('package.json'),
jshint: {
eslint: {
options: {
jshintrc: '.jshintrc'
},
files: [
'dist/cjs/**/!(*.min|parser).js'
'*.js',
'bench/**/*.js',
'tasks/**/*.js',
'lib/**/!(*.min|parser).js',
'spec/**/!(*.amd|json2|require).js'
]
},
@@ -19,8 +21,8 @@ module.exports = function(grunt) {
copy: {
dist: {
options: {
processContent: function(content, path) {
return grunt.template.process('/*!\n\n <%= pkg.name %> v<%= pkg.version %>\n\n<%= grunt.file.read("LICENSE") %>\n@license\n*/\n')
processContent: function(content) {
return grunt.template.process('/**!\n\n @license\n <%= pkg.name %> v<%= pkg.version %>\n\n<%= grunt.file.read("LICENSE") %>\n*/\n')
+ content;
}
},
@@ -41,21 +43,16 @@ module.exports = function(grunt) {
}
},
packager: {
global: {
type: 'umd',
export: 'Handlebars',
files: [{
cwd: 'lib/',
expand: true,
src: ['handlebars*.js'],
dest: 'dist/'
}]
babel: {
options: {
sourceMaps: 'inline',
loose: ['es6.modules'],
auxiliaryCommentBefore: 'istanbul ignore next'
},
amd: {
type: 'amd',
anonymous: true,
options: {
modules: 'amd'
},
files: [{
expand: true,
cwd: 'lib/',
@@ -65,30 +62,61 @@ module.exports = function(grunt) {
},
cjs: {
type: 'cjs',
options: {
modules: 'common'
},
files: [{
expand: true,
cwd: 'lib/',
expand: true,
src: '**/!(index).js',
dest: 'dist/cjs/'
}]
}
},
webpack: {
options: {
context: __dirname,
module: {
loaders: [
// the optional 'runtime' transformer tells babel to require the runtime instead of inlining it.
{ test: /\.jsx?$/, exclude: /node_modules/, loader: 'babel-loader?optional=runtime&loose=es6.modules&auxiliaryCommentBefore=istanbul%20ignore%20next' }
]
},
output: {
path: 'dist/',
library: 'Handlebars',
libraryTarget: 'umd'
}
},
handlebars: {
entry: './lib/handlebars.js',
output: {
filename: 'handlebars.js'
}
},
runtime: {
entry: './lib/handlebars.runtime.js',
output: {
filename: 'handlebars.runtime.js'
}
}
},
requirejs: {
options: {
optimize: "none",
baseUrl: "dist/amd/"
optimize: 'none',
baseUrl: 'dist/amd/'
},
dist: {
options: {
name: "handlebars",
out: "dist/handlebars.amd.js"
name: 'handlebars',
out: 'dist/handlebars.amd.js'
}
},
runtime: {
options: {
name: "handlebars.runtime",
out: "dist/handlebars.runtime.amd.js"
name: 'handlebars.runtime',
out: 'dist/handlebars.runtime.amd.js'
}
}
},
@@ -97,7 +125,7 @@ module.exports = function(grunt) {
options: {
mangle: true,
compress: true,
preserveComments: 'some'
preserveComments: /(?:^!|@(?:license|preserve|cc_on))/
},
dist: {
files: [{
@@ -134,15 +162,14 @@ module.exports = function(grunt) {
build: process.env.TRAVIS_JOB_ID,
urls: ['http://localhost:9999/spec/?headless=true', 'http://localhost:9999/spec/amd.html?headless=true'],
detailedError: true,
concurrency: 2,
concurrency: 4,
browsers: [
{browserName: 'chrome'},
{browserName: 'firefox'},
{browserName: 'safari', version: 7, platform: 'OS X 10.9'},
{browserName: 'safari', version: 6, platform: 'OS X 10.8'},
{browserName: 'firefox', platform: 'Linux'},
{browserName: 'safari', version: 9, platform: 'OS X 10.11'},
{browserName: 'safari', version: 8, platform: 'OS X 10.10'},
{browserName: 'internet explorer', version: 11, platform: 'Windows 8.1'},
{browserName: 'internet explorer', version: 10, platform: 'Windows 8'},
{browserName: 'internet explorer', version: 9, platform: 'Windows 7'}
{browserName: 'internet explorer', version: 10, platform: 'Windows 8'}
]
}
},
@@ -172,18 +199,18 @@ module.exports = function(grunt) {
});
// Build a new version of the library
this.registerTask('build', "Builds a distributable version of the current project", [
this.registerTask('build', 'Builds a distributable version of the current project', [
'eslint',
'parser',
'node',
'globals',
'jshint']);
'globals']);
this.registerTask('amd', ['packager:amd', 'requirejs']);
this.registerTask('node', ['packager:cjs']);
this.registerTask('globals', ['packager:global']);
this.registerTask('amd', ['babel:amd', 'requirejs']);
this.registerTask('node', ['babel:cjs']);
this.registerTask('globals', ['webpack']);
this.registerTask('tests', ['concat:tests']);
this.registerTask('release', 'Build final packages', ['amd', 'jshint', 'uglify', 'copy:dist', 'copy:components', 'copy:cdnjs']);
this.registerTask('release', 'Build final packages', ['eslint', 'amd', 'uglify', 'test:min', 'copy:dist', 'copy:components', 'copy:cdnjs']);
// Load tasks from npm
grunt.loadNpmTasks('grunt-contrib-clean');
@@ -191,11 +218,12 @@ module.exports = function(grunt) {
grunt.loadNpmTasks('grunt-contrib-connect');
grunt.loadNpmTasks('grunt-contrib-copy');
grunt.loadNpmTasks('grunt-contrib-requirejs');
grunt.loadNpmTasks('grunt-contrib-jshint');
grunt.loadNpmTasks('grunt-contrib-uglify');
grunt.loadNpmTasks('grunt-contrib-watch');
grunt.loadNpmTasks('grunt-babel');
grunt.loadNpmTasks('grunt-eslint');
grunt.loadNpmTasks('grunt-saucelabs');
grunt.loadNpmTasks('es6-module-packager');
grunt.loadNpmTasks('grunt-webpack');
grunt.task.loadTasks('tasks');
+1 -1
View File
@@ -1,4 +1,4 @@
Copyright (C) 2011-2014 by Yehuda Katz
Copyright (C) 2011-2016 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
+21 -261
View File
@@ -10,24 +10,12 @@ 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).
[http://www.handlebarsjs.com](http://www.handlebarsjs.com) and the live demo at [http://tryhandlebarsjs.com/](http://tryhandlebarsjs.com/).
Installing
----------
Installing Handlebars is easy. Simply download the package [from the official site](http://handlebarsjs.com/) or the [bower repository][bower-repo] and add it to your web pages (you should usually use the most recent version).
For web browsers, a free CDN is available at [jsDelivr](http://www.jsdelivr.com/#!handlebarsjs). Advanced usage, such as [version aliasing & concocting](https://github.com/jsdelivr/jsdelivr#usage), is available.
Alternatively, if you prefer having the latest version of handlebars from
the 'master' branch, passing builds of the 'master' branch are automatically
published to S3. You may download the latest passing master build by grabbing
a `handlebars-latest.js` file from the [builds page][builds-page]. When the
build is published, it is also available as a `handlebars-gitSHA.js` file on
the builds page if you need a version to refer to others.
`handlebars-runtime.js` builds are also available.
**Note**: The S3 builds page is provided as a convenience for the community,
but you should not use it for hosting Handlebars in production.
See our [installation documentation](http://handlebarsjs.com/installation.html).
Usage
-----
@@ -57,266 +45,34 @@ var result = template(data);
// </ul>
```
Full documentation and more examples are at [handlebarsjs.com](http://handlebarsjs.com/).
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() {
return new Handlebars.SafeString("<a href='" + Handlebars.Utils.escapeExpression(this.url) + "'>" + Handlebars.Utils.escapeExpression(this.body) + "</a>");
});
var context = { posts: [{url: "/hello-world", body: "Hello World!"}] };
var source = "<ul>{{#posts}}<li>{{link_to}}</li>{{/posts}}</ul>"
var template = Handlebars.compile(source);
template(context);
// Would render:
//
// <ul>
// <li><a href='/hello-world'>Hello World!</a></li>
// </ul>
```
Helpers take precedence over fields defined on the context. To access a field
that is masked by a helper, a path reference may be used. In the example above
a field named `link_to` on the `context` object would be referenced using:
```
{{./link_to}}
```
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.
Precompiling Templates
----------------------
Handlebars allows templates to be precompiled and included as javascript code rather than the handlebars template allowing for faster startup time. Full details are located [here](http://handlebarsjs.com/precompilation.html).
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 descendants and ancestors of the
current context.
To display data from descendant 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:
```
{{#with person}}{{name}} - {{../company.name}}{{/with}}
```
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, options) {
return "<a href='/posts" + this.url + "'>" + title + "!</a>"
});
var context = { posts: [{url: "/hello-world", body: "Hello World!"}] };
var source = '<ul>{{#posts}}<li>{{{link_to "Post"}}}</li>{{/posts}}</ul>'
var template = Handlebars.compile(source);
template(context);
// Would render:
//
// <ul>
// <li><a href='/posts/hello-world'>Post!</a></li>
// </ul>
```
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 = "<ul>{{#people}}<li>{{#link}}{{name}}{{/link}}</li>{{/people}}</ul>";
Handlebars.registerHelper('link', function(options) {
return '<a href="/people/' + this.id + '">' + options.fn(this) + '</a>';
});
var template = Handlebars.compile(source);
var data = { "people": [
{ "name": "Alan", "id": 1 },
{ "name": "Yehuda", "id": 2 }
]};
template(data);
// Should render:
// <ul>
// <li><a href="/people/1">Alan</a></li>
// <li><a href="/people/2">Yehuda</a></li>
// </ul>
```
Whenever the block helper is called it is given one or more parameters,
any arguments that are passed into the helper in the call, and an `options`
object containing the `fn` function which executes the block's child.
The block's current context may be accessed through `this`.
Block helpers have the same syntax as mustache sections but should not be
confused with one another. Sections are akin to an implicit `each` or
`with` statement depending on the input data and helpers are explicit
pieces of code that are free to implement whatever behavior they like.
The [mustache spec](http://mustache.github.io/mustache.5.html)
defines the exact behavior of sections. In the case of name conflicts,
helpers are given priority.
### 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 = "<ul>{{#people}}<li>{{> link}}</li>{{/people}}</ul>";
Handlebars.registerPartial('link', '<a href="/people/{{id}}">{{name}}</a>')
var template = Handlebars.compile(source);
var data = { "people": [
{ "name": "Alan", "id": 1 },
{ "name": "Yehuda", "id": 2 }
]};
template(data);
// Should render:
// <ul>
// <li><a href="/people/1">Alan</a></li>
// <li><a href="/people/2">Yehuda</a></li>
// </ul>
```
### 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
<div>
{{! This comment will not end up in the output }}
<!-- This comment will show up in the output -->
</div>
```
- [Nested Paths](http://handlebarsjs.com/#paths)
- [Helpers](http://handlebarsjs.com/#helpers)
- [Block Expressions](http://handlebarsjs.com/#block-expressions)
- [Literal Values](http://handlebarsjs.com/#literals)
- [Delimited Comments](http://handlebarsjs.com/#comments)
Block expressions have the same syntax as mustache sections but should not be confused with one another. Sections are akin to an implicit `each` or `with` statement depending on the input data and helpers are explicit pieces of code that are free to implement whatever behavior they like. The [mustache spec](http://mustache.github.io/mustache.5.html) defines the exact behavior of sections. In the case of name conflicts, helpers are given priority.
### 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 it's own lambda resolution that follows the behaviors of helpers.
- Alternative delimeters are not supported.
- The optional Mustache-style lambdas are not supported. Instead Handlebars provides its own lambda resolution that follows the behaviors of helpers.
- Alternative delimiters are not supported.
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
<pre>
Precompile handlebar templates.
Usage: handlebars template...
Options:
-a, --amd Create an AMD format function (allows loading with RequireJS) [boolean]
-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]
-c, --commonjs Exports CommonJS style, path to Handlebars module [string]
-h, --handlebarPath Path to handlebar.js (only valid for amd-style) [string]
-n, --namespace Template namespace [string]
-p, --partial Compiling a partial template [boolean]
-d, --data Include data when compiling [boolean]
-e, --extension Template extension. [string]
-b, --bom Removes the BOM (Byte Order Mark) from the beginning of the templates. [boolean]
</pre>
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 `Handlebars.template` method and the resulting object may be used 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.
- Implementations that do not use `@data` variables can improve performance of
iteration centric templates by specifying `{data: false}` in the compiler options.
Supported Environments
----------------------
@@ -365,7 +121,9 @@ Handlebars in the Wild
* [Assemble](http://assemble.io), by [@jonschlinkert](https://github.com/jonschlinkert)
and [@doowb](https://github.com/doowb), is a static site generator that uses Handlebars.js
as its template engine.
* [Cory](https://github.com/leo/cory), by [@leo](https://github.com/leo), is another tiny static site generator
* [CoSchedule](http://coschedule.com) An editorial calendar for WordPress that uses Handlebars.js
* [dashbars](https://github.com/pismute/dashbars) A modern helper library for Handlebars.js.
* [Ember.js](http://www.emberjs.com) makes Handlebars.js the primary way to
structure your views, also with automatic data binding support.
* [Ghost](https://ghost.org/) Just a blogging platform.
@@ -373,6 +131,7 @@ Handlebars in the Wild
from Les Hill (@leshill).
* [handlebars-helpers](https://github.com/assemble/handlebars-helpers) is an extensive library
with 100+ handlebars helpers.
* [handlebars-layouts](https://github.com/shannonmoeller/handlebars-layouts) is a set of helpers which implement extendible and embeddable layout blocks as seen in other popular templating languages.
* [hbs](http://github.com/donpark/hbs): An Express.js view engine adapter for Handlebars.js,
from Don Park.
* [koa-hbs](https://github.com/jwilm/koa-hbs): [koa](https://github.com/koajs/koa) generator based
@@ -383,13 +142,16 @@ Handlebars in the Wild
Handlebars.js with [jQuery](http://jquery.com/).
* [Lumbar](http://walmartlabs.github.io/lumbar) provides easy module-based template management for
handlebars projects.
* [Marionette.Handlebars](https://github.com/hashchange/marionette.handlebars) adds support for Handlebars and Mustache templates to Marionette.
* [sammy.js](http://github.com/quirkey/sammy) by Aaron Quint, a.k.a. quirkey,
supports Handlebars.js as one of its template plugins.
* [SproutCore](http://www.sproutcore.com) uses Handlebars.js as its main
templating engine, extending it with automatic data binding support.
* [YUI](http://yuilibrary.com/yui/docs/handlebars/) implements a port of handlebars
* [Swag](https://github.com/elving/swag) by [@elving](https://github.com/elving) is a growing collection of helpers for handlebars.js. Give your handlebars.js templates some swag son!
* [DOMBars](https://github.com/blakeembrey/dombars) is a DOM-based templating engine built on the Handlebars parser and runtime
* [DOMBars](https://github.com/blakeembrey/dombars) is a DOM-based templating engine built on the Handlebars parser and runtime **DEPRECATED**
* [promised-handlebars](https://github.com/nknapp/promised-handlebars) is a wrapper for Handlebars that allows helpers to return Promises.
* [just-handlebars-helpers](https://github.com/leapfrogtechnology/just-handlebars-helpers) A fully tested lightweight package with common Handlebars helpers.
External Resources
------------------
@@ -402,6 +164,4 @@ License
-------
Handlebars.js is released under the MIT license.
[bower-repo]: https://github.com/components/handlebars.js
[builds-page]: http://builds.handlebarsjs.com.s3.amazonaws.com/bucket-listing.html?sort=lastmod&sortdir=desc
[pull-request]: https://github.com/wycats/handlebars.js/pull/new/master
+14
View File
@@ -0,0 +1,14 @@
{
"globals": {
"require": true
},
"rules": {
// Disabling for tests, for now.
"no-path-concat": 0,
"no-var": 0,
"no-shadow": 0,
"handle-callback-err": 0,
"no-console": 0
}
}
+1 -2
View File
@@ -1,5 +1,4 @@
var _ = require('underscore'),
async = require('async'),
var async = require('async'),
fs = require('fs'),
zlib = require('zlib');
+1 -1
View File
@@ -2,7 +2,7 @@ var fs = require('fs');
var metrics = fs.readdirSync(__dirname);
metrics.forEach(function(metric) {
if (metric === 'index.js' || !/(.*)\.js$/.test(metric)) {
if (metric === 'index.js' || !(/(.*)\.js$/.test(metric))) {
return;
}
+1 -1
View File
@@ -1,6 +1,6 @@
module.exports = {
helpers: {
foo: function(options) {
foo: function() {
return '';
}
},
+5 -5
View File
@@ -1,7 +1,7 @@
module.exports = {
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 %>"
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 %>'
};
+3 -3
View File
@@ -1,4 +1,4 @@
module.exports = {
context: { names: [{name: "Moe"}, {name: "Larry"}, {name: "Curly"}, {name: "Shemp"}] },
handlebars: "{{#names}}{{name}}{{/names}}"
}
context: { names: [{name: 'Moe'}, {name: 'Larry'}, {name: 'Curly'}, {name: 'Shemp'}] },
handlebars: '{{#names}}{{name}}{{/names}}'
};
+4 -4
View File
@@ -3,13 +3,13 @@ var fs = require('fs');
module.exports = {
context: {
header: function() {
return "Colors";
return 'Colors';
},
hasItems: true, // To make things fairer in mustache land due to no `{{if}}` construct on arrays
items: [
{name: "red", current: true, url: "#Red"},
{name: "green", current: false, url: "#Green"},
{name: "blue", current: false, url: "#Blue"}
{name: 'red', current: true, url: '#Red'},
{name: 'green', current: false, url: '#Green'},
{name: 'blue', current: false, url: '#Blue'}
]
},
+3 -3
View File
@@ -1,4 +1,4 @@
module.exports = {
context: { names: [{name: "Moe"}, {name: "Larry"}, {name: "Curly"}, {name: "Shemp"}] },
handlebars: "{{#each names}}{{@index}}{{name}}{{/each}}"
}
context: { names: [{name: 'Moe'}, {name: 'Larry'}, {name: 'Curly'}, {name: 'Shemp'}] },
handlebars: '{{#each names}}{{@index}}{{name}}{{/each}}'
};
+4 -4
View File
@@ -1,6 +1,6 @@
module.exports = {
context: { names: [{name: "Moe"}, {name: "Larry"}, {name: "Curly"}, {name: "Shemp"}], foo: 'bar' },
handlebars: "{{#each names}}{{../foo}}{{/each}}",
mustache: "{{#names}}{{foo}}{{/names}}",
eco: "<% for item in @names: %><%= @foo %><% end %>"
context: { names: [{name: 'Moe'}, {name: 'Larry'}, {name: 'Curly'}, {name: 'Shemp'}], foo: 'bar' },
handlebars: '{{#each names}}{{../foo}}{{/each}}',
mustache: '{{#names}}{{foo}}{{/names}}',
eco: '<% for item in @names: %><%= @foo %><% end %>'
};
+4 -4
View File
@@ -1,6 +1,6 @@
module.exports = {
context: { names: [{bat: 'foo', name: ["Moe"]}, {bat: 'foo', name: ["Larry"]}, {bat: 'foo', name: ["Curly"]}, {bat: 'foo', name: ["Shemp"]}], foo: 'bar' },
handlebars: "{{#each names}}{{#each name}}{{../bat}}{{../../foo}}{{/each}}{{/each}}",
mustache: "{{#names}}{{#name}}{{bat}}{{foo}}{{/name}}{{/names}}",
eco: "<% for item in @names: %><% for child in item.name: %><%= item.bat %><%= @foo %><% end %><% end %>"
context: { names: [{bat: 'foo', name: ['Moe']}, {bat: 'foo', name: ['Larry']}, {bat: 'foo', name: ['Curly']}, {bat: 'foo', name: ['Shemp']}], foo: 'bar' },
handlebars: '{{#each names}}{{#each name}}{{../bat}}{{../../foo}}{{/each}}{{/each}}',
mustache: '{{#names}}{{#name}}{{bat}}{{foo}}{{/name}}{{/names}}',
eco: '<% for item in @names: %><% for child in item.name: %><%= item.bat %><%= @foo %><% end %><% end %>'
};
+1 -1
View File
@@ -2,7 +2,7 @@ var fs = require('fs');
var templates = fs.readdirSync(__dirname);
templates.forEach(function(template) {
if (template === 'index.js' || !/(.*)\.js$/.test(template)) {
if (template === 'index.js' || !(/(.*)\.js$/.test(template))) {
return;
}
module.exports[RegExp.$1] = require('./' + RegExp.$1);
+2 -2
View File
@@ -1,4 +1,4 @@
module.exports = {
context: { person: { name: "Larry", age: 45 } },
handlebars: "{{#person}}{{name}}{{age}}{{/person}}"
context: { person: { name: 'Larry', age: 45 } },
handlebars: '{{#person}}{{name}}{{age}}{{/person}}'
};
+5 -5
View File
@@ -1,7 +1,7 @@
module.exports = {
context: { person: { name: "Larry", age: 45 } },
handlebars: "{{#with person}}{{name}}{{age}}{{/with}}",
dust: "{#person}{name}{age}{/person}",
eco: "<%= @person.name %><%= @person.age %>",
mustache: "{{#person}}{{name}}{{age}}{{/person}}"
context: { person: { name: 'Larry', age: 45 } },
handlebars: '{{#with person}}{{name}}{{age}}{{/with}}',
dust: '{#person}{name}{age}{/person}',
eco: '<%= @person.name %><%= @person.age %>',
mustache: '{{#person}}{{name}}{{age}}{{/person}}'
};
+5 -5
View File
@@ -1,10 +1,10 @@
module.exports = {
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}}" }
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}}"
handlebars: '{{name}}{{#each kids}}{{>recursion}}{{/each}}',
dust: '{name}{#kids}{>recursion:./}{/kids}',
mustache: '{{name}}{{#kids}}{{>recursion}}{{/kids}}'
};
+6 -6
View File
@@ -1,11 +1,11 @@
module.exports = {
context: { peeps: [{name: "Moe", count: 15}, {name: "Larry", count: 5}, {name: "Curly", count: 1}] },
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." }
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}}"
handlebars: '{{#each peeps}}{{>variables}}{{/each}}',
dust: '{#peeps}{>variables/}{/peeps}',
mustache: '{{#peeps}}{{>variables}}{{/peeps}}'
};
+5 -5
View File
@@ -1,7 +1,7 @@
module.exports = {
context: { person: { name: {bar: {baz: "Larry"}}, age: 45 } },
handlebars: "{{person.name.bar.baz}}{{person.age}}{{person.foo}}{{animal.age}}",
dust: "{person.name.bar.baz}{person.age}{person.foo}{animal.age}",
eco: "<%= @person.name.bar.baz %><%= @person.age %><%= @person.foo %><% if @animal: %><%= @animal.age %><% end %>",
mustache: "{{person.name.bar.baz}}{{person.age}}{{person.foo}}{{animal.age}}"
context: { person: { name: {bar: {baz: 'Larry'}}, age: 45 } },
handlebars: '{{person.name.bar.baz}}{{person.age}}{{person.foo}}{{animal.age}}',
dust: '{person.name.bar.baz}{person.age}{person.foo}{animal.age}',
eco: '<%= @person.name.bar.baz %><%= @person.age %><%= @person.foo %><% if @animal: %><%= @animal.age %><% end %>',
mustache: '{{person.name.bar.baz}}{{person.age}}{{person.foo}}{{animal.age}}'
};
+4 -4
View File
@@ -1,7 +1,7 @@
module.exports = {
context: {},
handlebars: "Hello world",
dust: "Hello world",
mustache: "Hello world",
eco: "Hello world"
handlebars: 'Hello world',
dust: 'Hello world',
mustache: 'Hello world',
eco: 'Hello world'
};
+3 -3
View File
@@ -4,11 +4,11 @@ module.exports = {
return 'foo ' + value;
},
header: function() {
return "Colors";
return 'Colors';
}
},
handlebars: "{{echo (header)}}",
eco: "<%= @echo(@header()) %>"
handlebars: '{{echo (header)}}',
eco: '<%= @echo(@header()) %>'
};
module.exports.context = module.exports.helpers;
+5 -5
View File
@@ -1,8 +1,8 @@
module.exports = {
context: {name: "Mick", count: 30},
handlebars: "Hello {{name}}! You have {{count}} new messages.",
dust: "Hello {name}! You have {count} new messages.",
mustache: "Hello {{name}}! You have {{count}} new messages.",
eco: "Hello <%= @name %>! You have <%= @count %> new messages."
context: {name: 'Mick', count: 30},
handlebars: 'Hello {{name}}! You have {{count}} new messages.',
dust: 'Hello {name}! You have {count} new messages.',
mustache: 'Hello {{name}}! You have {{count}} new messages.',
eco: 'Hello <%= @name %>! You have <%= @count %> new messages.'
};
+15 -16
View File
@@ -1,23 +1,22 @@
var _ = require('underscore'),
runner = require('./util/template-runner'),
templates = require('./templates'),
eco, dust, Handlebars, Mustache, eco;
eco, dust, Handlebars, Mustache;
try {
dust = require("dustjs-linkedin");
dust = require('dustjs-linkedin');
} catch (err) { /* NOP */ }
try {
Mustache = require("mustache");
Mustache = require('mustache');
} catch (err) { /* NOP */ }
try {
eco = require("eco");
eco = require('eco');
} catch (err) { /* NOP */ }
function error() {
throw new Error("EWOT");
throw new Error('EWOT');
}
function makeSuite(bench, name, template, handlebarsOnly) {
@@ -36,17 +35,17 @@ function makeSuite(bench, name, template, handlebarsOnly) {
var handlebar = Handlebars.compile(template.handlebars, {data: false}),
compat = Handlebars.compile(template.handlebars, {data: false, compat: true}),
options = {helpers: template.helpers};
_.each(template.partials && template.partials.handlebars, function(partial, name) {
Handlebars.registerPartial(name, Handlebars.compile(partial, {data: false}));
_.each(template.partials && template.partials.handlebars, function(partial, partialName) {
Handlebars.registerPartial(partialName, Handlebars.compile(partial, {data: false}));
});
handlebarsOut = handlebar(context, options);
bench("handlebars", function() {
bench('handlebars', function() {
handlebar(context, options);
});
compatOut = compat(context, options);
bench("compat", function() {
bench('compat', function() {
compat(context, options);
});
@@ -61,8 +60,8 @@ function makeSuite(bench, name, template, handlebarsOnly) {
dust.render(templateName, context, function(err, out) { dustOut = out; });
bench("dust", function() {
dust.render(templateName, context, function(err, out) { });
bench('dust', function() {
dust.render(templateName, context, function() {});
});
} else {
bench('dust', error);
@@ -75,11 +74,11 @@ function makeSuite(bench, name, template, handlebarsOnly) {
ecoOut = ecoTemplate(context);
bench("eco", function() {
bench('eco', function() {
ecoTemplate(context);
});
} else {
bench("eco", error);
bench('eco', error);
}
}
@@ -90,11 +89,11 @@ function makeSuite(bench, name, template, handlebarsOnly) {
if (mustacheSource) {
mustacheOut = Mustache.to_html(mustacheSource, context, mustachePartials);
bench("mustache", function() {
bench('mustache', function() {
Mustache.to_html(mustacheSource, context, mustachePartials);
});
} else {
bench("mustache", error);
bench('mustache', error);
}
}
+20 -22
View File
@@ -1,7 +1,7 @@
var _ = require('underscore'),
Benchmark = require("benchmark");
Benchmark = require('benchmark');
var BenchWarmer = function(names) {
function BenchWarmer() {
this.benchmarks = [];
this.currentBenches = [];
this.names = [];
@@ -9,9 +9,9 @@ var BenchWarmer = function(names) {
this.minimum = Infinity;
this.maximum = -Infinity;
this.errors = {};
};
}
var print = require("sys").print;
var print = require('sys').print;
BenchWarmer.prototype = {
winners: function(benches) {
@@ -37,7 +37,7 @@ BenchWarmer.prototype = {
this.first = false;
var bench = new Benchmark(fn, {
name: this.suiteName + ": " + name,
name: this.suiteName + ': ' + name,
onComplete: function() {
if (first) { self.startLine(suiteName); }
self.writeBench(bench);
@@ -58,7 +58,7 @@ BenchWarmer.prototype = {
this.printHeader('ops/msec', true);
Benchmark.invoke(this.benchmarks, {
name: "run",
name: 'run',
onComplete: function() {
self.scaleTimes();
@@ -85,17 +85,17 @@ BenchWarmer.prototype = {
}
if (errors) {
print("\n\nErrors:\n");
print('\n\nErrors:\n');
for (prop in self.errors) {
if (self.errors.hasOwnProperty(prop)
&& self.errors[prop].error.message !== 'EWOT') {
bench = self.errors[prop];
print("\n" + bench.name + ":\n");
print('\n' + bench.name + ':\n');
print(bench.error.message);
if (bench.error.stack) {
print(bench.error.stack.join("\n"));
print(bench.error.stack.join('\n'));
}
print("\n");
print('\n');
}
}
}
@@ -104,7 +104,7 @@ BenchWarmer.prototype = {
}
});
print("\n");
print('\n');
},
scaleTimes: function() {
@@ -139,22 +139,22 @@ BenchWarmer.prototype = {
}
if (winners) {
print("WINNER(S)");
horSize = horSize + "WINNER(S)".length;
print('WINNER(S)');
horSize = horSize + 'WINNER(S)'.length;
}
print("\n" + new Array(horSize + 1).join("-"));
print('\n' + new Array(horSize + 1).join('-'));
},
startLine: function(name) {
var winners = Benchmark.map(this.winners(this.currentBenches), function(bench) {
return bench.name.split(": ")[1];
return bench.name.split(': ')[1];
});
this.currentBenches = [];
print(winners.join(", "));
print("\n");
print(winners.join(', '));
print('\n');
if (name) {
this.writeValue(name);
@@ -174,24 +174,22 @@ BenchWarmer.prototype = {
minimum = count - moe;
maximum = count + moe;
out = count + " ±" + moe + " (" + bench.cycles + ")";
out = count + ' ±' + moe + ' (' + bench.cycles + ')';
this.times[bench.suiteName][bench.benchName] = count;
this.minimum = Math.min(this.minimum, minimum);
this.maximum = Math.max(this.maximum, maximum);
} else {
if (bench.error.message === 'EWOT') {
} else if (bench.error.message === 'EWOT') {
out = 'NA';
} else {
out = 'E';
}
}
this.writeValue(out);
},
writeValue: function(out) {
var padding = this.benchSize - out.length + 1;
out = out + new Array(padding).join(" ");
out = out + new Array(padding).join(' ');
print(out);
}
};
+3 -1
View File
@@ -22,6 +22,8 @@ module.exports = function(grunt, makeSuite, callback) {
});
warmer.bench(function() {
callback && callback(warmer.times, warmer.scaled);
if (callback) {
callback(warmer.times, warmer.scaled);
}
});
};
+22 -5
View File
@@ -54,6 +54,16 @@ var optimist = require('optimist')
'description': 'Output template function only.',
'alias': 'simple'
},
'N': {
'type': 'string',
'description': 'Name of passed string templates. Optional if running in a simple mode. Required when operating on multiple templates.',
'alias': 'name'
},
'i': {
'type': 'string',
'description': 'Generates a template from the passed CLI argument.\n"-" is treated as a special value and causes stdin to be read for the template value.',
'alias': 'string'
},
'r': {
'type': 'string',
'description': 'Template root. Base value that will be stripped from template names.',
@@ -92,6 +102,7 @@ var optimist = require('optimist')
}
})
.wrap(120)
.check(function(argv) {
if (argv.version) {
return;
@@ -100,12 +111,18 @@ var optimist = require('optimist')
var argv = optimist.argv;
argv.templates = argv._;
argv.files = argv._;
delete argv._;
if (argv.help || !argv.templates.length) {
optimist.showHelp();
return;
var Precompiler = require('../dist/cjs/precompiler');
Precompiler.loadTemplates(argv, function(err, opts) {
if (err) {
throw err;
}
return require('../lib/precompiler').cli(argv);
if (opts.help || (!opts.templates.length && !opts.version)) {
optimist.showHelp();
} else {
Precompiler.cli(opts);
}
});
+2 -1
View File
@@ -1,6 +1,7 @@
{
"name": "handlebars",
"version": "3.0.0",
"version": "4.0.6",
"main": "handlebars.js",
"license": "MIT",
"dependencies": {}
}
+1 -1
View File
@@ -2,7 +2,7 @@
<package>
<metadata>
<id>handlebars.js</id>
<version>3.0.0</version>
<version>4.0.6</version>
<authors>handlebars.js Authors</authors>
<licenseUrl>https://github.com/wycats/handlebars.js/blob/master/LICENSE</licenseUrl>
<projectUrl>https://github.com/wycats/handlebars.js/</projectUrl>
+49 -1
View File
@@ -87,6 +87,19 @@ interface PartialStatement <: Statement {
indent: string;
strip: StripFlags | null;
}
interface PartialBlockStatement <: Statement {
type: "PartialBlockStatement";
name: PathExpression | SubExpression;
params: [ Expression ];
hash: Hash;
program: Program | null;
indent: string;
openStrip: StripFlags | null;
closeStrip: StripFlags | null;
}
```
`name` will be a `SubExpression` when tied to a dynamic partial, i.e. `{{> (foo) }}`, otherwise this is a path or literal whose `original` value is used to lookup the desired partial.
@@ -107,6 +120,33 @@ interface CommentStatement <: Statement {
}
```
```java
interface Decorator <: Statement {
type: "Decorator";
path: PathExpression | Literal;
params: [ Expression ];
hash: Hash;
strip: StripFlags | null;
}
interface DecoratorBlock <: Statement {
type: "DecoratorBlock";
path: PathExpression | Literal;
params: [ Expression ];
hash: Hash;
program: Program | null;
openStrip: StripFlags | null;
closeStrip: StripFlags | null;
}
```
Decorator paths only utilize the `path.original` value and as a consequence do not support depthed evaluation.
### Expressions
```java
@@ -164,6 +204,14 @@ interface NumberLiteral <: Literal {
value: number;
original: number;
}
interface UndefinedLiteral <: Literal {
type: "UndefinedLiteral";
}
interface NullLiteral <: Literal {
type: "NullLiteral";
}
```
@@ -228,7 +276,7 @@ The `Handlebars.JavaScriptCompiler` object has a number of methods that may be c
- `parent` is the existing code in the path resolution
- `name` is the current path component
- `type` is the type of name being evaluated. May be one of `context`, `data`, `helper`, or `partial`.
- `type` is the type of name being evaluated. May be one of `context`, `data`, `helper`, `decorator`, or `partial`.
Note that this does not impact dynamic partials, which implementors need to be aware of. Overriding `VM.resolvePartial` may be required to support dynamic cases.
+19
View File
@@ -0,0 +1,19 @@
# Decorators
Decorators allow for blocks to be annotated with metadata or wrapped in functionality prior to execution of the block. This may be used to communicate with the containing helper or to setup a particular state in the system prior to running the block.
Decorators are registered through similar methods as helpers, `registerDecorators` and `unregisterDecorators`. These can then be referenced via the friendly name in the template using the `{{* decorator}}` and `{{#* decorator}}{/decorator}}` syntaxes. These syntaxes are derivatives of the normal mustache syntax and as such have all of the same argument and whitespace behaviors.
Decorators are executed when the block program is instantiated and are passed `(program, props, container, context, data, blockParams, depths)`.
- `program`: The block to wrap
- `props`: Object used to set metadata on the final function. Any values set on this object will be set on the function, regardless of if the original function is replaced or not. Metadata should be applied using this object as values applied to `program` may be masked by subsequent decorators that may wrap `program`.
- `container`: The current runtime container
- `context`: The current context. Since the decorator is run before the block that contains it, this is the parent context.
- `data`: The current `@data` values
- `blockParams`: The current block parameters stack
- `depths`: The current context stack
Decorators may set values on `props` or return a modified function that wraps `program` in particular behaviors. If the decorator returns nothing, then `program` is left unaltered.
The [inline partial](https://github.com/wycats/handlebars.js/blob/master/lib/handlebars/decorators/inline.js) implementation provides an example of decorators being used for both metadata and wrapping behaviors.
+20 -25
View File
@@ -1,15 +1,17 @@
/*globals Handlebars: true */
import Handlebars from "./handlebars.runtime";
import runtime from './handlebars.runtime';
// Compiler imports
import AST from "./handlebars/compiler/ast";
import { parser as Parser, parse } from "./handlebars/compiler/base";
import { Compiler, compile, precompile } from "./handlebars/compiler/compiler";
import JavaScriptCompiler from "./handlebars/compiler/javascript-compiler";
import AST from './handlebars/compiler/ast';
import { parser as Parser, parse } from './handlebars/compiler/base';
import { Compiler, compile, precompile } from './handlebars/compiler/compiler';
import JavaScriptCompiler from './handlebars/compiler/javascript-compiler';
import Visitor from './handlebars/compiler/visitor';
var _create = Handlebars.create;
var create = function() {
var hb = _create();
import noConflict from './handlebars/no-conflict';
let _create = runtime.create;
function create() {
let hb = _create();
hb.compile = function(input, options) {
return compile(input, options, hb);
@@ -25,22 +27,15 @@ var create = function() {
hb.parse = parse;
return hb;
};
Handlebars = create();
Handlebars.create = create;
/*jshint -W040 */
/* istanbul ignore next */
var root = typeof global !== 'undefined' ? global : window,
$Handlebars = root.Handlebars;
/* istanbul ignore next */
Handlebars.noConflict = function() {
if (root.Handlebars === Handlebars) {
root.Handlebars = $Handlebars;
}
};
Handlebars['default'] = Handlebars;
let inst = create();
inst.create = create;
export default Handlebars;
noConflict(inst);
inst.Visitor = Visitor;
inst['default'] = inst;
export default inst;
+16 -24
View File
@@ -1,16 +1,17 @@
/*globals Handlebars: true */
module base from "./handlebars/base";
import * as base from './handlebars/base';
// Each of these augment the Handlebars object. No need to setup here.
// (This is done to easily share code between commonjs and browse envs)
import SafeString from "./handlebars/safe-string";
import Exception from "./handlebars/exception";
module Utils from "./handlebars/utils";
module runtime from "./handlebars/runtime";
import SafeString from './handlebars/safe-string';
import Exception from './handlebars/exception';
import * as Utils from './handlebars/utils';
import * as runtime from './handlebars/runtime';
import noConflict from './handlebars/no-conflict';
// For compatibility and usage outside of module systems, make the Handlebars object a namespace
var create = function() {
var hb = new base.HandlebarsEnvironment();
function create() {
let hb = new base.HandlebarsEnvironment();
Utils.extend(hb, base);
hb.SafeString = SafeString;
@@ -24,22 +25,13 @@ var create = function() {
};
return hb;
};
var Handlebars = create();
Handlebars.create = create;
/*jshint -W040 */
/* istanbul ignore next */
var root = typeof global !== 'undefined' ? global : window,
$Handlebars = root.Handlebars;
/* istanbul ignore next */
Handlebars.noConflict = function() {
if (root.Handlebars === Handlebars) {
root.Handlebars = $Handlebars;
}
};
Handlebars['default'] = Handlebars;
let inst = create();
inst.create = create;
export default Handlebars;
noConflict(inst);
inst['default'] = inst;
export default inst;
+32 -195
View File
@@ -1,40 +1,43 @@
module Utils from "./utils";
import Exception from "./exception";
import {createFrame, extend, toString} from './utils';
import Exception from './exception';
import {registerDefaultHelpers} from './helpers';
import {registerDefaultDecorators} from './decorators';
import logger from './logger';
export var VERSION = "3.0.0";
export var COMPILER_REVISION = 6;
export const VERSION = '4.0.6';
export const COMPILER_REVISION = 7;
export var REVISION_CHANGES = {
export const REVISION_CHANGES = {
1: '<= 1.0.rc.2', // 1.0.rc.2 is actually rev2 but doesn't report it
2: '== 1.0.0-rc.3',
3: '== 1.0.0-rc.4',
4: '== 1.x.x',
5: '== 2.0.0-alpha.x',
6: '>= 2.0.0-beta.1'
6: '>= 2.0.0-beta.1',
7: '>= 4.0.0'
};
var isArray = Utils.isArray,
isFunction = Utils.isFunction,
toString = Utils.toString,
objectType = '[object Object]';
const objectType = '[object Object]';
export function HandlebarsEnvironment(helpers, partials) {
export function HandlebarsEnvironment(helpers, partials, decorators) {
this.helpers = helpers || {};
this.partials = partials || {};
this.decorators = decorators || {};
registerDefaultHelpers(this);
registerDefaultDecorators(this);
}
HandlebarsEnvironment.prototype = {
constructor: HandlebarsEnvironment,
logger: logger,
log: log,
log: logger.log,
registerHelper: function(name, fn) {
if (toString.call(name) === objectType) {
if (fn) { throw new Exception('Arg not supported with multiple helpers'); }
Utils.extend(this.helpers, name);
extend(this.helpers, name);
} else {
this.helpers[name] = fn;
}
@@ -45,197 +48,31 @@ HandlebarsEnvironment.prototype = {
registerPartial: function(name, partial) {
if (toString.call(name) === objectType) {
Utils.extend(this.partials, name);
extend(this.partials, name);
} else {
if (typeof partial === 'undefined') {
throw new Exception('Attempting to register a partial as undefined');
throw new Exception(`Attempting to register a partial called "${name}" as undefined`);
}
this.partials[name] = partial;
}
},
unregisterPartial: function(name) {
delete this.partials[name];
},
registerDecorator: function(name, fn) {
if (toString.call(name) === objectType) {
if (fn) { throw new Exception('Arg not supported with multiple decorators'); }
extend(this.decorators, name);
} else {
this.decorators[name] = fn;
}
},
unregisterDecorator: function(name) {
delete this.decorators[name];
}
};
function registerDefaultHelpers(instance) {
instance.registerHelper('helperMissing', function(/* [args, ]options */) {
if(arguments.length === 1) {
// A missing field in a {{foo}} constuct.
return undefined;
} else {
// Someone is actually trying to call something, blow up.
throw new Exception("Missing helper: '" + arguments[arguments.length-1].name + "'");
}
});
export let log = logger.log;
instance.registerHelper('blockHelperMissing', function(context, options) {
var inverse = options.inverse,
fn = options.fn;
if(context === true) {
return fn(this);
} else if(context === false || context == null) {
return inverse(this);
} else if (isArray(context)) {
if(context.length > 0) {
if (options.ids) {
options.ids = [options.name];
}
return instance.helpers.each(context, options);
} else {
return inverse(this);
}
} else {
if (options.data && options.ids) {
var data = createFrame(options.data);
data.contextPath = Utils.appendContextPath(options.data.contextPath, options.name);
options = {data: data};
}
return fn(context, options);
}
});
instance.registerHelper('each', function(context, options) {
if (!options) {
throw new Exception('Must pass iterator to #each');
}
var fn = options.fn, inverse = options.inverse;
var i = 0, ret = "", data;
var contextPath;
if (options.data && options.ids) {
contextPath = Utils.appendContextPath(options.data.contextPath, options.ids[0]) + '.';
}
if (isFunction(context)) { context = context.call(this); }
if (options.data) {
data = createFrame(options.data);
}
function execIteration(key, i, last) {
if (data) {
data.key = key;
data.index = i;
data.first = i === 0;
data.last = !!last;
if (contextPath) {
data.contextPath = contextPath + key;
}
}
ret = ret + fn(context[key], {
data: data,
blockParams: Utils.blockParams([context[key], key], [contextPath + key, null])
});
}
if(context && typeof context === 'object') {
if (isArray(context)) {
for(var j = context.length; i<j; i++) {
execIteration(i, i, i === context.length-1);
}
} else {
var priorKey;
for(var key in context) {
if(context.hasOwnProperty(key)) {
// We're running the iterations one step out of sync so we can detect
// the last iteration without have to scan the object twice and create
// an itermediate keys array.
if (priorKey) {
execIteration(priorKey, i-1);
}
priorKey = key;
i++;
}
}
if (priorKey) {
execIteration(priorKey, i-1, true);
}
}
}
if(i === 0){
ret = inverse(this);
}
return ret;
});
instance.registerHelper('if', function(conditional, options) {
if (isFunction(conditional)) { conditional = conditional.call(this); }
// Default behavior is to render the positive path if the value is truthy and not empty.
// The `includeZero` option may be set to treat the condtional as purely not empty based on the
// behavior of isEmpty. Effectively this determines if 0 is handled by the positive path or negative.
if ((!options.hash.includeZero && !conditional) || Utils.isEmpty(conditional)) {
return options.inverse(this);
} else {
return options.fn(this);
}
});
instance.registerHelper('unless', function(conditional, options) {
return instance.helpers['if'].call(this, conditional, {fn: options.inverse, inverse: options.fn, hash: options.hash});
});
instance.registerHelper('with', function(context, options) {
if (isFunction(context)) { context = context.call(this); }
var fn = options.fn;
if (!Utils.isEmpty(context)) {
if (options.data && options.ids) {
var data = createFrame(options.data);
data.contextPath = Utils.appendContextPath(options.data.contextPath, options.ids[0]);
options = {data:data};
}
return fn(context, options);
} else {
return options.inverse(this);
}
});
instance.registerHelper('log', function(message, options) {
var level = options.data && options.data.level != null ? parseInt(options.data.level, 10) : 1;
instance.log(level, message);
});
instance.registerHelper('lookup', function(obj, field) {
return obj && obj[field];
});
}
export var logger = {
methodMap: { 0: 'debug', 1: 'info', 2: 'warn', 3: 'error' },
// State enum
DEBUG: 0,
INFO: 1,
WARN: 2,
ERROR: 3,
level: 1,
// Can be overridden in the host environment
log: function(level, message) {
if (typeof console !== 'undefined' && logger.level <= level) {
var method = logger.methodMap[level];
(console[method] || console.log).call(console, message);
}
}
};
export var log = logger.log;
export var createFrame = function(object) {
var frame = Utils.extend({}, object);
frame._parent = object;
return frame;
};
export {createFrame, logger};
+4 -117
View File
@@ -1,126 +1,13 @@
var AST = {
Program: function(statements, blockParams, strip, locInfo) {
this.loc = locInfo;
this.type = 'Program';
this.body = statements;
this.blockParams = blockParams;
this.strip = strip;
},
MustacheStatement: function(path, params, hash, escaped, strip, locInfo) {
this.loc = locInfo;
this.type = 'MustacheStatement';
this.path = path;
this.params = params || [];
this.hash = hash;
this.escaped = escaped;
this.strip = strip;
},
BlockStatement: function(path, params, hash, program, inverse, openStrip, inverseStrip, closeStrip, locInfo) {
this.loc = locInfo;
this.type = 'BlockStatement';
this.path = path;
this.params = params || [];
this.hash = hash;
this.program = program;
this.inverse = inverse;
this.openStrip = openStrip;
this.inverseStrip = inverseStrip;
this.closeStrip = closeStrip;
},
PartialStatement: function(name, params, hash, strip, locInfo) {
this.loc = locInfo;
this.type = 'PartialStatement';
this.name = name;
this.params = params || [];
this.hash = hash;
this.indent = '';
this.strip = strip;
},
ContentStatement: function(string, locInfo) {
this.loc = locInfo;
this.type = 'ContentStatement';
this.original = this.value = string;
},
CommentStatement: function(comment, strip, locInfo) {
this.loc = locInfo;
this.type = 'CommentStatement';
this.value = comment;
this.strip = strip;
},
SubExpression: function(path, params, hash, locInfo) {
this.loc = locInfo;
this.type = 'SubExpression';
this.path = path;
this.params = params || [];
this.hash = hash;
},
PathExpression: function(data, depth, parts, original, locInfo) {
this.loc = locInfo;
this.type = 'PathExpression';
this.data = data;
this.original = original;
this.parts = parts;
this.depth = depth;
},
StringLiteral: function(string, locInfo) {
this.loc = locInfo;
this.type = 'StringLiteral';
this.original =
this.value = string;
},
NumberLiteral: function(number, locInfo) {
this.loc = locInfo;
this.type = 'NumberLiteral';
this.original =
this.value = Number(number);
},
BooleanLiteral: function(bool, locInfo) {
this.loc = locInfo;
this.type = 'BooleanLiteral';
this.original =
this.value = bool === 'true';
},
Hash: function(pairs, locInfo) {
this.loc = locInfo;
this.type = 'Hash';
this.pairs = pairs;
},
HashPair: function(key, value, locInfo) {
this.loc = locInfo;
this.type = 'HashPair';
this.key = key;
this.value = value;
},
let AST = {
// Public API used to evaluate derived attributes regarding AST nodes
helpers: {
// a mustache is definitely a helper if:
// * it is an eligible helper, and
// * it has at least one parameter or hash segment
// TODO: Make these public utility methods
helperExpression: function(node) {
return !!(node.type === 'SubExpression' || node.params.length || node.hash);
return (node.type === 'SubExpression')
|| ((node.type === 'MustacheStatement' || node.type === 'BlockStatement')
&& !!((node.params && node.params.length) || node.hash));
},
scopedId: function(path) {
+7 -8
View File
@@ -1,13 +1,12 @@
import parser from "./parser";
import AST from "./ast";
import WhitespaceControl from "./whitespace-control";
module Helpers from "./helpers";
import { extend } from "../utils";
import parser from './parser';
import WhitespaceControl from './whitespace-control';
import * as Helpers from './helpers';
import { extend } from '../utils';
export { parser };
var yy = {};
extend(yy, Helpers, AST);
let yy = {};
extend(yy, Helpers);
export function parse(input, options) {
// Just return if an already-compiled AST was passed in.
@@ -20,6 +19,6 @@ export function parse(input, options) {
return new yy.SourceLocation(options && options.srcName, locInfo);
};
var strip = new WhitespaceControl();
let strip = new WhitespaceControl(options);
return strip.accept(parser.parse(input));
}
+35 -21
View File
@@ -1,10 +1,22 @@
import {isArray} from "../utils";
/* global define */
import {isArray} from '../utils';
let SourceNode;
try {
var SourceMap = require('source-map'),
/* istanbul ignore next */
if (typeof define !== 'function' || !define.amd) {
// We don't support this in AMD environments. For these environments, we asusme that
// they are running on the browser and thus have no need for the source-map library.
let SourceMap = require('source-map');
SourceNode = SourceMap.SourceNode;
}
} catch (err) {
/* istanbul ignore next: tested but not covered in istanbul due to dist build */
/* NOP */
}
/* istanbul ignore if: tested but not covered in istanbul due to dist build */
if (!SourceNode) {
SourceNode = function(line, column, srcFile, chunks) {
this.src = '';
if (chunks) {
@@ -37,9 +49,9 @@ try {
function castChunk(chunk, codeGen, loc) {
if (isArray(chunk)) {
var ret = [];
let ret = [];
for (var i = 0, len = chunk.length; i < len; i++) {
for (let i = 0, len = chunk.length; i < len; i++) {
ret.push(codeGen.wrap(chunk[i], loc));
}
return ret;
@@ -57,6 +69,9 @@ function CodeGen(srcFile) {
}
CodeGen.prototype = {
isEmpty() {
return !this.source.length;
},
prepend: function(source, loc) {
this.source.unshift(this.wrap(source, loc));
},
@@ -65,7 +80,7 @@ CodeGen.prototype = {
},
merge: function() {
var source = this.empty();
let source = this.empty();
this.each(function(line) {
source.add([' ', line, '\n']);
});
@@ -73,21 +88,20 @@ CodeGen.prototype = {
},
each: function(iter) {
for (var i = 0, len = this.source.length; i < len; i++) {
for (let i = 0, len = this.source.length; i < len; i++) {
iter(this.source[i]);
}
},
empty: function(loc) {
loc = loc || this.currentLocation || {start:{}};
empty: function() {
let loc = this.currentLocation || {start: {}};
return new SourceNode(loc.start.line, loc.start.column, this.srcFile);
},
wrap: function(chunk, loc) {
wrap: function(chunk, loc = this.currentLocation || {start: {}}) {
if (chunk instanceof SourceNode) {
return chunk;
}
loc = loc || this.currentLocation || {start:{}};
chunk = castChunk(chunk, this, loc);
return new SourceNode(loc.start.line, loc.start.column, this.srcFile, chunk);
@@ -109,40 +123,40 @@ CodeGen.prototype = {
},
objectLiteral: function(obj) {
var pairs = [];
let pairs = [];
for (var key in obj) {
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
var value = castChunk(obj[key], this);
let value = castChunk(obj[key], this);
if (value !== 'undefined') {
pairs.push([this.quotedString(key), ':', value]);
}
}
}
var ret = this.generateList(pairs);
let ret = this.generateList(pairs);
ret.prepend('{');
ret.add('}');
return ret;
},
generateList: function(entries, loc) {
var ret = this.empty(loc);
generateList: function(entries) {
let ret = this.empty();
for (var i = 0, len = entries.length; i < len; i++) {
for (let i = 0, len = entries.length; i < len; i++) {
if (i) {
ret.add(',');
}
ret.add(castChunk(entries[i], this, loc));
ret.add(castChunk(entries[i], this));
}
return ret;
},
generateArray: function(entries, loc) {
var ret = this.generateList(entries, loc);
generateArray: function(entries) {
let ret = this.generateList(entries);
ret.prepend('[');
ret.add(']');
+129 -70
View File
@@ -1,9 +1,10 @@
import Exception from "../exception";
import {isArray, indexOf} from "../utils";
import AST from "./ast";
/* eslint-disable new-cap */
var slice = [].slice;
import Exception from '../exception';
import {isArray, indexOf} from '../utils';
import AST from './ast';
const slice = [].slice;
export function Compiler() {}
@@ -16,13 +17,13 @@ Compiler.prototype = {
compiler: Compiler,
equals: function(other) {
var len = this.opcodes.length;
let len = this.opcodes.length;
if (other.opcodes.length !== len) {
return false;
}
for (var i = 0; i < len; i++) {
var opcode = this.opcodes[i],
for (let i = 0; i < len; i++) {
let opcode = this.opcodes[i],
otherOpcode = other.opcodes[i];
if (opcode.opcode !== otherOpcode.opcode || !argEquals(opcode.args, otherOpcode.args)) {
return false;
@@ -32,7 +33,7 @@ Compiler.prototype = {
// We know that length is the same between the two arrays because they are directly tied
// to the opcode behavior above.
len = this.children.length;
for (i = 0; i < len; i++) {
for (let i = 0; i < len; i++) {
if (!this.children[i].equals(other.children[i])) {
return false;
}
@@ -54,7 +55,7 @@ Compiler.prototype = {
options.blockParams = options.blockParams || [];
// These changes will propagate to the other compiler components
var knownHelpers = options.knownHelpers;
let knownHelpers = options.knownHelpers;
options.knownHelpers = {
'helperMissing': true,
'blockHelperMissing': true,
@@ -66,17 +67,21 @@ Compiler.prototype = {
'lookup': true
};
if (knownHelpers) {
for (var name in knownHelpers) {
for (let name in knownHelpers) {
/* istanbul ignore else */
if (name in knownHelpers) {
options.knownHelpers[name] = knownHelpers[name];
}
}
}
return this.accept(program);
},
compileProgram: function(program) {
var result = new this.compiler().compile(program, this.options);
var guid = this.guid++;
let childCompiler = new this.compiler(), // eslint-disable-line new-cap
result = childCompiler.compile(program, this.options),
guid = this.guid++;
this.usePartial = this.usePartial || result.usePartial;
@@ -87,8 +92,13 @@ Compiler.prototype = {
},
accept: function(node) {
/* istanbul ignore next: Sanity code */
if (!this[node.type]) {
throw new Exception('Unknown type: ' + node.type, node);
}
this.sourceNode.unshift(node);
var ret = this[node.type](node);
let ret = this[node.type](node);
this.sourceNode.shift();
return ret;
},
@@ -96,14 +106,15 @@ Compiler.prototype = {
Program: function(program) {
this.options.blockParams.unshift(program.blockParams);
var body = program.body;
for(var i=0, l=body.length; i<l; i++) {
let body = program.body,
bodyLength = body.length;
for (let i = 0; i < bodyLength; i++) {
this.accept(body[i]);
}
this.options.blockParams.shift();
this.isSimple = l === 1;
this.isSimple = bodyLength === 1;
this.blockParams = program.blockParams ? program.blockParams.length : 0;
return this;
@@ -112,13 +123,13 @@ Compiler.prototype = {
BlockStatement: function(block) {
transformLiteralToPath(block);
var program = block.program,
let program = block.program,
inverse = block.inverse;
program = program && this.compileProgram(program);
inverse = inverse && this.compileProgram(inverse);
var type = this.classifySexpr(block);
let type = this.classifySexpr(block);
if (type === 'helper') {
this.helperSexpr(block, program, inverse);
@@ -145,25 +156,43 @@ Compiler.prototype = {
this.opcode('append');
},
DecoratorBlock(decorator) {
let program = decorator.program && this.compileProgram(decorator.program);
let params = this.setupFullMustacheParams(decorator, program, undefined),
path = decorator.path;
this.useDecorators = true;
this.opcode('registerDecorator', params.length, path.original);
},
PartialStatement: function(partial) {
this.usePartial = true;
var params = partial.params;
let program = partial.program;
if (program) {
program = this.compileProgram(partial.program);
}
let params = partial.params;
if (params.length > 1) {
throw new Exception('Unsupported number of partial arguments: ' + params.length, partial);
} else if (!params.length) {
if (this.options.explicitPartialContext) {
this.opcode('pushLiteral', 'undefined');
} else {
params.push({type: 'PathExpression', parts: [], depth: 0});
}
}
var partialName = partial.name.original,
let partialName = partial.name.original,
isDynamic = partial.name.type === 'SubExpression';
if (isDynamic) {
this.accept(partial.name);
}
this.setupFullMustacheParams(partial, undefined, undefined, true);
this.setupFullMustacheParams(partial, program, undefined, true);
var indent = partial.indent || '';
let indent = partial.indent || '';
if (this.options.preventIndent && indent) {
this.opcode('appendContent', indent);
indent = '';
@@ -172,6 +201,9 @@ Compiler.prototype = {
this.opcode('invokePartial', isDynamic, partialName, indent);
this.opcode('append');
},
PartialBlockStatement: function(partialBlock) {
this.PartialStatement(partialBlock);
},
MustacheStatement: function(mustache) {
this.SubExpression(mustache);
@@ -182,6 +214,10 @@ Compiler.prototype = {
this.opcode('append');
}
},
Decorator(decorator) {
this.DecoratorBlock(decorator);
},
ContentStatement: function(content) {
if (content.value) {
@@ -193,7 +229,7 @@ Compiler.prototype = {
SubExpression: function(sexpr) {
transformLiteralToPath(sexpr);
var type = this.classifySexpr(sexpr);
let type = this.classifySexpr(sexpr);
if (type === 'simple') {
this.simpleSexpr(sexpr);
@@ -204,7 +240,7 @@ Compiler.prototype = {
}
},
ambiguousSexpr: function(sexpr, program, inverse) {
var path = sexpr.path,
let path = sexpr.path,
name = path.parts[0],
isBlock = program != null || inverse != null;
@@ -213,26 +249,30 @@ Compiler.prototype = {
this.opcode('pushProgram', program);
this.opcode('pushProgram', inverse);
path.strict = true;
this.accept(path);
this.opcode('invokeAmbiguous', name, isBlock);
},
simpleSexpr: function(sexpr) {
this.accept(sexpr.path);
let path = sexpr.path;
path.strict = true;
this.accept(path);
this.opcode('resolvePossibleLambda');
},
helperSexpr: function(sexpr, program, inverse) {
var params = this.setupFullMustacheParams(sexpr, program, inverse),
let params = this.setupFullMustacheParams(sexpr, program, inverse),
path = sexpr.path,
name = path.parts[0];
if (this.options.knownHelpers[name]) {
this.opcode('invokeKnownHelper', params.length, name);
} else if (this.options.knownHelpersOnly) {
throw new Exception("You specified knownHelpersOnly, but used the unknown helper " + name, sexpr);
throw new Exception('You specified knownHelpersOnly, but used the unknown helper ' + name, sexpr);
} else {
path.strict = true;
path.falsy = true;
this.accept(path);
@@ -244,7 +284,7 @@ Compiler.prototype = {
this.addDepth(path.depth);
this.opcode('getContext', path.depth);
var name = path.parts[0],
let name = path.parts[0],
scoped = AST.helpers.scopedId(path),
blockParamId = !path.depth && !scoped && this.blockParamIndex(name);
@@ -255,9 +295,9 @@ Compiler.prototype = {
this.opcode('pushContext');
} else if (path.data) {
this.options.data = true;
this.opcode('lookupData', path.depth, path.parts);
this.opcode('lookupData', path.depth, path.parts, path.strict);
} else {
this.opcode('lookupOnContext', path.parts, path.falsy, scoped);
this.opcode('lookupOnContext', path.parts, path.falsy, path.strict, scoped);
}
},
@@ -273,12 +313,22 @@ Compiler.prototype = {
this.opcode('pushLiteral', bool.value);
},
UndefinedLiteral: function() {
this.opcode('pushLiteral', 'undefined');
},
NullLiteral: function() {
this.opcode('pushLiteral', 'null');
},
Hash: function(hash) {
var pairs = hash.pairs, i, l;
let pairs = hash.pairs,
i = 0,
l = pairs.length;
this.opcode('pushHash');
for (i=0, l=pairs.length; i<l; i++) {
for (; i < l; i++) {
this.pushParam(pairs[i].value);
}
while (i--) {
@@ -301,25 +351,24 @@ Compiler.prototype = {
},
classifySexpr: function(sexpr) {
var isSimple = AST.helpers.simpleId(sexpr.path);
let isSimple = AST.helpers.simpleId(sexpr.path);
var isBlockParam = isSimple && !!this.blockParamIndex(sexpr.path.parts[0]);
let isBlockParam = isSimple && !!this.blockParamIndex(sexpr.path.parts[0]);
// a mustache is an eligible helper if:
// * its id is simple (a single part, not `this` or `..`)
var isHelper = !isBlockParam && AST.helpers.helperExpression(sexpr);
let isHelper = !isBlockParam && AST.helpers.helperExpression(sexpr);
// if a mustache is an eligible helper but not a definite
// helper, it is ambiguous, and will be resolved in a later
// pass or at runtime.
var isEligible = !isBlockParam && (isHelper || isSimple);
var options = this.options;
let isEligible = !isBlockParam && (isHelper || isSimple);
// if ambiguous, we can possibly resolve the ambiguity now
// An eligible helper is one that does not have a complex path, i.e. `this.foo`, `../foo` etc.
if (isEligible && !isHelper) {
var name = sexpr.path.parts[0];
let name = sexpr.path.parts[0],
options = this.options;
if (options.knownHelpers[name]) {
isHelper = true;
@@ -328,19 +377,23 @@ Compiler.prototype = {
}
}
if (isHelper) { return 'helper'; }
else if (isEligible) { return 'ambiguous'; }
else { return 'simple'; }
if (isHelper) {
return 'helper';
} else if (isEligible) {
return 'ambiguous';
} else {
return 'simple';
}
},
pushParams: function(params) {
for(var i=0, l=params.length; i<l; i++) {
for (let i = 0, l = params.length; i < l; i++) {
this.pushParam(params[i]);
}
},
pushParam: function(val) {
var value = val.value != null ? val.value : val.original || '';
let value = val.value != null ? val.value : val.original || '';
if (this.stringParams) {
if (value.replace) {
@@ -362,19 +415,20 @@ Compiler.prototype = {
}
} else {
if (this.trackIds) {
var blockParamIndex;
let blockParamIndex;
if (val.parts && !AST.helpers.scopedId(val) && !val.depth) {
blockParamIndex = this.blockParamIndex(val.parts[0]);
}
if (blockParamIndex) {
var blockParamChild = val.parts.slice(1).join('.');
let blockParamChild = val.parts.slice(1).join('.');
this.opcode('pushId', 'BlockParam', blockParamIndex, blockParamChild);
} else {
value = val.original || value;
if (value.replace) {
value = value
.replace(/^\.\//g, '')
.replace(/^\.$/g, '');
.replace(/^this(?:\.|$)/, '')
.replace(/^\.\//, '')
.replace(/^\.$/, '');
}
this.opcode('pushId', val.type, value);
@@ -385,7 +439,7 @@ Compiler.prototype = {
},
setupFullMustacheParams: function(sexpr, program, inverse, omitEmpty) {
var params = sexpr.params;
let params = sexpr.params;
this.pushParams(params);
this.opcode('pushProgram', program);
@@ -401,8 +455,8 @@ Compiler.prototype = {
},
blockParamIndex: function(name) {
for (var depth = 0, len = this.options.blockParams.length; depth < len; depth++) {
var blockParams = this.options.blockParams[depth],
for (let depth = 0, len = this.options.blockParams.length; depth < len; depth++) {
let blockParams = this.options.blockParams[depth],
param = blockParams && indexOf(blockParams, name);
if (blockParams && param >= 0) {
return [depth, param];
@@ -413,7 +467,7 @@ Compiler.prototype = {
export function precompile(input, options, env) {
if (input == null || (typeof input !== 'string' && input.type !== 'Program')) {
throw new Exception("You must pass a string or Handlebars AST to Handlebars.precompile. You passed " + input);
throw new Exception('You must pass a string or Handlebars AST to Handlebars.precompile. You passed ' + input);
}
options = options || {};
@@ -424,18 +478,16 @@ export function precompile(input, options, env) {
options.useDepths = true;
}
var ast = env.parse(input, options);
var environment = new env.Compiler().compile(ast, options);
let ast = env.parse(input, options),
environment = new env.Compiler().compile(ast, options);
return new env.JavaScriptCompiler().compile(environment, options);
}
export function compile(input, options, env) {
export function compile(input, options = {}, env) {
if (input == null || (typeof input !== 'string' && input.type !== 'Program')) {
throw new Exception("You must pass a string or Handlebars AST to Handlebars.compile. You passed " + input);
throw new Exception('You must pass a string or Handlebars AST to Handlebars.compile. You passed ' + input);
}
options = options || {};
if (!('data' in options)) {
options.data = true;
}
@@ -443,27 +495,27 @@ export function compile(input, options, env) {
options.useDepths = true;
}
var compiled;
let compiled;
function compileInput() {
var ast = env.parse(input, options);
var environment = new env.Compiler().compile(ast, options);
var templateSpec = new env.JavaScriptCompiler().compile(environment, options, undefined, true);
let ast = env.parse(input, options),
environment = new env.Compiler().compile(ast, options),
templateSpec = new env.JavaScriptCompiler().compile(environment, options, undefined, true);
return env.template(templateSpec);
}
// Template is only compiled on first use and cached after that point.
var ret = function(context, options) {
function ret(context, execOptions) {
if (!compiled) {
compiled = compileInput();
}
return compiled.call(this, context, options);
};
ret._setup = function(options) {
return compiled.call(this, context, execOptions);
}
ret._setup = function(setupOptions) {
if (!compiled) {
compiled = compileInput();
}
return compiled._setup(options);
return compiled._setup(setupOptions);
};
ret._child = function(i, data, blockParams, depths) {
if (!compiled) {
@@ -480,7 +532,7 @@ function argEquals(a, b) {
}
if (isArray(a) && isArray(b) && a.length === b.length) {
for (var i = 0; i < a.length; i++) {
for (let i = 0; i < a.length; i++) {
if (!argEquals(a[i], b[i])) {
return false;
}
@@ -491,9 +543,16 @@ function argEquals(a, b) {
function transformLiteralToPath(sexpr) {
if (!sexpr.path.parts) {
var literal = sexpr.path;
let literal = sexpr.path;
// Casting to string here to make false and 0 literal values play nicely with the rest
// of the system.
sexpr.path = new AST.PathExpression(false, 0, [literal.original+''], literal.original+'', literal.log);
sexpr.path = {
type: 'PathExpression',
data: false,
depth: 0,
parts: [literal.original + ''],
original: literal.original + '',
loc: literal.loc
};
}
}
+134 -38
View File
@@ -1,4 +1,14 @@
import Exception from "../exception";
import Exception from '../exception';
function validateClose(open, close) {
close = close.path ? close.path.original : close;
if (open.path.original !== close) {
let errorNode = {loc: open.path.loc};
throw new Exception(open.path.original + " doesn't match " + close, errorNode);
}
}
export function SourceLocation(source, locInfo) {
this.source = source;
@@ -12,6 +22,14 @@ export function SourceLocation(source, locInfo) {
};
}
export function id(token) {
if (/^\[.*\]$/.test(token)) {
return token.substr(1, token.length - 2);
} else {
return token;
}
}
export function stripFlags(open, close) {
return {
open: open.charAt(2) === '~',
@@ -24,22 +42,24 @@ export function stripComment(comment) {
.replace(/-?-?~?\}\}$/, '');
}
export function preparePath(data, parts, locInfo) {
/*jshint -W040 */
locInfo = this.locInfo(locInfo);
export function preparePath(data, parts, loc) {
loc = this.locInfo(loc);
var original = data ? '@' : '',
let original = data ? '@' : '',
dig = [],
depth = 0,
depthString = '';
for(var i=0,l=parts.length; i<l; i++) {
var part = parts[i].part;
for (let i = 0, l = parts.length; i < l; i++) {
let part = parts[i].part,
// If we have [] syntax then we do not treat path references as operators,
// i.e. foo.[this] resolves to approximately context.foo['this']
isLiteral = parts[i].original !== part;
original += (parts[i].separator || '') + part;
if (part === '..' || part === '.' || part === 'this') {
if (!isLiteral && (part === '..' || part === '.' || part === 'this')) {
if (dig.length > 0) {
throw new Exception('Invalid path: ' + original, {loc: locInfo});
throw new Exception('Invalid path: ' + original, {loc});
} else if (part === '..') {
depth++;
depthString += '../';
@@ -49,51 +69,74 @@ export function preparePath(data, parts, locInfo) {
}
}
return new this.PathExpression(data, depth, dig, original, locInfo);
return {
type: 'PathExpression',
data,
depth,
parts: dig,
original,
loc
};
}
export function prepareMustache(path, params, hash, open, strip, locInfo) {
/*jshint -W040 */
// Must use charAt to support IE pre-10
var escapeFlag = open.charAt(3) || open.charAt(2),
let escapeFlag = open.charAt(3) || open.charAt(2),
escaped = escapeFlag !== '{' && escapeFlag !== '&';
return new this.MustacheStatement(path, params, hash, escaped, strip, this.locInfo(locInfo));
let decorator = (/\*/.test(open));
return {
type: decorator ? 'Decorator' : 'MustacheStatement',
path,
params,
hash,
escaped,
strip,
loc: this.locInfo(locInfo)
};
}
export function prepareRawBlock(openRawBlock, content, close, locInfo) {
/*jshint -W040 */
if (openRawBlock.path.original !== close) {
var errorNode = {loc: openRawBlock.path.loc};
throw new Exception(openRawBlock.path.original + " doesn't match " + close, errorNode);
}
export function prepareRawBlock(openRawBlock, contents, close, locInfo) {
validateClose(openRawBlock, close);
locInfo = this.locInfo(locInfo);
var program = new this.Program([content], null, {}, locInfo);
let program = {
type: 'Program',
body: contents,
strip: {},
loc: locInfo
};
return new this.BlockStatement(
openRawBlock.path, openRawBlock.params, openRawBlock.hash,
program, undefined,
{}, {}, {},
locInfo);
return {
type: 'BlockStatement',
path: openRawBlock.path,
params: openRawBlock.params,
hash: openRawBlock.hash,
program,
openStrip: {},
inverseStrip: {},
closeStrip: {},
loc: locInfo
};
}
export function prepareBlock(openBlock, program, inverseAndProgram, close, inverted, locInfo) {
/*jshint -W040 */
// When we are chaining inverse calls, we will not have a close path
if (close && close.path && openBlock.path.original !== close.path.original) {
var errorNode = {loc: openBlock.path.loc};
throw new Exception(openBlock.path.original + ' doesn\'t match ' + close.path.original, errorNode);
if (close && close.path) {
validateClose(openBlock, close);
}
let decorator = (/\*/.test(openBlock.open));
program.blockParams = openBlock.blockParams;
var inverse,
let inverse,
inverseStrip;
if (inverseAndProgram) {
if (decorator) {
throw new Exception('Unexpected inverse block on decorator', inverseAndProgram);
}
if (inverseAndProgram.chain) {
inverseAndProgram.program.body[0].closeStrip = close.strip;
}
@@ -108,9 +151,62 @@ export function prepareBlock(openBlock, program, inverseAndProgram, close, inver
program = inverted;
}
return new this.BlockStatement(
openBlock.path, openBlock.params, openBlock.hash,
program, inverse,
openBlock.strip, inverseStrip, close && close.strip,
this.locInfo(locInfo));
return {
type: decorator ? 'DecoratorBlock' : 'BlockStatement',
path: openBlock.path,
params: openBlock.params,
hash: openBlock.hash,
program,
inverse,
openStrip: openBlock.strip,
inverseStrip,
closeStrip: close && close.strip,
loc: this.locInfo(locInfo)
};
}
export function prepareProgram(statements, loc) {
if (!loc && statements.length) {
const firstLoc = statements[0].loc,
lastLoc = statements[statements.length - 1].loc;
/* istanbul ignore else */
if (firstLoc && lastLoc) {
loc = {
source: firstLoc.source,
start: {
line: firstLoc.start.line,
column: firstLoc.start.column
},
end: {
line: lastLoc.end.line,
column: lastLoc.end.column
}
};
}
}
return {
type: 'Program',
body: statements,
strip: {},
loc: loc
};
}
export function preparePartialBlock(open, program, close, locInfo) {
validateClose(open, close);
return {
type: 'PartialBlockStatement',
name: open.path,
params: open.params,
hash: open.hash,
program,
openStrip: open.strip,
closeStrip: close && close.strip,
loc: this.locInfo(locInfo)
};
}
+192 -126
View File
@@ -1,7 +1,7 @@
import { COMPILER_REVISION, REVISION_CHANGES } from "../base";
import Exception from "../exception";
import {isArray} from "../utils";
import CodeGen from "./code-gen";
import { COMPILER_REVISION, REVISION_CHANGES } from '../base';
import Exception from '../exception';
import {isArray} from '../utils';
import CodeGen from './code-gen';
function Literal(value) {
this.value = value;
@@ -14,17 +14,17 @@ JavaScriptCompiler.prototype = {
// alternative compiled forms for name lookup and buffering semantics
nameLookup: function(parent, name/* , type*/) {
if (JavaScriptCompiler.isValidJavaScriptVariableName(name)) {
return [parent, ".", name];
return [parent, '.', name];
} else {
return [parent, "['", name, "']"];
return [parent, '[', JSON.stringify(name), ']'];
}
},
depthedLookup: function(name) {
return [this.aliasable('this.lookup'), '(depths, "', name, '")'];
return [this.aliasable('container.lookup'), '(depths, "', name, '")'];
},
compilerInfo: function() {
var revision = COMPILER_REVISION,
const revision = COMPILER_REVISION,
versions = REVISION_CHANGES[revision];
return [revision, versions];
},
@@ -50,7 +50,7 @@ JavaScriptCompiler.prototype = {
},
initializeBuffer: function() {
return this.quotedString("");
return this.quotedString('');
},
// END PUBLIC API
@@ -64,6 +64,7 @@ JavaScriptCompiler.prototype = {
this.name = this.environment.name;
this.isChild = !!context;
this.context = context || {
decorators: [],
programs: [],
environments: []
};
@@ -81,10 +82,10 @@ JavaScriptCompiler.prototype = {
this.compileChildren(environment, options);
this.useDepths = this.useDepths || environment.useDepths || this.options.compat;
this.useDepths = this.useDepths || environment.useDepths || environment.useDecorators || this.options.compat;
this.useBlockParams = this.useBlockParams || environment.useBlockParams;
var opcodes = environment.opcodes,
let opcodes = environment.opcodes,
opcode,
firstLoc,
i,
@@ -107,16 +108,43 @@ JavaScriptCompiler.prototype = {
throw new Exception('Compile completed with content left on stack');
}
var fn = this.createFunctionContext(asObject);
if (!this.decorators.isEmpty()) {
this.useDecorators = true;
this.decorators.prepend('var decorators = container.decorators;\n');
this.decorators.push('return fn;');
if (asObject) {
this.decorators = Function.apply(this, ['fn', 'props', 'container', 'depth0', 'data', 'blockParams', 'depths', this.decorators.merge()]);
} else {
this.decorators.prepend('function(fn, props, container, depth0, data, blockParams, depths) {\n');
this.decorators.push('}\n');
this.decorators = this.decorators.merge();
}
} else {
this.decorators = undefined;
}
let fn = this.createFunctionContext(asObject);
if (!this.isChild) {
var ret = {
let ret = {
compiler: this.compilerInfo(),
main: fn
};
var programs = this.context.programs;
if (this.decorators) {
ret.main_d = this.decorators; // eslint-disable-line camelcase
ret.useDecorators = true;
}
let {programs, decorators} = this.context;
for (i = 0, l = programs.length; i < l; i++) {
if (programs[i]) {
ret[i] = programs[i];
if (decorators[i]) {
ret[i + '_d'] = decorators[i];
ret.useDecorators = true;
}
}
}
@@ -163,14 +191,15 @@ JavaScriptCompiler.prototype = {
// getContext opcode when it would be a noop
this.lastContext = 0;
this.source = new CodeGen(this.options.srcName);
this.decorators = new CodeGen(this.options.srcName);
},
createFunctionContext: function(asObject) {
var varDeclarations = '';
let varDeclarations = '';
var locals = this.stackVars.concat(this.registers.list);
let locals = this.stackVars.concat(this.registers.list);
if (locals.length > 0) {
varDeclarations += ", " + locals.join(", ");
varDeclarations += ', ' + locals.join(', ');
}
// Generate minimizer alias mappings
@@ -179,9 +208,9 @@ JavaScriptCompiler.prototype = {
// as the source nodes are reused in situ. For the non-source node compilation mode,
// aliases will not be used, but this case is already being run on the client and
// we aren't concern about minimizing the template size.
var aliasCount = 0;
for (var alias in this.aliases) {
var node = this.aliases[alias];
let aliasCount = 0;
for (let alias in this.aliases) { // eslint-disable-line guard-for-in
let node = this.aliases[alias];
if (this.aliases.hasOwnProperty(alias) && node.children && node.referenceCount > 1) {
varDeclarations += ', alias' + (++aliasCount) + '=' + alias;
@@ -189,7 +218,7 @@ JavaScriptCompiler.prototype = {
}
}
var params = ["depth0", "helpers", "partials", "data"];
let params = ['container', 'depth0', 'helpers', 'partials', 'data'];
if (this.useBlockParams || this.useDepths) {
params.push('blockParams');
@@ -199,7 +228,7 @@ JavaScriptCompiler.prototype = {
}
// Perform a second pass over the output to merge content when possible
var source = this.mergeSource(varDeclarations);
let source = this.mergeSource(varDeclarations);
if (asObject) {
params.push(source);
@@ -210,14 +239,14 @@ JavaScriptCompiler.prototype = {
}
},
mergeSource: function(varDeclarations) {
var isSimple = this.environment.isSimple,
let isSimple = this.environment.isSimple,
appendOnly = !this.forceBuffer,
appendFirst,
sourceSeen,
bufferStart,
bufferEnd;
this.source.each(function(line) {
this.source.each((line) => {
if (line.appendToBuffer) {
if (bufferStart) {
line.prepend(' + ');
@@ -252,7 +281,7 @@ JavaScriptCompiler.prototype = {
this.source.push('return "";');
}
} else {
varDeclarations += ", buffer = " + (appendFirst ? '' : this.initializeBuffer());
varDeclarations += ', buffer = ' + (appendFirst ? '' : this.initializeBuffer());
if (bufferStart) {
bufferStart.prepend('return buffer + ');
@@ -279,11 +308,11 @@ JavaScriptCompiler.prototype = {
// replace it on the stack with the result of properly
// invoking blockHelperMissing.
blockValue: function(name) {
var blockHelperMissing = this.aliasable('helpers.blockHelperMissing'),
let blockHelperMissing = this.aliasable('helpers.blockHelperMissing'),
params = [this.contextName(0)];
this.setupHelperArgs(name, 0, params);
var blockName = this.popStack();
let blockName = this.popStack();
params.splice(1, 0, blockName);
this.push(this.source.functionCall(blockHelperMissing, 'call', params));
@@ -297,13 +326,13 @@ JavaScriptCompiler.prototype = {
// On stack, after, if lastHelper: value
ambiguousBlockValue: function() {
// We're being a bit cheeky and reusing the options value from the prior exec
var blockHelperMissing = this.aliasable('helpers.blockHelperMissing'),
let blockHelperMissing = this.aliasable('helpers.blockHelperMissing'),
params = [this.contextName(0)];
this.setupHelperArgs('', 0, params, true);
this.flushInline();
var current = this.topStack();
let current = this.topStack();
params.splice(1, 0, current);
this.pushSource([
@@ -339,13 +368,11 @@ JavaScriptCompiler.prototype = {
// Otherwise, the empty string is appended
append: function() {
if (this.isInline()) {
this.replaceStack(function(current) {
return [' != null ? ', current, ' : ""'];
});
this.replaceStack((current) => [' != null ? ', current, ' : ""']);
this.pushSource(this.appendToBuffer(this.popStack()));
} else {
var local = this.popStack();
let local = this.popStack();
this.pushSource(['if (', local, ' != null) { ', this.appendToBuffer(local, undefined, true), ' }']);
if (this.environment.isSimple) {
this.pushSource(['else { ', this.appendToBuffer("''", undefined, true), ' }']);
@@ -361,7 +388,7 @@ JavaScriptCompiler.prototype = {
// Escape `value` and append it to the buffer
appendEscaped: function() {
this.pushSource(this.appendToBuffer(
[this.aliasable('this.escapeExpression'), '(', this.popStack(), ')']));
[this.aliasable('container.escapeExpression'), '(', this.popStack(), ')']));
},
// [getContext]
@@ -392,8 +419,8 @@ JavaScriptCompiler.prototype = {
//
// Looks up the value of `name` on the current context and pushes
// it onto the stack.
lookupOnContext: function(parts, falsy, scoped) {
var i = 0;
lookupOnContext: function(parts, falsy, strict, scoped) {
let i = 0;
if (!scoped && this.options.compat && !this.lastContext) {
// The depthed query is expected to handle the undefined logic for the root level that
@@ -403,7 +430,7 @@ JavaScriptCompiler.prototype = {
this.pushContext();
}
this.resolvePath('context', parts, i, falsy);
this.resolvePath('context', parts, i, falsy, strict);
},
// [lookupBlockParam]
@@ -426,28 +453,27 @@ JavaScriptCompiler.prototype = {
// On stack, after: data, ...
//
// Push the data lookup operator
lookupData: function(depth, parts) {
/*jshint -W083 */
lookupData: function(depth, parts, strict) {
if (!depth) {
this.pushStackLiteral('data');
} else {
this.pushStackLiteral('this.data(data, ' + depth + ')');
this.pushStackLiteral('container.data(data, ' + depth + ')');
}
this.resolvePath('data', parts, 0, true);
this.resolvePath('data', parts, 0, true, strict);
},
resolvePath: function(type, parts, i, falsy) {
/*jshint -W083 */
resolvePath: function(type, parts, i, falsy, strict) {
if (this.options.strict || this.options.assumeObjects) {
this.push(strictLookup(this.options.strict, this, parts, type));
this.push(strictLookup(this.options.strict && strict, this, parts, type));
return;
}
var len = parts.length;
let len = parts.length;
for (; i < len; i++) {
this.replaceStack(function(current) {
var lookup = this.nameLookup(current, parts[i], type);
/* eslint-disable no-loop-func */
this.replaceStack((current) => {
let lookup = this.nameLookup(current, parts[i], type);
// We want to ensure that zero and false are handled properly if the context (falsy flag)
// needs to have the special handling for these values.
if (!falsy) {
@@ -457,6 +483,7 @@ JavaScriptCompiler.prototype = {
return [' && ', lookup];
}
});
/* eslint-enable no-loop-func */
}
},
@@ -468,7 +495,7 @@ JavaScriptCompiler.prototype = {
// If the `value` is a lambda, replace it on the stack by
// the return value of the lambda
resolvePossibleLambda: function() {
this.push([this.aliasable('this.lambda'), '(', this.popStack(), ', ', this.contextName(0), ')']);
this.push([this.aliasable('container.lambda'), '(', this.popStack(), ', ', this.contextName(0), ')']);
},
// [pushStringParam]
@@ -511,7 +538,7 @@ JavaScriptCompiler.prototype = {
this.hash = {values: [], types: [], contexts: [], ids: []};
},
popHash: function() {
var hash = this.hash;
let hash = this.hash;
this.hash = this.hashes.pop();
if (this.trackIds) {
@@ -563,6 +590,24 @@ JavaScriptCompiler.prototype = {
}
},
// [registerDecorator]
//
// On stack, before: hash, program, params..., ...
// On stack, after: ...
//
// Pops off the decorator's parameters, invokes the decorator,
// and inserts the decorator into the decorators list.
registerDecorator(paramSize, name) {
let foundDecorator = this.nameLookup('decorators', name, 'decorator'),
options = this.setupHelperArgs(name, paramSize);
this.decorators.push([
'fn = ',
this.decorators.functionCall(foundDecorator, '', ['fn', 'props', 'container', options]),
' || fn;'
]);
},
// [invokeHelper]
//
// On stack, before: hash, inverse, program, params..., ...
@@ -573,11 +618,11 @@ JavaScriptCompiler.prototype = {
//
// If the helper is not found, `helperMissing` is called.
invokeHelper: function(paramSize, name, isSimple) {
var nonHelper = this.popStack();
var helper = this.setupHelper(paramSize, name);
var simple = isSimple ? [helper.name, ' || '] : '';
let nonHelper = this.popStack(),
helper = this.setupHelper(paramSize, name),
simple = isSimple ? [helper.name, ' || '] : '';
var lookup = ['('].concat(simple, nonHelper);
let lookup = ['('].concat(simple, nonHelper);
if (!this.options.strict) {
lookup.push(' || ', this.aliasable('helpers.helperMissing'));
}
@@ -594,7 +639,7 @@ JavaScriptCompiler.prototype = {
// This operation is used when the helper is known to exist,
// so a `helperMissing` fallback is not required.
invokeKnownHelper: function(paramSize, name) {
var helper = this.setupHelper(paramSize, name);
let helper = this.setupHelper(paramSize, name);
this.push(this.source.functionCall(helper.name, 'call', helper.callParams));
},
@@ -613,14 +658,14 @@ JavaScriptCompiler.prototype = {
invokeAmbiguous: function(name, helperCall) {
this.useRegister('helper');
var nonHelper = this.popStack();
let nonHelper = this.popStack();
this.emptyHash();
var helper = this.setupHelper(0, name, helperCall);
let helper = this.setupHelper(0, name, helperCall);
var helperName = this.lastHelper = this.nameLookup('helpers', name, 'helper');
let helperName = this.lastHelper = this.nameLookup('helpers', name, 'helper');
var lookup = ['(', '(helper = ', helperName, ' || ', nonHelper, ')'];
let lookup = ['(', '(helper = ', helperName, ' || ', nonHelper, ')'];
if (!this.options.strict) {
lookup[0] = '(helper = ';
lookup.push(
@@ -645,8 +690,8 @@ JavaScriptCompiler.prototype = {
// This operation pops off a context, invokes a partial with that context,
// and pushes the result of the invocation back.
invokePartial: function(isDynamic, name, indent) {
var params = [],
options = this.setupParams(name, 1, params, false);
let params = [],
options = this.setupParams(name, 1, params);
if (isDynamic) {
name = this.popStack();
@@ -658,6 +703,7 @@ JavaScriptCompiler.prototype = {
}
options.helpers = 'helpers';
options.partials = 'partials';
options.decorators = 'container.decorators';
if (!isDynamic) {
params.unshift(this.nameLookup('partials', name, 'partial'));
@@ -671,7 +717,7 @@ JavaScriptCompiler.prototype = {
options = this.objectLiteral(options);
params.push(options);
this.push(this.source.functionCall('this.invokePartial', '', params));
this.push(this.source.functionCall('container.invokePartial', '', params));
},
// [assignToHash]
@@ -681,7 +727,7 @@ JavaScriptCompiler.prototype = {
//
// Pops a value off the stack and assigns it to the current hash
assignToHash: function(key) {
var value = this.popStack(),
let value = this.popStack(),
context,
type,
id;
@@ -694,7 +740,7 @@ JavaScriptCompiler.prototype = {
context = this.popStack();
}
var hash = this.hash;
let hash = this.hash;
if (context) {
hash.contexts[key] = context;
}
@@ -726,44 +772,47 @@ JavaScriptCompiler.prototype = {
compiler: JavaScriptCompiler,
compileChildren: function(environment, options) {
var children = environment.children, child, compiler;
let children = environment.children, child, compiler;
for(var i=0, l=children.length; i<l; i++) {
for (let i = 0, l = children.length; i < l; i++) {
child = children[i];
compiler = new this.compiler();
compiler = new this.compiler(); // eslint-disable-line new-cap
var index = this.matchExistingProgram(child);
let existing = this.matchExistingProgram(child);
if (index == null) {
if (existing == null) {
this.context.programs.push(''); // Placeholder to prevent name conflicts for nested children
index = this.context.programs.length;
let index = this.context.programs.length;
child.index = index;
child.name = 'program' + index;
this.context.programs[index] = compiler.compile(child, options, this.context, !this.precompile);
this.context.decorators[index] = compiler.decorators;
this.context.environments[index] = child;
this.useDepths = this.useDepths || compiler.useDepths;
this.useBlockParams = this.useBlockParams || compiler.useBlockParams;
child.useDepths = this.useDepths;
child.useBlockParams = this.useBlockParams;
} else {
child.index = index;
child.name = 'program' + index;
child.index = existing.index;
child.name = 'program' + existing.index;
this.useDepths = this.useDepths || child.useDepths;
this.useBlockParams = this.useBlockParams || child.useBlockParams;
this.useDepths = this.useDepths || existing.useDepths;
this.useBlockParams = this.useBlockParams || existing.useBlockParams;
}
}
},
matchExistingProgram: function(child) {
for (var i = 0, len = this.context.environments.length; i < len; i++) {
var environment = this.context.environments[i];
for (let i = 0, len = this.context.environments.length; i < len; i++) {
let environment = this.context.environments[i];
if (environment && environment.equals(child)) {
return i;
return environment;
}
}
},
programExpression: function(guid) {
var child = this.environment.children[guid],
let child = this.environment.children[guid],
programParams = [child.index, 'data', child.blockParams];
if (this.useBlockParams || this.useDepths) {
@@ -773,7 +822,7 @@ JavaScriptCompiler.prototype = {
programParams.push('depths');
}
return 'this.program(' + programParams.join(', ') + ')';
return 'container.program(' + programParams.join(', ') + ')';
},
useRegister: function(name) {
@@ -809,7 +858,7 @@ JavaScriptCompiler.prototype = {
},
replaceStack: function(callback) {
var prefix = ['('],
let prefix = ['('],
stack,
createdStack,
usedLiteral;
@@ -820,7 +869,7 @@ JavaScriptCompiler.prototype = {
}
// We want to merge the inline statement into the replacement statement via ','
var top = this.popStack(true);
let top = this.popStack(true);
if (top instanceof Literal) {
// Literals do not need to be inlined
@@ -830,13 +879,13 @@ JavaScriptCompiler.prototype = {
} else {
// Get or create the current stack name for use by the inline
createdStack = true;
var name = this.incrStack();
let name = this.incrStack();
prefix = ['((', this.push(name), ' = ', top, ')'];
stack = this.topStack();
}
var item = callback.call(this, stack);
let item = callback.call(this, stack);
if (!usedLiteral) {
this.popStack();
@@ -849,22 +898,22 @@ JavaScriptCompiler.prototype = {
incrStack: function() {
this.stackSlot++;
if(this.stackSlot > this.stackVars.length) { this.stackVars.push("stack" + this.stackSlot); }
if (this.stackSlot > this.stackVars.length) { this.stackVars.push('stack' + this.stackSlot); }
return this.topStackName();
},
topStackName: function() {
return "stack" + this.stackSlot;
return 'stack' + this.stackSlot;
},
flushInline: function() {
var inlineStack = this.inlineStack;
let inlineStack = this.inlineStack;
this.inlineStack = [];
for (var i = 0, len = inlineStack.length; i < len; i++) {
var entry = inlineStack[i];
for (let i = 0, len = inlineStack.length; i < len; i++) {
let entry = inlineStack[i];
/* istanbul ignore if */
if (entry instanceof Literal) {
this.compileStack.push(entry);
} else {
var stack = this.incrStack();
let stack = this.incrStack();
this.pushSource([stack, ' = ', entry, ';']);
this.compileStack.push(stack);
}
@@ -875,7 +924,7 @@ JavaScriptCompiler.prototype = {
},
popStack: function(wrapped) {
var inline = this.isInline(),
let inline = this.isInline(),
item = (inline ? this.inlineStack : this.compileStack).pop();
if (!wrapped && (item instanceof Literal)) {
@@ -893,7 +942,7 @@ JavaScriptCompiler.prototype = {
},
topStack: function() {
var stack = (this.isInline() ? this.inlineStack : this.compileStack),
let stack = (this.isInline() ? this.inlineStack : this.compileStack),
item = stack[stack.length - 1];
/* istanbul ignore if */
@@ -921,7 +970,7 @@ JavaScriptCompiler.prototype = {
},
aliasable: function(name) {
var ret = this.aliases[name];
let ret = this.aliases[name];
if (ret) {
ret.referenceCount++;
return ret;
@@ -935,20 +984,30 @@ JavaScriptCompiler.prototype = {
},
setupHelper: function(paramSize, name, blockHelper) {
var params = [],
let params = [],
paramsInit = this.setupHelperArgs(name, paramSize, params, blockHelper);
var foundHelper = this.nameLookup('helpers', name, 'helper');
let foundHelper = this.nameLookup('helpers', name, 'helper'),
callContext = this.aliasable(`${this.contextName(0)} != null ? ${this.contextName(0)} : {}`);
return {
params: params,
paramsInit: paramsInit,
name: foundHelper,
callParams: [this.contextName(0)].concat(params)
callParams: [callContext].concat(params)
};
},
setupParams: function(helper, paramSize, params) {
var options = {}, contexts = [], types = [], ids = [], param;
let options = {},
contexts = [],
types = [],
ids = [],
objectArgs = !params,
param;
if (objectArgs) {
params = [];
}
options.name = this.quotedString(helper);
options.hash = this.popStack();
@@ -961,19 +1020,19 @@ JavaScriptCompiler.prototype = {
options.hashContexts = this.popStack();
}
var inverse = this.popStack(),
let inverse = this.popStack(),
program = this.popStack();
// Avoid setting fn and inverse if neither are set. This allows
// helpers to do a check for `if (options.fn)`
if (program || inverse) {
options.fn = program || 'this.noop';
options.inverse = inverse || 'this.noop';
options.fn = program || 'container.noop';
options.inverse = inverse || 'container.noop';
}
// The parameters go on to the stack in order (making sure that they are evaluated in order)
// so we need to pop them off the stack in reverse order
var i = paramSize;
let i = paramSize;
while (i--) {
param = this.popStack();
params[i] = param;
@@ -987,6 +1046,10 @@ JavaScriptCompiler.prototype = {
}
}
if (objectArgs) {
options.args = this.source.generateArray(params);
}
if (this.trackIds) {
options.ids = this.source.generateArray(ids);
}
@@ -1005,53 +1068,56 @@ JavaScriptCompiler.prototype = {
},
setupHelperArgs: function(helper, paramSize, params, useRegister) {
var options = this.setupParams(helper, paramSize, params, true);
let options = this.setupParams(helper, paramSize, params);
options = this.objectLiteral(options);
if (useRegister) {
this.useRegister('options');
params.push('options');
return ['options=', options];
} else {
} else if (params) {
params.push(options);
return '';
} else {
return options;
}
}
};
var reservedWords = (
"break else new var" +
" case finally return void" +
" catch for switch while" +
" continue function this with" +
" default if throw" +
" delete in try" +
" do instanceof typeof" +
" abstract enum int short" +
" boolean export interface static" +
" byte extends long super" +
" char final native synchronized" +
" class float package throws" +
" const goto private transient" +
" debugger implements protected volatile" +
" double import public let yield await" +
" null true false"
).split(" ");
(function() {
const reservedWords = (
'break else new var' +
' case finally return void' +
' catch for switch while' +
' continue function this with' +
' default if throw' +
' delete in try' +
' do instanceof typeof' +
' abstract enum int short' +
' boolean export interface static' +
' byte extends long super' +
' char final native synchronized' +
' class float package throws' +
' const goto private transient' +
' debugger implements protected volatile' +
' double import public let yield await' +
' null true false'
).split(' ');
var compilerWords = JavaScriptCompiler.RESERVED_WORDS = {};
const compilerWords = JavaScriptCompiler.RESERVED_WORDS = {};
for(var i=0, l=reservedWords.length; i<l; i++) {
for (let i = 0, l = reservedWords.length; i < l; i++) {
compilerWords[reservedWords[i]] = true;
}
}());
JavaScriptCompiler.isValidJavaScriptVariableName = function(name) {
return !JavaScriptCompiler.RESERVED_WORDS[name] && /^[a-zA-Z_$][0-9a-zA-Z_$]*$/.test(name);
return !JavaScriptCompiler.RESERVED_WORDS[name] && (/^[a-zA-Z_$][0-9a-zA-Z_$]*$/).test(name);
};
function strictLookup(requireTerminal, compiler, parts, type) {
var stack = compiler.popStack();
var i = 0,
let stack = compiler.popStack(),
i = 0,
len = parts.length;
if (requireTerminal) {
len--;
@@ -1062,7 +1128,7 @@ function strictLookup(requireTerminal, compiler, parts, type) {
}
if (requireTerminal) {
return [compiler.aliasable('this.strict'), '(', stack, ', ', compiler.quotedString(parts[i]), ')'];
return [compiler.aliasable('container.strict'), '(', stack, ', ', compiler.quotedString(parts[i]), ')'];
} else {
return stack;
}
+60 -28
View File
@@ -1,4 +1,5 @@
import Visitor from "./visitor";
/* eslint-disable new-cap */
import Visitor from './visitor';
export function print(ast) {
return new PrintVisitor().accept(ast);
@@ -11,23 +12,23 @@ export function PrintVisitor() {
PrintVisitor.prototype = new Visitor();
PrintVisitor.prototype.pad = function(string) {
var out = "";
let out = '';
for(var i=0,l=this.padding; i<l; i++) {
out = out + " ";
for (let i = 0, l = this.padding; i < l; i++) {
out += ' ';
}
out = out + string + "\n";
out += string + '\n';
return out;
};
PrintVisitor.prototype.Program = function(program) {
var out = '',
let out = '',
body = program.body,
i, l;
if (program.blockParams) {
var blockParams = 'BLOCK PARAMS: [';
let blockParams = 'BLOCK PARAMS: [';
for (i = 0, l = program.blockParams.length; i < l; i++) {
blockParams += ' ' + program.blockParams[i];
}
@@ -36,7 +37,7 @@ PrintVisitor.prototype.Program = function(program) {
}
for (i = 0, l = body.length; i < l; i++) {
out = out + this.accept(body[i]);
out += this.accept(body[i]);
}
this.padding--;
@@ -47,24 +48,28 @@ PrintVisitor.prototype.Program = function(program) {
PrintVisitor.prototype.MustacheStatement = function(mustache) {
return this.pad('{{ ' + this.SubExpression(mustache) + ' }}');
};
PrintVisitor.prototype.Decorator = function(mustache) {
return this.pad('{{ DIRECTIVE ' + this.SubExpression(mustache) + ' }}');
};
PrintVisitor.prototype.BlockStatement = function(block) {
var out = "";
PrintVisitor.prototype.BlockStatement =
PrintVisitor.prototype.DecoratorBlock = function(block) {
let out = '';
out = out + this.pad('BLOCK:');
out += this.pad((block.type === 'DecoratorBlock' ? 'DIRECTIVE ' : '') + 'BLOCK:');
this.padding++;
out = out + this.pad(this.SubExpression(block));
out += this.pad(this.SubExpression(block));
if (block.program) {
out = out + this.pad('PROGRAM:');
out += this.pad('PROGRAM:');
this.padding++;
out = out + this.accept(block.program);
out += this.accept(block.program);
this.padding--;
}
if (block.inverse) {
if (block.program) { this.padding++; }
out = out + this.pad('{{^}}');
out += this.pad('{{^}}');
this.padding++;
out = out + this.accept(block.inverse);
out += this.accept(block.inverse);
this.padding--;
if (block.program) { this.padding--; }
}
@@ -74,7 +79,7 @@ PrintVisitor.prototype.BlockStatement = function(block) {
};
PrintVisitor.prototype.PartialStatement = function(partial) {
var content = 'PARTIAL:' + partial.name.original;
let content = 'PARTIAL:' + partial.name.original;
if (partial.params[0]) {
content += ' ' + this.accept(partial.params[0]);
}
@@ -83,6 +88,22 @@ PrintVisitor.prototype.PartialStatement = function(partial) {
}
return this.pad('{{> ' + content + ' }}');
};
PrintVisitor.prototype.PartialBlockStatement = function(partial) {
let content = 'PARTIAL BLOCK:' + partial.name.original;
if (partial.params[0]) {
content += ' ' + this.accept(partial.params[0]);
}
if (partial.hash) {
content += ' ' + this.accept(partial.hash);
}
content += ' ' + this.pad('PROGRAM:');
this.padding++;
content += this.accept(partial.program);
this.padding--;
return this.pad('{{> ' + content + ' }}');
};
PrintVisitor.prototype.ContentStatement = function(content) {
return this.pad("CONTENT[ '" + content.value + "' ]");
@@ -93,21 +114,23 @@ PrintVisitor.prototype.CommentStatement = function(comment) {
};
PrintVisitor.prototype.SubExpression = function(sexpr) {
var params = sexpr.params, paramStrings = [], hash;
let params = sexpr.params,
paramStrings = [],
hash;
for(var i=0, l=params.length; i<l; i++) {
for (let i = 0, l = params.length; i < l; i++) {
paramStrings.push(this.accept(params[i]));
}
params = "[" + paramStrings.join(", ") + "]";
params = '[' + paramStrings.join(', ') + ']';
hash = sexpr.hash ? " " + this.accept(sexpr.hash) : "";
hash = sexpr.hash ? ' ' + this.accept(sexpr.hash) : '';
return this.accept(sexpr.path) + " " + params + hash;
return this.accept(sexpr.path) + ' ' + params + hash;
};
PrintVisitor.prototype.PathExpression = function(id) {
var path = id.parts.join('/');
let path = id.parts.join('/');
return (id.data ? '@' : '') + 'PATH:' + path;
};
@@ -117,18 +140,26 @@ PrintVisitor.prototype.StringLiteral = function(string) {
};
PrintVisitor.prototype.NumberLiteral = function(number) {
return "NUMBER{" + number.value + "}";
return 'NUMBER{' + number.value + '}';
};
PrintVisitor.prototype.BooleanLiteral = function(bool) {
return "BOOLEAN{" + bool.value + "}";
return 'BOOLEAN{' + bool.value + '}';
};
PrintVisitor.prototype.UndefinedLiteral = function() {
return 'UNDEFINED';
};
PrintVisitor.prototype.NullLiteral = function() {
return 'NULL';
};
PrintVisitor.prototype.Hash = function(hash) {
var pairs = hash.pairs;
var joinedPairs = [];
let pairs = hash.pairs,
joinedPairs = [];
for (var i=0, l=pairs.length; i<l; i++) {
for (let i = 0, l = pairs.length; i < l; i++) {
joinedPairs.push(this.accept(pairs[i]));
}
@@ -137,3 +168,4 @@ PrintVisitor.prototype.Hash = function(hash) {
PrintVisitor.prototype.HashPair = function(pair) {
return pair.key + '=' + this.accept(pair.value);
};
/* eslint-enable new-cap */
+40 -33
View File
@@ -1,5 +1,4 @@
import Exception from "../exception";
import AST from "./ast";
import Exception from '../exception';
function Visitor() {
this.parents = [];
@@ -11,10 +10,11 @@ Visitor.prototype = {
// Visits a given value. If mutating, will replace the value if necessary.
acceptKey: function(node, name) {
var value = this.accept(node[name]);
let value = this.accept(node[name]);
if (this.mutating) {
// Hacky sanity check:
if (value && (!value.type || !AST[value.type])) {
// Hacky sanity check: This may have a few false positives for type for the helper
// methods but will generally do the right thing without a lot of overhead.
if (value && !Visitor.prototype[value.type]) {
throw new Exception('Unexpected node type "' + value.type + '" found when accepting ' + name + ' on ' + node.type);
}
node[name] = value;
@@ -34,7 +34,7 @@ Visitor.prototype = {
// Traverses a given array. If mutating, empty respnses will be removed
// for child elements.
acceptArray: function(array) {
for (var i = 0, l = array.length; i < l; i++) {
for (let i = 0, l = array.length; i < l; i++) {
this.acceptKey(array, i);
if (!array[i]) {
@@ -50,12 +50,17 @@ Visitor.prototype = {
return;
}
/* istanbul ignore next: Sanity code */
if (!this[object.type]) {
throw new Exception('Unknown type: ' + object.type, object);
}
if (this.current) {
this.parents.unshift(this.current);
}
this.current = object;
var ret = this[object.type](object);
let ret = this[object.type](object);
this.current = this.parents.shift();
@@ -70,46 +75,31 @@ Visitor.prototype = {
this.acceptArray(program.body);
},
MustacheStatement: function(mustache) {
this.acceptRequired(mustache, 'path');
this.acceptArray(mustache.params);
this.acceptKey(mustache, 'hash');
},
MustacheStatement: visitSubExpression,
Decorator: visitSubExpression,
BlockStatement: function(block) {
this.acceptRequired(block, 'path');
this.acceptArray(block.params);
this.acceptKey(block, 'hash');
BlockStatement: visitBlock,
DecoratorBlock: visitBlock,
this.acceptKey(block, 'program');
this.acceptKey(block, 'inverse');
},
PartialStatement: visitPartial,
PartialBlockStatement: function(partial) {
visitPartial.call(this, partial);
PartialStatement: function(partial) {
this.acceptRequired(partial, 'name');
this.acceptArray(partial.params);
this.acceptKey(partial, 'hash');
this.acceptKey(partial, 'program');
},
ContentStatement: function(/* content */) {},
CommentStatement: function(/* comment */) {},
SubExpression: function(sexpr) {
this.acceptRequired(sexpr, 'path');
this.acceptArray(sexpr.params);
this.acceptKey(sexpr, 'hash');
},
PartialExpression: function(partial) {
this.acceptRequired(partial, 'name');
this.acceptArray(partial.params);
this.acceptKey(partial, 'hash');
},
SubExpression: visitSubExpression,
PathExpression: function(/* path */) {},
StringLiteral: function(/* string */) {},
NumberLiteral: function(/* number */) {},
BooleanLiteral: function(/* bool */) {},
UndefinedLiteral: function(/* literal */) {},
NullLiteral: function(/* literal */) {},
Hash: function(hash) {
this.acceptArray(hash.pairs);
@@ -119,4 +109,21 @@ Visitor.prototype = {
}
};
function visitSubExpression(mustache) {
this.acceptRequired(mustache, 'path');
this.acceptArray(mustache.params);
this.acceptKey(mustache, 'hash');
}
function visitBlock(block) {
visitSubExpression.call(this, block);
this.acceptKey(block, 'program');
this.acceptKey(block, 'inverse');
}
function visitPartial(partial) {
this.acceptRequired(partial, 'name');
this.acceptArray(partial.params);
this.acceptKey(partial, 'hash');
}
export default Visitor;
+31 -26
View File
@@ -1,23 +1,26 @@
import Visitor from "./visitor";
import Visitor from './visitor';
function WhitespaceControl() {
function WhitespaceControl(options = {}) {
this.options = options;
}
WhitespaceControl.prototype = new Visitor();
WhitespaceControl.prototype.Program = function(program) {
var isRoot = !this.isRootSeen;
const doStandalone = !this.options.ignoreStandalone;
let isRoot = !this.isRootSeen;
this.isRootSeen = true;
var body = program.body;
for (var i = 0, l = body.length; i < l; i++) {
var current = body[i],
let body = program.body;
for (let i = 0, l = body.length; i < l; i++) {
let current = body[i],
strip = this.accept(current);
if (!strip) {
continue;
}
var _isPrevWhitespace = isPrevWhitespace(body, i, isRoot),
let _isPrevWhitespace = isPrevWhitespace(body, i, isRoot),
_isNextWhitespace = isNextWhitespace(body, i, isRoot),
openStandalone = strip.openStandalone && _isPrevWhitespace,
@@ -31,7 +34,7 @@ WhitespaceControl.prototype.Program = function(program) {
omitLeft(body, i, true);
}
if (inlineStandalone) {
if (doStandalone && inlineStandalone) {
omitRight(body, i);
if (omitLeft(body, i)) {
@@ -42,13 +45,13 @@ WhitespaceControl.prototype.Program = function(program) {
}
}
}
if (openStandalone) {
if (doStandalone && openStandalone) {
omitRight((current.program || current.inverse).body);
// Strip out the previous content node if it's whitespace only
omitLeft(body, i);
}
if (closeStandalone) {
if (doStandalone && closeStandalone) {
// Always strip the next node
omitRight(body, i);
@@ -58,12 +61,15 @@ WhitespaceControl.prototype.Program = function(program) {
return program;
};
WhitespaceControl.prototype.BlockStatement = function(block) {
WhitespaceControl.prototype.BlockStatement =
WhitespaceControl.prototype.DecoratorBlock =
WhitespaceControl.prototype.PartialBlockStatement = function(block) {
this.accept(block.program);
this.accept(block.inverse);
// Find the inverse program that is involed with whitespace stripping.
var program = block.program || block.inverse,
let program = block.program || block.inverse,
inverse = block.program && block.inverse,
firstInverse = inverse,
lastInverse = inverse;
@@ -77,7 +83,7 @@ WhitespaceControl.prototype.BlockStatement = function(block) {
}
}
var strip = {
let strip = {
open: block.openStrip.open,
close: block.closeStrip.close,
@@ -92,7 +98,7 @@ WhitespaceControl.prototype.BlockStatement = function(block) {
}
if (inverse) {
var inverseStrip = block.inverseStrip;
let inverseStrip = block.inverseStrip;
if (inverseStrip.open) {
omitLeft(program.body, null, true);
@@ -106,21 +112,20 @@ WhitespaceControl.prototype.BlockStatement = function(block) {
}
// Find standalone else statments
if (isPrevWhitespace(program.body)
if (!this.options.ignoreStandalone
&& isPrevWhitespace(program.body)
&& isNextWhitespace(firstInverse.body)) {
omitLeft(program.body);
omitRight(firstInverse.body);
}
} else {
if (block.closeStrip.open) {
} else if (block.closeStrip.open) {
omitLeft(program.body, null, true);
}
}
return strip;
};
WhitespaceControl.prototype.Decorator =
WhitespaceControl.prototype.MustacheStatement = function(mustache) {
return mustache.strip;
};
@@ -128,7 +133,7 @@ WhitespaceControl.prototype.MustacheStatement = function(mustache) {
WhitespaceControl.prototype.PartialStatement =
WhitespaceControl.prototype.CommentStatement = function(node) {
/* istanbul ignore next */
var strip = node.strip || {};
let strip = node.strip || {};
return {
inlineStandalone: true,
open: strip.open,
@@ -144,7 +149,7 @@ function isPrevWhitespace(body, i, isRoot) {
// Nodes that end with newlines are considered whitespace (but are special
// cased for strip operations)
var prev = body[i-1],
let prev = body[i - 1],
sibling = body[i - 2];
if (!prev) {
return isRoot;
@@ -159,7 +164,7 @@ function isNextWhitespace(body, i, isRoot) {
i = -1;
}
var next = body[i+1],
let next = body[i + 1],
sibling = body[i + 2];
if (!next) {
return isRoot;
@@ -178,12 +183,12 @@ function isNextWhitespace(body, i, isRoot) {
// If mulitple is truthy then all whitespace will be stripped out until non-whitespace
// content is met.
function omitRight(body, i, multiple) {
var current = body[i == null ? 0 : i + 1];
let current = body[i == null ? 0 : i + 1];
if (!current || current.type !== 'ContentStatement' || (!multiple && current.rightStripped)) {
return;
}
var original = current.value;
let original = current.value;
current.value = current.value.replace(multiple ? (/^\s+/) : (/^[ \t]*\r?\n?/), '');
current.rightStripped = current.value !== original;
}
@@ -196,13 +201,13 @@ function omitRight(body, i, multiple) {
// If mulitple is truthy then all whitespace will be stripped out until non-whitespace
// content is met.
function omitLeft(body, i, multiple) {
var current = body[i == null ? body.length - 1 : i - 1];
let current = body[i == null ? body.length - 1 : i - 1];
if (!current || current.type !== 'ContentStatement' || (!multiple && current.leftStripped)) {
return;
}
// We omit the last node if it's whitespace only and not preceeded by a non-content node.
var original = current.value;
let original = current.value;
current.value = current.value.replace(multiple ? (/\s+$/) : (/[ \t]+$/), '');
current.leftStripped = current.value !== original;
return current.leftStripped;
+6
View File
@@ -0,0 +1,6 @@
import registerInline from './decorators/inline';
export function registerDefaultDecorators(instance) {
registerInline(instance);
}
+22
View File
@@ -0,0 +1,22 @@
import {extend} from '../utils';
export default function(instance) {
instance.registerDecorator('inline', function(fn, props, container, options) {
let ret = fn;
if (!props.partials) {
props.partials = {};
ret = function(context, options) {
// Create a new partials stack frame prior to exec.
let original = container.partials;
container.partials = extend({}, original, props.partials);
let ret = fn(context, options);
container.partials = original;
return ret;
};
}
props.partials[options.args[0]] = options.fn;
return ret;
});
}
+23 -4
View File
@@ -1,8 +1,8 @@
var errorProps = ['description', 'fileName', 'lineNumber', 'message', 'name', 'number', 'stack'];
const errorProps = ['description', 'fileName', 'lineNumber', 'message', 'name', 'number', 'stack'];
function Exception(message, node) {
var loc = node && node.loc,
let loc = node && node.loc,
line,
column;
if (loc) {
@@ -12,18 +12,37 @@ function Exception(message, node) {
message += ' - ' + line + ':' + column;
}
var tmp = Error.prototype.constructor.call(this, message);
let tmp = Error.prototype.constructor.call(this, message);
// Unfortunately errors are not enumerable in Chrome (at least), so `for prop in tmp` doesn't work.
for (var idx = 0; idx < errorProps.length; idx++) {
for (let idx = 0; idx < errorProps.length; idx++) {
this[errorProps[idx]] = tmp[errorProps[idx]];
}
/* istanbul ignore else */
if (Error.captureStackTrace) {
Error.captureStackTrace(this, Exception);
}
try {
if (loc) {
this.lineNumber = line;
// Work around issue under safari where we can't directly set the column value
/* istanbul ignore next */
if (Object.defineProperty) {
Object.defineProperty(this, 'column', {
value: column,
enumerable: true
});
} else {
this.column = column;
}
}
} catch (nop) {
/* Ignore if the browser is very particular */
}
}
Exception.prototype = new Error();
+17
View File
@@ -0,0 +1,17 @@
import registerBlockHelperMissing from './helpers/block-helper-missing';
import registerEach from './helpers/each';
import registerHelperMissing from './helpers/helper-missing';
import registerIf from './helpers/if';
import registerLog from './helpers/log';
import registerLookup from './helpers/lookup';
import registerWith from './helpers/with';
export function registerDefaultHelpers(instance) {
registerBlockHelperMissing(instance);
registerEach(instance);
registerHelperMissing(instance);
registerIf(instance);
registerLog(instance);
registerLookup(instance);
registerWith(instance);
}
@@ -0,0 +1,32 @@
import {appendContextPath, createFrame, isArray} from '../utils';
export default function(instance) {
instance.registerHelper('blockHelperMissing', function(context, options) {
let inverse = options.inverse,
fn = options.fn;
if (context === true) {
return fn(this);
} else if (context === false || context == null) {
return inverse(this);
} else if (isArray(context)) {
if (context.length > 0) {
if (options.ids) {
options.ids = [options.name];
}
return instance.helpers.each(context, options);
} else {
return inverse(this);
}
} else {
if (options.data && options.ids) {
let data = createFrame(options.data);
data.contextPath = appendContextPath(options.data.contextPath, options.name);
options = {data: data};
}
return fn(context, options);
}
});
}
+79
View File
@@ -0,0 +1,79 @@
import {appendContextPath, blockParams, createFrame, isArray, isFunction} from '../utils';
import Exception from '../exception';
export default function(instance) {
instance.registerHelper('each', function(context, options) {
if (!options) {
throw new Exception('Must pass iterator to #each');
}
let fn = options.fn,
inverse = options.inverse,
i = 0,
ret = '',
data,
contextPath;
if (options.data && options.ids) {
contextPath = appendContextPath(options.data.contextPath, options.ids[0]) + '.';
}
if (isFunction(context)) { context = context.call(this); }
if (options.data) {
data = createFrame(options.data);
}
function execIteration(field, index, last) {
if (data) {
data.key = field;
data.index = index;
data.first = index === 0;
data.last = !!last;
if (contextPath) {
data.contextPath = contextPath + field;
}
}
ret = ret + fn(context[field], {
data: data,
blockParams: blockParams([context[field], field], [contextPath + field, null])
});
}
if (context && typeof context === 'object') {
if (isArray(context)) {
for (let j = context.length; i < j; i++) {
if (i in context) {
execIteration(i, i, i === context.length - 1);
}
}
} else {
let priorKey;
for (let key in context) {
if (context.hasOwnProperty(key)) {
// We're running the iterations one step out of sync so we can detect
// the last iteration without have to scan the object twice and create
// an itermediate keys array.
if (priorKey !== undefined) {
execIteration(priorKey, i - 1);
}
priorKey = key;
i++;
}
}
if (priorKey !== undefined) {
execIteration(priorKey, i - 1, true);
}
}
}
if (i === 0) {
ret = inverse(this);
}
return ret;
});
}
+13
View File
@@ -0,0 +1,13 @@
import Exception from '../exception';
export default function(instance) {
instance.registerHelper('helperMissing', function(/* [args, ]options */) {
if (arguments.length === 1) {
// A missing field in a {{foo}} construct.
return undefined;
} else {
// Someone is actually trying to call something, blow up.
throw new Exception('Missing helper: "' + arguments[arguments.length - 1].name + '"');
}
});
}
+20
View File
@@ -0,0 +1,20 @@
import {isEmpty, isFunction} from '../utils';
export default function(instance) {
instance.registerHelper('if', function(conditional, options) {
if (isFunction(conditional)) { conditional = conditional.call(this); }
// Default behavior is to render the positive path if the value is truthy and not empty.
// The `includeZero` option may be set to treat the condtional as purely not empty based on the
// behavior of isEmpty. Effectively this determines if 0 is handled by the positive path or negative.
if ((!options.hash.includeZero && !conditional) || isEmpty(conditional)) {
return options.inverse(this);
} else {
return options.fn(this);
}
});
instance.registerHelper('unless', function(conditional, options) {
return instance.helpers['if'].call(this, conditional, {fn: options.inverse, inverse: options.fn, hash: options.hash});
});
}
+19
View File
@@ -0,0 +1,19 @@
export default function(instance) {
instance.registerHelper('log', function(/* message, options */) {
let args = [undefined],
options = arguments[arguments.length - 1];
for (let i = 0; i < arguments.length - 1; i++) {
args.push(arguments[i]);
}
let level = 1;
if (options.hash.level != null) {
level = options.hash.level;
} else if (options.data && options.data.level != null) {
level = options.data.level;
}
args[0] = level;
instance.log(... args);
});
}
+5
View File
@@ -0,0 +1,5 @@
export default function(instance) {
instance.registerHelper('lookup', function(obj, field) {
return obj && obj[field];
});
}
+24
View File
@@ -0,0 +1,24 @@
import {appendContextPath, blockParams, createFrame, isEmpty, isFunction} from '../utils';
export default function(instance) {
instance.registerHelper('with', function(context, options) {
if (isFunction(context)) { context = context.call(this); }
let fn = options.fn;
if (!isEmpty(context)) {
let data = options.data;
if (options.data && options.ids) {
data = createFrame(options.data);
data.contextPath = appendContextPath(options.data.contextPath, options.ids[0]);
}
return fn(context, {
data: data,
blockParams: blockParams([context], [data && data.contextPath])
});
} else {
return options.inverse(this);
}
});
}
+35
View File
@@ -0,0 +1,35 @@
import {indexOf} from './utils';
let logger = {
methodMap: ['debug', 'info', 'warn', 'error'],
level: 'info',
// Maps a given level value to the `methodMap` indexes above.
lookupLevel: function(level) {
if (typeof level === 'string') {
let levelMap = indexOf(logger.methodMap, level.toLowerCase());
if (levelMap >= 0) {
level = levelMap;
} else {
level = parseInt(level, 10);
}
}
return level;
},
// Can be overridden in the host environment
log: function(level, ...message) {
level = logger.lookupLevel(level);
if (typeof console !== 'undefined' && logger.lookupLevel(logger.level) <= level) {
let method = logger.methodMap[level];
if (!console[method]) { // eslint-disable-line no-console
method = 'log';
}
console[method](...message); // eslint-disable-line no-console
}
}
};
export default logger;
+13
View File
@@ -0,0 +1,13 @@
/* global window */
export default function(Handlebars) {
/* istanbul ignore next */
let root = typeof global !== 'undefined' ? global : window,
$Handlebars = root.Handlebars;
/* istanbul ignore next */
Handlebars.noConflict = function() {
if (root.Handlebars === Handlebars) {
root.Handlebars = $Handlebars;
}
return Handlebars;
};
}
+109 -48
View File
@@ -1,47 +1,50 @@
module Utils from "./utils";
import Exception from "./exception";
import { COMPILER_REVISION, REVISION_CHANGES, createFrame } from "./base";
import * as Utils from './utils';
import Exception from './exception';
import { COMPILER_REVISION, REVISION_CHANGES, createFrame } from './base';
export function checkRevision(compilerInfo) {
var compilerRevision = compilerInfo && compilerInfo[0] || 1,
const compilerRevision = compilerInfo && compilerInfo[0] || 1,
currentRevision = COMPILER_REVISION;
if (compilerRevision !== currentRevision) {
if (compilerRevision < currentRevision) {
var runtimeVersions = REVISION_CHANGES[currentRevision],
const runtimeVersions = REVISION_CHANGES[currentRevision],
compilerVersions = REVISION_CHANGES[compilerRevision];
throw new Exception("Template was precompiled with an older version of Handlebars than the current runtime. "+
"Please update your precompiler to a newer version ("+runtimeVersions+") or downgrade your runtime to an older version ("+compilerVersions+").");
throw new Exception('Template was precompiled with an older version of Handlebars than the current runtime. ' +
'Please update your precompiler to a newer version (' + runtimeVersions + ') or downgrade your runtime to an older version (' + compilerVersions + ').');
} else {
// Use the embedded version info since the runtime doesn't know about this revision yet
throw new Exception("Template was precompiled with a newer version of Handlebars than the current runtime. "+
"Please update your runtime to a newer version ("+compilerInfo[1]+").");
throw new Exception('Template was precompiled with a newer version of Handlebars than the current runtime. ' +
'Please update your runtime to a newer version (' + compilerInfo[1] + ').');
}
}
}
// TODO: Remove this line and break up compilePartial
export function template(templateSpec, env) {
/* istanbul ignore next */
if (!env) {
throw new Exception("No environment passed to template");
throw new Exception('No environment passed to template');
}
if (!templateSpec || !templateSpec.main) {
throw new Exception('Unknown template object: ' + typeof templateSpec);
}
templateSpec.main.decorator = templateSpec.main_d;
// Note: Using env.VM references rather than local var references throughout this section to allow
// for external users to override these as psuedo-supported APIs.
env.VM.checkRevision(templateSpec.compiler);
var invokePartialWrapper = function(partial, context, options) {
function invokePartialWrapper(partial, context, options) {
if (options.hash) {
context = Utils.extend({}, context, options.hash);
if (options.ids) {
options.ids[0] = true;
}
}
partial = env.VM.resolvePartial.call(this, partial, context, options);
var result = env.VM.invokePartial.call(this, partial, context, options);
let result = env.VM.invokePartial.call(this, partial, context, options);
if (result == null && env.compile) {
options.partials[options.name] = env.compile(partial, templateSpec.compilerOptions, env);
@@ -49,8 +52,8 @@ export function template(templateSpec, env) {
}
if (result != null) {
if (options.indent) {
var lines = result.split('\n');
for (var i = 0, l = lines.length; i < l; i++) {
let lines = result.split('\n');
for (let i = 0, l = lines.length; i < l; i++) {
if (!lines[i] && i + 1 === l) {
break;
}
@@ -61,12 +64,12 @@ export function template(templateSpec, env) {
}
return result;
} else {
throw new Exception("The partial " + options.name + " could not be compiled when running in runtime-only mode");
throw new Exception('The partial ' + options.name + ' could not be compiled when running in runtime-only mode');
}
}
};
// Just add water
var container = {
let container = {
strict: function(obj, name) {
if (!(name in obj)) {
throw new Exception('"' + name + '" not defined in ' + obj);
@@ -74,8 +77,8 @@ export function template(templateSpec, env) {
return obj[name];
},
lookup: function(depths, name) {
var len = depths.length;
for (var i = 0; i < len; i++) {
const len = depths.length;
for (let i = 0; i < len; i++) {
if (depths[i] && depths[i][name] != null) {
return depths[i][name];
}
@@ -89,57 +92,66 @@ export function template(templateSpec, env) {
invokePartial: invokePartialWrapper,
fn: function(i) {
return templateSpec[i];
let ret = templateSpec[i];
ret.decorator = templateSpec[i + '_d'];
return ret;
},
programs: [],
program: function(i, data, declaredBlockParams, blockParams, depths) {
var programWrapper = this.programs[i],
let programWrapper = this.programs[i],
fn = this.fn(i);
if (data || depths || blockParams || declaredBlockParams) {
programWrapper = program(this, i, fn, data, declaredBlockParams, blockParams, depths);
programWrapper = wrapProgram(this, i, fn, data, declaredBlockParams, blockParams, depths);
} else if (!programWrapper) {
programWrapper = this.programs[i] = program(this, i, fn);
programWrapper = this.programs[i] = wrapProgram(this, i, fn);
}
return programWrapper;
},
data: function(data, depth) {
while (data && depth--) {
data = data._parent;
data: function(value, depth) {
while (value && depth--) {
value = value._parent;
}
return data;
return value;
},
merge: function(param, common) {
var ret = param || common;
let obj = param || common;
if (param && common && (param !== common)) {
ret = Utils.extend({}, common, param);
obj = Utils.extend({}, common, param);
}
return ret;
return obj;
},
noop: env.VM.noop,
compilerInfo: templateSpec.compiler
};
var ret = function(context, options) {
options = options || {};
var data = options.data;
function ret(context, options = {}) {
let data = options.data;
ret._setup(options);
if (!options.partial && templateSpec.useData) {
data = initData(context, data);
}
var depths,
let depths,
blockParams = templateSpec.useBlockParams ? [] : undefined;
if (templateSpec.useDepths) {
depths = options.depths ? [context].concat(options.depths) : [context];
if (options.depths) {
depths = context != options.depths[0] ? [context].concat(options.depths) : options.depths;
} else {
depths = [context];
}
}
return templateSpec.main.call(container, context, container.helpers, container.partials, data, blockParams, depths);
};
function main(context/*, options*/) {
return '' + templateSpec.main(container, context, container.helpers, container.partials, data, blockParams, depths);
}
main = executeDecorators(templateSpec.main, main, container, options.depths || [], data, blockParams);
return main(context, options);
}
ret.isTop = true;
ret._setup = function(options) {
@@ -149,9 +161,13 @@ export function template(templateSpec, env) {
if (templateSpec.usePartial) {
container.partials = container.merge(options.partials, env.partials);
}
if (templateSpec.usePartial || templateSpec.useDecorators) {
container.decorators = container.merge(options.decorators, env.decorators);
}
} else {
container.helpers = options.helpers;
container.partials = options.partials;
container.decorators = options.decorators;
}
};
@@ -163,22 +179,28 @@ export function template(templateSpec, env) {
throw new Exception('must pass parent depths');
}
return program(container, i, templateSpec[i], data, 0, blockParams, depths);
return wrapProgram(container, i, templateSpec[i], data, 0, blockParams, depths);
};
return ret;
}
export function program(container, i, fn, data, declaredBlockParams, blockParams, depths) {
var prog = function(context, options) {
options = options || {};
export function wrapProgram(container, i, fn, data, declaredBlockParams, blockParams, depths) {
function prog(context, options = {}) {
let currentDepths = depths;
if (depths && context != depths[0]) {
currentDepths = [context].concat(depths);
}
return fn.call(container,
return fn(container,
context,
container.helpers, container.partials,
options.data || data,
blockParams && [options.blockParams].concat(blockParams),
depths && [context].concat(depths));
};
currentDepths);
}
prog = executeDecorators(fn, prog, container, depths, data, blockParams);
prog.program = i;
prog.depth = depths ? depths.length : 0;
prog.blockParams = declaredBlockParams || 0;
@@ -187,7 +209,11 @@ export function program(container, i, fn, data, declaredBlockParams, blockParams
export function resolvePartial(partial, context, options) {
if (!partial) {
if (options.name === '@partial-block') {
partial = options.data['partial-block'];
} else {
partial = options.partials[options.name];
}
} else if (!partial.call && !options.name) {
// This is a dynamic partial that returned a string
options.name = partial;
@@ -197,16 +223,42 @@ export function resolvePartial(partial, context, options) {
}
export function invokePartial(partial, context, options) {
// Use the current closure context to save the partial-block if this partial
const currentPartialBlock = options.data && options.data['partial-block'];
options.partial = true;
if (options.ids) {
options.data.contextPath = options.ids[0] || options.data.contextPath;
}
let partialBlock;
if (options.fn && options.fn !== noop) {
options.data = createFrame(options.data);
// Wrapper function to get access to currentPartialBlock from the closure
let fn = options.fn;
partialBlock = options.data['partial-block'] = function partialBlockWrapper(context, options) {
// Restore the partial-block from the closure for the execution of the block
// i.e. the part inside the block of the partial call.
options.data = createFrame(options.data);
options.data['partial-block'] = currentPartialBlock;
return fn(context, options);
};
if (fn.partials) {
options.partials = Utils.extend({}, options.partials, fn.partials);
}
}
if (partial === undefined && partialBlock) {
partial = partialBlock;
}
if (partial === undefined) {
throw new Exception("The partial " + options.name + " could not be found");
throw new Exception('The partial ' + options.name + ' could not be found');
} else if (partial instanceof Function) {
return partial(context, options);
}
}
export function noop() { return ""; }
export function noop() { return ''; }
function initData(context, data) {
if (!data || !('root' in data)) {
@@ -215,3 +267,12 @@ function initData(context, data) {
}
return data;
}
function executeDecorators(fn, prog, container, depths, data, blockParams) {
if (fn.decorator) {
let props = {};
prog = fn.decorator(prog, props, container, depths && depths[0], data, blockParams, depths);
Utils.extend(prog, props);
}
return prog;
}
+1 -1
View File
@@ -4,7 +4,7 @@ function SafeString(string) {
}
SafeString.prototype.toString = SafeString.prototype.toHTML = function() {
return "" + this.string;
return '' + this.string;
};
export default SafeString;
+29 -19
View File
@@ -1,23 +1,23 @@
/*jshint -W004 */
var escape = {
"&": "&amp;",
"<": "&lt;",
">": "&gt;",
'"': "&quot;",
"'": "&#x27;",
"`": "&#x60;"
const escape = {
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
"'": '&#x27;',
'`': '&#x60;',
'=': '&#x3D;'
};
var badChars = /[&<>"'`]/g;
var possible = /[&<>"'`]/;
const badChars = /[&<>"'`=]/g,
possible = /[&<>"'`=]/;
function escapeChar(chr) {
return escape[chr];
}
export function extend(obj/* , ...source */) {
for (var i = 1; i < arguments.length; i++) {
for (var key in arguments[i]) {
for (let i = 1; i < arguments.length; i++) {
for (let key in arguments[i]) {
if (Object.prototype.hasOwnProperty.call(arguments[i], key)) {
obj[key] = arguments[i][key];
}
@@ -27,11 +27,12 @@ export function extend(obj /* , ...source */) {
return obj;
}
export var toString = Object.prototype.toString;
export let toString = Object.prototype.toString;
// Sourced from lodash
// https://github.com/bestiejs/lodash/blob/master/LICENSE.txt
var isFunction = function(value) {
/* eslint-disable func-style */
let isFunction = function(value) {
return typeof value === 'function';
};
// fallback for older versions of Chrome and Safari
@@ -41,16 +42,17 @@ if (isFunction(/x/)) {
return typeof value === 'function' && toString.call(value) === '[object Function]';
};
}
export var isFunction;
export {isFunction};
/* eslint-enable func-style */
/* istanbul ignore next */
export var isArray = Array.isArray || function(value) {
export const isArray = Array.isArray || function(value) {
return (value && typeof value === 'object') ? toString.call(value) === '[object Array]' : false;
};
// Older IE versions do not directly support indexOf so we must implement our own, sadly.
export function indexOf(array, value) {
for (var i = 0, len = array.length; i < len; i++) {
for (let i = 0, len = array.length; i < len; i++) {
if (array[i] === value) {
return i;
}
@@ -60,11 +62,12 @@ export function indexOf(array, value) {
export function escapeExpression(string) {
if (typeof string !== 'string') {
// don't escape SafeStrings, since they're already safe
if (string && string.toHTML) {
return string.toHTML();
} else if (string == null) {
return "";
return '';
} else if (!string) {
return string + '';
}
@@ -72,7 +75,8 @@ export function escapeExpression(string) {
// Force a string conversion as this will be done by the append regardless and
// the regex test will do this transparently behind the scenes, causing issues if
// an object's to string has escaped characters in it.
string = "" + string;
string = '' + string;
}
if (!possible.test(string)) { return string; }
return string.replace(badChars, escapeChar);
@@ -88,6 +92,12 @@ export function isEmpty(value) {
}
}
export function createFrame(object) {
let frame = extend({}, object);
frame._parent = object;
return frame;
}
export function blockParams(params, ids) {
params.path = ids;
return params;
+9 -10
View File
@@ -1,11 +1,10 @@
// USAGE:
// var handlebars = require('handlebars');
/* eslint-disable no-var */
// var local = handlebars.create();
var handlebars = require('../dist/cjs/handlebars')["default"];
handlebars.Visitor = require('../dist/cjs/handlebars/compiler/visitor')["default"];
var handlebars = require('../dist/cjs/handlebars')['default'];
var printer = require('../dist/cjs/handlebars/compiler/printer');
handlebars.PrintVisitor = printer.PrintVisitor;
@@ -14,13 +13,13 @@ handlebars.print = printer.print;
module.exports = handlebars;
// Publish a Node.js require() handler for .handlebars and .hbs files
function extension(module, filename) {
var fs = require('fs');
var templateString = fs.readFileSync(filename, 'utf8');
module.exports = handlebars.compile(templateString);
}
/* istanbul ignore else */
if (typeof require !== 'undefined' && require.extensions) {
var extension = function(module, filename) {
var fs = require("fs");
var templateString = fs.readFileSync(filename, "utf8");
module.exports = handlebars.compile(templateString);
};
require.extensions[".handlebars"] = extension;
require.extensions[".hbs"] = extension;
require.extensions['.handlebars'] = extension;
require.extensions['.hbs'] = extension;
}
+167 -71
View File
@@ -1,11 +1,135 @@
/* eslint-disable no-console */
import Async from 'async';
import fs from 'fs';
import * as Handlebars from './handlebars';
import {basename} from 'path';
import {SourceMapConsumer, SourceNode} from 'source-map';
import uglify from 'uglify-js';
var fs = require('fs'),
Handlebars = require('./index'),
basename = require('path').basename,
SourceMap = require('source-map'),
SourceMapConsumer = SourceMap.SourceMapConsumer,
SourceNode = SourceMap.SourceNode,
uglify = require('uglify-js');
module.exports.loadTemplates = function(opts, callback) {
loadStrings(opts, function(err, strings) {
if (err) {
callback(err);
} else {
loadFiles(opts, function(err, files) {
if (err) {
callback(err);
} else {
opts.templates = strings.concat(files);
callback(undefined, opts);
}
});
}
});
};
function loadStrings(opts, callback) {
let strings = arrayCast(opts.string),
names = arrayCast(opts.name);
if (names.length !== strings.length
&& strings.length > 1) {
return callback(new Handlebars.Exception('Number of names did not match the number of string inputs'));
}
Async.map(strings, function(string, callback) {
if (string !== '-') {
callback(undefined, string);
} else {
// Load from stdin
let buffer = '';
process.stdin.setEncoding('utf8');
process.stdin.on('data', function(chunk) {
buffer += chunk;
});
process.stdin.on('end', function() {
callback(undefined, buffer);
});
}
},
function(err, strings) {
strings = strings.map((string, index) => ({
name: names[index],
path: names[index],
source: string
}));
callback(err, strings);
});
}
function loadFiles(opts, callback) {
// Build file extension pattern
let extension = (opts.extension || 'handlebars').replace(/[\\^$*+?.():=!|{}\-\[\]]/g, function(arg) { return '\\' + arg; });
extension = new RegExp('\\.' + extension + '$');
let ret = [],
queue = (opts.files || []).map((template) => ({template, root: opts.root}));
Async.whilst(() => queue.length, function(callback) {
let {template: path, root} = queue.shift();
fs.stat(path, function(err, stat) {
if (err) {
return callback(new Handlebars.Exception(`Unable to open template file "${path}"`));
}
if (stat.isDirectory()) {
opts.hasDirectory = true;
fs.readdir(path, function(err, children) {
/* istanbul ignore next : Race condition that being too lazy to test */
if (err) {
return callback(err);
}
children.forEach(function(file) {
let childPath = path + '/' + file;
if (extension.test(childPath) || fs.statSync(childPath).isDirectory()) {
queue.push({template: childPath, root: root || path});
}
});
callback();
});
} else {
fs.readFile(path, 'utf8', function(err, data) {
/* istanbul ignore next : Race condition that being too lazy to test */
if (err) {
return callback(err);
}
if (opts.bom && data.indexOf('\uFEFF') === 0) {
data = data.substring(1);
}
// Clean the template name
let name = path;
if (!root) {
name = basename(name);
} else if (name.indexOf(root) === 0) {
name = name.substring(root.length + 1);
}
name = name.replace(extension, '');
ret.push({
path: path,
name: name,
source: data
});
callback();
});
}
});
},
function(err) {
if (err) {
callback(err);
} else {
callback(undefined, ret);
}
});
}
module.exports.cli = function(opts) {
if (opts.version) {
@@ -13,41 +137,39 @@ module.exports.cli = function(opts) {
return;
}
if (!opts.templates.length) {
if (!opts.templates.length && !opts.hasDirectory) {
throw new Handlebars.Exception('Must define at least one template or directory.');
}
opts.templates.forEach(function(template) {
try {
fs.statSync(template);
} catch (err) {
throw new Handlebars.Exception('Unable to open template file "' + template + '"');
}
});
if (opts.simple && opts.min) {
throw new Handlebars.Exception('Unable to minimize simple output');
}
if (opts.simple && (opts.templates.length !== 1 || fs.statSync(opts.templates[0]).isDirectory())) {
const multiple = opts.templates.length !== 1 || opts.hasDirectory;
if (opts.simple && multiple) {
throw new Handlebars.Exception('Unable to output multiple templates in simple mode');
}
// Force simple mode if we have only one template and it's unnamed.
if (!opts.amd && !opts.commonjs && opts.templates.length === 1
&& !opts.templates[0].name) {
opts.simple = true;
}
// Convert the known list into a hash
var known = {};
let known = {};
if (opts.known && !Array.isArray(opts.known)) {
opts.known = [opts.known];
}
if (opts.known) {
for (var i = 0, len = opts.known.length; i < len; i++) {
for (let i = 0, len = opts.known.length; i < len; i++) {
known[opts.known[i]] = true;
}
}
// Build file extension pattern
var extension = opts.extension.replace(/[\\^$*+?.():=!|{}\-\[\]]/g, function(arg) { return '\\' + arg; });
extension = new RegExp('\\.' + extension + '$');
const objectName = opts.partial ? 'Handlebars.partials' : 'templates';
var output = new SourceNode();
let output = new SourceNode();
if (!opts.simple) {
if (opts.amd) {
output.add('define([\'' + opts.handlebarPath + 'handlebars.runtime\'], function(Handlebars) {\n Handlebars = Handlebars["default"];');
@@ -65,81 +187,47 @@ module.exports.cli = function(opts) {
}
output.add('{};\n');
}
function processTemplate(template, root) {
var path = template,
stat = fs.statSync(path);
if (stat.isDirectory()) {
fs.readdirSync(template).map(function(file) {
var path = template + '/' + file;
if (extension.test(path) || fs.statSync(path).isDirectory()) {
processTemplate(path, root || template);
}
});
} else {
var data = fs.readFileSync(path, 'utf8');
if (opts.bom && data.indexOf('\uFEFF') === 0) {
data = data.substring(1);
}
var options = {
opts.templates.forEach(function(template) {
let options = {
knownHelpers: known,
knownHelpersOnly: opts.o
};
if (opts.map) {
options.srcName = path;
options.srcName = template.path;
}
if (opts.data) {
options.data = true;
}
// Clean the template name
if (!root) {
template = basename(template);
} else if (template.indexOf(root) === 0) {
template = template.substring(root.length+1);
}
template = template.replace(extension, '');
var precompiled = Handlebars.precompile(data, options);
let precompiled = Handlebars.precompile(template.source, options);
// If we are generating a source map, we have to reconstruct the SourceNode object
if (opts.map) {
var consumer = new SourceMapConsumer(precompiled.map);
let consumer = new SourceMapConsumer(precompiled.map);
precompiled = SourceNode.fromStringWithSourceMap(precompiled.code, consumer);
}
if (opts.simple) {
output.add([precompiled, '\n']);
} else if (opts.partial) {
if(opts.amd && (opts.templates.length == 1 && !fs.statSync(opts.templates[0]).isDirectory())) {
output.add('return ');
}
output.add(['Handlebars.partials[\'', template, '\'] = template(', precompiled, ');\n']);
} else {
if(opts.amd && (opts.templates.length == 1 && !fs.statSync(opts.templates[0]).isDirectory())) {
output.add('return ');
}
output.add(['templates[\'', template, '\'] = template(', precompiled, ');\n']);
}
}
if (!template.name) {
throw new Handlebars.Exception('Name missing for template');
}
opts.templates.forEach(function(template) {
processTemplate(template, opts.root);
if (opts.amd && !multiple) {
output.add('return ');
}
output.add([objectName, '[\'', template.name, '\'] = template(', precompiled, ');\n']);
}
});
// Output the content
if (!opts.simple) {
if (opts.amd) {
if(opts.templates.length > 1 || (opts.templates.length == 1 && fs.statSync(opts.templates[0]).isDirectory())) {
if(opts.partial){
output.add('return Handlebars.partials;\n');
} else {
output.add('return templates;\n');
}
if (multiple) {
output.add(['return ', objectName, ';\n']);
}
output.add('});');
} else if (!opts.commonjs) {
@@ -178,3 +266,11 @@ module.exports.cli = function(opts) {
console.log(output);
}
};
function arrayCast(value) {
value = value != null ? value : [];
if (!Array.isArray(value)) {
value = [value];
}
return value;
}
+16 -11
View File
@@ -1,7 +1,7 @@
{
"name": "handlebars",
"barename": "handlebars",
"version": "3.0.0",
"version": "4.0.6",
"description": "Handlebars provides the power necessary to let you build semantic templates effectively with no frustration",
"homepage": "http://www.handlebarsjs.com/",
"keywords": [
@@ -21,37 +21,42 @@
"node": ">=0.4.7"
},
"dependencies": {
"async": "^1.4.0",
"optimist": "^0.6.1",
"source-map": "^0.1.40"
"source-map": "^0.4.4"
},
"optionalDependencies": {
"uglify-js": "~2.3"
"uglify-js": "^2.6"
},
"devDependencies": {
"async": "^0.9.0",
"aws-sdk": "~1.5.0",
"aws-sdk": "^2.1.49",
"babel-loader": "^5.0.0",
"babel-runtime": "^5.1.10",
"benchmark": "~1.0",
"dustjs-linkedin": "^2.0.2",
"eco": "~1.1.0-rc-3",
"es6-module-packager": "^2.0.0",
"grunt": "~0.4.1",
"grunt-babel": "^5.0.0",
"grunt-cli": "~0.1.10",
"grunt-contrib-clean": "0.x",
"grunt-contrib-concat": "0.x",
"grunt-contrib-connect": "0.x",
"grunt-contrib-copy": "0.x",
"grunt-contrib-jshint": "0.x",
"grunt-contrib-requirejs": "0.x",
"grunt-contrib-uglify": "0.x",
"grunt-contrib-watch": "0.x",
"grunt-eslint": "^17.1.0",
"grunt-saucelabs": "8.x",
"grunt-webpack": "^1.0.8",
"istanbul": "^0.3.0",
"jison": "~0.3.0",
"keen.io": "0.0.3",
"mocha": "~1.20.0",
"mustache": "0.x",
"semver": "^4.0.0",
"underscore": "^1.5.1"
"mock-stdin": "^0.3.0",
"mustache": "^2.1.3",
"semver": "^5.0.1",
"underscore": "^1.5.1",
"webpack": "^1.12.6",
"webpack-dev-server": "^1.12.1"
},
"main": "lib/index.js",
"bin": {
Executable
+95
View File
@@ -0,0 +1,95 @@
#! /usr/bin/env node
/* eslint-disable no-console, no-var */
// Util script for debugging source code generation issues
var script = process.argv[2].replace(/\\n/g, '\n'),
verbose = process.argv[3] === '-v';
var Handlebars = require('./lib'),
SourceMap = require('source-map'),
SourceMapConsumer = SourceMap.SourceMapConsumer;
var template = Handlebars.precompile(script, {
srcName: 'input.hbs',
destName: 'output.js',
assumeObjects: true,
compat: false,
strict: true,
trackIds: true,
knownHelpersOnly: false
});
if (!verbose) {
console.log(template);
} else {
var consumer = new SourceMapConsumer(template.map),
lines = template.code.split('\n'),
srcLines = script.split('\n');
console.log();
console.log('Source:');
srcLines.forEach(function(source, index) {
console.log(index + 1, source);
});
console.log();
console.log('Generated:');
console.log(template.code);
lines.forEach(function(source, index) {
console.log(index + 1, source);
});
console.log();
console.log('Map:');
console.log(template.map);
console.log();
function collectSource(lines, lineName, colName, order) {
var ret = {},
ordered = [],
last;
function collect(current) {
if (last) {
var mapLines = lines.slice(last[lineName] - 1, current && current[lineName]);
if (mapLines.length) {
if (current) {
mapLines[mapLines.length - 1] = mapLines[mapLines.length - 1].slice(0, current[colName]);
}
mapLines[0] = mapLines[0].slice(last[colName]);
}
ret[last[lineName] + ':' + last[colName]] = mapLines.join('\n');
ordered.push({
startLine: last[lineName],
startCol: last[colName],
endLine: current && current[lineName]
});
}
last = current;
}
consumer.eachMapping(collect, undefined, order);
collect();
return ret;
}
srcLines = collectSource(srcLines, 'originalLine', 'originalColumn', SourceMapConsumer.ORIGINAL_ORDER);
lines = collectSource(lines, 'generatedLine', 'generatedColumn');
consumer.eachMapping(function(mapping) {
var originalSrc = srcLines[mapping.originalLine + ':' + mapping.originalColumn],
generatedSrc = lines[mapping.generatedLine + ':' + mapping.generatedColumn];
if (!mapping.originalLine) {
console.log('generated', mapping.generatedLine + ':' + mapping.generatedColumn, generatedSrc);
} else {
console.log('map',
mapping.source,
mapping.originalLine + ':' + mapping.originalColumn,
originalSrc,
'->',
mapping.generatedLine + ':' + mapping.generatedColumn,
generatedSrc);
}
});
}
+132 -2
View File
@@ -2,7 +2,137 @@
## Development
[Commits](https://github.com/wycats/handlebars.js/compare/v3.0.0...master)
[Commits](https://github.com/lawnsea/handlebars.js/compare/v4.0.6...master)
## v4.0.6 - November 12th, 2016
- [#1243](https://github.com/wycats/handlebars.js/pull/1243) - Walk up data frames for nested @partial-block ([@lawnsea](https://github.com/lawnsea))
- [#1210](https://github.com/wycats/handlebars.js/pull/1210) - Add a new lightweight package based on handlebars in the README ([@kabirbaidhya](https://github.com/kabirbaidhya))
- [#1187](https://github.com/wycats/handlebars.js/pull/1187) - Ensure that existing blockParams and depths are respected on dupe programs ([@charleso](https://github.com/charleso))
- [#1191](https://github.com/wycats/handlebars.js/pull/1191) - Added cory ([@leo](https://github.com/leo))
- [#1177](https://github.com/wycats/handlebars.js/pull/1177) - Preserve License info in Closure Compiler ([@gennadiylitvinyuk](https://github.com/gennadiylitvinyuk))
- [#1171](https://github.com/wycats/handlebars.js/pull/1171) - Contributing doc fix: failing thats -> failing tests ([@paulfalgout](https://github.com/paulfalgout))
- [#1166](https://github.com/wycats/handlebars.js/pull/1166) - Update license date ([@timwangdev](https://github.com/timwangdev))
- Update jsfiddle to point to latest - 959ee55 (originally dfc7554 by [@kpdecker](https://github.com/kpdecker))
- [#1163](https://github.com/wycats/handlebars.js/pull/1163) - Fix typos on decorators-api.md. ([@adjohnson916](https://github.com/adjohnson916))
- Drop extra Error params - 8c19874 (originally 63fdb92 by [@kpdecker](https://github.com/kpdecker))
- [#1153](https://github.com/wycats/handlebars.js/pull/1153) - Add documentation for running tests to contributing.md ([@ryanmurakami](https://github.com/ryanmurakami))
- Avoid error in older browsers in test - 400916c (originally a6121ca by [@kpdecker](https://github.com/kpdecker))
- Update target browser test versions - fee2334 (originally 871c32a by [@kpdecker](https://github.com/kpdecker))
- Exclude coverage check in exception conditional - 32d6363 (originally 326734b by [@kpdecker](https://github.com/kpdecker))
- Fix throw when creating exception object in Safari - 20c965c (originally 2ea6119 by [@kpdecker](https://github.com/kpdecker))
- Update build for modern node versions - 6c9f98c (originally 8289c0b by [@kpdecker](https://github.com/kpdecker))
- [#1135](https://github.com/wycats/handlebars.js/issues/1135) - Relax depth check for context push - c393c81 (originally 25458fd by [@kpdecker](https://github.com/kpdecker))
[Commits](https://github.com/wycats/handlebars.js/compare/v4.0.5...v4.0.6)
## v4.0.5 - November 19th, 2015
- [#1132](https://github.com/wycats/handlebars.js/pull/1132) - Update uglify-js to avoid vulnerability ([@plynchnlm](https://api.github.com/users/plynchnlm))
- [#1129](https://github.com/wycats/handlebars.js/issues/1129) - Minified lib returns an empty string ([@bricss](https://api.github.com/users/bricss))
- Return current handlebars instance from noConflict - 685cf92
- Add webpack to dev dependency to support npm 3 - 7a6c228
- Further relax uglify dependency - 0a3b3c2
- Include tests for minimized artifacts - c21118d
- Fix lint errors under latest eslint - 9f59de9
- Add print-script helper script - 98a6717
[Commits](https://github.com/wycats/handlebars.js/compare/v4.0.4...v4.0.5)
## v4.0.4 - October 29th, 2015
- [#1121](https://github.com/wycats/handlebars.js/pull/1121) - Include partial name in 'undefined partial' exception message ([@shinypb](https://api.github.com/users/shinypb))
- [#1125](https://github.com/wycats/handlebars.js/pull/1125) - Add promised-handlebars to "in-the-wild"-list ([@nknapp](https://api.github.com/users/nknapp))
[Commits](https://github.com/wycats/handlebars.js/compare/v4.0.3...v4.0.4)
## v4.0.3 - September 23rd, 2015
- [#1099](https://github.com/wycats/handlebars.js/issues/1099) - @partial-block is overridden ([@btmorex](https://api.github.com/users/btmorex))
- [#1093](https://github.com/wycats/handlebars.js/issues/1093) - #each skips iteration on undefined values ([@florianpilz](https://api.github.com/users/florianpilz))
- [#1092](https://github.com/wycats/handlebars.js/issues/1092) - Square braces in key name ([@distantnative](https://api.github.com/users/distantnative))
- [#1091](https://github.com/wycats/handlebars.js/pull/1091) - fix typo in release notes ([@nikolas](https://api.github.com/users/nikolas))
- [#1090](https://github.com/wycats/handlebars.js/pull/1090) - grammar fixes in 4.0.0 release notes ([@nikolas](https://api.github.com/users/nikolas))
Compatibility notes:
- `each` iteration with `undefined` values has been restored to the 3.0 behaviors. Helper calls with undefined context values will now execute against an arbitrary empty object to avoid executing against global object in non-strict mode.
- `]` can now be included in `[]` wrapped identifiers by escaping with `\`. Any `[]` identifiers that include `\` will now have to properly escape these values.
[Commits](https://github.com/wycats/handlebars.js/compare/v4.0.2...v4.0.3)
## v4.0.2 - September 4th, 2015
- [#1089](https://github.com/wycats/handlebars.js/issues/1089) - "Failover content" not working in multiple levels of inline partials ([@michaellopez](https://api.github.com/users/michaellopez))
[Commits](https://github.com/wycats/handlebars.js/compare/v4.0.1...v4.0.2)
## v4.0.1 - September 2nd, 2015
- Fix failure when using decorators in partials - 05b82a2
[Commits](https://github.com/wycats/handlebars.js/compare/v4.0.0...v4.0.1)
## v4.0.0 - September 1st, 2015
- [#1082](https://github.com/wycats/handlebars.js/pull/1082) - Decorators and Inline Partials ([@kpdecker](https://api.github.com/users/kpdecker))
- [#1076](https://github.com/wycats/handlebars.js/pull/1076) - Implement partial blocks ([@kpdecker](https://api.github.com/users/kpdecker))
- [#1087](https://github.com/wycats/handlebars.js/pull/1087) - Fix #each when last object entry has empty key ([@denniskuczynski](https://api.github.com/users/denniskuczynski))
- [#1084](https://github.com/wycats/handlebars.js/pull/1084) - Bump uglify version to fix vulnerability ([@John-Steidley](https://api.github.com/users/John-Steidley))
- [#1068](https://github.com/wycats/handlebars.js/pull/1068) - Fix typo ([@0xack13](https://api.github.com/users/0xack13))
- [#1060](https://github.com/wycats/handlebars.js/pull/1060) - #1056 Fixed grammar for nested raw blocks ([@ericbn](https://api.github.com/users/ericbn))
- [#1052](https://github.com/wycats/handlebars.js/pull/1052) - Updated year in License ([@maqnouch](https://api.github.com/users/maqnouch))
- [#1037](https://github.com/wycats/handlebars.js/pull/1037) - Fix minor typos in README ([@tomxtobin](https://api.github.com/users/tomxtobin))
- [#1032](https://github.com/wycats/handlebars.js/issues/1032) - Is it possible to render a partial without the parent scope? ([@aputinski](https://api.github.com/users/aputinski))
- [#1019](https://github.com/wycats/handlebars.js/pull/1019) - Fixes typo in tests ([@aymerick](https://api.github.com/users/aymerick))
- [#1016](https://github.com/wycats/handlebars.js/issues/1016) - Version mis-match ([@mayankdedhia](https://api.github.com/users/mayankdedhia))
- [#1023](https://github.com/wycats/handlebars.js/issues/1023) - is it possible for nested custom helpers to communicate between each other?
- [#893](https://github.com/wycats/handlebars.js/issues/893) - [Proposal] Section blocks.
- [#792](https://github.com/wycats/handlebars.js/issues/792) - feature request: inline partial definitions
- [#583](https://github.com/wycats/handlebars.js/issues/583) - Parent path continues to drill down depth with multiple conditionals
- [#404](https://github.com/wycats/handlebars.js/issues/404) - Add named child helpers that can be referenced by block helpers
- Escape = in HTML content - [83b8e84](https://github.com/wycats/handlebars.js/commit/83b8e84)
- Drop AST constructors in favor of JSON - [95d84ba](https://github.com/wycats/handlebars.js/commit/95d84ba)
- Pass container rather than exec as context - [9a2d1d6](https://github.com/wycats/handlebars.js/commit/9a2d1d6)
- Add ignoreStandalone compiler option - [ea3a5a1](https://github.com/wycats/handlebars.js/commit/ea3a5a1)
- Ignore empty when iterating on sparse arrays - [06d515a](https://github.com/wycats/handlebars.js/commit/06d515a)
- Add support for string and stdin precompilation - [0de8dac](https://github.com/wycats/handlebars.js/commit/0de8dac)
- Simplify object assignment generation logic - [77e6bfc](https://github.com/wycats/handlebars.js/commit/77e6bfc)
- Bulletproof AST.helpers.helperExpression - [93b0760](https://github.com/wycats/handlebars.js/commit/93b0760)
- Always return string responses - [8e868ab](https://github.com/wycats/handlebars.js/commit/8e868ab)
- Pass undefined fields to helpers in strict mode - [5d4b8da](https://github.com/wycats/handlebars.js/commit/5d4b8da)
- Avoid depth creation when context remains the same - [279e038](https://github.com/wycats/handlebars.js/commit/279e038)
- Improve logging API - [9a49d35](https://github.com/wycats/handlebars.js/commit/9a49d35)
- Fix with operator in no @data mode - [231a8d7](https://github.com/wycats/handlebars.js/commit/231a8d7)
- Allow empty key name in each iteration - [1bb640b](https://github.com/wycats/handlebars.js/commit/1bb640b)
- Add with block parameter support - [2a85106](https://github.com/wycats/handlebars.js/commit/2a85106)
- Fix escaping of non-javascript identifiers - [410141c](https://github.com/wycats/handlebars.js/commit/410141c)
- Fix location information for programs - [93faffa](https://github.com/wycats/handlebars.js/commit/93faffa)
Compatibility notes:
- Depthed paths are now conditionally pushed on to the stack. If the helper uses the same context, then a new stack is not created. This leads to behavior that better matches expectations for helpers like `if` that do not seem to alter the context. Any instances of `../` in templates will need to be checked for the correct behavior under 4.0.0. In general templates will either reduce the number of `../` instances or leave them as is. See [#1028](https://github.com/wycats/handlebars.js/issues/1028).
- The `=` character is now HTML escaped. This closes a potential exploit case when using unquoted attributes, i.e. `<div foo={{bar}}>`. In general it's recommended that attributes always be quoted when their values are generated from a mustache to avoid any potential exploit surfaces.
- AST constructors have been dropped in favor of plain old javascript objects
- The runtime version has been increased. Precompiled templates will need to use runtime of at least 4.0.0.
[Commits](https://github.com/wycats/handlebars.js/compare/v3.0.3...v4.0.0)
## v3.0.3 - April 28th, 2015
- [#1004](https://github.com/wycats/handlebars.js/issues/1004) - Latest version breaks with RequireJS (global is undefined) ([@boskee](https://api.github.com/users/boskee))
[Commits](https://github.com/wycats/handlebars.js/compare/v3.0.2...v3.0.3)
## v3.0.2 - April 20th, 2015
- [#998](https://github.com/wycats/handlebars.js/pull/998) - Add full support for es6 ([@kpdecker](https://api.github.com/users/kpdecker))
- [#994](https://github.com/wycats/handlebars.js/issues/994) - Access Handlebars.Visitor in browser ([@tamlyn](https://api.github.com/users/tamlyn))
- [#990](https://github.com/wycats/handlebars.js/issues/990) - Allow passing null/undefined literals subexpressions ([@blimmer](https://api.github.com/users/blimmer))
- [#989](https://github.com/wycats/handlebars.js/issues/989) - Source-map error with requirejs ([@SteppeEagle](https://api.github.com/users/SteppeEagle))
- [#967](https://github.com/wycats/handlebars.js/issues/967) - can't access "this" property ([@75lb](https://api.github.com/users/75lb))
- Use captureStackTrace for error handler - a009a97
- Ignore branches tested without coverage monitoring - 37a664b
[Commits](https://github.com/wycats/handlebars.js/compare/v3.0.1...v3.0.2)
## v3.0.1 - March 24th, 2015
- [#984](https://github.com/wycats/handlebars.js/pull/984) - Adding documentation for passing arguments into partials ([@johneke](https://api.github.com/users/johneke))
- [#973](https://github.com/wycats/handlebars.js/issues/973) - version 3 is slower than version 2 ([@elover](https://api.github.com/users/elover))
- [#966](https://github.com/wycats/handlebars.js/issues/966) - "handlebars --version" does not work with v3.0.0 ([@abloomston](https://api.github.com/users/abloomston))
- [#964](https://github.com/wycats/handlebars.js/pull/964) - default is a reserved word ([@grassick](https://api.github.com/users/grassick))
- [#962](https://github.com/wycats/handlebars.js/pull/962) - Add dashbars' link on README. ([@pismute](https://api.github.com/users/pismute))
[Commits](https://github.com/wycats/handlebars.js/compare/v3.0.0...v3.0.1)
## v3.0.0 - February 10th, 2015
- [#941](https://github.com/wycats/handlebars.js/pull/941) - Add support for dynamic partial names ([@kpdecker](https://api.github.com/users/kpdecker))
@@ -160,7 +290,7 @@ Compatibility notes:
- INTEGER -> NUMBER
- Additional PartialNode hash parameter
- New RawBlockNode type
- Data frames now have a `_parent` field. This is internal but is enumerable for performance/compatability reasons.
- Data frames now have a `_parent` field. This is internal but is enumerable for performance/compatibility reasons.
[Commits](https://github.com/wycats/handlebars.js/compare/v1.3.0...v2.0.0-alpha.1)
+1 -1
View File
@@ -1,3 +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;
module.exports = require('./dist/cjs/handlebars.runtime')['default'];
+36
View File
@@ -0,0 +1,36 @@
{
"globals": {
"CompilerContext": true,
"Handlebars": true,
"handlebarsEnv": true,
"shouldCompileTo": true,
"shouldCompileToWithPartials": true,
"shouldThrow": true,
"compileWithPartials": true,
"console": true,
"require": true,
"suite": true,
"equal": true,
"equals": true,
"test": true,
"testBoth": true,
"raises": true,
"deepEqual": true,
"start": true,
"stop": true,
"ok": true,
"strictEqual": true,
"define": true
},
"env": {
"mocha": true
},
"rules": {
// Disabling for tests, for now.
"no-path-concat": 0,
"no-var": 0
}
}
+16
View File
@@ -28,11 +28,16 @@
<script src="/spec/env/common.js"></script>
<script>
var requireFailure;
requirejs.config({
paths: {
'handlebars.runtime': '/dist/handlebars.runtime.amd'
}
});
requirejs.onError = function (err) {
requireFailure = err;
};
</script>
<script>
onload = function(){
@@ -60,6 +65,17 @@
runner.on('fail', logFailure);
// Inject any require initilizer failures into the first test so they are properly
// reported.
if (requireFailure) {
runner.on('hook end', function(hook){
if (requireFailure) {
runner.uncaught(requireFailure);
requireFailure = undefined;
}
});
}
function logFailure(test, err){
var flattenTitles = function(test){
+20 -4
View File
@@ -1,3 +1,4 @@
<!doctype html>
<html>
<head>
<title>Mocha</title>
@@ -28,12 +29,17 @@
<script src="/spec/env/common.js"></script>
<script>
var requireFailure;
requirejs.config({
paths: {
handlebars: '/dist/handlebars.amd',
tests: '/tmp/tests'
}
});
requirejs.onError = function (err) {
requireFailure = err;
};
var CompilerContext = {
compile: function(template, options) {
@@ -79,16 +85,26 @@
runner.on('fail', logFailure);
function logFailure(test, err){
// Inject any require initilizer failures into the first test so they are properly
// reported.
if (requireFailure) {
runner.on('hook end', function(hook){
if (requireFailure) {
runner.uncaught(requireFailure);
requireFailure = undefined;
}
});
}
var flattenTitles = function(test){
function logFailure(test, err){
function flattenTitles(test){
var titles = [];
while (test.parent.title){
titles.push(test.parent.title);
test = test.parent;
}
return titles.reverse();
};
}
failedTests.push({
name: test.title,
@@ -97,7 +113,7 @@
stack: err.stack,
titles: flattenTitles(test)
});
};
}
});
});
};
+58 -106
View File
@@ -1,122 +1,55 @@
/*global Handlebars, handlebarsEnv, shouldThrow */
describe('ast', function() {
if (!Handlebars.AST) {
return;
}
var LOCATION_INFO = {
start: {
line: 1,
column: 1
},
end: {
line: 1,
column: 1
}
};
var AST = Handlebars.AST;
function testLocationInfoStorage(node){
equals(node.loc.start.line, 1);
equals(node.loc.start.column, 1);
equals(node.loc.end.line, 1);
equals(node.loc.end.column, 1);
}
describe('MustacheStatement', function() {
it('should store args', function() {
var id = {isSimple: true},
hash = {},
mustache = new handlebarsEnv.AST.MustacheStatement({}, null, null, true, {}, LOCATION_INFO);
equals(mustache.type, 'MustacheStatement');
equals(mustache.escaped, true);
testLocationInfoStorage(mustache);
});
});
describe('BlockStatement', function() {
it('should throw on mustache mismatch', function() {
shouldThrow(function() {
handlebarsEnv.parse("\n {{#foo}}{{/bar}}");
handlebarsEnv.parse('\n {{#foo}}{{/bar}}');
}, Handlebars.Exception, "foo doesn't match bar - 2:5");
});
});
it('stores location info', function(){
var mustacheNode = new handlebarsEnv.AST.MustacheStatement([{ original: 'foo'}], null, null, false, {});
var block = new handlebarsEnv.AST.BlockStatement(
mustacheNode,
null, null,
{body: []},
{body: []},
{},
{},
{},
LOCATION_INFO);
testLocationInfoStorage(block);
describe('helpers', function() {
describe('#helperExpression', function() {
it('should handle mustache statements', function() {
equals(AST.helpers.helperExpression({type: 'MustacheStatement', params: [], hash: undefined}), false);
equals(AST.helpers.helperExpression({type: 'MustacheStatement', params: [1], hash: undefined}), true);
equals(AST.helpers.helperExpression({type: 'MustacheStatement', params: [], hash: {}}), true);
});
it('should handle block statements', function() {
equals(AST.helpers.helperExpression({type: 'BlockStatement', params: [], hash: undefined}), false);
equals(AST.helpers.helperExpression({type: 'BlockStatement', params: [1], hash: undefined}), true);
equals(AST.helpers.helperExpression({type: 'BlockStatement', params: [], hash: {}}), true);
});
it('should handle subexpressions', function() {
equals(AST.helpers.helperExpression({type: 'SubExpression'}), true);
});
it('should work with non-helper nodes', function() {
equals(AST.helpers.helperExpression({type: 'Program'}), false);
equals(AST.helpers.helperExpression({type: 'PartialStatement'}), false);
equals(AST.helpers.helperExpression({type: 'ContentStatement'}), false);
equals(AST.helpers.helperExpression({type: 'CommentStatement'}), false);
equals(AST.helpers.helperExpression({type: 'PathExpression'}), false);
equals(AST.helpers.helperExpression({type: 'StringLiteral'}), false);
equals(AST.helpers.helperExpression({type: 'NumberLiteral'}), false);
equals(AST.helpers.helperExpression({type: 'BooleanLiteral'}), false);
equals(AST.helpers.helperExpression({type: 'UndefinedLiteral'}), false);
equals(AST.helpers.helperExpression({type: 'NullLiteral'}), false);
equals(AST.helpers.helperExpression({type: 'Hash'}), false);
equals(AST.helpers.helperExpression({type: 'HashPair'}), false);
});
describe('PathExpression', function() {
it('stores location info', function(){
var idNode = new handlebarsEnv.AST.PathExpression(false, 0, [], 'foo', LOCATION_INFO);
testLocationInfoStorage(idNode);
});
});
describe('Hash', function(){
it('stores location info', function(){
var hash = new handlebarsEnv.AST.Hash([], LOCATION_INFO);
testLocationInfoStorage(hash);
});
});
describe('ContentStatement', function(){
it('stores location info', function(){
var content = new handlebarsEnv.AST.ContentStatement("HI", LOCATION_INFO);
testLocationInfoStorage(content);
});
});
describe('CommentStatement', function(){
it('stores location info', function(){
var comment = new handlebarsEnv.AST.CommentStatement("HI", {}, LOCATION_INFO);
testLocationInfoStorage(comment);
});
});
describe('NumberLiteral', function(){
it('stores location info', function(){
var integer = new handlebarsEnv.AST.NumberLiteral("6", LOCATION_INFO);
testLocationInfoStorage(integer);
});
});
describe('StringLiteral', function(){
it('stores location info', function(){
var string = new handlebarsEnv.AST.StringLiteral("6", LOCATION_INFO);
testLocationInfoStorage(string);
});
});
describe('BooleanLiteral', function(){
it('stores location info', function(){
var bool = new handlebarsEnv.AST.BooleanLiteral("true", LOCATION_INFO);
testLocationInfoStorage(bool);
});
});
describe('PartialStatement', function(){
it('stores location info', function(){
var pn = new handlebarsEnv.AST.PartialStatement('so_partial', [], {}, {}, LOCATION_INFO);
testLocationInfoStorage(pn);
});
});
describe('Program', function(){
it('storing location info', function(){
var pn = new handlebarsEnv.AST.Program([], null, {}, LOCATION_INFO);
testLocationInfoStorage(pn);
});
});
describe("Line Numbers", function(){
describe('Line Numbers', function() {
var ast, body;
function testColumns(node, firstLine, lastLine, firstColumn, lastColumn) {
@@ -126,8 +59,18 @@ describe('ast', function() {
equals(node.loc.end.column, lastColumn);
}
ast = Handlebars.parse("line 1 {{line1Token}}\n line 2 {{line2token}}\n line 3 {{#blockHelperOnLine3}}\nline 4{{line4token}}\n" +
"line5{{else}}\n{{line6Token}}\n{{/blockHelperOnLine3}}");
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() {
@@ -158,14 +101,23 @@ describe('ast', function() {
var blockHelperNode = body[5],
program = blockHelperNode.program;
testColumns(program, 3, 5, 8, 5);
testColumns(program, 3, 5, 31, 5);
});
it('correctly records the line numbers of an inverse of a block helper', function() {
var blockHelperNode = body[5],
inverse = blockHelperNode.inverse;
testColumns(inverse, 5, 7, 5, 0);
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);
});
});
+167 -123
View File
@@ -1,38 +1,41 @@
/*global CompilerContext, Handlebars, beforeEach, shouldCompileTo, shouldThrow */
global.handlebarsEnv = null;
beforeEach(function() {
global.handlebarsEnv = Handlebars.create();
});
describe("basic context", function() {
it("most basic", function() {
shouldCompileTo("{{foo}}", { foo: "foo" }, "foo");
describe('basic context', function() {
it('most basic', function() {
shouldCompileTo('{{foo}}', { foo: 'foo' }, 'foo');
});
it("escaping", function() {
shouldCompileTo("\\{{foo}}", { foo: "food" }, "{{foo}}");
shouldCompileTo("content \\{{foo}}", { foo: "food" }, "content {{foo}}");
shouldCompileTo("\\\\{{foo}}", { foo: "food" }, "\\food");
shouldCompileTo("content \\\\{{foo}}", { foo: "food" }, "content \\food");
shouldCompileTo("\\\\ {{foo}}", { foo: "food" }, "\\\\ food");
it('escaping', function() {
shouldCompileTo('\\{{foo}}', { foo: 'food' }, '{{foo}}');
shouldCompileTo('content \\{{foo}}', { foo: 'food' }, 'content {{foo}}');
shouldCompileTo('\\\\{{foo}}', { foo: 'food' }, '\\food');
shouldCompileTo('content \\\\{{foo}}', { foo: 'food' }, 'content \\food');
shouldCompileTo('\\\\ {{foo}}', { foo: 'food' }, '\\\\ food');
});
it("compiling with a basic context", function() {
shouldCompileTo("Goodbye\n{{cruel}}\n{{world}}!", {cruel: "cruel", world: "world"}, "Goodbye\ncruel\nworld!",
"It works if all the required keys are provided");
it('compiling with a basic context', function() {
shouldCompileTo('Goodbye\n{{cruel}}\n{{world}}!', {cruel: 'cruel', world: 'world'}, 'Goodbye\ncruel\nworld!',
'It works if all the required keys are provided');
});
it("compiling with an undefined context", function() {
shouldCompileTo("Goodbye\n{{cruel}}\n{{world.bar}}!", undefined, "Goodbye\n\n!");
shouldCompileTo("{{#unless foo}}Goodbye{{../test}}{{test2}}{{/unless}}", undefined, "Goodbye");
it('compiling with a string context', function() {
shouldCompileTo('{{.}}{{length}}', 'bye', 'bye3');
});
it("comments", function() {
shouldCompileTo("{{! Goodbye}}Goodbye\n{{cruel}}\n{{world}}!",
{cruel: "cruel", world: "world"}, "Goodbye\ncruel\nworld!",
"comments are ignored");
it('compiling with an undefined context', function() {
shouldCompileTo('Goodbye\n{{cruel}}\n{{world.bar}}!', undefined, 'Goodbye\n\n!');
shouldCompileTo('{{#unless foo}}Goodbye{{../test}}{{test2}}{{/unless}}', undefined, 'Goodbye');
});
it('comments', function() {
shouldCompileTo('{{! Goodbye}}Goodbye\n{{cruel}}\n{{world}}!',
{cruel: 'cruel', world: 'world'}, 'Goodbye\ncruel\nworld!',
'comments are ignored');
shouldCompileTo(' {{~! comment ~}} blah', {}, 'blah');
shouldCompileTo(' {{~!-- long-comment --~}} blah', {}, 'blah');
@@ -42,192 +45,233 @@ describe("basic context", function() {
shouldCompileTo(' {{~!-- long-comment --}} blah', {}, ' blah');
});
it("boolean", function() {
var string = "{{#goodbye}}GOODBYE {{/goodbye}}cruel {{world}}!";
shouldCompileTo(string, {goodbye: true, world: "world"}, "GOODBYE cruel world!",
"booleans show the contents when true");
it('boolean', function() {
var string = '{{#goodbye}}GOODBYE {{/goodbye}}cruel {{world}}!';
shouldCompileTo(string, {goodbye: true, world: 'world'}, 'GOODBYE cruel world!',
'booleans show the contents when true');
shouldCompileTo(string, {goodbye: false, world: "world"}, "cruel world!",
"booleans do not show the contents when false");
shouldCompileTo(string, {goodbye: false, world: 'world'}, 'cruel world!',
'booleans do not show the contents when false');
});
it("zeros", function() {
shouldCompileTo("num1: {{num1}}, num2: {{num2}}", {num1: 42, num2: 0},
"num1: 42, num2: 0");
shouldCompileTo("num: {{.}}", 0, "num: 0");
shouldCompileTo("num: {{num1/num2}}", {num1: {num2: 0}}, "num: 0");
it('zeros', function() {
shouldCompileTo('num1: {{num1}}, num2: {{num2}}', {num1: 42, num2: 0},
'num1: 42, num2: 0');
shouldCompileTo('num: {{.}}', 0, 'num: 0');
shouldCompileTo('num: {{num1/num2}}', {num1: {num2: 0}}, 'num: 0');
});
it('false', function() {
/* eslint-disable no-new-wrappers */
shouldCompileTo('val1: {{val1}}, val2: {{val2}}', {val1: false, val2: new Boolean(false)}, 'val1: false, val2: false');
shouldCompileTo('val: {{.}}', false, 'val: false');
shouldCompileTo('val: {{val1/val2}}', {val1: {val2: false}}, 'val: false');
shouldCompileTo('val1: {{{val1}}}, val2: {{{val2}}}', {val1: false, val2: new Boolean(false)}, 'val1: false, val2: false');
shouldCompileTo('val: {{{val1/val2}}}', {val1: {val2: false}}, 'val: false');
/* eslint-enable */
});
it("newlines", function() {
it('should handle undefined and null', function() {
shouldCompileTo('{{awesome undefined null}}',
{
awesome: function(_undefined, _null, options) {
return (_undefined === undefined) + ' ' + (_null === null) + ' ' + (typeof options);
}
},
'true true object');
shouldCompileTo('{{undefined}}',
{
'undefined': function() {
return 'undefined!';
}
},
'undefined!');
shouldCompileTo('{{null}}',
{
'null': function() {
return 'null!';
}
},
'null!');
});
it('newlines', function() {
shouldCompileTo("Alan's\nTest", {}, "Alan's\nTest");
shouldCompileTo("Alan's\rTest", {}, "Alan's\rTest");
});
it("escaping text", function() {
it('escaping text', function() {
shouldCompileTo("Awesome's", {}, "Awesome's", "text is escaped so that it doesn't get caught on single quotes");
shouldCompileTo("Awesome\\", {}, "Awesome\\", "text is escaped so that the closing quote can't be ignored");
shouldCompileTo("Awesome\\\\ foo", {}, "Awesome\\\\ foo", "text is escaped so that it doesn't mess up backslashes");
shouldCompileTo("Awesome {{foo}}", {foo: '\\'}, "Awesome \\", "text is escaped so that it doesn't mess up backslashes");
shouldCompileTo(' " " ', {}, ' " " ', "double quotes never produce invalid javascript");
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');
});
it("escaping expressions", function() {
shouldCompileTo("{{{awesome}}}", {awesome: "&\"\\<>"}, '&\"\\<>',
it('escaping expressions', function() {
shouldCompileTo('{{{awesome}}}', {awesome: '&\'\\<>'}, '&\'\\<>',
"expressions with 3 handlebars aren't escaped");
shouldCompileTo("{{&awesome}}", {awesome: "&\"\\<>"}, '&\"\\<>',
shouldCompileTo('{{&awesome}}', {awesome: '&\'\\<>'}, '&\'\\<>',
"expressions with {{& handlebars aren't escaped");
shouldCompileTo("{{awesome}}", {awesome: "&\"'`\\<>"}, '&amp;&quot;&#x27;&#x60;\\&lt;&gt;',
"by default expressions should be escaped");
shouldCompileTo('{{awesome}}', {awesome: "&\"'`\\<>"}, '&amp;&quot;&#x27;&#x60;\\&lt;&gt;',
'by default expressions should be escaped');
shouldCompileTo("{{awesome}}", {awesome: "Escaped, <b> looks like: &lt;b&gt;"}, 'Escaped, &lt;b&gt; looks like: &amp;lt;b&amp;gt;',
"escaping should properly handle amperstands");
shouldCompileTo('{{awesome}}', {awesome: 'Escaped, <b> looks like: &lt;b&gt;'}, 'Escaped, &lt;b&gt; looks like: &amp;lt;b&amp;gt;',
'escaping should properly handle amperstands');
});
it("functions returning safestrings shouldn't be escaped", function() {
var hash = {awesome: function() { return new Handlebars.SafeString("&\"\\<>"); }};
shouldCompileTo("{{awesome}}", hash, '&\"\\<>',
var hash = {awesome: function() { return new Handlebars.SafeString('&\'\\<>'); }};
shouldCompileTo('{{awesome}}', hash, '&\'\\<>',
"functions returning safestrings aren't escaped");
});
it("functions", function() {
shouldCompileTo("{{awesome}}", {awesome: function() { return "Awesome"; }}, "Awesome",
"functions are called and render their output");
shouldCompileTo("{{awesome}}", {awesome: function() { return this.more; }, more: "More awesome"}, "More awesome",
"functions are bound to the context");
it('functions', function() {
shouldCompileTo('{{awesome}}', {awesome: function() { return 'Awesome'; }}, 'Awesome',
'functions are called and render their output');
shouldCompileTo('{{awesome}}', {awesome: function() { return this.more; }, more: 'More awesome'}, 'More awesome',
'functions are bound to the context');
});
it("functions with context argument", function() {
shouldCompileTo("{{awesome frank}}",
it('functions with context argument', function() {
shouldCompileTo('{{awesome frank}}',
{awesome: function(context) { return context; },
frank: "Frank"},
"Frank", "functions are called with context arguments");
frank: 'Frank'},
'Frank', 'functions are called with context arguments');
});
it("pathed functions with context argument", function() {
shouldCompileTo("{{bar.awesome frank}}",
it('pathed functions with context argument', function() {
shouldCompileTo('{{bar.awesome frank}}',
{bar: {awesome: function(context) { return context; }},
frank: "Frank"},
"Frank", "functions are called with context arguments");
frank: 'Frank'},
'Frank', 'functions are called with context arguments');
});
it("depthed functions with context argument", function() {
shouldCompileTo("{{#with frank}}{{../awesome .}}{{/with}}",
it('depthed functions with context argument', function() {
shouldCompileTo('{{#with frank}}{{../awesome .}}{{/with}}',
{awesome: function(context) { return context; },
frank: "Frank"},
"Frank", "functions are called with context arguments");
frank: 'Frank'},
'Frank', 'functions are called with context arguments');
});
it("block functions with context argument", function() {
shouldCompileTo("{{#awesome 1}}inner {{.}}{{/awesome}}",
it('block functions with context argument', function() {
shouldCompileTo('{{#awesome 1}}inner {{.}}{{/awesome}}',
{awesome: function(context, options) { return options.fn(context); }},
"inner 1", "block functions are called with context and options");
'inner 1', 'block functions are called with context and options');
});
it("depthed block functions with context argument", function() {
shouldCompileTo("{{#with value}}{{#../awesome 1}}inner {{.}}{{/../awesome}}{{/with}}",
it('depthed block functions with context argument', function() {
shouldCompileTo('{{#with value}}{{#../awesome 1}}inner {{.}}{{/../awesome}}{{/with}}',
{value: true, awesome: function(context, options) { return options.fn(context); }},
"inner 1", "block functions are called with context and options");
'inner 1', 'block functions are called with context and options');
});
it("block functions without context argument", function() {
shouldCompileTo("{{#awesome}}inner{{/awesome}}",
it('block functions without context argument', function() {
shouldCompileTo('{{#awesome}}inner{{/awesome}}',
{awesome: function(options) { return options.fn(this); }},
"inner", "block functions are called with options");
'inner', 'block functions are called with options');
});
it("pathed block functions without context argument", function() {
shouldCompileTo("{{#foo.awesome}}inner{{/foo.awesome}}",
it('pathed block functions without context argument', function() {
shouldCompileTo('{{#foo.awesome}}inner{{/foo.awesome}}',
{foo: {awesome: function() { return this; }}},
"inner", "block functions are called with options");
'inner', 'block functions are called with options');
});
it("depthed block functions without context argument", function() {
shouldCompileTo("{{#with value}}{{#../awesome}}inner{{/../awesome}}{{/with}}",
it('depthed block functions without context argument', function() {
shouldCompileTo('{{#with value}}{{#../awesome}}inner{{/../awesome}}{{/with}}',
{value: true, awesome: function() { return this; }},
"inner", "block functions are called with options");
'inner', 'block functions are called with options');
});
it("paths with hyphens", function() {
shouldCompileTo("{{foo-bar}}", {"foo-bar": "baz"}, "baz", "Paths can contain hyphens (-)");
shouldCompileTo("{{foo.foo-bar}}", {foo: {"foo-bar": "baz"}}, "baz", "Paths can contain hyphens (-)");
shouldCompileTo("{{foo/foo-bar}}", {foo: {"foo-bar": "baz"}}, "baz", "Paths can contain hyphens (-)");
it('paths with hyphens', function() {
shouldCompileTo('{{foo-bar}}', {'foo-bar': 'baz'}, 'baz', 'Paths can contain hyphens (-)');
shouldCompileTo('{{foo.foo-bar}}', {foo: {'foo-bar': 'baz'}}, 'baz', 'Paths can contain hyphens (-)');
shouldCompileTo('{{foo/foo-bar}}', {foo: {'foo-bar': 'baz'}}, 'baz', 'Paths can contain hyphens (-)');
});
it("nested paths", function() {
shouldCompileTo("Goodbye {{alan/expression}} world!", {alan: {expression: "beautiful"}},
"Goodbye beautiful world!", "Nested paths access nested objects");
it('nested paths', function() {
shouldCompileTo('Goodbye {{alan/expression}} world!', {alan: {expression: 'beautiful'}},
'Goodbye beautiful world!', 'Nested paths access nested objects');
});
it("nested paths with empty string value", function() {
shouldCompileTo("Goodbye {{alan/expression}} world!", {alan: {expression: ""}},
"Goodbye world!", "Nested paths access nested objects with empty string");
it('nested paths with empty string value', function() {
shouldCompileTo('Goodbye {{alan/expression}} world!', {alan: {expression: ''}},
'Goodbye world!', 'Nested paths access nested objects with empty string');
});
it("literal paths", function() {
shouldCompileTo("Goodbye {{[@alan]/expression}} world!", {"@alan": {expression: "beautiful"}},
"Goodbye beautiful world!", "Literal paths can be used");
shouldCompileTo("Goodbye {{[foo bar]/expression}} world!", {"foo bar": {expression: "beautiful"}},
"Goodbye beautiful world!", "Literal paths can be used");
it('literal paths', function() {
shouldCompileTo('Goodbye {{[@alan]/expression}} world!', {'@alan': {expression: 'beautiful'}},
'Goodbye beautiful world!', 'Literal paths can be used');
shouldCompileTo('Goodbye {{[foo bar]/expression}} world!', {'foo bar': {expression: 'beautiful'}},
'Goodbye beautiful world!', 'Literal paths can be used');
});
it('literal references', function() {
shouldCompileTo("Goodbye {{[foo bar]}} world!", {"foo bar": "beautiful"},
"Goodbye beautiful world!", "Literal paths can be used");
shouldCompileTo('Goodbye {{[foo bar]}} world!', {'foo bar': 'beautiful'}, 'Goodbye beautiful world!');
shouldCompileTo('Goodbye {{"foo bar"}} world!', {'foo bar': 'beautiful'}, 'Goodbye beautiful world!');
shouldCompileTo("Goodbye {{'foo bar'}} world!", {'foo bar': 'beautiful'}, 'Goodbye beautiful world!');
shouldCompileTo('Goodbye {{"foo[bar"}} world!', {'foo[bar': 'beautiful'}, 'Goodbye beautiful world!');
shouldCompileTo('Goodbye {{"foo\'bar"}} world!', {"foo'bar": 'beautiful'}, 'Goodbye beautiful world!');
shouldCompileTo("Goodbye {{'foo\"bar'}} world!", {'foo"bar': 'beautiful'}, 'Goodbye beautiful world!');
});
it("that current context path ({{.}}) doesn't hit helpers", function() {
shouldCompileTo("test: {{.}}", [null, {helper: "awesome"}], "test: ");
shouldCompileTo('test: {{.}}', [null, {helper: 'awesome'}], 'test: ');
});
it("complex but empty paths", function() {
shouldCompileTo("{{person/name}}", {person: {name: null}}, "");
shouldCompileTo("{{person/name}}", {person: {}}, "");
it('complex but empty paths', function() {
shouldCompileTo('{{person/name}}', {person: {name: null}}, '');
shouldCompileTo('{{person/name}}', {person: {}}, '');
});
it("this keyword in paths", function() {
var string = "{{#goodbyes}}{{this}}{{/goodbyes}}";
var hash = {goodbyes: ["goodbye", "Goodbye", "GOODBYE"]};
shouldCompileTo(string, hash, "goodbyeGoodbyeGOODBYE",
"This keyword in paths evaluates to current context");
it('this keyword in paths', function() {
var string = '{{#goodbyes}}{{this}}{{/goodbyes}}';
var hash = {goodbyes: ['goodbye', 'Goodbye', 'GOODBYE']};
shouldCompileTo(string, hash, 'goodbyeGoodbyeGOODBYE',
'This keyword in paths evaluates to current context');
string = "{{#hellos}}{{this/text}}{{/hellos}}";
hash = {hellos: [{text: "hello"}, {text: "Hello"}, {text: "HELLO"}]};
shouldCompileTo(string, hash, "helloHelloHELLO", "This keyword evaluates in more complex paths");
string = '{{#hellos}}{{this/text}}{{/hellos}}';
hash = {hellos: [{text: 'hello'}, {text: 'Hello'}, {text: 'HELLO'}]};
shouldCompileTo(string, hash, 'helloHelloHELLO', 'This keyword evaluates in more complex paths');
});
it("this keyword nested inside path", function() {
var string = "{{#hellos}}{{text/this/foo}}{{/hellos}}";
it('this keyword nested inside path', function() {
shouldThrow(function() {
CompilerContext.compile(string);
}, Error);
CompilerContext.compile('{{#hellos}}{{text/this/foo}}{{/hellos}}');
}, Error, 'Invalid path: text/this - 1:13');
shouldCompileTo('{{[this]}}', {'this': 'bar'}, 'bar');
shouldCompileTo('{{text/[this]}}', {text: {'this': 'bar'}}, 'bar');
});
it("this keyword in helpers", function() {
it('this keyword in helpers', function() {
var helpers = {foo: function(value) {
return 'bar ' + value;
}};
var string = "{{#goodbyes}}{{foo this}}{{/goodbyes}}";
var hash = {goodbyes: ["goodbye", "Goodbye", "GOODBYE"]};
shouldCompileTo(string, [hash, helpers], "bar goodbyebar Goodbyebar GOODBYE",
"This keyword in paths evaluates to current context");
var string = '{{#goodbyes}}{{foo this}}{{/goodbyes}}';
var hash = {goodbyes: ['goodbye', 'Goodbye', 'GOODBYE']};
shouldCompileTo(string, [hash, helpers], 'bar goodbyebar Goodbyebar GOODBYE',
'This keyword in paths evaluates to current context');
string = "{{#hellos}}{{foo this/text}}{{/hellos}}";
hash = {hellos: [{text: "hello"}, {text: "Hello"}, {text: "HELLO"}]};
shouldCompileTo(string, [hash, helpers], "bar hellobar Hellobar HELLO", "This keyword evaluates in more complex paths");
string = '{{#hellos}}{{foo this/text}}{{/hellos}}';
hash = {hellos: [{text: 'hello'}, {text: 'Hello'}, {text: 'HELLO'}]};
shouldCompileTo(string, [hash, helpers], 'bar hellobar Hellobar HELLO', 'This keyword evaluates in more complex paths');
});
it("this keyword nested inside helpers param", function() {
var string = "{{#hellos}}{{foo text/this/foo}}{{/hellos}}";
it('this keyword nested inside helpers param', function() {
var string = '{{#hellos}}{{foo text/this/foo}}{{/hellos}}';
shouldThrow(function() {
CompilerContext.compile(string);
}, Error);
}, Error, 'Invalid path: text/this - 1:17');
shouldCompileTo(
'{{foo [this]}}',
{foo: function(value) { return value; }, 'this': 'bar'},
'bar');
shouldCompileTo(
'{{foo text/[this]}}',
{foo: function(value) { return value; }, text: {'this': 'bar'}},
'bar');
});
it('pass string literals', function() {
+262 -68
View File
@@ -1,47 +1,46 @@
/*global CompilerContext, shouldCompileTo, shouldThrow */
describe('blocks', function() {
it("array", function() {
var string = "{{#goodbyes}}{{text}}! {{/goodbyes}}cruel {{world}}!";
var hash = {goodbyes: [{text: "goodbye"}, {text: "Goodbye"}, {text: "GOODBYE"}], world: "world"};
shouldCompileTo(string, hash, "goodbye! Goodbye! GOODBYE! cruel world!",
"Arrays iterate over the contents when not empty");
it('array', function() {
var string = '{{#goodbyes}}{{text}}! {{/goodbyes}}cruel {{world}}!';
var hash = {goodbyes: [{text: 'goodbye'}, {text: 'Goodbye'}, {text: 'GOODBYE'}], world: 'world'};
shouldCompileTo(string, hash, 'goodbye! Goodbye! GOODBYE! cruel world!',
'Arrays iterate over the contents when not empty');
shouldCompileTo(string, {goodbyes: [], world: "world"}, "cruel world!",
"Arrays ignore the contents when empty");
shouldCompileTo(string, {goodbyes: [], world: 'world'}, 'cruel world!',
'Arrays ignore the contents when empty');
});
it('array without data', function() {
var string = '{{#goodbyes}}{{text}}{{/goodbyes}} {{#goodbyes}}{{text}}{{/goodbyes}}';
var hash = {goodbyes: [{text: 'goodbye'}, {text: 'Goodbye'}, {text: 'GOODBYE'}], world: 'world'};
shouldCompileTo(string, [hash,,,,false], 'goodbyeGoodbyeGOODBYE goodbyeGoodbyeGOODBYE');
shouldCompileTo(string, [hash,,, false], 'goodbyeGoodbyeGOODBYE goodbyeGoodbyeGOODBYE');
});
it("array with @index", function() {
var string = "{{#goodbyes}}{{@index}}. {{text}}! {{/goodbyes}}cruel {{world}}!";
var hash = {goodbyes: [{text: "goodbye"}, {text: "Goodbye"}, {text: "GOODBYE"}], world: "world"};
it('array with @index', function() {
var string = '{{#goodbyes}}{{@index}}. {{text}}! {{/goodbyes}}cruel {{world}}!';
var hash = {goodbyes: [{text: 'goodbye'}, {text: 'Goodbye'}, {text: 'GOODBYE'}], world: 'world'};
var template = CompilerContext.compile(string);
var result = template(hash);
equal(result, "0. goodbye! 1. Goodbye! 2. GOODBYE! cruel world!", "The @index variable is used");
equal(result, '0. goodbye! 1. Goodbye! 2. GOODBYE! cruel world!', 'The @index variable is used');
});
it("empty block", function() {
var string = "{{#goodbyes}}{{/goodbyes}}cruel {{world}}!";
var hash = {goodbyes: [{text: "goodbye"}, {text: "Goodbye"}, {text: "GOODBYE"}], world: "world"};
shouldCompileTo(string, hash, "cruel world!",
"Arrays iterate over the contents when not empty");
it('empty block', function() {
var string = '{{#goodbyes}}{{/goodbyes}}cruel {{world}}!';
var hash = {goodbyes: [{text: 'goodbye'}, {text: 'Goodbye'}, {text: 'GOODBYE'}], world: 'world'};
shouldCompileTo(string, hash, 'cruel world!',
'Arrays iterate over the contents when not empty');
shouldCompileTo(string, {goodbyes: [], world: "world"}, "cruel world!",
"Arrays ignore the contents when empty");
shouldCompileTo(string, {goodbyes: [], world: 'world'}, 'cruel world!',
'Arrays ignore the contents when empty');
});
it("block with complex lookup", function() {
var string = "{{#goodbyes}}{{text}} cruel {{../name}}! {{/goodbyes}}";
var hash = {name: "Alan", goodbyes: [{text: "goodbye"}, {text: "Goodbye"}, {text: "GOODBYE"}]};
it('block with complex lookup', function() {
var string = '{{#goodbyes}}{{text}} cruel {{../name}}! {{/goodbyes}}';
var hash = {name: 'Alan', goodbyes: [{text: 'goodbye'}, {text: 'Goodbye'}, {text: 'GOODBYE'}]};
shouldCompileTo(string, hash, "goodbye cruel Alan! Goodbye cruel Alan! GOODBYE cruel Alan! ",
"Templates can access variables in contexts up the stack with relative path syntax");
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');
});
it('multiple blocks with complex lookup', function() {
@@ -51,62 +50,69 @@ describe('blocks', function() {
shouldCompileTo(string, hash, 'AlanAlanAlanAlanAlanAlan');
});
it("block with complex lookup using nested context", function() {
var string = "{{#goodbyes}}{{text}} cruel {{foo/../name}}! {{/goodbyes}}";
it('block with complex lookup using nested context', function() {
var string = '{{#goodbyes}}{{text}} cruel {{foo/../name}}! {{/goodbyes}}';
shouldThrow(function() {
CompilerContext.compile(string);
}, Error);
});
it("block with deep nested complex lookup", function() {
var string = "{{#outer}}Goodbye {{#inner}}cruel {{../sibling}} {{../../omg}}{{/inner}}{{/outer}}";
var hash = {omg: "OMG!", outer: [{ sibling: 'sad', inner: [{ text: "goodbye" }] }] };
it('block with deep nested complex lookup', function() {
var string = '{{#outer}}Goodbye {{#inner}}cruel {{../sibling}} {{../../omg}}{{/inner}}{{/outer}}';
var hash = {omg: 'OMG!', outer: [{ sibling: 'sad', inner: [{ text: 'goodbye' }] }] };
shouldCompileTo(string, hash, "Goodbye cruel sad OMG!");
shouldCompileTo(string, hash, 'Goodbye cruel sad OMG!');
});
it('works with cached blocks', function() {
var template = CompilerContext.compile('{{#each person}}{{#with .}}{{first}} {{last}}{{/with}}{{/each}}', {data: false});
var result = template({person: [{first: 'Alan', last: 'Johnson'}, {first: 'Alan', last: 'Johnson'}]});
equals(result, 'Alan JohnsonAlan Johnson');
});
describe('inverted sections', function() {
it("inverted sections with unset value", function() {
var string = "{{#goodbyes}}{{this}}{{/goodbyes}}{{^goodbyes}}Right On!{{/goodbyes}}";
it('inverted sections with unset value', function() {
var string = '{{#goodbyes}}{{this}}{{/goodbyes}}{{^goodbyes}}Right On!{{/goodbyes}}';
var hash = {};
shouldCompileTo(string, hash, "Right On!", "Inverted section rendered when value isn't set.");
shouldCompileTo(string, hash, 'Right On!', "Inverted section rendered when value isn't set.");
});
it("inverted section with false value", function() {
var string = "{{#goodbyes}}{{this}}{{/goodbyes}}{{^goodbyes}}Right On!{{/goodbyes}}";
it('inverted section with false value', function() {
var string = '{{#goodbyes}}{{this}}{{/goodbyes}}{{^goodbyes}}Right On!{{/goodbyes}}';
var hash = {goodbyes: false};
shouldCompileTo(string, hash, "Right On!", "Inverted section rendered when value is false.");
shouldCompileTo(string, hash, 'Right On!', 'Inverted section rendered when value is false.');
});
it("inverted section with empty set", function() {
var string = "{{#goodbyes}}{{this}}{{/goodbyes}}{{^goodbyes}}Right On!{{/goodbyes}}";
it('inverted section with empty set', function() {
var string = '{{#goodbyes}}{{this}}{{/goodbyes}}{{^goodbyes}}Right On!{{/goodbyes}}';
var hash = {goodbyes: []};
shouldCompileTo(string, hash, "Right On!", "Inverted section rendered when value is empty set.");
shouldCompileTo(string, hash, 'Right On!', 'Inverted section rendered when value is empty set.');
});
it("block inverted sections", function() {
shouldCompileTo("{{#people}}{{name}}{{^}}{{none}}{{/people}}", {none: "No people"},
"No people");
it('block inverted sections', function() {
shouldCompileTo('{{#people}}{{name}}{{^}}{{none}}{{/people}}', {none: 'No people'},
'No people');
});
it("chained inverted sections", function() {
shouldCompileTo("{{#people}}{{name}}{{else if none}}{{none}}{{/people}}", {none: "No people"},
"No people");
shouldCompileTo("{{#people}}{{name}}{{else if nothere}}fail{{else unless nothere}}{{none}}{{/people}}", {none: "No people"},
"No people");
shouldCompileTo("{{#people}}{{name}}{{else if none}}{{none}}{{else}}fail{{/people}}", {none: "No people"},
"No people");
it('chained inverted sections', function() {
shouldCompileTo('{{#people}}{{name}}{{else if none}}{{none}}{{/people}}', {none: 'No people'},
'No people');
shouldCompileTo('{{#people}}{{name}}{{else if nothere}}fail{{else unless nothere}}{{none}}{{/people}}', {none: 'No people'},
'No people');
shouldCompileTo('{{#people}}{{name}}{{else if none}}{{none}}{{else}}fail{{/people}}', {none: 'No people'},
'No people');
});
it("chained inverted sections with mismatch", function() {
it('chained inverted sections with mismatch', function() {
shouldThrow(function() {
shouldCompileTo("{{#people}}{{name}}{{else if none}}{{none}}{{/if}}", {none: "No people"},
"No people");
shouldCompileTo('{{#people}}{{name}}{{else if none}}{{none}}{{/if}}', {none: 'No people'},
'No people');
}, Error);
});
it("block inverted sections with empty arrays", function() {
shouldCompileTo("{{#people}}{{name}}{{^}}{{none}}{{/people}}", {none: "No people", people: []},
"No people");
it('block inverted sections with empty arrays', function() {
shouldCompileTo('{{#people}}{{name}}{{^}}{{none}}{{/people}}', {none: 'No people', people: []},
'No people');
});
});
@@ -119,6 +125,16 @@ describe('blocks', function() {
shouldCompileTo('{{#people}}\n{{name}}\n{{^}}\n{{none}}\n{{/people}}\n', {none: 'No people'},
'No people\n');
});
it('block standalone else sections can be disabled', function() {
shouldCompileTo(
'{{#people}}\n{{name}}\n{{^}}\n{{none}}\n{{/people}}\n',
[{none: 'No people'}, {}, {}, {ignoreStandalone: true}],
'\nNo people\n\n');
shouldCompileTo(
'{{#none}}\n{{.}}\n{{^}}\nFail\n{{/none}}\n',
[{none: 'No people'}, {}, {}, {ignoreStandalone: true}],
'\nNo people\n\n');
});
it('block standalone chained else sections', function() {
shouldCompileTo('{{#people}}\n{{name}}\n{{else if none}}\n{{none}}\n{{/people}}\n', {none: 'No people'},
'No people\n');
@@ -131,23 +147,201 @@ describe('blocks', function() {
});
describe('compat mode', function() {
it("block with deep recursive lookup lookup", function() {
var string = "{{#outer}}Goodbye {{#inner}}cruel {{omg}}{{/inner}}{{/outer}}";
var hash = {omg: "OMG!", outer: [{ inner: [{ text: "goodbye" }] }] };
it('block with deep recursive lookup lookup', function() {
var string = '{{#outer}}Goodbye {{#inner}}cruel {{omg}}{{/inner}}{{/outer}}';
var hash = {omg: 'OMG!', outer: [{ inner: [{ text: 'goodbye' }] }] };
shouldCompileTo(string, [hash, undefined, undefined, true], "Goodbye cruel OMG!");
shouldCompileTo(string, [hash, undefined, undefined, true], 'Goodbye cruel OMG!');
});
it("block with deep recursive pathed lookup", function() {
var string = "{{#outer}}Goodbye {{#inner}}cruel {{omg.yes}}{{/inner}}{{/outer}}";
var hash = {omg: {yes: "OMG!"}, outer: [{ inner: [{ yes: 'no', text: "goodbye" }] }] };
it('block with deep recursive pathed lookup', function() {
var string = '{{#outer}}Goodbye {{#inner}}cruel {{omg.yes}}{{/inner}}{{/outer}}';
var hash = {omg: {yes: 'OMG!'}, outer: [{ inner: [{ yes: 'no', text: 'goodbye' }] }] };
shouldCompileTo(string, [hash, undefined, undefined, true], "Goodbye cruel OMG!");
shouldCompileTo(string, [hash, undefined, undefined, true], 'Goodbye cruel OMG!');
});
it("block with missed recursive lookup", function() {
var string = "{{#outer}}Goodbye {{#inner}}cruel {{omg.yes}}{{/inner}}{{/outer}}";
var hash = {omg: {no: "OMG!"}, outer: [{ inner: [{ yes: 'no', text: "goodbye" }] }] };
it('block with missed recursive lookup', function() {
var string = '{{#outer}}Goodbye {{#inner}}cruel {{omg.yes}}{{/inner}}{{/outer}}';
var hash = {omg: {no: 'OMG!'}, outer: [{ inner: [{ yes: 'no', text: 'goodbye' }] }] };
shouldCompileTo(string, [hash, undefined, undefined, true], "Goodbye cruel ");
shouldCompileTo(string, [hash, undefined, undefined, true], 'Goodbye cruel ');
});
});
describe('decorators', function() {
it('should apply mustache decorators', function() {
var helpers = {
helper: function(options) {
return options.fn.run;
}
};
var decorators = {
decorator: function(fn) {
fn.run = 'success';
return fn;
}
};
shouldCompileTo(
'{{#helper}}{{*decorator}}{{/helper}}',
{hash: {}, helpers: helpers, decorators: decorators},
'success');
});
it('should apply allow undefined return', function() {
var helpers = {
helper: function(options) {
return options.fn() + options.fn.run;
}
};
var decorators = {
decorator: function(fn) {
fn.run = 'cess';
}
};
shouldCompileTo(
'{{#helper}}{{*decorator}}suc{{/helper}}',
{hash: {}, helpers: helpers, decorators: decorators},
'success');
});
it('should apply block decorators', function() {
var helpers = {
helper: function(options) {
return options.fn.run;
}
};
var decorators = {
decorator: function(fn, props, container, options) {
fn.run = options.fn();
return fn;
}
};
shouldCompileTo(
'{{#helper}}{{#*decorator}}success{{/decorator}}{{/helper}}',
{hash: {}, helpers: helpers, decorators: decorators},
'success');
});
it('should support nested decorators', function() {
var helpers = {
helper: function(options) {
return options.fn.run;
}
};
var decorators = {
decorator: function(fn, props, container, options) {
fn.run = options.fn.nested + options.fn();
return fn;
},
nested: function(fn, props, container, options) {
props.nested = options.fn();
}
};
shouldCompileTo(
'{{#helper}}{{#*decorator}}{{#*nested}}suc{{/nested}}cess{{/decorator}}{{/helper}}',
{hash: {}, helpers: helpers, decorators: decorators},
'success');
});
it('should apply multiple decorators', function() {
var helpers = {
helper: function(options) {
return options.fn.run;
}
};
var decorators = {
decorator: function(fn, props, container, options) {
fn.run = (fn.run || '') + options.fn();
return fn;
}
};
shouldCompileTo(
'{{#helper}}{{#*decorator}}suc{{/decorator}}{{#*decorator}}cess{{/decorator}}{{/helper}}',
{hash: {}, helpers: helpers, decorators: decorators},
'success');
});
it('should access parent variables', function() {
var helpers = {
helper: function(options) {
return options.fn.run;
}
};
var decorators = {
decorator: function(fn, props, container, options) {
fn.run = options.args;
return fn;
}
};
shouldCompileTo(
'{{#helper}}{{*decorator foo}}{{/helper}}',
{hash: {'foo': 'success'}, helpers: helpers, decorators: decorators},
'success');
});
it('should work with root program', function() {
var run;
var decorators = {
decorator: function(fn, props, container, options) {
equals(options.args[0], 'success');
run = true;
return fn;
}
};
shouldCompileTo(
'{{*decorator "success"}}',
{hash: {'foo': 'success'}, decorators: decorators},
'');
equals(run, true);
});
it('should fail when accessing variables from root', function() {
var run;
var decorators = {
decorator: function(fn, props, container, options) {
equals(options.args[0], undefined);
run = true;
return fn;
}
};
shouldCompileTo(
'{{*decorator foo}}',
{hash: {'foo': 'fail'}, decorators: decorators},
'');
equals(run, true);
});
describe('registration', function() {
it('unregisters', function() {
handlebarsEnv.decorators = {};
handlebarsEnv.registerDecorator('foo', function() {
return 'fail';
});
equals(!!handlebarsEnv.decorators.foo, true);
handlebarsEnv.unregisterDecorator('foo');
equals(handlebarsEnv.decorators.foo, undefined);
});
it('allows multiple globals', function() {
handlebarsEnv.decorators = {};
handlebarsEnv.registerDecorator({
foo: function() {},
bar: function() {}
});
equals(!!handlebarsEnv.decorators.foo, true);
equals(!!handlebarsEnv.decorators.bar, true);
handlebarsEnv.unregisterDecorator('foo');
handlebarsEnv.unregisterDecorator('bar');
equals(handlebarsEnv.decorators.foo, undefined);
equals(handlebarsEnv.decorators.bar, undefined);
});
it('fails with multiple and args', function() {
shouldThrow(function() {
handlebarsEnv.registerDecorator({
world: function() { return 'world!'; },
testHelper: function() { return 'found it!'; }
}, {});
}, Error, 'Arg not supported with multiple decorators');
});
});
});
});
+223 -131
View File
@@ -1,52 +1,66 @@
/*global CompilerContext, shouldCompileTo, shouldThrow, compileWithPartials, handlebarsEnv */
describe('builtin helpers', function() {
describe('#if', function() {
it("if", function() {
var string = "{{#if goodbye}}GOODBYE {{/if}}cruel {{world}}!";
shouldCompileTo(string, {goodbye: true, world: "world"}, "GOODBYE cruel world!",
"if with boolean argument shows the contents when true");
shouldCompileTo(string, {goodbye: "dummy", world: "world"}, "GOODBYE cruel world!",
"if with string argument shows the contents");
shouldCompileTo(string, {goodbye: false, world: "world"}, "cruel world!",
"if with boolean argument does not show the contents when false");
shouldCompileTo(string, {world: "world"}, "cruel world!",
"if with undefined does not show the contents");
shouldCompileTo(string, {goodbye: ['foo'], world: "world"}, "GOODBYE cruel world!",
"if with non-empty array shows the contents");
shouldCompileTo(string, {goodbye: [], world: "world"}, "cruel world!",
"if with empty array does not show the contents");
shouldCompileTo(string, {goodbye: 0, world: "world"}, "cruel world!",
"if with zero does not show the contents");
shouldCompileTo("{{#if goodbye includeZero=true}}GOODBYE {{/if}}cruel {{world}}!",
{goodbye: 0, world: "world"}, "GOODBYE cruel world!",
"if with zero does not show the contents");
it('if', function() {
var string = '{{#if goodbye}}GOODBYE {{/if}}cruel {{world}}!';
shouldCompileTo(string, {goodbye: true, world: 'world'}, 'GOODBYE cruel world!',
'if with boolean argument shows the contents when true');
shouldCompileTo(string, {goodbye: 'dummy', world: 'world'}, 'GOODBYE cruel world!',
'if with string argument shows the contents');
shouldCompileTo(string, {goodbye: false, world: 'world'}, 'cruel world!',
'if with boolean argument does not show the contents when false');
shouldCompileTo(string, {world: 'world'}, 'cruel world!',
'if with undefined does not show the contents');
shouldCompileTo(string, {goodbye: ['foo'], world: 'world'}, 'GOODBYE cruel world!',
'if with non-empty array shows the contents');
shouldCompileTo(string, {goodbye: [], world: 'world'}, 'cruel world!',
'if with empty array does not show the contents');
shouldCompileTo(string, {goodbye: 0, world: 'world'}, 'cruel world!',
'if with zero does not show the contents');
shouldCompileTo('{{#if goodbye includeZero=true}}GOODBYE {{/if}}cruel {{world}}!',
{goodbye: 0, world: 'world'}, 'GOODBYE cruel world!',
'if with zero does not show the contents');
});
it("if with function argument", function() {
var string = "{{#if goodbye}}GOODBYE {{/if}}cruel {{world}}!";
shouldCompileTo(string, {goodbye: function() {return true;}, world: "world"}, "GOODBYE cruel world!",
"if with function shows the contents when function returns true");
shouldCompileTo(string, {goodbye: function() {return this.world;}, world: "world"}, "GOODBYE cruel world!",
"if with function shows the contents when function returns string");
shouldCompileTo(string, {goodbye: function() {return false;}, world: "world"}, "cruel world!",
"if with function does not show the contents when returns false");
shouldCompileTo(string, {goodbye: function() {return this.foo;}, world: "world"}, "cruel world!",
"if with function does not show the contents when returns undefined");
it('if with function argument', function() {
var string = '{{#if goodbye}}GOODBYE {{/if}}cruel {{world}}!';
shouldCompileTo(string, {goodbye: function() {return true; }, world: 'world'}, 'GOODBYE cruel world!',
'if with function shows the contents when function returns true');
shouldCompileTo(string, {goodbye: function() {return this.world; }, world: 'world'}, 'GOODBYE cruel world!',
'if with function shows the contents when function returns string');
shouldCompileTo(string, {goodbye: function() {return false; }, world: 'world'}, 'cruel world!',
'if with function does not show the contents when returns false');
shouldCompileTo(string, {goodbye: function() {return this.foo; }, world: 'world'}, 'cruel world!',
'if with function does not show the contents when returns undefined');
});
it('should not change the depth list', function() {
var string = '{{#with foo}}{{#if goodbye}}GOODBYE cruel {{../world}}!{{/if}}{{/with}}';
shouldCompileTo(string, {foo: {goodbye: true}, world: 'world'}, 'GOODBYE cruel world!');
});
});
describe('#with', function() {
it("with", function() {
var string = "{{#with person}}{{first}} {{last}}{{/with}}";
shouldCompileTo(string, {person: {first: "Alan", last: "Johnson"}}, "Alan Johnson");
it('with', function() {
var string = '{{#with person}}{{first}} {{last}}{{/with}}';
shouldCompileTo(string, {person: {first: 'Alan', last: 'Johnson'}}, 'Alan Johnson');
});
it("with with function argument", function() {
var string = "{{#with person}}{{first}} {{last}}{{/with}}";
shouldCompileTo(string, {person: function() { return {first: "Alan", last: "Johnson"};}}, "Alan Johnson");
it('with with function argument', function() {
var string = '{{#with person}}{{first}} {{last}}{{/with}}';
shouldCompileTo(string, {person: function() { return {first: 'Alan', last: 'Johnson'}; }}, 'Alan Johnson');
});
it("with with else", function() {
var string = "{{#with person}}Person is present{{else}}Person is not present{{/with}}";
shouldCompileTo(string, {}, "Person is not present");
it('with with else', function() {
var string = '{{#with person}}Person is present{{else}}Person is not present{{/with}}';
shouldCompileTo(string, {}, 'Person is not present');
});
it('with provides block parameter', function() {
var string = '{{#with person as |foo|}}{{foo.first}} {{last}}{{/with}}';
shouldCompileTo(string, {person: {first: 'Alan', last: 'Johnson'}}, 'Alan Johnson');
});
it('works when data is disabled', function() {
var template = CompilerContext.compile('{{#with person as |foo|}}{{foo.first}} {{last}}{{/with}}', {data: false});
var result = template({person: {first: 'Alan', last: 'Johnson'}});
equals(result, 'Alan Johnson');
});
});
@@ -57,13 +71,13 @@ describe('builtin helpers', function() {
});
});
it("each", function() {
var string = "{{#each goodbyes}}{{text}}! {{/each}}cruel {{world}}!";
var hash = {goodbyes: [{text: "goodbye"}, {text: "Goodbye"}, {text: "GOODBYE"}], world: "world"};
shouldCompileTo(string, hash, "goodbye! Goodbye! GOODBYE! cruel world!",
"each with array argument iterates over the contents when not empty");
shouldCompileTo(string, {goodbyes: [], world: "world"}, "cruel world!",
"each with array argument ignores the contents when empty");
it('each', function() {
var string = '{{#each goodbyes}}{{text}}! {{/each}}cruel {{world}}!';
var hash = {goodbyes: [{text: 'goodbye'}, {text: 'Goodbye'}, {text: 'GOODBYE'}], world: 'world'};
shouldCompileTo(string, hash, 'goodbye! Goodbye! GOODBYE! cruel world!',
'each with array argument iterates over the contents when not empty');
shouldCompileTo(string, {goodbyes: [], world: 'world'}, 'cruel world!',
'each with array argument ignores the contents when empty');
});
it('each without data', function() {
@@ -80,8 +94,8 @@ describe('builtin helpers', function() {
shouldCompileTo(string, [,,,, ], 'cruel !');
});
it("each with an object and @key", function() {
var string = "{{#each goodbyes}}{{@key}}. {{text}}! {{/each}}cruel {{world}}!";
it('each with an object and @key', function() {
var string = '{{#each goodbyes}}{{@key}}. {{text}}! {{/each}}cruel {{world}}!';
function Clazz() {
this['<b>#1</b>'] = {text: 'goodbye'};
@@ -94,31 +108,31 @@ describe('builtin helpers', function() {
// so we need to check both possible orders
// @see http://stackoverflow.com/questions/280713/elements-order-in-a-for-in-loop
var actual = compileWithPartials(string, hash);
var expected1 = "&lt;b&gt;#1&lt;/b&gt;. goodbye! 2. GOODBYE! cruel world!";
var expected2 = "2. GOODBYE! &lt;b&gt;#1&lt;/b&gt;. goodbye! cruel world!";
var expected1 = '&lt;b&gt;#1&lt;/b&gt;. goodbye! 2. GOODBYE! cruel world!';
var expected2 = '2. GOODBYE! &lt;b&gt;#1&lt;/b&gt;. goodbye! cruel world!';
equals(actual === expected1 || actual === expected2, true, "each with object argument iterates over the contents when not empty");
equals(actual === expected1 || actual === expected2, true, 'each with object argument iterates over the contents when not empty');
shouldCompileTo(string, {goodbyes: {}, world: 'world'}, 'cruel world!');
});
it("each with @index", function() {
var string = "{{#each goodbyes}}{{@index}}. {{text}}! {{/each}}cruel {{world}}!";
var hash = {goodbyes: [{text: "goodbye"}, {text: "Goodbye"}, {text: "GOODBYE"}], world: "world"};
it('each with @index', function() {
var string = '{{#each goodbyes}}{{@index}}. {{text}}! {{/each}}cruel {{world}}!';
var hash = {goodbyes: [{text: 'goodbye'}, {text: 'Goodbye'}, {text: 'GOODBYE'}], world: 'world'};
var template = CompilerContext.compile(string);
var result = template(hash);
equal(result, "0. goodbye! 1. Goodbye! 2. GOODBYE! cruel world!", "The @index variable is used");
equal(result, '0. goodbye! 1. Goodbye! 2. GOODBYE! cruel world!', 'The @index variable is used');
});
it("each with nested @index", function() {
var string = "{{#each goodbyes}}{{@index}}. {{text}}! {{#each ../goodbyes}}{{@index}} {{/each}}After {{@index}} {{/each}}{{@index}}cruel {{world}}!";
var hash = {goodbyes: [{text: "goodbye"}, {text: "Goodbye"}, {text: "GOODBYE"}], world: "world"};
it('each with nested @index', function() {
var string = '{{#each goodbyes}}{{@index}}. {{text}}! {{#each ../goodbyes}}{{@index}} {{/each}}After {{@index}} {{/each}}{{@index}}cruel {{world}}!';
var hash = {goodbyes: [{text: 'goodbye'}, {text: 'Goodbye'}, {text: 'GOODBYE'}], world: 'world'};
var template = CompilerContext.compile(string);
var result = template(hash);
equal(result, "0. goodbye! 0 1 2 After 0 1. Goodbye! 0 1 2 After 1 2. GOODBYE! 0 1 2 After 2 cruel world!", "The @index variable is used");
equal(result, '0. goodbye! 0 1 2 After 0 1. Goodbye! 0 1 2 After 1 2. GOODBYE! 0 1 2 After 2 cruel world!', 'The @index variable is used');
});
it('each with block params', function() {
@@ -131,88 +145,98 @@ describe('builtin helpers', function() {
equal(result, '0. goodbye! 0 0 0 1 After 0 1. Goodbye! 1 0 1 1 After 1 cruel world!');
});
it("each object with @index", function() {
var string = "{{#each goodbyes}}{{@index}}. {{text}}! {{/each}}cruel {{world}}!";
var hash = {goodbyes: {'a': {text: "goodbye"}, b: {text: "Goodbye"}, c: {text: "GOODBYE"}}, world: "world"};
it('each object with @index', function() {
var string = '{{#each goodbyes}}{{@index}}. {{text}}! {{/each}}cruel {{world}}!';
var hash = {goodbyes: {'a': {text: 'goodbye'}, b: {text: 'Goodbye'}, c: {text: 'GOODBYE'}}, world: 'world'};
var template = CompilerContext.compile(string);
var result = template(hash);
equal(result, "0. goodbye! 1. Goodbye! 2. GOODBYE! cruel world!", "The @index variable is used");
equal(result, '0. goodbye! 1. Goodbye! 2. GOODBYE! cruel world!', 'The @index variable is used');
});
it("each with @first", function() {
var string = "{{#each goodbyes}}{{#if @first}}{{text}}! {{/if}}{{/each}}cruel {{world}}!";
var hash = {goodbyes: [{text: "goodbye"}, {text: "Goodbye"}, {text: "GOODBYE"}], world: "world"};
it('each with @first', function() {
var string = '{{#each goodbyes}}{{#if @first}}{{text}}! {{/if}}{{/each}}cruel {{world}}!';
var hash = {goodbyes: [{text: 'goodbye'}, {text: 'Goodbye'}, {text: 'GOODBYE'}], world: 'world'};
var template = CompilerContext.compile(string);
var result = template(hash);
equal(result, "goodbye! cruel world!", "The @first variable is used");
equal(result, 'goodbye! cruel world!', 'The @first variable is used');
});
it("each with nested @first", function() {
var string = "{{#each goodbyes}}({{#if @first}}{{text}}! {{/if}}{{#each ../goodbyes}}{{#if @first}}{{text}}!{{/if}}{{/each}}{{#if @first}} {{text}}!{{/if}}) {{/each}}cruel {{world}}!";
var hash = {goodbyes: [{text: "goodbye"}, {text: "Goodbye"}, {text: "GOODBYE"}], world: "world"};
it('each with nested @first', function() {
var string = '{{#each goodbyes}}({{#if @first}}{{text}}! {{/if}}{{#each ../goodbyes}}{{#if @first}}{{text}}!{{/if}}{{/each}}{{#if @first}} {{text}}!{{/if}}) {{/each}}cruel {{world}}!';
var hash = {goodbyes: [{text: 'goodbye'}, {text: 'Goodbye'}, {text: 'GOODBYE'}], world: 'world'};
var template = CompilerContext.compile(string);
var result = template(hash);
equal(result, "(goodbye! goodbye! goodbye!) (goodbye!) (goodbye!) cruel world!", "The @first variable is used");
equal(result, '(goodbye! goodbye! goodbye!) (goodbye!) (goodbye!) cruel world!', 'The @first variable is used');
});
it("each object with @first", function() {
var string = "{{#each goodbyes}}{{#if @first}}{{text}}! {{/if}}{{/each}}cruel {{world}}!";
var hash = {goodbyes: {'foo': {text: "goodbye"}, bar: {text: "Goodbye"}}, world: "world"};
it('each object with @first', function() {
var string = '{{#each goodbyes}}{{#if @first}}{{text}}! {{/if}}{{/each}}cruel {{world}}!';
var hash = {goodbyes: {'foo': {text: 'goodbye'}, bar: {text: 'Goodbye'}}, world: 'world'};
var template = CompilerContext.compile(string);
var result = template(hash);
equal(result, "goodbye! cruel world!", "The @first variable is used");
equal(result, 'goodbye! cruel world!', 'The @first variable is used');
});
it("each with @last", function() {
var string = "{{#each goodbyes}}{{#if @last}}{{text}}! {{/if}}{{/each}}cruel {{world}}!";
var hash = {goodbyes: [{text: "goodbye"}, {text: "Goodbye"}, {text: "GOODBYE"}], world: "world"};
it('each with @last', function() {
var string = '{{#each goodbyes}}{{#if @last}}{{text}}! {{/if}}{{/each}}cruel {{world}}!';
var hash = {goodbyes: [{text: 'goodbye'}, {text: 'Goodbye'}, {text: 'GOODBYE'}], world: 'world'};
var template = CompilerContext.compile(string);
var result = template(hash);
equal(result, "GOODBYE! cruel world!", "The @last variable is used");
equal(result, 'GOODBYE! cruel world!', 'The @last variable is used');
});
it("each object with @last", function() {
var string = "{{#each goodbyes}}{{#if @last}}{{text}}! {{/if}}{{/each}}cruel {{world}}!";
var hash = {goodbyes: {'foo': {text: "goodbye"}, bar: {text: "Goodbye"}}, world: "world"};
it('each object with @last', function() {
var string = '{{#each goodbyes}}{{#if @last}}{{text}}! {{/if}}{{/each}}cruel {{world}}!';
var hash = {goodbyes: {'foo': {text: 'goodbye'}, bar: {text: 'Goodbye'}}, world: 'world'};
var template = CompilerContext.compile(string);
var result = template(hash);
equal(result, "Goodbye! cruel world!", "The @last variable is used");
equal(result, 'Goodbye! cruel world!', 'The @last variable is used');
});
it("each with nested @last", function() {
var string = "{{#each goodbyes}}({{#if @last}}{{text}}! {{/if}}{{#each ../goodbyes}}{{#if @last}}{{text}}!{{/if}}{{/each}}{{#if @last}} {{text}}!{{/if}}) {{/each}}cruel {{world}}!";
var hash = {goodbyes: [{text: "goodbye"}, {text: "Goodbye"}, {text: "GOODBYE"}], world: "world"};
it('each with nested @last', function() {
var string = '{{#each goodbyes}}({{#if @last}}{{text}}! {{/if}}{{#each ../goodbyes}}{{#if @last}}{{text}}!{{/if}}{{/each}}{{#if @last}} {{text}}!{{/if}}) {{/each}}cruel {{world}}!';
var hash = {goodbyes: [{text: 'goodbye'}, {text: 'Goodbye'}, {text: 'GOODBYE'}], world: 'world'};
var template = CompilerContext.compile(string);
var result = template(hash);
equal(result, "(GOODBYE!) (GOODBYE!) (GOODBYE! GOODBYE! GOODBYE!) cruel world!", "The @last variable is used");
equal(result, '(GOODBYE!) (GOODBYE!) (GOODBYE! GOODBYE! GOODBYE!) cruel world!', 'The @last variable is used');
});
it("each with function argument", function() {
var string = "{{#each goodbyes}}{{text}}! {{/each}}cruel {{world}}!";
var hash = {goodbyes: function () { return [{text: "goodbye"}, {text: "Goodbye"}, {text: "GOODBYE"}];}, world: "world"};
shouldCompileTo(string, hash, "goodbye! Goodbye! GOODBYE! cruel world!",
"each with array function argument iterates over the contents when not empty");
shouldCompileTo(string, {goodbyes: [], world: "world"}, "cruel world!",
"each with array function argument ignores the contents when empty");
it('each with function argument', function() {
var string = '{{#each goodbyes}}{{text}}! {{/each}}cruel {{world}}!';
var hash = {goodbyes: function() { return [{text: 'goodbye'}, {text: 'Goodbye'}, {text: 'GOODBYE'}]; }, world: 'world'};
shouldCompileTo(string, hash, 'goodbye! Goodbye! GOODBYE! cruel world!',
'each with array function argument iterates over the contents when not empty');
shouldCompileTo(string, {goodbyes: [], world: 'world'}, 'cruel world!',
'each with array function argument ignores the contents when empty');
});
it("data passed to helpers", function() {
var string = "{{#each letters}}{{this}}{{detectDataInsideEach}}{{/each}}";
it('each object when last key is an empty string', function() {
var string = '{{#each goodbyes}}{{@index}}. {{text}}! {{/each}}cruel {{world}}!';
var hash = {goodbyes: {'a': {text: 'goodbye'}, b: {text: 'Goodbye'}, '': {text: 'GOODBYE'}}, world: 'world'};
var template = CompilerContext.compile(string);
var result = template(hash);
equal(result, '0. goodbye! 1. Goodbye! 2. GOODBYE! cruel world!', 'Empty string key is not skipped');
});
it('data passed to helpers', function() {
var string = '{{#each letters}}{{this}}{{detectDataInsideEach}}{{/each}}';
var hash = {letters: ['a', 'b', 'c']};
var template = CompilerContext.compile(string);
@@ -224,36 +248,37 @@ describe('builtin helpers', function() {
equal(result, 'a!b!c!', 'should output data');
});
it("each on implicit context", function() {
it('each on implicit context', function() {
shouldThrow(function() {
var template = CompilerContext.compile("{{#each}}{{text}}! {{/each}}cruel world!");
var template = CompilerContext.compile('{{#each}}{{text}}! {{/each}}cruel world!');
template({});
}, handlebarsEnv.Exception, 'Must pass iterator to #each');
});
});
describe("#log", function() {
describe('#log', function() {
/* eslint-disable no-console */
if (typeof console === 'undefined') {
return;
}
var log,
info,
error;
var $log,
$info,
$error;
beforeEach(function() {
log = console.log;
info = console.info;
error = console.error;
$log = console.log;
$info = console.info;
$error = console.error;
});
afterEach(function() {
console.log = log;
console.info = info;
console.error = error;
console.log = $log;
console.info = $info;
console.error = $error;
});
it('should call logger at default level', function() {
var string = "{{log blah}}";
var hash = { blah: "whee" };
var string = '{{log blah}}';
var hash = { blah: 'whee' };
var levelArg, logArg;
handlebarsEnv.log = function(level, arg) {
@@ -261,13 +286,13 @@ describe('builtin helpers', function() {
logArg = arg;
};
shouldCompileTo(string, hash, "", "log should not display");
equals(1, levelArg, "should call log with 1");
equals("whee", logArg, "should call log with 'whee'");
shouldCompileTo(string, hash, '', 'log should not display');
equals(1, levelArg, 'should call log with 1');
equals('whee', logArg, "should call log with 'whee'");
});
it('should call logger at data level', function() {
var string = "{{log blah}}";
var hash = { blah: "whee" };
var string = '{{log blah}}';
var hash = { blah: 'whee' };
var levelArg, logArg;
handlebarsEnv.log = function(level, arg) {
@@ -275,48 +300,115 @@ describe('builtin helpers', function() {
logArg = arg;
};
shouldCompileTo(string, [hash,,,,{level: '03'}], "");
equals(3, levelArg);
equals("whee", logArg);
shouldCompileTo(string, [hash,,,, {level: '03'}], '');
equals('03', levelArg);
equals('whee', logArg);
});
it('should output to info', function() {
var string = "{{log blah}}";
var hash = { blah: "whee" };
var string = '{{log blah}}';
var hash = { blah: 'whee' };
var called;
console.info = function(log) {
equals("whee", log);
console.info = function(info) {
equals('whee', info);
called = true;
};
console.log = function(log) {
equals("whee", log);
equals('whee', log);
called = true;
};
shouldCompileTo(string, hash, "");
shouldCompileTo(string, hash, '');
equals(true, called);
});
it('should log at data level', function() {
var string = "{{log blah}}";
var hash = { blah: "whee" };
var string = '{{log blah}}';
var hash = { blah: 'whee' };
var called;
console.error = function(log) {
equals("whee", log);
equals('whee', log);
called = true;
};
shouldCompileTo(string, [hash,,,,{level: '03'}], "");
shouldCompileTo(string, [hash,,,, {level: '03'}], '');
equals(true, called);
});
it('should handle missing logger', function() {
var string = "{{log blah}}";
var hash = { blah: "whee" };
var string = '{{log blah}}';
var hash = { blah: 'whee' },
called = false;
console.error = undefined;
console.log = function(log) {
equals('whee', log);
called = true;
};
shouldCompileTo(string, [hash,,,,{level: '03'}], "");
shouldCompileTo(string, [hash,,,, {level: '03'}], '');
equals(true, called);
});
it('should handle string log levels', function() {
var string = '{{log blah}}';
var hash = { blah: 'whee' };
var called;
console.error = function(log) {
equals('whee', log);
called = true;
};
shouldCompileTo(string, [hash,,,, {level: 'error'}], '');
equals(true, called);
called = false;
shouldCompileTo(string, [hash,,,, {level: 'ERROR'}], '');
equals(true, called);
});
it('should handle hash log levels', function() {
var string = '{{log blah level="error"}}';
var hash = { blah: 'whee' };
var called;
console.error = function(log) {
equals('whee', log);
called = true;
};
shouldCompileTo(string, hash, '');
equals(true, called);
});
it('should handle hash log levels', function() {
var string = '{{log blah level="debug"}}';
var hash = { blah: 'whee' };
var called = false;
console.info = console.log = console.error = console.debug = function(log) {
equals('whee', log);
called = true;
};
shouldCompileTo(string, hash, '');
equals(false, called);
});
it('should pass multiple log arguments', function() {
var string = '{{log blah "foo" 1}}';
var hash = { blah: 'whee' };
var called;
console.info = console.log = function(log1, log2, log3) {
equals('whee', log1);
equals('foo', log2);
equals(1, log3);
called = true;
};
shouldCompileTo(string, hash, '');
equals(true, called);
});
/* eslint-enable no-console */
});
+36 -7
View File
@@ -1,5 +1,3 @@
/*global Handlebars, shouldThrow */
describe('compiler', function() {
if (!Handlebars.compile) {
return;
@@ -40,11 +38,39 @@ describe('compiler', function() {
}, Error, 'You must pass a string or Handlebars AST to Handlebars.compile. You passed [object Object]');
});
it('can utilize AST instance', function() {
equal(Handlebars.compile(new Handlebars.AST.Program([ new Handlebars.AST.ContentStatement("Hello")], null, {}))(), 'Hello');
it('should include the location in the error (row and column)', function() {
try {
Handlebars.compile(' \n {{#if}}\n{{/def}}')();
equal(true, false, 'Statement must throw exception. This line should not be executed.');
} catch (err) {
equal(err.message, 'if doesn\'t match def - 2:5', 'Checking error message');
if (Object.getOwnPropertyDescriptor(err, 'column').writable) {
// In Safari 8, the column-property is read-only. This means that even if it is set with defineProperty,
// its value won't change (https://github.com/jquery/esprima/issues/1290#issuecomment-132455482)
// Since this was neither working in Handlebars 3 nor in 4.0.5, we only check the column for other browsers.
equal(err.column, 5, 'Checking error column');
}
equal(err.lineNumber, 2, 'Checking error row');
}
});
it("can pass through an empty string", function() {
it('should include the location as enumerable property', function() {
try {
Handlebars.compile(' \n {{#if}}\n{{/def}}')();
equal(true, false, 'Statement must throw exception. This line should not be executed.');
} catch (err) {
equal(err.propertyIsEnumerable('column'), true, 'Checking error column');
}
});
it('can utilize AST instance', function() {
equal(Handlebars.compile({
type: 'Program',
body: [ {type: 'ContentStatement', value: 'Hello'}]
})(), 'Hello');
});
it('can pass through an empty string', function() {
equal(Handlebars.compile('')(), '');
});
});
@@ -60,10 +86,13 @@ describe('compiler', function() {
});
it('can utilize AST instance', function() {
equal(/return "Hello"/.test(Handlebars.precompile(new Handlebars.AST.Program([ new Handlebars.AST.ContentStatement("Hello")]), null, {})), true);
equal(/return "Hello"/.test(Handlebars.precompile({
type: 'Program',
body: [ {type: 'ContentStatement', value: 'Hello'}]
})), true);
});
it("can pass through an empty string", function() {
it('can pass through an empty string', function() {
equal(/return ""/.test(Handlebars.precompile('')), true);
});
});
+81 -78
View File
@@ -1,25 +1,24 @@
/*global CompilerContext, Handlebars, handlebarsEnv, shouldThrow */
describe('data', function() {
it("passing in data to a compiled function that expects data - works with helpers", function() {
var template = CompilerContext.compile("{{hello}}", {data: true});
it('passing in data to a compiled function that expects data - works with helpers', function() {
var template = CompilerContext.compile('{{hello}}', {data: true});
var helpers = {
hello: function(options) {
return options.data.adjective + " " + this.noun;
return options.data.adjective + ' ' + this.noun;
}
};
var result = template({noun: "cat"}, {helpers: helpers, data: {adjective: "happy"}});
equals("happy cat", result, "Data output by helper");
var result = template({noun: 'cat'}, {helpers: helpers, data: {adjective: 'happy'}});
equals('happy cat', result, 'Data output by helper');
});
it("data can be looked up via @foo", function() {
var template = CompilerContext.compile("{{@hello}}");
var result = template({}, { data: { hello: "hello" } });
equals("hello", result, "@foo retrieves template data");
it('data can be looked up via @foo', function() {
var template = CompilerContext.compile('{{@hello}}');
var result = template({}, { data: { hello: 'hello' } });
equals('hello', result, '@foo retrieves template data');
});
it("deep @foo triggers automatic top-level data", function() {
it('deep @foo triggers automatic top-level data', function() {
var template = CompilerContext.compile('{{#let world="world"}}{{#if foo}}{{#if foo}}Hello {{@world}}{{/if}}{{/if}}{{/let}}');
var helpers = Handlebars.createFrame(handlebarsEnv.helpers);
@@ -28,65 +27,67 @@ describe('data', function() {
var frame = Handlebars.createFrame(options.data);
for (var prop in options.hash) {
if (prop in options.hash) {
frame[prop] = options.hash[prop];
}
}
return options.fn(this, { data: frame });
};
var result = template({ foo: true }, { helpers: helpers });
equals("Hello world", result, "Automatic data was triggered");
equals('Hello world', result, 'Automatic data was triggered');
});
it("parameter data can be looked up via @foo", function() {
var template = CompilerContext.compile("{{hello @world}}");
it('parameter data can be looked up via @foo', function() {
var template = CompilerContext.compile('{{hello @world}}');
var helpers = {
hello: function(noun) {
return "Hello " + noun;
return 'Hello ' + noun;
}
};
var result = template({}, { helpers: helpers, data: { world: "world" } });
equals("Hello world", result, "@foo as a parameter retrieves template data");
var result = template({}, { helpers: helpers, data: { world: 'world' } });
equals('Hello world', result, '@foo as a parameter retrieves template data');
});
it("hash values can be looked up via @foo", function() {
var template = CompilerContext.compile("{{hello noun=@world}}");
it('hash values can be looked up via @foo', function() {
var template = CompilerContext.compile('{{hello noun=@world}}');
var helpers = {
hello: function(options) {
return "Hello " + options.hash.noun;
return 'Hello ' + options.hash.noun;
}
};
var result = template({}, { helpers: helpers, data: { world: "world" } });
equals("Hello world", result, "@foo as a parameter retrieves template data");
var result = template({}, { helpers: helpers, data: { world: 'world' } });
equals('Hello world', result, '@foo as a parameter retrieves template data');
});
it("nested parameter data can be looked up via @foo.bar", function() {
var template = CompilerContext.compile("{{hello @world.bar}}");
it('nested parameter data can be looked up via @foo.bar', function() {
var template = CompilerContext.compile('{{hello @world.bar}}');
var helpers = {
hello: function(noun) {
return "Hello " + noun;
return 'Hello ' + noun;
}
};
var result = template({}, { helpers: helpers, data: { world: {bar: "world" } } });
equals("Hello world", result, "@foo as a parameter retrieves template data");
var result = template({}, { helpers: helpers, data: { world: {bar: 'world' } } });
equals('Hello world', result, '@foo as a parameter retrieves template data');
});
it("nested parameter data does not fail with @world.bar", function() {
var template = CompilerContext.compile("{{hello @world.bar}}");
it('nested parameter data does not fail with @world.bar', function() {
var template = CompilerContext.compile('{{hello @world.bar}}');
var helpers = {
hello: function(noun) {
return "Hello " + noun;
return 'Hello ' + noun;
}
};
var result = template({}, { helpers: helpers, data: { foo: {bar: "world" } } });
equals("Hello undefined", result, "@foo as a parameter retrieves template data");
var result = template({}, { helpers: helpers, data: { foo: {bar: 'world' } } });
equals('Hello undefined', result, '@foo as a parameter retrieves template data');
});
it("parameter data throws when using complex scope references", function() {
var string = "{{#goodbyes}}{{text}} cruel {{@foo/../name}}! {{/goodbyes}}";
it('parameter data throws when using complex scope references', function() {
var string = '{{#goodbyes}}{{text}} cruel {{@foo/../name}}! {{/goodbyes}}';
shouldThrow(function() {
CompilerContext.compile(string);
@@ -104,131 +105,133 @@ describe('data', function() {
equals('hello', result);
});
it("data is inherited downstream", function() {
var template = CompilerContext.compile("{{#let foo=1 bar=2}}{{#let foo=bar.baz}}{{@bar}}{{@foo}}{{/let}}{{@foo}}{{/let}}", { data: true });
it('data is inherited downstream', function() {
var template = CompilerContext.compile('{{#let foo=1 bar=2}}{{#let foo=bar.baz}}{{@bar}}{{@foo}}{{/let}}{{@foo}}{{/let}}', { data: true });
var helpers = {
let: function(options) {
var frame = Handlebars.createFrame(options.data);
for (var prop in options.hash) {
if (prop in options.hash) {
frame[prop] = options.hash[prop];
}
}
return options.fn(this, {data: frame});
}
};
var result = template({ bar: { baz: "hello world" } }, { helpers: helpers, data: {} });
equals("2hello world1", result, "data variables are inherited downstream");
var result = template({ bar: { baz: 'hello world' } }, { helpers: helpers, data: {} });
equals('2hello world1', result, 'data variables are inherited downstream');
});
it("passing in data to a compiled function that expects data - works with helpers in partials", function() {
var template = CompilerContext.compile("{{>my_partial}}", {data: true});
it('passing in data to a compiled function that expects data - works with helpers in partials', function() {
var template = CompilerContext.compile('{{>myPartial}}', {data: true});
var partials = {
my_partial: CompilerContext.compile("{{hello}}", {data: true})
myPartial: CompilerContext.compile('{{hello}}', {data: true})
};
var helpers = {
hello: function(options) {
return options.data.adjective + " " + this.noun;
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");
var result = template({noun: 'cat'}, {helpers: helpers, partials: partials, data: {adjective: 'happy'}});
equals('happy cat', result, 'Data output by helper inside partial');
});
it("passing in data to a compiled function that expects data - works with helpers and parameters", function() {
var template = CompilerContext.compile("{{hello world}}", {data: true});
it('passing in data to a compiled function that expects data - works with helpers and parameters', function() {
var template = CompilerContext.compile('{{hello world}}', {data: true});
var helpers = {
hello: function(noun, options) {
return options.data.adjective + " " + noun + (this.exclaim ? "!" : "");
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");
var result = template({exclaim: true, world: 'world'}, {helpers: helpers, data: {adjective: 'happy'}});
equals('happy world!', result, 'Data output by helper');
});
it("passing in data to a compiled function that expects data - works with block helpers", function() {
var template = CompilerContext.compile("{{#hello}}{{world}}{{/hello}}", {data: true});
it('passing in data to a compiled function that expects data - works with block helpers', function() {
var template = CompilerContext.compile('{{#hello}}{{world}}{{/hello}}', {data: true});
var helpers = {
hello: function(options) {
return options.fn(this);
},
world: function(options) {
return options.data.adjective + " world" + (this.exclaim ? "!" : "");
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");
var result = template({exclaim: true}, {helpers: helpers, data: {adjective: 'happy'}});
equals('happy world!', result, 'Data output by helper');
});
it("passing in data to a compiled function that expects data - works with block helpers that use ..", function() {
var template = CompilerContext.compile("{{#hello}}{{world ../zomg}}{{/hello}}", {data: true});
it('passing in data to a compiled function that expects data - works with block helpers that use ..', function() {
var template = CompilerContext.compile('{{#hello}}{{world ../zomg}}{{/hello}}', {data: true});
var helpers = {
hello: function(options) {
return options.fn({exclaim: "?"});
return options.fn({exclaim: '?'});
},
world: function(thing, options) {
return options.data.adjective + " " + thing + (this.exclaim || "");
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");
var result = template({exclaim: true, zomg: 'world'}, {helpers: helpers, data: {adjective: 'happy'}});
equals('happy world?', result, 'Data output by helper');
});
it("passing in data to a compiled function that expects data - data is passed to with block helpers where children use ..", function() {
var template = CompilerContext.compile("{{#hello}}{{world ../zomg}}{{/hello}}", {data: true});
it('passing in data to a compiled function that expects data - data is passed to with block helpers where children use ..', function() {
var template = CompilerContext.compile('{{#hello}}{{world ../zomg}}{{/hello}}', {data: true});
var helpers = {
hello: function(options) {
return options.data.accessData + " " + options.fn({exclaim: "?"});
return options.data.accessData + ' ' + options.fn({exclaim: '?'});
},
world: function(thing, options) {
return options.data.adjective + " " + thing + (this.exclaim || "");
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");
var result = template({exclaim: true, zomg: 'world'}, {helpers: helpers, data: {adjective: 'happy', accessData: '#win'}});
equals('#win happy world?', result, 'Data output by helper');
});
it("you can override inherited data when invoking a helper", function() {
var template = CompilerContext.compile("{{#hello}}{{world zomg}}{{/hello}}", {data: true});
it('you can override inherited data when invoking a helper', function() {
var template = CompilerContext.compile('{{#hello}}{{world zomg}}{{/hello}}', {data: true});
var helpers = {
hello: function(options) {
return options.fn({exclaim: "?", zomg: "world"}, { data: {adjective: "sad"} });
return options.fn({exclaim: '?', zomg: 'world'}, { data: {adjective: 'sad'} });
},
world: function(thing, options) {
return options.data.adjective + " " + thing + (this.exclaim || "");
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");
var result = template({exclaim: true, zomg: 'planet'}, {helpers: helpers, data: {adjective: 'happy'}});
equals('sad world?', result, 'Overriden data output by helper');
});
it("you can override inherited data when invoking a helper with depth", function() {
var template = CompilerContext.compile("{{#hello}}{{world ../zomg}}{{/hello}}", {data: true});
it('you can override inherited data when invoking a helper with depth', function() {
var template = CompilerContext.compile('{{#hello}}{{world ../zomg}}{{/hello}}', {data: true});
var helpers = {
hello: function(options) {
return options.fn({exclaim: "?"}, { data: {adjective: "sad"} });
return options.fn({exclaim: '?'}, { data: {adjective: 'sad'} });
},
world: function(thing, options) {
return options.data.adjective + " " + thing + (this.exclaim || "");
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");
var result = template({exclaim: true, zomg: 'world'}, {helpers: helpers, data: {adjective: 'happy'}});
equals('sad world?', result, 'Overriden data output by helper');
});
describe('@root', function() {
+9 -4
View File
@@ -1,12 +1,15 @@
/*global handlebarsEnv */
require('./common');
var _ = require('underscore'),
fs = require('fs'),
var fs = require('fs'),
vm = require('vm');
global.Handlebars = 'no-conflict';
vm.runInThisContext(fs.readFileSync(__dirname + '/../../dist/handlebars.js'), 'dist/handlebars.js');
var filename = 'dist/handlebars.js';
if (global.minimizedTest) {
filename = 'dist/handlebars.min.js';
}
vm.runInThisContext(fs.readFileSync(__dirname + '/../../' + filename), filename);
global.CompilerContext = {
browser: true,
@@ -21,10 +24,12 @@ global.CompilerContext = {
};
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 */
}
+30 -12
View File
@@ -1,12 +1,27 @@
/*global CompilerContext, compileWithPartials, shouldCompileToWithPartials */
var AssertError;
if (Error.captureStackTrace) {
AssertError = function AssertError(message, caller) {
Error.prototype.constructor.call(this, message);
this.message = message;
if (Error.captureStackTrace) {
Error.captureStackTrace(this, caller || AssertError);
}
};
AssertError.prototype = new Error();
} else {
AssertError = Error;
}
global.shouldCompileTo = function(string, hashOrArray, expected, message) {
shouldCompileToWithPartials(string, hashOrArray, false, expected, message);
};
global.shouldCompileToWithPartials = function(string, hashOrArray, partials, expected, message) {
global.shouldCompileToWithPartials = function shouldCompileToWithPartials(string, hashOrArray, partials, expected, message) {
var result = compileWithPartials(string, hashOrArray, partials);
if (result !== expected) {
throw new Error("'" + result + "' should === '" + expected + "': " + message);
throw new AssertError("'" + result + "' should === '" + expected + "': " + message, shouldCompileToWithPartials);
}
};
@@ -14,7 +29,10 @@ global.compileWithPartials = function(string, hashOrArray, partials) {
var template,
ary,
options;
if(Object.prototype.toString.call(hashOrArray) === "[object Array]") {
if (hashOrArray && hashOrArray.hash) {
ary = [hashOrArray.hash, hashOrArray];
delete hashOrArray.hash;
} else if (Object.prototype.toString.call(hashOrArray) === '[object Array]') {
ary = [];
ary.push(hashOrArray[0]);
ary.push({ helpers: hashOrArray[1], partials: hashOrArray[2] });
@@ -32,9 +50,9 @@ global.compileWithPartials = function(string, hashOrArray, partials) {
};
global.equals = global.equal = function(a, b, msg) {
global.equals = global.equal = function equals(a, b, msg) {
if (a !== b) {
throw new Error("'" + a + "' should === '" + b + "'" + (msg ? ": " + msg : ''));
throw new AssertError("'" + a + "' should === '" + b + "'" + (msg ? ': ' + msg : ''), equals);
}
};
@@ -43,15 +61,15 @@ global.shouldThrow = function(callback, type, msg) {
try {
callback();
failed = true;
} catch (err) {
if (type && !(err instanceof type)) {
throw new Error('Type failure: ' + err);
} catch (caught) {
if (type && !(caught instanceof type)) {
throw new AssertError('Type failure: ' + caught);
}
if (msg && !(msg.test ? msg.test(err.message) : msg === err.message)) {
equal(msg, err.message);
if (msg && !(msg.test ? msg.test(caught.message) : msg === caught.message)) {
throw new AssertError('Throw mismatch: Expected ' + caught.message + ' to match ' + msg + '\n\n' + caught.stack, shouldThrow);
}
}
if (failed) {
throw new Error('It failed to throw');
throw new AssertError('It failed to throw', shouldThrow);
}
};
+2 -1
View File
@@ -1,4 +1,3 @@
/*global handlebarsEnv */
require('./common');
global.Handlebars = require('../../lib');
@@ -14,10 +13,12 @@ global.CompilerContext = {
};
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 */
}
+20 -3
View File
@@ -1,3 +1,4 @@
/* eslint-disable no-console */
var fs = require('fs'),
Mocha = require('mocha'),
path = require('path');
@@ -6,19 +7,35 @@ var errors = 0,
testDir = path.dirname(__dirname),
grep = process.argv[2];
var files = [ testDir + "/basic.js" ];
// Lazy hack, but whatever
if (grep === '--min') {
global.minimizedTest = true;
grep = undefined;
}
var files = fs.readdirSync(testDir)
.filter(function(name) { return (/.*\.js$/).test(name); })
.map(function(name) { return testDir + '/' + name; });
run('./node', function() {
run('./browser', function() {
if (global.minimizedTest) {
run('./runtime', function() {
run('./browser', function() {
/* eslint-disable no-process-exit */
process.exit(errors);
/* eslint-enable no-process-exit */
});
});
} else {
run('./runtime', function() {
run('./browser', function() {
run('./node', function() {
/* eslint-disable no-process-exit */
process.exit(errors);
/* eslint-enable no-process-exit */
});
});
});
}
function run(env, callback) {
+14 -9
View File
@@ -1,24 +1,27 @@
/*global handlebarsEnv */
require('./common');
var _ = require('underscore'),
fs = require('fs'),
var fs = require('fs'),
vm = require('vm');
global.Handlebars = 'no-conflict';
vm.runInThisContext(fs.readFileSync(__dirname + '/../../dist/handlebars.runtime.js'), 'dist/handlebars.runtime.js');
var filename = 'dist/handlebars.runtime.js';
if (global.minimizedTest) {
filename = 'dist/handlebars.runtime.min.js';
}
vm.runInThisContext(fs.readFileSync(__dirname + '/../../' + filename), filename);
var parse = require('../../dist/cjs/handlebars/compiler/base').parse;
var compiler = require('../../dist/cjs/handlebars/compiler/compiler');
var JavaScriptCompiler = require('../../dist/cjs/handlebars/compiler/javascript-compiler')['default'];
var JavaScriptCompiler = require('../../dist/cjs/handlebars/compiler/javascript-compiler');
global.CompilerContext = {
browser: true,
compile: function(template, options) {
// Hack the compiler on to the environment for these specific tests
handlebarsEnv.precompile = function(template, options) {
return compiler.precompile(template, options, handlebarsEnv);
handlebarsEnv.precompile = function(precompileTemplate, precompileOptions) {
return compiler.precompile(precompileTemplate, precompileOptions, handlebarsEnv);
};
handlebarsEnv.parse = parse;
handlebarsEnv.Compiler = compiler.Compiler;
@@ -29,8 +32,8 @@ global.CompilerContext = {
},
compileWithPartial: function(template, options) {
// Hack the compiler on to the environment for these specific tests
handlebarsEnv.compile = function(template, options) {
return compiler.compile(template, options, handlebarsEnv);
handlebarsEnv.compile = function(compileTemplate, compileOptions) {
return compiler.compile(compileTemplate, compileOptions, handlebarsEnv);
};
handlebarsEnv.parse = parse;
handlebarsEnv.Compiler = compiler.Compiler;
@@ -41,10 +44,12 @@ global.CompilerContext = {
};
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 */
}
+1 -1
View File
@@ -1,6 +1,6 @@
define(['handlebars.runtime'], function(Handlebars) {
Handlebars = Handlebars["default"]; var template = Handlebars.template, templates = Handlebars.templates = Handlebars.templates || {};
return templates['empty'] = template({"compiler":[6,">= 2.0.0-beta.1"],"main":function(depth0,helpers,partials,data) {
return templates['empty'] = template({"compiler":[7,">= 4.0.0"],"main":function(container,depth0,helpers,partials,data) {
return "";
},"useData":true});
});
+265 -245
View File
@@ -1,165 +1,187 @@
/*global CompilerContext, Handlebars, shouldCompileTo, shouldCompileToWithPartials, shouldThrow, handlebarsEnv */
describe('helpers', function() {
it("helper with complex lookup$", function() {
var string = "{{#goodbyes}}{{{link ../prefix}}}{{/goodbyes}}";
var hash = {prefix: "/root", goodbyes: [{text: "Goodbye", url: "goodbye"}]};
it('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 "<a href='" + prefix + "/" + this.url + "'>" + this.text + "</a>";
return '<a href="' + prefix + '/' + this.url + '">' + this.text + '</a>';
}};
shouldCompileTo(string, [hash, helpers], "<a href='/root/goodbye'>Goodbye</a>");
shouldCompileTo(string, [hash, helpers], '<a href="/root/goodbye">Goodbye</a>');
});
it("helper for raw block gets raw content", function() {
var string = "{{{{raw}}}} {{test}} {{{{/raw}}}}";
var hash = { test: "hello" };
it('helper for raw block gets raw content', function() {
var string = '{{{{raw}}}} {{test}} {{{{/raw}}}}';
var hash = { test: 'hello' };
var helpers = { raw: function(options) {
return options.fn();
} };
shouldCompileTo(string, [hash, helpers], " {{test}} ",
"raw block helper gets raw content");
shouldCompileTo(string, [hash, helpers], ' {{test}} ',
'raw block helper gets raw content');
});
it("helper for raw block gets parameters", function() {
var string = "{{{{raw 1 2 3}}}} {{test}} {{{{/raw}}}}";
var hash = { test: "hello" };
it('helper for raw block gets parameters', function() {
var string = '{{{{raw 1 2 3}}}} {{test}} {{{{/raw}}}}';
var hash = { test: 'hello' };
var helpers = { raw: function(a, b, c, options) {
return options.fn() + a + b + c;
} };
shouldCompileTo(string, [hash, helpers], " {{test}} 123",
"raw block helper gets raw content");
shouldCompileTo(string, [hash, helpers], ' {{test}} 123',
'raw block helper gets raw content');
});
it("helper block with complex lookup expression", function() {
var string = "{{#goodbyes}}{{../name}}{{/goodbyes}}";
var hash = {name: "Alan"};
it('helper for nested raw block gets raw content', function() {
var string = '{{{{a}}}} {{{{b}}}} {{{{/b}}}} {{{{/a}}}}';
var helpers = {
a: function(options) {
return options.fn();
}
};
shouldCompileTo(string, [{}, helpers], ' {{{{b}}}} {{{{/b}}}} ', 'raw block helper should get nested raw block as raw content');
});
it('helper block with identical context', function() {
var string = '{{#goodbyes}}{{name}}{{/goodbyes}}';
var hash = {name: 'Alan'};
var helpers = {goodbyes: function(options) {
var out = "";
var byes = ["Goodbye", "goodbye", "GOODBYE"];
var out = '';
var byes = ['Goodbye', 'goodbye', 'GOODBYE'];
for (var i = 0, j = byes.length; i < j; i++) {
out += byes[i] + " " + options.fn(this) + "! ";
out += byes[i] + ' ' + options.fn(this) + '! ';
}
return out;
}};
shouldCompileTo(string, [hash, helpers], "Goodbye Alan! goodbye Alan! GOODBYE Alan! ");
shouldCompileTo(string, [hash, helpers], 'Goodbye Alan! goodbye Alan! GOODBYE Alan! ');
});
it('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({}) + '! ';
}
return out;
}};
shouldCompileTo(string, [hash, helpers], 'Goodbye Alan! goodbye Alan! GOODBYE Alan! ');
});
it("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"}]};
it('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 "<a href='" + prefix + "/" + this.url + "'>" + options.fn(this) + "</a>";
return '<a href="' + prefix + '/' + this.url + '">' + options.fn(this) + '</a>';
}};
shouldCompileToWithPartials(string, [hash, helpers], false, "<a href='/root/goodbye'>Goodbye</a>");
shouldCompileToWithPartials(string, [hash, helpers], false, '<a href="/root/goodbye">Goodbye</a>');
});
it("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"}]};
it('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 "<a href='" + prefix + "/" + this.url + "'>" + options.fn(this) + "</a>";
return '<a href="' + prefix + '/' + this.url + '">' + options.fn(this) + '</a>';
}};
shouldCompileToWithPartials(string, [hash, helpers], true, "<a href='/root/goodbye'>Goodbye</a>");
shouldCompileToWithPartials(string, [hash, helpers], true, '<a href="/root/goodbye">Goodbye</a>');
});
it('helper returning undefined value', function() {
shouldCompileTo(' {{nothere}}', [{}, {nothere: function() {}}], ' ');
shouldCompileTo(' {{#nothere}}{{/nothere}}', [{}, {nothere: function() {}}], ' ');
});
it("block helper", function() {
var string = "{{#goodbyes}}{{text}}! {{/goodbyes}}cruel {{world}}!";
it('block helper', function() {
var string = '{{#goodbyes}}{{text}}! {{/goodbyes}}cruel {{world}}!';
var template = CompilerContext.compile(string);
var result = template({world: "world"}, { helpers: {goodbyes: function(options) { return options.fn({text: "GOODBYE"}); }}});
equal(result, "GOODBYE! cruel world!", "Block helper executed");
var result = template({world: 'world'}, { helpers: {goodbyes: function(options) { return options.fn({text: 'GOODBYE'}); }}});
equal(result, 'GOODBYE! cruel world!', 'Block helper executed');
});
it("block helper staying in the same context", function() {
var string = "{{#form}}<p>{{name}}</p>{{/form}}";
it('block helper staying in the same context', function() {
var string = '{{#form}}<p>{{name}}</p>{{/form}}';
var template = CompilerContext.compile(string);
var result = template({name: "Yehuda"}, {helpers: {form: function(options) { return "<form>" + options.fn(this) + "</form>"; } }});
equal(result, "<form><p>Yehuda</p></form>", "Block helper executed with current context");
var result = template({name: 'Yehuda'}, {helpers: {form: function(options) { return '<form>' + options.fn(this) + '</form>'; } }});
equal(result, '<form><p>Yehuda</p></form>', 'Block helper executed with current context');
});
it("block helper should have context in this", function() {
var source = "<ul>{{#people}}<li>{{#link}}{{name}}{{/link}}</li>{{/people}}</ul>";
var link = function(options) {
it('block helper should have context in this', function() {
var source = '<ul>{{#people}}<li>{{#link}}{{name}}{{/link}}</li>{{/people}}</ul>';
function link(options) {
return '<a href="/people/' + this.id + '">' + options.fn(this) + '</a>';
};
var data = { "people": [
{ "name": "Alan", "id": 1 },
{ "name": "Yehuda", "id": 2 }
}
var data = { 'people': [
{ 'name': 'Alan', 'id': 1 },
{ 'name': 'Yehuda', 'id': 2 }
]};
shouldCompileTo(source, [data, {link: link}], "<ul><li><a href=\"/people/1\">Alan</a></li><li><a href=\"/people/2\">Yehuda</a></li></ul>");
shouldCompileTo(source, [data, {link: link}], '<ul><li><a href=\"/people/1\">Alan</a></li><li><a href=\"/people/2\">Yehuda</a></li></ul>');
});
it("block helper for undefined value", function() {
shouldCompileTo("{{#empty}}shouldn't render{{/empty}}", {}, "");
it('block helper for undefined value', function() {
shouldCompileTo("{{#empty}}shouldn't render{{/empty}}", {}, '');
});
it("block helper passing a new context", function() {
var string = "{{#form yehuda}}<p>{{name}}</p>{{/form}}";
it('block helper passing a new context', function() {
var string = '{{#form yehuda}}<p>{{name}}</p>{{/form}}';
var template = CompilerContext.compile(string);
var result = template({yehuda: {name: "Yehuda"}}, { helpers: {form: function(context, options) { return "<form>" + options.fn(context) + "</form>"; }}});
equal(result, "<form><p>Yehuda</p></form>", "Context variable resolved");
var result = template({yehuda: {name: 'Yehuda'}}, { helpers: {form: function(context, options) { return '<form>' + options.fn(context) + '</form>'; }}});
equal(result, '<form><p>Yehuda</p></form>', 'Context variable resolved');
});
it("block helper passing a complex path context", function() {
var string = "{{#form yehuda/cat}}<p>{{name}}</p>{{/form}}";
it('block helper passing a complex path context', function() {
var string = '{{#form yehuda/cat}}<p>{{name}}</p>{{/form}}';
var template = CompilerContext.compile(string);
var result = template({yehuda: {name: "Yehuda", cat: {name: "Harold"}}}, { helpers: {form: function(context, options) { return "<form>" + options.fn(context) + "</form>"; }}});
equal(result, "<form><p>Harold</p></form>", "Complex path variable resolved");
var result = template({yehuda: {name: 'Yehuda', cat: {name: 'Harold'}}}, { helpers: {form: function(context, options) { return '<form>' + options.fn(context) + '</form>'; }}});
equal(result, '<form><p>Harold</p></form>', 'Complex path variable resolved');
});
it("nested block helpers", function() {
var string = "{{#form yehuda}}<p>{{name}}</p>{{#link}}Hello{{/link}}{{/form}}";
it('nested block helpers', function() {
var string = '{{#form yehuda}}<p>{{name}}</p>{{#link}}Hello{{/link}}{{/form}}';
var template = CompilerContext.compile(string);
var result = template({
yehuda: {name: "Yehuda" }
yehuda: {name: 'Yehuda' }
}, {
helpers: {
link: function(options) { return "<a href='" + this.name + "'>" + options.fn(this) + "</a>"; },
form: function(context, options) { return "<form>" + options.fn(context) + "</form>"; }
link: function(options) { return '<a href="' + this.name + '">' + options.fn(this) + '</a>'; },
form: function(context, options) { return '<form>' + options.fn(context) + '</form>'; }
}
});
equal(result, "<form><p>Yehuda</p><a href='Yehuda'>Hello</a></form>", "Both blocks executed");
equal(result, '<form><p>Yehuda</p><a href="Yehuda">Hello</a></form>', 'Both blocks executed');
});
it("block helper inverted sections", function() {
var string = "{{#list people}}{{name}}{{^}}<em>Nobody's here</em>{{/list}}";
var list = function(context, options) {
it('block helper inverted sections', function() {
var string = '{{#list people}}{{name}}{{^}}<em>Nobody\'s here</em>{{/list}}';
function list(context, options) {
if (context.length > 0) {
var out = "<ul>";
var out = '<ul>';
for (var i = 0, j = context.length; i < j; i++) {
out += "<li>";
out += '<li>';
out += options.fn(context[i]);
out += "</li>";
out += '</li>';
}
out += "</ul>";
out += '</ul>';
return out;
} else {
return "<p>" + options.inverse(this) + "</p>";
return '<p>' + options.inverse(this) + '</p>';
}
}
};
var hash = {people: [{name: "Alan"}, {name: "Yehuda"}]};
var hash = {people: [{name: 'Alan'}, {name: 'Yehuda'}]};
var empty = {people: []};
var rootMessage = {
people: [],
message: "Nobody's here"
message: 'Nobody\'s here'
};
var messageString = "{{#list people}}Hello{{^}}{{message}}{{/list}}";
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 }], "<ul><li>Alan</li><li>Yehuda</li></ul>", "an inverse wrapper is passed in as a new context");
shouldCompileTo(string, [empty, { list: list }], "<p><em>Nobody's here</em></p>", "an inverse wrapper can be optionally called");
shouldCompileTo(messageString, [rootMessage, { list: list }], "<p>Nobody&#x27;s here</p>", "the context of an inverse is the parent of the block");
shouldCompileTo(string, [hash, { list: list }], '<ul><li>Alan</li><li>Yehuda</li></ul>', 'an inverse wrapper is passed in as a new context');
shouldCompileTo(string, [empty, { list: list }], '<p><em>Nobody\'s here</em></p>', 'an inverse wrapper can be optionally called');
shouldCompileTo(messageString, [rootMessage, { list: list }], '<p>Nobody&#x27;s here</p>', 'the context of an inverse is the parent of the block');
});
it('pathed lambas with parameters', function() {
@@ -178,43 +200,47 @@ describe('helpers', function() {
shouldCompileTo('{{hash/helper 1}}', [hash, helpers], 'winning');
});
describe("helpers hash", function() {
it("providing a helpers hash", function() {
shouldCompileTo("Goodbye {{cruel}} {{world}}!", [{cruel: "cruel"}, {world: function() { return "world"; }}], "Goodbye cruel world!",
"helpers hash is available");
describe('helpers hash', function() {
it('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");
shouldCompileTo('Goodbye {{#iter}}{{cruel}} {{world}}{{/iter}}!', [{iter: [{cruel: 'cruel'}]}, {world: function() { return 'world'; }}],
'Goodbye cruel world!', 'helpers hash is available inside other blocks');
});
it("in cases of conflict, helpers win", function() {
shouldCompileTo("{{{lookup}}}", [{lookup: 'Explicit'}, {lookup: function() { return 'helpers'; }}], "helpers",
"helpers hash has precedence escaped expansion");
shouldCompileTo("{{lookup}}", [{lookup: 'Explicit'}, {lookup: function() { return 'helpers'; }}], "helpers",
"helpers hash has precedence simple expansion");
it('in cases of conflict, helpers win', function() {
shouldCompileTo('{{{lookup}}}', [{lookup: 'Explicit'}, {lookup: function() { return 'helpers'; }}], 'helpers',
'helpers hash has precedence escaped expansion');
shouldCompileTo('{{lookup}}', [{lookup: 'Explicit'}, {lookup: function() { return 'helpers'; }}], 'helpers',
'helpers hash has precedence simple expansion');
});
it("the helpers hash is available is nested contexts", function() {
shouldCompileTo("{{#outer}}{{#inner}}{{helper}}{{/inner}}{{/outer}}",
[{'outer': {'inner': {'unused':[]}}}, {'helper': function() { return 'helper'; }}], "helper",
"helpers hash is available in nested contexts.");
it('the helpers hash is available is nested contexts', function() {
shouldCompileTo(
'{{#outer}}{{#inner}}{{helper}}{{/inner}}{{/outer}}',
[
{'outer': {'inner': {'unused': []}}},
{'helper': function() { return 'helper'; }}
],
'helper',
'helpers hash is available in nested contexts.');
});
it("the helper hash should augment the global hash", function() {
it('the helper hash should augment the global hash', function() {
handlebarsEnv.registerHelper('test_helper', function() { return 'found it!'; });
shouldCompileTo(
"{{test_helper}} {{#if cruel}}Goodbye {{cruel}} {{world}}!{{/if}}", [
{cruel: "cruel"},
{world: function() { return "world!"; }}
'{{test_helper}} {{#if cruel}}Goodbye {{cruel}} {{world}}!{{/if}}', [
{cruel: 'cruel'},
{world: function() { return 'world!'; }}
],
"found it! Goodbye cruel world!!");
'found it! Goodbye cruel world!!');
});
});
describe('registration', function() {
it('unregisters', function() {
var helpers = handlebarsEnv.helpers;
handlebarsEnv.helpers = {};
handlebarsEnv.registerHelper('foo', function() {
@@ -230,134 +256,128 @@ describe('helpers', function() {
handlebarsEnv.registerHelper({
'if': helpers['if'],
world: function() { return "world!"; },
test_helper: function() { return 'found it!'; }
world: function() { return 'world!'; },
testHelper: function() { return 'found it!'; }
});
shouldCompileTo(
"{{test_helper}} {{#if cruel}}Goodbye {{cruel}} {{world}}!{{/if}}",
[{cruel: "cruel"}],
"found it! Goodbye cruel world!!");
'{{testHelper}} {{#if cruel}}Goodbye {{cruel}} {{world}}!{{/if}}',
[{cruel: 'cruel'}],
'found it! Goodbye cruel world!!');
});
it('fails with multiple and args', function() {
shouldThrow(function() {
handlebarsEnv.registerHelper({
world: function() { return "world!"; },
test_helper: function() { return 'found it!'; }
world: function() { return 'world!'; },
testHelper: function() { return 'found it!'; }
}, {});
}, Error, 'Arg not supported with multiple helpers');
});
});
it("decimal number literals work", function() {
it('decimal number literals work', function() {
var string = 'Message: {{hello -1.2 1.2}}';
var hash = {};
var helpers = {hello: function(times, times2) {
if(typeof times !== 'number') { times = "NaN"; }
if(typeof times2 !== 'number') { times2 = "NaN"; }
return "Hello " + times + " " + times2 + " times";
if (typeof times !== 'number') { times = 'NaN'; }
if (typeof times2 !== 'number') { times2 = 'NaN'; }
return 'Hello ' + times + ' ' + times2 + ' times';
}};
shouldCompileTo(string, [hash, helpers], "Message: Hello -1.2 1.2 times", "template with a negative integer literal");
shouldCompileTo(string, [{}, helpers], 'Message: Hello -1.2 1.2 times', 'template with a negative integer literal');
});
it("negative number literals work", function() {
it('negative number literals work', function() {
var string = 'Message: {{hello -12}}';
var hash = {};
var helpers = {hello: function(times) {
if(typeof times !== 'number') { times = "NaN"; }
return "Hello " + times + " times";
if (typeof times !== 'number') { times = 'NaN'; }
return 'Hello ' + times + ' times';
}};
shouldCompileTo(string, [hash, helpers], "Message: Hello -12 times", "template with a negative integer literal");
shouldCompileTo(string, [{}, helpers], 'Message: Hello -12 times', 'template with a negative integer literal');
});
describe("String literal parameters", function() {
it("simple literals work", function() {
describe('String literal parameters', function() {
it('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;
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");
shouldCompileTo(string, [{}, helpers], 'Message: Hello world 12 times: true false', 'template with a simple String literal');
});
it("using a quote in the middle of a parameter raises an error", function() {
it('using a quote in the middle of a parameter raises an error', function() {
var string = 'Message: {{hello wo"rld"}}';
shouldThrow(function() {
CompilerContext.compile(string);
}, Error);
});
it("escaping a String is possible", function(){
it('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");
var helpers = {hello: function(param) { return 'Hello ' + param; }};
shouldCompileTo(string, [{}, helpers], 'Message: Hello "world"', 'template with an escaped String literal');
});
it("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");
var helpers = {hello: function(param) { return 'Hello ' + param; }};
shouldCompileTo(string, [{}, helpers], "Message: Hello Alan's world", "template with a ' mark");
});
});
it("negative number literals work", function() {
it('negative number literals work', function() {
var string = 'Message: {{hello -12}}';
var hash = {};
var helpers = {hello: function(times) {
if(typeof times !== 'number') { times = "NaN"; }
return "Hello " + times + " times";
if (typeof times !== 'number') { times = 'NaN'; }
return 'Hello ' + times + ' times';
}};
shouldCompileTo(string, [hash, helpers], "Message: Hello -12 times", "template with a negative integer literal");
shouldCompileTo(string, [{}, helpers], 'Message: Hello -12 times', 'template with a negative integer literal');
});
describe("multiple parameters", function() {
it("simple multi-params work", function() {
describe('multiple parameters', function() {
it('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");
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');
});
it("block multi-params work", function() {
it('block multi-params work', function() {
var string = 'Message: {{#goodbye cruel world}}{{greeting}} {{adj}} {{noun}}{{/goodbye}}';
var hash = {cruel: "cruel", world: "world"};
var hash = {cruel: 'cruel', world: 'world'};
var helpers = {goodbye: function(cruel, world, options) {
return options.fn({greeting: "Goodbye", adj: cruel, noun: world});
return options.fn({greeting: 'Goodbye', adj: cruel, noun: world});
}};
shouldCompileTo(string, [hash, helpers], "Message: Goodbye cruel world", "block helpers with multiple params");
shouldCompileTo(string, [hash, helpers], 'Message: Goodbye cruel world', 'block helpers with multiple params');
});
});
describe('hash', function() {
it("helpers can take an optional hash", function() {
it('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";
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");
equals(result, 'GOODBYE CRUEL WORLD 12 TIMES', 'Helper output hash');
});
it("helpers can take an optional hash with booleans", function() {
it('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;
return 'GOODBYE ' + options.hash.cruel + ' ' + options.hash.world;
} else if (options.hash.print === false) {
return "NOT PRINTING";
return 'NOT PRINTING';
} else {
return "THIS SHOULD NOT HAPPEN";
return 'THIS SHOULD NOT HAPPEN';
}
}
};
@@ -366,161 +386,161 @@ describe('helpers', function() {
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");
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");
equals(result, 'NOT PRINTING', 'Boolean helper parameter honored');
});
it("block helpers can take an optional hash", function() {
it('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";
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");
equals(result, 'GOODBYE CRUEL world 12 TIMES', 'Hash parameters output');
});
it("block helpers can take an optional hash with single quoted stings", function() {
var template = CompilerContext.compile("{{#goodbye cruel='CRUEL' times=12}}world{{/goodbye}}");
it('block helpers can take an optional hash with single quoted stings', 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";
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");
equals(result, 'GOODBYE CRUEL world 12 TIMES', 'Hash parameters output');
});
it("block helpers can take an optional hash with booleans", function() {
it('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);
return 'GOODBYE ' + options.hash.cruel + ' ' + options.fn(this);
} else if (options.hash.print === false) {
return "NOT PRINTING";
return 'NOT PRINTING';
} else {
return "THIS SHOULD NOT HAPPEN";
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");
equals(result, 'GOODBYE CRUEL world', 'Boolean hash parameter honored');
template = CompilerContext.compile('{{#goodbye cruel="CRUEL" print=false}}world{{/goodbye}}');
result = template({}, {helpers: helpers});
equals(result, "NOT PRINTING", "Boolean hash parameter honored");
equals(result, 'NOT PRINTING', 'Boolean hash parameter honored');
});
});
describe("helperMissing", function() {
it("if a context is not found, helperMissing is used", function() {
describe('helperMissing', function() {
it('if a context is not found, helperMissing is used', function() {
shouldThrow(function() {
var template = CompilerContext.compile("{{hello}} {{link_to world}}");
var template = CompilerContext.compile('{{hello}} {{link_to world}}');
template({});
}, undefined, /Missing helper: 'link_to'/);
}, undefined, /Missing helper: "link_to"/);
});
it("if a context is not found, custom helperMissing is used", function() {
var string = "{{hello}} {{link_to world}}";
var context = { hello: "Hello", world: "world" };
it('if a context is not found, custom helperMissing is used', function() {
var string = '{{hello}} {{link_to world}}';
var context = { hello: 'Hello', world: 'world' };
var helpers = {
helperMissing: function(mesg, options) {
if(options.name === "link_to") {
return new Handlebars.SafeString("<a>" + mesg + "</a>");
if (options.name === 'link_to') {
return new Handlebars.SafeString('<a>' + mesg + '</a>');
}
}
};
shouldCompileTo(string, [context, helpers], "Hello <a>world</a>");
shouldCompileTo(string, [context, helpers], 'Hello <a>world</a>');
});
it("if a value is not found, custom helperMissing is used", function() {
var string = "{{hello}} {{link_to}}";
var context = { hello: "Hello", world: "world" };
it('if a value is not found, custom helperMissing is used', function() {
var string = '{{hello}} {{link_to}}';
var context = { hello: 'Hello', world: 'world' };
var helpers = {
helperMissing: function(options) {
if(options.name === "link_to") {
return new Handlebars.SafeString("<a>winning</a>");
if (options.name === 'link_to') {
return new Handlebars.SafeString('<a>winning</a>');
}
}
};
shouldCompileTo(string, [context, helpers], "Hello <a>winning</a>");
shouldCompileTo(string, [context, helpers], 'Hello <a>winning</a>');
});
});
describe("knownHelpers", function() {
it("Known helper should render helper", function() {
var template = CompilerContext.compile("{{hello}}", {knownHelpers: {"hello" : true}});
describe('knownHelpers', function() {
it('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);
var result = template({}, {helpers: {hello: function() { return 'foo'; }}});
equal(result, 'foo', "'foo' should === '" + result);
});
it("Unknown helper in knownHelpers only mode should be passed as undefined", function() {
var template = CompilerContext.compile("{{typeof hello}}", {knownHelpers: {'typeof': true}, knownHelpersOnly: true});
it('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);
var result = template({}, {helpers: {'typeof': function(arg) { return typeof arg; }, hello: function() { return 'foo'; }}});
equal(result, 'undefined', "'undefined' should === '" + result);
});
it("Builtin helpers available in knownHelpers only mode", function() {
var template = CompilerContext.compile("{{#unless foo}}bar{{/unless}}", {knownHelpersOnly: true});
it('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);
equal(result, 'bar', "'bar' should === '" + result);
});
it("Field lookup works in knownHelpers only mode", function() {
var template = CompilerContext.compile("{{foo}}", {knownHelpersOnly: true});
it('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);
equal(result, 'bar', "'bar' should === '" + result);
});
it("Conditional blocks work in knownHelpers only mode", function() {
var template = CompilerContext.compile("{{#foo}}bar{{/foo}}", {knownHelpersOnly: true});
it('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);
equal(result, 'bar', "'bar' should === '" + result);
});
it("Invert blocks work in knownHelpers only mode", function() {
var template = CompilerContext.compile("{{^foo}}bar{{/foo}}", {knownHelpersOnly: true});
it('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);
equal(result, 'bar', "'bar' should === '" + result);
});
it("Functions are bound to the context in knownHelpers only mode", function() {
var template = CompilerContext.compile("{{foo}}", {knownHelpersOnly: true});
it('Functions are bound to the context in knownHelpers only mode', function() {
var template = CompilerContext.compile('{{foo}}', {knownHelpersOnly: true});
var result = template({foo: function() { return this.bar; }, bar: 'bar'});
equal(result, "bar", "'bar' should === '" + result);
equal(result, 'bar', "'bar' should === '" + result);
});
it("Unknown helper call in knownHelpers only mode should throw", function() {
it('Unknown helper call in knownHelpers only mode should throw', function() {
shouldThrow(function() {
CompilerContext.compile("{{typeof hello}}", {knownHelpersOnly: true});
CompilerContext.compile('{{typeof hello}}', {knownHelpersOnly: true});
}, Error);
});
});
describe("blockHelperMissing", function() {
it("lambdas are resolved by blockHelperMissing, not handlebars proper", function() {
var string = "{{#truthy}}yep{{/truthy}}";
describe('blockHelperMissing', function() {
it('lambdas are resolved by blockHelperMissing, not handlebars proper', function() {
var string = '{{#truthy}}yep{{/truthy}}';
var data = { truthy: function() { return true; } };
shouldCompileTo(string, data, "yep");
shouldCompileTo(string, data, 'yep');
});
it("lambdas resolved by blockHelperMissing are bound to the context", function() {
var string = "{{#truthy}}yep{{/truthy}}";
it('lambdas resolved by blockHelperMissing are bound to the context', function() {
var string = '{{#truthy}}yep{{/truthy}}';
var boundData = { truthy: function() { return this.truthiness(); }, truthiness: function() { return false; } };
shouldCompileTo(string, boundData, "");
shouldCompileTo(string, boundData, '');
});
});
@@ -554,7 +574,7 @@ describe('helpers', function() {
shouldCompileTo('{{#helper 1}}{{/helper}}', [context, helpers], 'ran: helper');
});
it('should include in known helper calls', function() {
var template = CompilerContext.compile("{{helper}}", {knownHelpers: {'helper': true}, knownHelpersOnly: true});
var template = CompilerContext.compile('{{helper}}', {knownHelpers: {'helper': true}, knownHelpersOnly: true});
equal(template({}, {helpers: helpers}), 'ran: helper');
});
@@ -569,8 +589,8 @@ describe('helpers', function() {
});
describe('name conflicts', function() {
it("helpers take precedence over same-named context properties", function() {
var template = CompilerContext.compile("{{goodbye}} {{cruel world}}");
it('helpers take precedence over same-named context properties', function() {
var template = CompilerContext.compile('{{goodbye}} {{cruel world}}');
var helpers = {
goodbye: function() {
@@ -578,21 +598,21 @@ describe('helpers', function() {
},
cruel: function(world) {
return "cruel " + world.toUpperCase();
return 'cruel ' + world.toUpperCase();
}
};
var context = {
goodbye: "goodbye",
world: "world"
goodbye: 'goodbye',
world: 'world'
};
var result = template(context, {helpers: helpers});
equals(result, "GOODBYE cruel WORLD", "Helper executed");
equals(result, 'GOODBYE cruel WORLD', 'Helper executed');
});
it("helpers take precedence over same-named context properties$", function() {
var template = CompilerContext.compile("{{#goodbye}} {{cruel world}}{{/goodbye}}");
it('helpers take precedence over same-named context properties$', function() {
var template = CompilerContext.compile('{{#goodbye}} {{cruel world}}{{/goodbye}}');
var helpers = {
goodbye: function(options) {
@@ -600,21 +620,21 @@ describe('helpers', function() {
},
cruel: function(world) {
return "cruel " + world.toUpperCase();
return 'cruel ' + world.toUpperCase();
}
};
var context = {
goodbye: "goodbye",
world: "world"
goodbye: 'goodbye',
world: 'world'
};
var result = template(context, {helpers: helpers});
equals(result, "GOODBYE cruel WORLD", "Helper executed");
equals(result, 'GOODBYE cruel WORLD', 'Helper executed');
});
it("Scoped names take precedence over helpers", function() {
var template = CompilerContext.compile("{{this.goodbye}} {{cruel world}} {{cruel this.goodbye}}");
it('Scoped names take precedence over helpers', function() {
var template = CompilerContext.compile('{{this.goodbye}} {{cruel world}} {{cruel this.goodbye}}');
var helpers = {
goodbye: function() {
@@ -622,21 +642,21 @@ describe('helpers', function() {
},
cruel: function(world) {
return "cruel " + world.toUpperCase();
return 'cruel ' + world.toUpperCase();
}
};
var context = {
goodbye: "goodbye",
world: "world"
goodbye: 'goodbye',
world: 'world'
};
var result = template(context, {helpers: helpers});
equals(result, "goodbye cruel WORLD cruel GOODBYE", "Helper not executed");
equals(result, 'goodbye cruel WORLD cruel GOODBYE', 'Helper not executed');
});
it("Scoped names take precedence over block helpers", function() {
var template = CompilerContext.compile("{{#goodbye}} {{cruel world}}{{/goodbye}} {{this.goodbye}}");
it('Scoped names take precedence over block helpers', function() {
var template = CompilerContext.compile('{{#goodbye}} {{cruel world}}{{/goodbye}} {{this.goodbye}}');
var helpers = {
goodbye: function(options) {
@@ -644,17 +664,17 @@ describe('helpers', function() {
},
cruel: function(world) {
return "cruel " + world.toUpperCase();
return 'cruel ' + world.toUpperCase();
}
};
var context = {
goodbye: "goodbye",
world: "world"
goodbye: 'goodbye',
world: 'world'
};
var result = template(context, {helpers: helpers});
equals(result, "GOODBYE cruel WORLD goodbye", "Helper executed");
equals(result, 'GOODBYE cruel WORLD goodbye', 'Helper executed');
});
});
+1
View File
@@ -1,3 +1,4 @@
<!doctype html>
<html>
<head>
<title>Mocha</title>
+7 -6
View File
@@ -1,4 +1,3 @@
/*global Handlebars, beforeEach, handlebarsEnv, shouldCompileTo */
describe('javascript-compiler api', function() {
if (!Handlebars.JavaScriptCompiler) {
return;
@@ -17,13 +16,15 @@ describe('javascript-compiler api', function() {
handlebarsEnv.JavaScriptCompiler.prototype.nameLookup = function(parent, name) {
return parent + '.bar_' + name;
};
shouldCompileTo("{{foo}}", { bar_foo: "food" }, "food");
/* eslint-disable camelcase */
shouldCompileTo('{{foo}}', { bar_foo: 'food' }, 'food');
/* eslint-enable camelcase */
});
// Tests nameLookup dot vs. bracket behavior. Bracket is required in certain cases
// to avoid errors in older browsers.
it('should handle reserved words', function() {
shouldCompileTo("{{foo}} {{~null~}}", { foo: "food" }, "food");
shouldCompileTo('{{foo}} {{~null~}}', { foo: 'food' }, 'food');
});
});
describe('#compilerInfo', function() {
@@ -45,7 +46,7 @@ describe('javascript-compiler api', function() {
throw new Error('It didn\'t work');
}
};
shouldCompileTo("{{foo}} ", { foo: "food" }, "food ");
shouldCompileTo('{{foo}} ', { foo: 'food' }, 'food ');
});
});
describe('buffer', function() {
@@ -65,13 +66,13 @@ describe('javascript-compiler api', function() {
handlebarsEnv.JavaScriptCompiler.prototype.initializeBuffer = function() {
return this.quotedString('foo_');
};
shouldCompileTo("{{foo}} ", { foo: "food" }, "foo_food ");
shouldCompileTo('{{foo}} ', { foo: 'food' }, 'foo_food ');
});
it('should allow append buffer override', function() {
handlebarsEnv.JavaScriptCompiler.prototype.appendToBuffer = function(string) {
return $superAppend.call(this, [string, ' + "_foo"']);
};
shouldCompileTo("{{foo}}", { foo: "food" }, "food_foo");
shouldCompileTo('{{foo}}', { foo: 'food' }, 'food_foo');
});
});
});
+139 -73
View File
@@ -1,227 +1,293 @@
/*global Handlebars, shouldThrow */
describe('parser', function() {
if (!Handlebars.print) {
return;
}
function ast_for(template) {
function astFor(template) {
var ast = Handlebars.parse(template);
return Handlebars.print(ast);
}
it('parses simple mustaches', function() {
equals(ast_for('{{123}}'), "{{ NUMBER{123} [] }}\n");
equals(ast_for('{{"foo"}}'), '{{ "foo" [] }}\n');
equals(ast_for('{{false}}'), '{{ BOOLEAN{false} [] }}\n');
equals(ast_for('{{true}}'), '{{ BOOLEAN{true} [] }}\n');
equals(ast_for('{{foo}}'), "{{ PATH:foo [] }}\n");
equals(ast_for('{{foo?}}'), "{{ PATH:foo? [] }}\n");
equals(ast_for('{{foo_}}'), "{{ PATH:foo_ [] }}\n");
equals(ast_for('{{foo-}}'), "{{ PATH:foo- [] }}\n");
equals(ast_for('{{foo:}}'), "{{ PATH:foo: [] }}\n");
equals(astFor('{{123}}'), '{{ NUMBER{123} [] }}\n');
equals(astFor('{{"foo"}}'), '{{ "foo" [] }}\n');
equals(astFor('{{false}}'), '{{ BOOLEAN{false} [] }}\n');
equals(astFor('{{true}}'), '{{ BOOLEAN{true} [] }}\n');
equals(astFor('{{foo}}'), '{{ PATH:foo [] }}\n');
equals(astFor('{{foo?}}'), '{{ PATH:foo? [] }}\n');
equals(astFor('{{foo_}}'), '{{ PATH:foo_ [] }}\n');
equals(astFor('{{foo-}}'), '{{ PATH:foo- [] }}\n');
equals(astFor('{{foo:}}'), '{{ PATH:foo: [] }}\n');
});
it('parses simple mustaches with data', function() {
equals(ast_for("{{@foo}}"), "{{ @PATH:foo [] }}\n");
equals(astFor('{{@foo}}'), '{{ @PATH:foo [] }}\n');
});
it('parses simple mustaches with data paths', function() {
equals(ast_for("{{@../foo}}"), "{{ @PATH:foo [] }}\n");
equals(astFor('{{@../foo}}'), '{{ @PATH:foo [] }}\n');
});
it('parses mustaches with paths', function() {
equals(ast_for("{{foo/bar}}"), "{{ PATH:foo/bar [] }}\n");
equals(astFor('{{foo/bar}}'), '{{ PATH:foo/bar [] }}\n');
});
it('parses mustaches with this/foo', function() {
equals(ast_for("{{this/foo}}"), "{{ PATH:foo [] }}\n");
equals(astFor('{{this/foo}}'), '{{ PATH:foo [] }}\n');
});
it('parses mustaches with - in a path', function() {
equals(ast_for("{{foo-bar}}"), "{{ PATH:foo-bar [] }}\n");
equals(astFor('{{foo-bar}}'), '{{ PATH:foo-bar [] }}\n');
});
it('parses mustaches with escaped [] in a path', function() {
equals(astFor('{{[foo[\\]]}}'), '{{ PATH:foo[] [] }}\n');
});
it('parses escaped \\\\ in path', function() {
equals(astFor('{{[foo\\\\]}}'), '{{ PATH:foo\\ [] }}\n');
});
it('parses mustaches with parameters', function() {
equals(ast_for("{{foo bar}}"), "{{ PATH:foo [PATH:bar] }}\n");
equals(astFor('{{foo bar}}'), '{{ PATH:foo [PATH:bar] }}\n');
});
it('parses mustaches with string parameters', function() {
equals(ast_for("{{foo bar \"baz\" }}"), '{{ PATH:foo [PATH:bar, "baz"] }}\n');
equals(astFor('{{foo bar \"baz\" }}'), '{{ PATH:foo [PATH:bar, "baz"] }}\n');
});
it('parses mustaches with NUMBER parameters', function() {
equals(ast_for("{{foo 1}}"), "{{ PATH:foo [NUMBER{1}] }}\n");
equals(astFor('{{foo 1}}'), '{{ PATH:foo [NUMBER{1}] }}\n');
});
it('parses mustaches with BOOLEAN parameters', function() {
equals(ast_for("{{foo true}}"), "{{ PATH:foo [BOOLEAN{true}] }}\n");
equals(ast_for("{{foo false}}"), "{{ PATH:foo [BOOLEAN{false}] }}\n");
equals(astFor('{{foo true}}'), '{{ PATH:foo [BOOLEAN{true}] }}\n');
equals(astFor('{{foo false}}'), '{{ PATH:foo [BOOLEAN{false}] }}\n');
});
it('parses mutaches with DATA parameters', function() {
equals(ast_for("{{foo @bar}}"), "{{ PATH:foo [@PATH:bar] }}\n");
it('parses mustaches with undefined and null paths', function() {
equals(astFor('{{undefined}}'), '{{ UNDEFINED [] }}\n');
equals(astFor('{{null}}'), '{{ NULL [] }}\n');
});
it('parses mustaches with undefined and null parameters', function() {
equals(astFor('{{foo undefined null}}'), '{{ PATH:foo [UNDEFINED, NULL] }}\n');
});
it('parses mustaches with DATA parameters', function() {
equals(astFor('{{foo @bar}}'), '{{ PATH:foo [@PATH:bar] }}\n');
});
it('parses mustaches with hash arguments', function() {
equals(ast_for("{{foo bar=baz}}"), "{{ PATH:foo [] HASH{bar=PATH:baz} }}\n");
equals(ast_for("{{foo bar=1}}"), "{{ PATH:foo [] HASH{bar=NUMBER{1}} }}\n");
equals(ast_for("{{foo bar=true}}"), "{{ PATH:foo [] HASH{bar=BOOLEAN{true}} }}\n");
equals(ast_for("{{foo bar=false}}"), "{{ PATH:foo [] HASH{bar=BOOLEAN{false}} }}\n");
equals(ast_for("{{foo bar=@baz}}"), "{{ PATH:foo [] HASH{bar=@PATH:baz} }}\n");
equals(astFor('{{foo bar=baz}}'), '{{ PATH:foo [] HASH{bar=PATH:baz} }}\n');
equals(astFor('{{foo bar=1}}'), '{{ PATH:foo [] HASH{bar=NUMBER{1}} }}\n');
equals(astFor('{{foo bar=true}}'), '{{ PATH:foo [] HASH{bar=BOOLEAN{true}} }}\n');
equals(astFor('{{foo bar=false}}'), '{{ PATH:foo [] HASH{bar=BOOLEAN{false}} }}\n');
equals(astFor('{{foo bar=@baz}}'), '{{ PATH:foo [] HASH{bar=@PATH:baz} }}\n');
equals(ast_for("{{foo bar=baz bat=bam}}"), "{{ PATH:foo [] HASH{bar=PATH:baz, bat=PATH:bam} }}\n");
equals(ast_for("{{foo bar=baz bat=\"bam\"}}"), '{{ PATH:foo [] HASH{bar=PATH:baz, bat="bam"} }}\n');
equals(astFor('{{foo bar=baz bat=bam}}'), '{{ PATH:foo [] HASH{bar=PATH:baz, bat=PATH:bam} }}\n');
equals(astFor('{{foo bar=baz bat="bam"}}'), '{{ PATH:foo [] HASH{bar=PATH:baz, bat="bam"} }}\n');
equals(ast_for("{{foo bat='bam'}}"), '{{ PATH:foo [] HASH{bat="bam"} }}\n');
equals(astFor("{{foo bat='bam'}}"), '{{ PATH:foo [] HASH{bat="bam"} }}\n');
equals(ast_for("{{foo omg bar=baz bat=\"bam\"}}"), '{{ PATH:foo [PATH:omg] HASH{bar=PATH:baz, bat="bam"} }}\n');
equals(ast_for("{{foo omg bar=baz bat=\"bam\" baz=1}}"), '{{ PATH:foo [PATH:omg] HASH{bar=PATH:baz, bat="bam", baz=NUMBER{1}} }}\n');
equals(ast_for("{{foo omg bar=baz bat=\"bam\" baz=true}}"), '{{ PATH:foo [PATH:omg] HASH{bar=PATH:baz, bat="bam", baz=BOOLEAN{true}} }}\n');
equals(ast_for("{{foo omg bar=baz bat=\"bam\" baz=false}}"), '{{ PATH:foo [PATH:omg] HASH{bar=PATH:baz, bat="bam", baz=BOOLEAN{false}} }}\n');
equals(astFor('{{foo omg bar=baz bat=\"bam\"}}'), '{{ PATH:foo [PATH:omg] HASH{bar=PATH:baz, bat="bam"} }}\n');
equals(astFor('{{foo omg bar=baz bat=\"bam\" baz=1}}'), '{{ PATH:foo [PATH:omg] HASH{bar=PATH:baz, bat="bam", baz=NUMBER{1}} }}\n');
equals(astFor('{{foo omg bar=baz bat=\"bam\" baz=true}}'), '{{ PATH:foo [PATH:omg] HASH{bar=PATH:baz, bat="bam", baz=BOOLEAN{true}} }}\n');
equals(astFor('{{foo omg bar=baz bat=\"bam\" baz=false}}'), '{{ PATH:foo [PATH:omg] HASH{bar=PATH:baz, bat="bam", baz=BOOLEAN{false}} }}\n');
});
it('parses contents followed by a mustache', function() {
equals(ast_for("foo bar {{baz}}"), "CONTENT[ \'foo bar \' ]\n{{ PATH:baz [] }}\n");
equals(astFor('foo bar {{baz}}'), 'CONTENT[ \'foo bar \' ]\n{{ PATH:baz [] }}\n');
});
it('parses a partial', function() {
equals(ast_for("{{> foo }}"), "{{> PARTIAL:foo }}\n");
equals(ast_for("{{> 'foo' }}"), "{{> PARTIAL:foo }}\n");
equals(ast_for("{{> 1 }}"), "{{> PARTIAL:1 }}\n");
equals(astFor('{{> foo }}'), '{{> PARTIAL:foo }}\n');
equals(astFor('{{> "foo" }}'), '{{> PARTIAL:foo }}\n');
equals(astFor('{{> 1 }}'), '{{> PARTIAL:1 }}\n');
});
it('parses a partial with context', function() {
equals(ast_for("{{> foo bar}}"), "{{> PARTIAL:foo PATH:bar }}\n");
equals(astFor('{{> foo bar}}'), '{{> PARTIAL:foo PATH:bar }}\n');
});
it('parses a partial with hash', function() {
equals(ast_for("{{> foo bar=bat}}"), "{{> PARTIAL:foo HASH{bar=PATH:bat} }}\n");
equals(astFor('{{> foo bar=bat}}'), '{{> PARTIAL:foo HASH{bar=PATH:bat} }}\n');
});
it('parses a partial with context and hash', function() {
equals(ast_for("{{> foo bar bat=baz}}"), "{{> PARTIAL:foo PATH:bar HASH{bat=PATH:baz} }}\n");
equals(astFor('{{> foo bar bat=baz}}'), '{{> PARTIAL:foo PATH:bar HASH{bat=PATH:baz} }}\n');
});
it('parses a partial with a complex name', function() {
equals(ast_for("{{> shared/partial?.bar}}"), "{{> PARTIAL:shared/partial?.bar }}\n");
equals(astFor('{{> shared/partial?.bar}}'), '{{> PARTIAL:shared/partial?.bar }}\n');
});
it('parsers partial blocks', function() {
equals(astFor('{{#> foo}}bar{{/foo}}'), '{{> PARTIAL BLOCK:foo PROGRAM:\n CONTENT[ \'bar\' ]\n }}\n');
});
it('should handle parser block mismatch', function() {
shouldThrow(function() {
astFor('{{#> goodbyes}}{{/hellos}}');
}, Error, (/goodbyes doesn't match hellos/));
});
it('parsers partial blocks with arguments', function() {
equals(astFor('{{#> foo context hash=value}}bar{{/foo}}'), '{{> PARTIAL BLOCK:foo PATH:context HASH{hash=PATH:value} PROGRAM:\n CONTENT[ \'bar\' ]\n }}\n');
});
it('parses a comment', function() {
equals(ast_for("{{! this is a comment }}"), "{{! ' this is a comment ' }}\n");
equals(astFor('{{! this is a comment }}'), "{{! ' this is a comment ' }}\n");
});
it('parses a multi-line comment', function() {
equals(ast_for("{{!\nthis is a multi-line comment\n}}"), "{{! \'\nthis is a multi-line comment\n\' }}\n");
equals(astFor('{{!\nthis is a multi-line comment\n}}'), "{{! \'\nthis is a multi-line comment\n\' }}\n");
});
it('parses an inverse section', function() {
equals(ast_for("{{#foo}} bar {{^}} baz {{/foo}}"), "BLOCK:\n PATH:foo []\n PROGRAM:\n CONTENT[ ' bar ' ]\n {{^}}\n CONTENT[ ' baz ' ]\n");
equals(astFor('{{#foo}} bar {{^}} baz {{/foo}}'), "BLOCK:\n PATH:foo []\n PROGRAM:\n CONTENT[ ' bar ' ]\n {{^}}\n CONTENT[ ' baz ' ]\n");
});
it('parses an inverse (else-style) section', function() {
equals(ast_for("{{#foo}} bar {{else}} baz {{/foo}}"), "BLOCK:\n PATH:foo []\n PROGRAM:\n CONTENT[ ' bar ' ]\n {{^}}\n CONTENT[ ' baz ' ]\n");
equals(astFor('{{#foo}} bar {{else}} baz {{/foo}}'), "BLOCK:\n PATH:foo []\n PROGRAM:\n CONTENT[ ' bar ' ]\n {{^}}\n CONTENT[ ' baz ' ]\n");
});
it('parses multiple inverse sections', function() {
equals(ast_for("{{#foo}} bar {{else if bar}}{{else}} baz {{/foo}}"), "BLOCK:\n PATH:foo []\n PROGRAM:\n CONTENT[ ' bar ' ]\n {{^}}\n BLOCK:\n PATH:if [PATH:bar]\n PROGRAM:\n {{^}}\n CONTENT[ ' baz ' ]\n");
equals(astFor('{{#foo}} bar {{else if bar}}{{else}} baz {{/foo}}'), "BLOCK:\n PATH:foo []\n PROGRAM:\n CONTENT[ ' bar ' ]\n {{^}}\n BLOCK:\n PATH:if [PATH:bar]\n PROGRAM:\n {{^}}\n CONTENT[ ' baz ' ]\n");
});
it('parses empty blocks', function() {
equals(ast_for("{{#foo}}{{/foo}}"), "BLOCK:\n PATH:foo []\n PROGRAM:\n");
equals(astFor('{{#foo}}{{/foo}}'), 'BLOCK:\n PATH:foo []\n PROGRAM:\n');
});
it('parses empty blocks with empty inverse section', function() {
equals(ast_for("{{#foo}}{{^}}{{/foo}}"), "BLOCK:\n PATH:foo []\n PROGRAM:\n {{^}}\n");
equals(astFor('{{#foo}}{{^}}{{/foo}}'), 'BLOCK:\n PATH:foo []\n PROGRAM:\n {{^}}\n');
});
it('parses empty blocks with empty inverse (else-style) section', function() {
equals(ast_for("{{#foo}}{{else}}{{/foo}}"), "BLOCK:\n PATH:foo []\n PROGRAM:\n {{^}}\n");
equals(astFor('{{#foo}}{{else}}{{/foo}}'), 'BLOCK:\n PATH:foo []\n PROGRAM:\n {{^}}\n');
});
it('parses non-empty blocks with empty inverse section', function() {
equals(ast_for("{{#foo}} bar {{^}}{{/foo}}"), "BLOCK:\n PATH:foo []\n PROGRAM:\n CONTENT[ ' bar ' ]\n {{^}}\n");
equals(astFor('{{#foo}} bar {{^}}{{/foo}}'), "BLOCK:\n PATH:foo []\n PROGRAM:\n CONTENT[ ' bar ' ]\n {{^}}\n");
});
it('parses non-empty blocks with empty inverse (else-style) section', function() {
equals(ast_for("{{#foo}} bar {{else}}{{/foo}}"), "BLOCK:\n PATH:foo []\n PROGRAM:\n CONTENT[ ' bar ' ]\n {{^}}\n");
equals(astFor('{{#foo}} bar {{else}}{{/foo}}'), "BLOCK:\n PATH:foo []\n PROGRAM:\n CONTENT[ ' bar ' ]\n {{^}}\n");
});
it('parses empty blocks with non-empty inverse section', function() {
equals(ast_for("{{#foo}}{{^}} bar {{/foo}}"), "BLOCK:\n PATH:foo []\n PROGRAM:\n {{^}}\n CONTENT[ ' bar ' ]\n");
equals(astFor('{{#foo}}{{^}} bar {{/foo}}'), "BLOCK:\n PATH:foo []\n PROGRAM:\n {{^}}\n CONTENT[ ' bar ' ]\n");
});
it('parses empty blocks with non-empty inverse (else-style) section', function() {
equals(ast_for("{{#foo}}{{else}} bar {{/foo}}"), "BLOCK:\n PATH:foo []\n PROGRAM:\n {{^}}\n CONTENT[ ' bar ' ]\n");
equals(astFor('{{#foo}}{{else}} bar {{/foo}}'), "BLOCK:\n PATH:foo []\n PROGRAM:\n {{^}}\n CONTENT[ ' bar ' ]\n");
});
it('parses a standalone inverse section', function() {
equals(ast_for("{{^foo}}bar{{/foo}}"), "BLOCK:\n PATH:foo []\n {{^}}\n CONTENT[ 'bar' ]\n");
equals(astFor('{{^foo}}bar{{/foo}}'), "BLOCK:\n PATH:foo []\n {{^}}\n CONTENT[ 'bar' ]\n");
});
it('throws on old inverse section', function() {
shouldThrow(function() {
ast_for("{{else foo}}bar{{/foo}}");
astFor('{{else foo}}bar{{/foo}}');
}, Error);
});
it('parses block with block params', function() {
equals(ast_for("{{#foo as |bar baz|}}content{{/foo}}"), "BLOCK:\n PATH:foo []\n PROGRAM:\n BLOCK PARAMS: [ bar baz ]\n CONTENT[ 'content' ]\n");
equals(astFor('{{#foo as |bar baz|}}content{{/foo}}'), "BLOCK:\n PATH:foo []\n PROGRAM:\n BLOCK PARAMS: [ bar baz ]\n CONTENT[ 'content' ]\n");
});
it('parses inverse block with block params', function() {
equals(ast_for("{{^foo as |bar baz|}}content{{/foo}}"), "BLOCK:\n PATH:foo []\n {{^}}\n BLOCK PARAMS: [ bar baz ]\n CONTENT[ 'content' ]\n");
equals(astFor('{{^foo as |bar baz|}}content{{/foo}}'), "BLOCK:\n PATH:foo []\n {{^}}\n BLOCK PARAMS: [ bar baz ]\n CONTENT[ 'content' ]\n");
});
it('parses chained inverse block with block params', function() {
equals(ast_for("{{#foo}}{{else foo as |bar baz|}}content{{/foo}}"), "BLOCK:\n PATH:foo []\n PROGRAM:\n {{^}}\n BLOCK:\n PATH:foo []\n PROGRAM:\n BLOCK PARAMS: [ bar baz ]\n CONTENT[ 'content' ]\n");
equals(astFor('{{#foo}}{{else foo as |bar baz|}}content{{/foo}}'), "BLOCK:\n PATH:foo []\n PROGRAM:\n {{^}}\n BLOCK:\n PATH:foo []\n PROGRAM:\n BLOCK PARAMS: [ bar baz ]\n CONTENT[ 'content' ]\n");
});
it("raises if there's a Parse error", function() {
it('raises if there\'s a Parse error', function() {
shouldThrow(function() {
ast_for("foo{{^}}bar");
astFor('foo{{^}}bar');
}, Error, /Parse error on line 1/);
shouldThrow(function() {
ast_for("{{foo}");
astFor('{{foo}');
}, Error, /Parse error on line 1/);
shouldThrow(function() {
ast_for("{{foo &}}");
astFor('{{foo &}}');
}, Error, /Parse error on line 1/);
shouldThrow(function() {
ast_for("{{#goodbyes}}{{/hellos}}");
astFor('{{#goodbyes}}{{/hellos}}');
}, Error, /goodbyes doesn't match hellos/);
shouldThrow(function() {
ast_for("{{{{goodbyes}}}} {{{{/hellos}}}}");
astFor('{{{{goodbyes}}}} {{{{/hellos}}}}');
}, Error, /goodbyes doesn't match hellos/);
});
it('should handle invalid paths', function() {
shouldThrow(function() {
ast_for("{{foo/../bar}}");
astFor('{{foo/../bar}}');
}, Error, /Invalid path: foo\/\.\. - 1:2/);
shouldThrow(function() {
ast_for("{{foo/./bar}}");
astFor('{{foo/./bar}}');
}, Error, /Invalid path: foo\/\. - 1:2/);
shouldThrow(function() {
ast_for("{{foo/this/bar}}");
astFor('{{foo/this/bar}}');
}, Error, /Invalid path: foo\/this - 1:2/);
});
it('knows how to report the correct line number in errors', function() {
shouldThrow(function() {
ast_for("hello\nmy\n{{foo}");
astFor('hello\nmy\n{{foo}');
}, Error, /Parse error on line 3/);
shouldThrow(function() {
ast_for("hello\n\nmy\n\n{{foo}");
astFor('hello\n\nmy\n\n{{foo}');
}, Error, /Parse error on line 5/);
});
it('knows how to report the correct line number in errors when the first character is a newline', function() {
shouldThrow(function() {
ast_for("\n\nhello\n\nmy\n\n{{foo}");
astFor('\n\nhello\n\nmy\n\n{{foo}');
}, Error, /Parse error on line 7/);
});
describe('externally compiled AST', function() {
it('can pass through an already-compiled AST', function() {
equals(ast_for(new Handlebars.AST.Program([new Handlebars.AST.ContentStatement("Hello")], null)), "CONTENT[ \'Hello\' ]\n");
equals(astFor({
type: 'Program',
body: [ {type: 'ContentStatement', value: 'Hello'}]
}), 'CONTENT[ \'Hello\' ]\n');
});
});
describe('directives', function() {
it('should parse block directives', function() {
equals(astFor('{{#* foo}}{{/foo}}'), 'DIRECTIVE BLOCK:\n PATH:foo []\n PROGRAM:\n');
});
it('should parse directives', function() {
equals(astFor('{{* foo}}'), '{{ DIRECTIVE PATH:foo [] }}\n');
});
it('should fail if directives have inverse', function() {
shouldThrow(function() {
astFor('{{#* foo}}{{^}}{{/foo}}');
}, Error, /Unexpected inverse/);
});
});
it('GH1024 - should track program location properly', function() {
var p = Handlebars.parse('\n'
+ ' {{#if foo}}\n'
+ ' {{bar}}\n'
+ ' {{else}} {{baz}}\n'
+ '\n'
+ ' {{/if}}\n'
+ ' ');
// We really need a deep equals but for now this should be stable...
equals(JSON.stringify(p.loc), JSON.stringify({
start: { line: 1, column: 0 },
end: { line: 7, column: 4 }
}));
equals(JSON.stringify(p.body[1].program.loc), JSON.stringify({
start: { line: 2, column: 13 },
end: { line: 4, column: 7 }
}));
equals(JSON.stringify(p.body[1].inverse.loc), JSON.stringify({
start: { line: 4, column: 15 },
end: { line: 6, column: 5 }
}));
});
});
+351 -106
View File
@@ -1,4 +1,3 @@
/*global CompilerContext, Handlebars, handlebarsEnv, shouldCompileTo, shouldCompileToWithPartials, shouldThrow */
describe('partials', function() {
it('basic partials', function() {
var string = 'Dudes: {{#dudes}}{{> dude}}{{/dudes}}';
@@ -34,19 +33,41 @@ describe('partials', function() {
}, Handlebars.Exception, 'The partial missing could not be found');
});
it("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");
it('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');
});
it("partials with undefined context", function() {
var string = "Dudes: {{>dude dudes}}";
var partial = "{{foo}} Empty";
it('partials with no context', function() {
var partial = '{{name}} ({{url}}) ';
var hash = {dudes: [{name: 'Yehuda', url: 'http://yehuda'}, {name: 'Alan', url: 'http://alan'}]};
shouldCompileToWithPartials(
'Dudes: {{#dudes}}{{>dude}}{{/dudes}}',
[hash, {}, {dude: partial}, {explicitPartialContext: true}],
true,
'Dudes: () () ');
shouldCompileToWithPartials(
'Dudes: {{#dudes}}{{>dude name="foo"}}{{/dudes}}',
[hash, {}, {dude: partial}, {explicitPartialContext: true}],
true,
'Dudes: foo () foo () ');
});
it('partials with string context', function() {
var string = 'Dudes: {{>dude "dudes"}}';
var partial = '{{.}}';
var hash = {};
shouldCompileToWithPartials(string, [hash, {}, {dude: partial}], true, "Dudes: Empty");
shouldCompileToWithPartials(string, [hash, {}, {dude: partial}], true, 'Dudes: dudes');
});
it('partials with undefined context', function() {
var string = 'Dudes: {{>dude dudes}}';
var partial = '{{foo}} Empty';
var hash = {};
shouldCompileToWithPartials(string, [hash, {}, {dude: partial}], true, 'Dudes: Empty');
});
it('partials with duplicate parameters', function() {
@@ -55,133 +76,133 @@ describe('partials', function() {
}, Error, 'Unsupported number of partial arguments: 2 - 1:7');
});
it("partials with parameters", function() {
var string = "Dudes: {{#dudes}}{{> dude others=..}}{{/dudes}}";
var partial = "{{others.foo}}{{name}} ({{url}}) ";
var hash = {foo: 'bar', dudes: [{name: "Yehuda", url: "http://yehuda"}, {name: "Alan", url: "http://alan"}]};
shouldCompileToWithPartials(string, [hash, {}, {dude: partial}], true, "Dudes: barYehuda (http://yehuda) barAlan (http://alan) ",
"Basic partials output based on current context.");
it('partials with parameters', function() {
var string = 'Dudes: {{#dudes}}{{> dude others=..}}{{/dudes}}';
var partial = '{{others.foo}}{{name}} ({{url}}) ';
var hash = {foo: 'bar', dudes: [{name: 'Yehuda', url: 'http://yehuda'}, {name: 'Alan', url: 'http://alan'}]};
shouldCompileToWithPartials(string, [hash, {}, {dude: partial}], true, 'Dudes: barYehuda (http://yehuda) barAlan (http://alan) ',
'Basic partials output based on current context.');
});
it("partial in a partial", function() {
var string = "Dudes: {{#dudes}}{{>dude}}{{/dudes}}";
var dude = "{{name}} {{> url}} ";
var url = "<a href='{{url}}'>{{url}}</a>";
var hash = {dudes: [{name: "Yehuda", url: "http://yehuda"}, {name: "Alan", url: "http://alan"}]};
shouldCompileToWithPartials(string, [hash, {}, {dude: dude, url: url}], true, "Dudes: Yehuda <a href='http://yehuda'>http://yehuda</a> Alan <a href='http://alan'>http://alan</a> ", "Partials are rendered inside of other partials");
it('partial in a partial', function() {
var string = 'Dudes: {{#dudes}}{{>dude}}{{/dudes}}';
var dude = '{{name}} {{> url}} ';
var url = '<a href="{{url}}">{{url}}</a>';
var hash = {dudes: [{name: 'Yehuda', url: 'http://yehuda'}, {name: 'Alan', url: 'http://alan'}]};
shouldCompileToWithPartials(string, [hash, {}, {dude: dude, url: url}], true, 'Dudes: Yehuda <a href="http://yehuda">http://yehuda</a> Alan <a href="http://alan">http://alan</a> ', 'Partials are rendered inside of other partials');
});
it("rendering undefined partial throws an exception", function() {
it('rendering undefined partial throws an exception', function() {
shouldThrow(function() {
var template = CompilerContext.compile("{{> whatever}}");
var template = CompilerContext.compile('{{> whatever}}');
template();
}, Handlebars.Exception, 'The partial whatever could not be found');
});
it("registering undefined partial throws an exception", function() {
it('registering undefined partial throws an exception', function() {
shouldThrow(function() {
var undef;
handlebarsEnv.registerPartial('undefined_test', undef);
}, Handlebars.Exception, 'Attempting to register a partial as undefined');
}, Handlebars.Exception, 'Attempting to register a partial called "undefined_test" as undefined');
});
it("rendering template partial in vm mode throws an exception", function() {
it('rendering template partial in vm mode throws an exception', function() {
shouldThrow(function() {
var template = CompilerContext.compile("{{> whatever}}");
var template = CompilerContext.compile('{{> whatever}}');
template();
}, Handlebars.Exception, 'The partial whatever could not be found');
});
it("rendering function partial in vm mode", function() {
var string = "Dudes: {{#dudes}}{{> dude}}{{/dudes}}";
var partial = function(context) {
it('rendering function partial in vm mode', function() {
var string = 'Dudes: {{#dudes}}{{> dude}}{{/dudes}}';
function partial(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.");
}
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.');
});
it("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");
it('GH-14: a partial preceding a selector', function() {
var string = 'Dudes: {{>dude}} {{anotherDude}}';
var dude = '{{name}}';
var hash = {name: 'Jeepers', anotherDude: 'Creepers'};
shouldCompileToWithPartials(string, [hash, {}, {dude: dude}], true, 'Dudes: Jeepers Creepers', 'Regular selectors can follow a partial');
});
it("Partials with slash paths", function() {
var string = "Dudes: {{> shared/dude}}";
var dude = "{{name}}";
var hash = {name:"Jeepers", another_dude:"Creepers"};
shouldCompileToWithPartials(string, [hash, {}, {'shared/dude':dude}], true, "Dudes: Jeepers", "Partials can use literal paths");
it('Partials with slash paths', function() {
var string = 'Dudes: {{> shared/dude}}';
var dude = '{{name}}';
var hash = {name: 'Jeepers', anotherDude: 'Creepers'};
shouldCompileToWithPartials(string, [hash, {}, {'shared/dude': dude}], true, 'Dudes: Jeepers', 'Partials can use literal paths');
});
it("Partials with slash and point paths", function() {
var string = "Dudes: {{> shared/dude.thing}}";
var dude = "{{name}}";
var hash = {name:"Jeepers", another_dude:"Creepers"};
shouldCompileToWithPartials(string, [hash, {}, {'shared/dude.thing':dude}], true, "Dudes: Jeepers", "Partials can use literal with points in paths");
it('Partials with slash and point paths', function() {
var string = 'Dudes: {{> shared/dude.thing}}';
var dude = '{{name}}';
var hash = {name: 'Jeepers', anotherDude: 'Creepers'};
shouldCompileToWithPartials(string, [hash, {}, {'shared/dude.thing': dude}], true, 'Dudes: Jeepers', 'Partials can use literal with points in paths');
});
it("Global Partials", function() {
handlebarsEnv.registerPartial('global_test', '{{another_dude}}');
it('Global Partials', function() {
handlebarsEnv.registerPartial('globalTest', '{{anotherDude}}');
var string = "Dudes: {{> shared/dude}} {{> global_test}}";
var dude = "{{name}}";
var hash = {name:"Jeepers", another_dude:"Creepers"};
shouldCompileToWithPartials(string, [hash, {}, {'shared/dude':dude}], true, "Dudes: Jeepers Creepers", "Partials can use globals or passed");
var string = 'Dudes: {{> shared/dude}} {{> globalTest}}';
var dude = '{{name}}';
var hash = {name: 'Jeepers', anotherDude: 'Creepers'};
shouldCompileToWithPartials(string, [hash, {}, {'shared/dude': dude}], true, 'Dudes: Jeepers Creepers', 'Partials can use globals or passed');
handlebarsEnv.unregisterPartial('global_test');
equals(handlebarsEnv.partials.global_test, undefined);
handlebarsEnv.unregisterPartial('globalTest');
equals(handlebarsEnv.partials.globalTest, undefined);
});
it("Multiple partial registration", function() {
it('Multiple partial registration', function() {
handlebarsEnv.registerPartial({
'shared/dude': '{{name}}',
global_test: '{{another_dude}}'
globalTest: '{{anotherDude}}'
});
var string = "Dudes: {{> shared/dude}} {{> global_test}}";
var hash = {name:"Jeepers", another_dude:"Creepers"};
shouldCompileToWithPartials(string, [hash], true, "Dudes: Jeepers Creepers", "Partials can use globals or passed");
var string = 'Dudes: {{> shared/dude}} {{> globalTest}}';
var hash = {name: 'Jeepers', anotherDude: 'Creepers'};
shouldCompileToWithPartials(string, [hash], true, 'Dudes: Jeepers Creepers', 'Partials can use globals or passed');
});
it("Partials with integer path", function() {
var string = "Dudes: {{> 404}}";
var dude = "{{name}}";
var hash = {name:"Jeepers", another_dude:"Creepers"};
shouldCompileToWithPartials(string, [hash, {}, {404:dude}], true, "Dudes: Jeepers", "Partials can use literal paths");
it('Partials with integer path', function() {
var string = 'Dudes: {{> 404}}';
var dude = '{{name}}';
var hash = {name: 'Jeepers', anotherDude: 'Creepers'};
shouldCompileToWithPartials(string, [hash, {}, {404: dude}], true, 'Dudes: Jeepers', 'Partials can use literal paths');
});
it("Partials with complex path", function() {
var string = "Dudes: {{> 404/asdf?.bar}}";
var dude = "{{name}}";
var hash = {name:"Jeepers", another_dude:"Creepers"};
shouldCompileToWithPartials(string, [hash, {}, {'404/asdf?.bar':dude}], true, "Dudes: Jeepers", "Partials can use literal paths");
it('Partials with complex path', function() {
var string = 'Dudes: {{> 404/asdf?.bar}}';
var dude = '{{name}}';
var hash = {name: 'Jeepers', anotherDude: 'Creepers'};
shouldCompileToWithPartials(string, [hash, {}, {'404/asdf?.bar': dude}], true, 'Dudes: Jeepers', 'Partials can use literal paths');
});
it("Partials with escaped", function() {
var string = "Dudes: {{> [+404/asdf?.bar]}}";
var dude = "{{name}}";
var hash = {name:"Jeepers", another_dude:"Creepers"};
shouldCompileToWithPartials(string, [hash, {}, {'+404/asdf?.bar':dude}], true, "Dudes: Jeepers", "Partials can use literal paths");
it('Partials with escaped', function() {
var string = 'Dudes: {{> [+404/asdf?.bar]}}';
var dude = '{{name}}';
var hash = {name: 'Jeepers', anotherDude: 'Creepers'};
shouldCompileToWithPartials(string, [hash, {}, {'+404/asdf?.bar': dude}], true, 'Dudes: Jeepers', 'Partials can use literal paths');
});
it("Partials with string", function() {
var string = "Dudes: {{> \"+404/asdf?.bar\"}}";
var dude = "{{name}}";
var hash = {name:"Jeepers", another_dude:"Creepers"};
shouldCompileToWithPartials(string, [hash, {}, {'+404/asdf?.bar':dude}], true, "Dudes: Jeepers", "Partials can use literal paths");
it('Partials with string', function() {
var string = 'Dudes: {{> \'+404/asdf?.bar\'}}';
var dude = '{{name}}';
var hash = {name: 'Jeepers', anotherDude: 'Creepers'};
shouldCompileToWithPartials(string, [hash, {}, {'+404/asdf?.bar': dude}], true, 'Dudes: Jeepers', 'Partials can use literal paths');
});
it('should handle empty partial', function() {
var string = "Dudes: {{#dudes}}{{> dude}}{{/dudes}}";
var partial = "";
var hash = {dudes: [{name: "Yehuda", url: "http://yehuda"}, {name: "Alan", url: "http://alan"}]};
shouldCompileToWithPartials(string, [hash, {}, {dude: partial}], true, "Dudes: ");
var string = 'Dudes: {{#dudes}}{{> dude}}{{/dudes}}';
var partial = '';
var hash = {dudes: [{name: 'Yehuda', url: 'http://yehuda'}, {name: 'Alan', url: 'http://alan'}]};
shouldCompileToWithPartials(string, [hash, {}, {dude: partial}], true, 'Dudes: ');
});
it("throw on missing partial", function() {
it('throw on missing partial', function() {
var compile = handlebarsEnv.compile;
handlebarsEnv.compile = undefined;
shouldThrow(function() {
@@ -190,6 +211,224 @@ describe('partials', function() {
handlebarsEnv.compile = compile;
});
describe('partial blocks', function() {
it('should render partial block as default', function() {
shouldCompileToWithPartials(
'{{#> dude}}success{{/dude}}',
[{}, {}, {}],
true,
'success');
});
it('should execute default block with proper context', function() {
shouldCompileToWithPartials(
'{{#> dude context}}{{value}}{{/dude}}',
[{context: {value: 'success'}}, {}, {}],
true,
'success');
});
it('should propagate block parameters to default block', function() {
shouldCompileToWithPartials(
'{{#with context as |me|}}{{#> dude}}{{me.value}}{{/dude}}{{/with}}',
[{context: {value: 'success'}}, {}, {}],
true,
'success');
});
it('should not use partial block if partial exists', function() {
shouldCompileToWithPartials(
'{{#> dude}}fail{{/dude}}',
[{}, {}, {dude: 'success'}],
true,
'success');
});
it('should render block from partial', function() {
shouldCompileToWithPartials(
'{{#> dude}}success{{/dude}}',
[{}, {}, {dude: '{{> @partial-block }}'}],
true,
'success');
});
it('should be able to render the partial-block twice', function() {
shouldCompileToWithPartials(
'{{#> dude}}success{{/dude}}',
[{}, {}, {dude: '{{> @partial-block }} {{> @partial-block }}'}],
true,
'success success');
});
it('should render block from partial with context', function() {
shouldCompileToWithPartials(
'{{#> dude}}{{value}}{{/dude}}',
[{context: {value: 'success'}}, {}, {dude: '{{#with context}}{{> @partial-block }}{{/with}}'}],
true,
'success');
});
it('should allow the #each-helper to be used along with partial-blocks', function() {
shouldCompileToWithPartials(
'<template>{{#> list value}}value = {{.}}{{/list}}</template>',
[
{value: ['a', 'b', 'c']},
{},
{
list: '<list>{{#each .}}<item>{{> @partial-block}}</item>{{/each}}</list>'
}
],
true,
'<template><list><item>value = a</item><item>value = b</item><item>value = c</item></list></template>');
});
it('should render block from partial with context (twice)', function() {
shouldCompileToWithPartials(
'{{#> dude}}{{value}}{{/dude}}',
[
{context: {value: 'success'}},
{},
{
dude: '{{#with context}}{{> @partial-block }} {{> @partial-block }}{{/with}}'
}
],
true,
'success success');
});
it('should render block from partial with context', function() {
shouldCompileToWithPartials(
'{{#> dude}}{{../context/value}}{{/dude}}',
[{context: {value: 'success'}}, {}, {dude: '{{#with context}}{{> @partial-block }}{{/with}}'}],
true,
'success');
});
it('should render block from partial with block params', function() {
shouldCompileToWithPartials(
'{{#with context as |me|}}{{#> dude}}{{me.value}}{{/dude}}{{/with}}',
[{context: {value: 'success'}}, {}, {dude: '{{> @partial-block }}'}],
true,
'success');
});
it('should render nested partial blocks', function() {
shouldCompileToWithPartials(
'<template>{{#> outer}}{{value}}{{/outer}}</template>',
[
{value: 'success'},
{},
{
outer: '<outer>{{#> nested}}<outer-block>{{> @partial-block}}</outer-block>{{/nested}}</outer>',
nested: '<nested>{{> @partial-block}}</nested>'
}
],
true,
'<template><outer><nested><outer-block>success</outer-block></nested></outer></template>');
});
it('should render nested partial blocks at different nesting levels', function() {
shouldCompileToWithPartials(
'<template>{{#> outer}}{{value}}{{/outer}}</template>',
[
{value: 'success'},
{},
{
outer: '<outer>{{#> nested}}<outer-block>{{> @partial-block}}</outer-block>{{/nested}}{{> @partial-block}}</outer>',
nested: '<nested>{{> @partial-block}}</nested>'
}
],
true,
'<template><outer><nested><outer-block>success</outer-block></nested>success</outer></template>');
});
it('should render nested partial blocks at different nesting levels (twice)', function() {
shouldCompileToWithPartials(
'<template>{{#> outer}}{{value}}{{/outer}}</template>',
[
{value: 'success'},
{},
{
outer: '<outer>{{#> nested}}<outer-block>{{> @partial-block}} {{> @partial-block}}</outer-block>{{/nested}}{{> @partial-block}}+{{> @partial-block}}</outer>',
nested: '<nested>{{> @partial-block}}</nested>'
}
],
true,
'<template><outer><nested><outer-block>success success</outer-block></nested>success+success</outer></template>');
});
it('should render nested partial blocks (twice at each level)', function() {
shouldCompileToWithPartials(
'<template>{{#> outer}}{{value}}{{/outer}}</template>',
[
{value: 'success'},
{},
{
outer: '<outer>{{#> nested}}<outer-block>{{> @partial-block}} {{> @partial-block}}</outer-block>{{/nested}}</outer>',
nested: '<nested>{{> @partial-block}}{{> @partial-block}}</nested>'
}
],
true,
'<template><outer>' +
'<nested><outer-block>success success</outer-block><outer-block>success success</outer-block></nested>' +
'</outer></template>');
});
});
describe('inline partials', function() {
it('should define inline partials for template', function() {
shouldCompileTo('{{#*inline "myPartial"}}success{{/inline}}{{> myPartial}}', {}, 'success');
});
it('should overwrite multiple partials in the same template', function() {
shouldCompileTo('{{#*inline "myPartial"}}fail{{/inline}}{{#*inline "myPartial"}}success{{/inline}}{{> myPartial}}', {}, 'success');
});
it('should define inline partials for block', function() {
shouldCompileTo('{{#with .}}{{#*inline "myPartial"}}success{{/inline}}{{> myPartial}}{{/with}}', {}, 'success');
shouldThrow(function() {
shouldCompileTo('{{#with .}}{{#*inline "myPartial"}}success{{/inline}}{{/with}}{{> myPartial}}', {}, 'success');
}, Error, /myPartial could not/);
});
it('should override global partials', function() {
shouldCompileTo('{{#*inline "myPartial"}}success{{/inline}}{{> myPartial}}', {hash: {}, partials: {myPartial: function() { return 'fail'; }}}, 'success');
});
it('should override template partials', function() {
shouldCompileTo('{{#*inline "myPartial"}}fail{{/inline}}{{#with .}}{{#*inline "myPartial"}}success{{/inline}}{{> myPartial}}{{/with}}', {}, 'success');
});
it('should override partials down the entire stack', function() {
shouldCompileTo('{{#with .}}{{#*inline "myPartial"}}success{{/inline}}{{#with .}}{{#with .}}{{> myPartial}}{{/with}}{{/with}}{{/with}}', {}, 'success');
});
it('should define inline partials for partial call', function() {
shouldCompileToWithPartials(
'{{#*inline "myPartial"}}success{{/inline}}{{> dude}}',
[{}, {}, {dude: '{{> myPartial }}'}],
true,
'success');
});
it('should define inline partials in partial block call', function() {
shouldCompileToWithPartials(
'{{#> dude}}{{#*inline "myPartial"}}success{{/inline}}{{/dude}}',
[{}, {}, {dude: '{{> myPartial }}'}],
true,
'success');
});
it('should render nested inline partials', function() {
shouldCompileToWithPartials(
'{{#*inline "outer"}}{{#>inner}}<outer-block>{{>@partial-block}}</outer-block>{{/inner}}{{/inline}}' +
'{{#*inline "inner"}}<inner>{{>@partial-block}}</inner>{{/inline}}' +
'{{#>outer}}{{value}}{{/outer}}',
[{value: 'success'}, {}, {}],
true,
'<inner><outer-block>success</outer-block></inner>');
});
it('should render nested inline partials with partial-blocks on different nesting levels', function() {
shouldCompileToWithPartials(
'{{#*inline "outer"}}{{#>inner}}<outer-block>{{>@partial-block}}</outer-block>{{/inner}}{{>@partial-block}}{{/inline}}' +
'{{#*inline "inner"}}<inner>{{>@partial-block}}</inner>{{/inline}}' +
'{{#>outer}}{{value}}{{/outer}}',
[{value: 'success'}, {}, {}],
true,
'<inner><outer-block>success</outer-block></inner>success');
});
it('should render nested inline partials (twice at each level)', function() {
shouldCompileToWithPartials(
'{{#*inline "outer"}}{{#>inner}}<outer-block>{{>@partial-block}} {{>@partial-block}}</outer-block>{{/inner}}{{/inline}}' +
'{{#*inline "inner"}}<inner>{{>@partial-block}}{{>@partial-block}}</inner>{{/inline}}' +
'{{#>outer}}{{value}}{{/outer}}',
[{value: 'success'}, {}, {}],
true,
'<inner><outer-block>success success</outer-block><outer-block>success success</outer-block></inner>');
});
});
it('should pass compiler flags', function() {
if (Handlebars.compile) {
var env = Handlebars.create();
@@ -200,28 +439,28 @@ describe('partials', function() {
});
describe('standalone partials', function() {
it("indented partials", function() {
var string = "Dudes:\n{{#dudes}}\n {{>dude}}\n{{/dudes}}";
var dude = "{{name}}\n";
var hash = {dudes: [{name: "Yehuda", url: "http://yehuda"}, {name: "Alan", url: "http://alan"}]};
it('indented partials', function() {
var string = 'Dudes:\n{{#dudes}}\n {{>dude}}\n{{/dudes}}';
var dude = '{{name}}\n';
var hash = {dudes: [{name: 'Yehuda', url: 'http://yehuda'}, {name: 'Alan', url: 'http://alan'}]};
shouldCompileToWithPartials(string, [hash, {}, {dude: dude}], true,
"Dudes:\n Yehuda\n Alan\n");
'Dudes:\n Yehuda\n Alan\n');
});
it("nested indented partials", function() {
var string = "Dudes:\n{{#dudes}}\n {{>dude}}\n{{/dudes}}";
var dude = "{{name}}\n {{> url}}";
var url = "{{url}}!\n";
var hash = {dudes: [{name: "Yehuda", url: "http://yehuda"}, {name: "Alan", url: "http://alan"}]};
it('nested indented partials', function() {
var string = 'Dudes:\n{{#dudes}}\n {{>dude}}\n{{/dudes}}';
var dude = '{{name}}\n {{> url}}';
var url = '{{url}}!\n';
var hash = {dudes: [{name: 'Yehuda', url: 'http://yehuda'}, {name: 'Alan', url: 'http://alan'}]};
shouldCompileToWithPartials(string, [hash, {}, {dude: dude, url: url}], true,
"Dudes:\n Yehuda\n http://yehuda!\n Alan\n http://alan!\n");
'Dudes:\n Yehuda\n http://yehuda!\n Alan\n http://alan!\n');
});
it("prevent nested indented partials", function() {
var string = "Dudes:\n{{#dudes}}\n {{>dude}}\n{{/dudes}}";
var dude = "{{name}}\n {{> url}}";
var url = "{{url}}!\n";
var hash = {dudes: [{name: "Yehuda", url: "http://yehuda"}, {name: "Alan", url: "http://alan"}]};
it('prevent nested indented partials', function() {
var string = 'Dudes:\n{{#dudes}}\n {{>dude}}\n{{/dudes}}';
var dude = '{{name}}\n {{> url}}';
var url = '{{url}}!\n';
var hash = {dudes: [{name: 'Yehuda', url: 'http://yehuda'}, {name: 'Alan', url: 'http://alan'}]};
shouldCompileToWithPartials(string, [hash, {}, {dude: dude, url: url}, {preventIndent: true}], true,
"Dudes:\n Yehuda\n http://yehuda!\n Alan\n http://alan!\n");
'Dudes:\n Yehuda\n http://yehuda!\n Alan\n http://alan!\n');
});
});
@@ -232,6 +471,12 @@ describe('partials', function() {
var hash = {root: 'yes', dudes: [{name: 'Yehuda', url: 'http://yehuda'}, {name: 'Alan', url: 'http://alan'}]};
shouldCompileToWithPartials(string, [hash, {}, {dude: partial}, true], true, 'Dudes: Yehuda (http://yehuda) yes Alan (http://alan) yes ');
});
it('partials can access parents with custom context', function() {
var string = 'Dudes: {{#dudes}}{{> dude "test"}}{{/dudes}}';
var partial = '{{name}} ({{url}}) {{root}} ';
var hash = {root: 'yes', dudes: [{name: 'Yehuda', url: 'http://yehuda'}, {name: 'Alan', url: 'http://alan'}]};
shouldCompileToWithPartials(string, [hash, {}, {dude: partial}, true], true, 'Dudes: Yehuda (http://yehuda) yes Alan (http://alan) yes ');
});
it('partials can access parents without data', function() {
var string = 'Dudes: {{#dudes}}{{> dude}}{{/dudes}}';
var partial = '{{name}} ({{url}}) {{root}} ';
+123 -44
View File
@@ -1,6 +1,4 @@
/*global shouldThrow */
/* eslint-disable no-console */
describe('precompiler', function() {
// NOP Under non-node environments
if (typeof process === 'undefined') {
@@ -8,7 +6,7 @@ describe('precompiler', function() {
}
var Handlebars = require('../lib'),
Precompiler = require('../lib/precompiler'),
Precompiler = require('../dist/cjs/precompiler'),
fs = require('fs'),
uglify = require('uglify-js');
@@ -18,6 +16,12 @@ describe('precompiler', function() {
precompile,
minify,
emptyTemplate = {
path: __dirname + '/artifacts/empty.handlebars',
name: 'empty',
source: ''
},
file,
content,
writeFileSync;
@@ -53,10 +57,9 @@ describe('precompiler', function() {
Precompiler.cli({templates: []});
}, Handlebars.Exception, 'Must define at least one template or directory.');
});
it('should throw on missing template', function() {
shouldThrow(function() {
Precompiler.cli({templates: ['foo']});
}, Handlebars.Exception, 'Unable to open template file "foo"');
it('should handle empty/filtered directories', function() {
Precompiler.cli({hasDirectory: true, templates: []});
// Success is not throwing
});
it('should throw when combining simple and minimized', function() {
shouldThrow(function() {
@@ -68,107 +71,183 @@ describe('precompiler', function() {
Precompiler.cli({templates: [__dirname + '/artifacts/empty.handlebars', __dirname + '/artifacts/empty.handlebars'], simple: true});
}, Handlebars.Exception, 'Unable to output multiple templates in simple mode');
});
it('should throw when missing name', function() {
shouldThrow(function() {
Precompiler.cli({templates: [{source: ''}], amd: true});
}, Handlebars.Exception, 'Name missing for template');
});
it('should throw when combining simple and directories', function() {
shouldThrow(function() {
Precompiler.cli({templates: [__dirname], simple: true});
Precompiler.cli({hasDirectory: true, templates: [1], simple: true});
}, Handlebars.Exception, 'Unable to output multiple templates in simple mode');
});
it('should enumerate directories by extension', function() {
Precompiler.cli({templates: [__dirname + '/artifacts'], extension: 'hbs'});
equal(/'example_2'/.test(log), true);
log = '';
Precompiler.cli({templates: [__dirname + '/artifacts'], extension: 'handlebars'});
equal(/'empty'/.test(log), true);
equal(/'example_1'/.test(log), true);
});
it('should protect from regexp patterns', function() {
Precompiler.cli({templates: [__dirname + '/artifacts'], extension: 'hb(s'});
// Success is not throwing
});
it('should output simple templates', function() {
Handlebars.precompile = function() { return 'simple'; };
Precompiler.cli({templates: [__dirname + '/artifacts/empty.handlebars'], simple: true, extension: 'handlebars'});
Precompiler.cli({templates: [emptyTemplate], simple: true});
equal(log, 'simple\n');
});
it('should default to simple templates', function() {
Handlebars.precompile = function() { return 'simple'; };
Precompiler.cli({templates: [{source: ''}]});
equal(log, 'simple\n');
});
it('should output amd templates', function() {
Handlebars.precompile = function() { return 'amd'; };
Precompiler.cli({templates: [__dirname + '/artifacts/empty.handlebars'], amd: true, extension: 'handlebars'});
Precompiler.cli({templates: [emptyTemplate], amd: true});
equal(/template\(amd\)/.test(log), true);
});
it('should output multiple amd', function() {
Handlebars.precompile = function() { return 'amd'; };
Precompiler.cli({templates: [__dirname + '/artifacts'], amd: true, extension: 'handlebars', namespace: 'foo'});
Precompiler.cli({templates: [emptyTemplate, emptyTemplate], amd: true, namespace: 'foo'});
equal(/templates = foo = foo \|\|/.test(log), true);
equal(/return templates/.test(log), true);
equal(/template\(amd\)/.test(log), true);
});
it('should output amd partials', function() {
Handlebars.precompile = function() { return 'amd'; };
Precompiler.cli({templates: [__dirname + '/artifacts/empty.handlebars'], amd: true, partial: true, extension: 'handlebars'});
Precompiler.cli({templates: [emptyTemplate], amd: true, partial: true});
equal(/return Handlebars\.partials\['empty'\]/.test(log), true);
equal(/template\(amd\)/.test(log), true);
});
it('should output multiple amd partials', function() {
Handlebars.precompile = function() { return 'amd'; };
Precompiler.cli({templates: [__dirname + '/artifacts'], amd: true, partial: true, extension: 'handlebars'});
Precompiler.cli({templates: [emptyTemplate, emptyTemplate], amd: true, partial: true});
equal(/return Handlebars\.partials\[/.test(log), false);
equal(/template\(amd\)/.test(log), true);
});
it('should output commonjs templates', function() {
Handlebars.precompile = function() { return 'commonjs'; };
Precompiler.cli({templates: [__dirname + '/artifacts/empty.handlebars'], commonjs: true, extension: 'handlebars'});
Precompiler.cli({templates: [emptyTemplate], commonjs: true});
equal(/template\(commonjs\)/.test(log), true);
});
it('should set data flag', function() {
Handlebars.precompile = function(data, options) { equal(options.data, true); return 'simple'; };
Precompiler.cli({templates: [__dirname + '/artifacts/empty.handlebars'], simple: true, extension: 'handlebars', data: true});
Precompiler.cli({templates: [emptyTemplate], simple: true, data: true});
equal(log, 'simple\n');
});
it('should set known helpers', function() {
Handlebars.precompile = function(data, options) { equal(options.knownHelpers.foo, true); return 'simple'; };
Precompiler.cli({templates: [__dirname + '/artifacts/empty.handlebars'], simple: true, extension: 'handlebars', known: 'foo'});
equal(log, 'simple\n');
});
it('should handle different root', function() {
Handlebars.precompile = function() { return 'simple'; };
Precompiler.cli({templates: [__dirname + '/artifacts/empty.handlebars'], simple: true, extension: 'handlebars', root: 'foo/'});
Precompiler.cli({templates: [emptyTemplate], simple: true, known: 'foo'});
equal(log, 'simple\n');
});
it('should output to file system', function() {
Handlebars.precompile = function() { return 'simple'; };
Precompiler.cli({templates: [__dirname + '/artifacts/empty.handlebars'], simple: true, extension: 'handlebars', output: 'file!'});
Precompiler.cli({templates: [emptyTemplate], simple: true, output: 'file!'});
equal(file, 'file!');
equal(content, 'simple\n');
equal(log, '');
});
it('should handle BOM', function() {
Handlebars.precompile = function(template) { return template === 'a' ? 'simple' : 'fail'; };
Precompiler.cli({templates: [__dirname + '/artifacts/bom.handlebars'], simple: true, extension: 'handlebars', bom: true});
equal(log, 'simple\n');
});
it('should output minimized templates', function() {
Handlebars.precompile = function() { return 'amd'; };
uglify.minify = function() { return {code: 'min'}; };
Precompiler.cli({templates: [__dirname + '/artifacts/empty.handlebars'], min: true, extension: 'handlebars'});
Precompiler.cli({templates: [emptyTemplate], min: true});
equal(log, 'min');
});
it('should output map', function() {
Precompiler.cli({templates: [__dirname + '/artifacts/empty.handlebars'], map: 'foo.js.map', extension: 'handlebars'});
Precompiler.cli({templates: [emptyTemplate], map: 'foo.js.map'});
equal(file, 'foo.js.map');
equal(/sourceMappingURL=/.test(log), true);
});
it('should output map', function() {
Precompiler.cli({templates: [__dirname + '/artifacts/empty.handlebars'], min: true, map: 'foo.js.map', extension: 'handlebars'});
Precompiler.cli({templates: [emptyTemplate], min: true, map: 'foo.js.map'});
equal(file, 'foo.js.map');
equal(/sourceMappingURL=/.test(log), true);
});
describe('#loadTemplates', function() {
it('should throw on missing template', function(done) {
Precompiler.loadTemplates({files: ['foo']}, function(err) {
equal(err.message, 'Unable to open template file "foo"');
done();
});
});
it('should enumerate directories by extension', function(done) {
Precompiler.loadTemplates({files: [__dirname + '/artifacts'], extension: 'hbs'}, function(err, opts) {
equal(opts.templates.length, 1);
equal(opts.templates[0].name, 'example_2');
done(err);
});
});
it('should enumerate all templates by extension', function(done) {
Precompiler.loadTemplates({files: [__dirname + '/artifacts'], extension: 'handlebars'}, function(err, opts) {
equal(opts.templates.length, 3);
equal(opts.templates[0].name, 'bom');
equal(opts.templates[1].name, 'empty');
equal(opts.templates[2].name, 'example_1');
done(err);
});
});
it('should handle regular expression characters in extensions', function(done) {
Precompiler.loadTemplates({files: [__dirname + '/artifacts'], extension: 'hb(s'}, function(err) {
// Success is not throwing
done(err);
});
});
it('should handle BOM', function(done) {
var opts = {files: [__dirname + '/artifacts/bom.handlebars'], extension: 'handlebars', bom: true};
Precompiler.loadTemplates(opts, function(err, opts) {
equal(opts.templates[0].source, 'a');
done(err);
});
});
it('should handle different root', function(done) {
var opts = {files: [__dirname + '/artifacts/empty.handlebars'], simple: true, root: 'foo/'};
Precompiler.loadTemplates(opts, function(err, opts) {
equal(opts.templates[0].name, __dirname + '/artifacts/empty');
done(err);
});
});
it('should accept string inputs', function(done) {
var opts = {string: ''};
Precompiler.loadTemplates(opts, function(err, opts) {
equal(opts.templates[0].name, undefined);
equal(opts.templates[0].source, '');
done(err);
});
});
it('should accept string array inputs', function(done) {
var opts = {string: ['', 'bar'], name: ['beep', 'boop']};
Precompiler.loadTemplates(opts, function(err, opts) {
equal(opts.templates[0].name, 'beep');
equal(opts.templates[0].source, '');
equal(opts.templates[1].name, 'boop');
equal(opts.templates[1].source, 'bar');
done(err);
});
});
it('should accept stdin input', function(done) {
var stdin = require('mock-stdin').stdin();
Precompiler.loadTemplates({string: '-'}, function(err, opts) {
equal(opts.templates[0].source, 'foo');
done(err);
});
stdin.send('fo');
stdin.send('o');
stdin.end();
});
it('error on name missing', function(done) {
var opts = {string: ['', 'bar']};
Precompiler.loadTemplates(opts, function(err) {
equal(err.message, 'Number of names did not match the number of string inputs');
done();
});
});
it('should complete when no args are passed', function(done) {
Precompiler.loadTemplates({}, function(err, opts) {
equal(opts.templates.length, 0);
done(err);
});
});
});
});
+148 -33
View File
@@ -1,26 +1,34 @@
/*global CompilerContext, shouldCompileTo, shouldThrow */
describe('Regressions', function() {
it("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");
it('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');
});
it("GH-150: Inverted sections print when they shouldn't", function() {
var string = "{{^set}}not set{{/set}} :: {{#set}}set{{/set}}";
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");
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");
});
it("GH-158: Using array index twice, breaks the template", function() {
var string = "{{arr.[0]}}, {{arr.[1]}}";
var data = { "arr": [1,2] };
it('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");
shouldCompileTo(string, data, '1, 2', 'it works as expected');
});
it("bug reported by @fat where lambdas weren't being properly resolved", function() {
@@ -39,12 +47,12 @@ describe('Regressions', function() {
+ '{{/hasThings}}';
var data = {
thing: function() {
return "blah";
return 'blah';
},
things: [
{className: "one", word: "@fat"},
{className: "two", word: "@dhg"},
{className: "three", word:"@sayrer"}
{className: 'one', word: '@fat'},
{className: 'two', word: '@dhg'},
{className: 'three', word: '@sayrer'}
],
hasThings: function() {
return true;
@@ -63,14 +71,14 @@ describe('Regressions', function() {
it('GH-408: Multiple loops fail', function() {
var context = [
{ name: "John Doe", location: { city: "Chicago" } },
{ name: "Jane Doe", location: { city: "New York"} }
{ name: 'John Doe', location: { city: 'Chicago' } },
{ name: 'Jane Doe', location: { city: 'New York'} }
];
var template = CompilerContext.compile('{{#.}}{{name}}{{/.}}{{#.}}{{name}}{{/.}}{{#.}}{{name}}{{/.}}');
var result = template(context);
equals(result, "John DoeJane DoeJohn DoeJane DoeJohn DoeJane Doe", 'It should output multiple times');
equals(result, 'John DoeJane DoeJohn DoeJane DoeJohn DoeJane Doe', 'It should output multiple times');
});
it('GS-428: Nested if else rendering', function() {
@@ -95,11 +103,13 @@ describe('Regressions', function() {
});
it('GH-534: Object prototype aliases', function() {
/* eslint-disable no-extend-native */
Object.prototype[0xD834] = true;
shouldCompileTo('{{foo}}', { foo: 'bar' }, 'bar');
delete Object.prototype[0xD834];
/* eslint-enable no-extend-native */
});
it('GH-437: Matching escaping', function() {
@@ -111,23 +121,23 @@ describe('Regressions', function() {
}, Error);
});
it("GH-676: Using array in escaping mustache fails", function() {
var string = "{{arr}}";
var data = { "arr": [1,2] };
it('GH-676: Using array in escaping mustache fails', function() {
var string = '{{arr}}';
var data = { 'arr': [1, 2] };
shouldCompileTo(string, data, data.arr.toString(), "it works as expected");
shouldCompileTo(string, data, data.arr.toString(), 'it works as expected');
});
it("Mustache man page", function() {
var string = "Hello {{name}}. You have just won ${{value}}!{{#in_ca}} Well, ${{taxed_value}}, after taxes.{{/in_ca}}";
it('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
'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");
shouldCompileTo(string, data, 'Hello Chris. You have just won $10000! Well, $6000, after taxes.', 'the hello world mustache example works');
});
it('GH-731: zero context rendering', function() {
@@ -162,4 +172,109 @@ describe('Regressions', function() {
var result = template(context);
equals(result, 'foo');
});
it('GH-1021: Each empty string key', function() {
var data = {
'': 'foo',
'name': 'Chris',
'value': 10000
};
shouldCompileTo('{{#each data}}Key: {{@key}}\n{{/each}}', {data: data}, 'Key: \nKey: name\nKey: value\n');
});
it('GH-1054: Should handle simple safe string responses', function() {
var root = '{{#wrap}}{{>partial}}{{/wrap}}';
var partials = {
partial: '{{#wrap}}<partial>{{/wrap}}'
};
var helpers = {
wrap: function(options) {
return new Handlebars.SafeString(options.fn());
}
};
shouldCompileToWithPartials(root, [{}, helpers, partials], true, '<partial>');
});
it('GH-1065: Sparse arrays', function() {
var array = [];
array[1] = 'foo';
array[3] = 'bar';
shouldCompileTo('{{#each array}}{{@index}}{{.}}{{/each}}', {array: array}, '1foo3bar');
});
it('GH-1093: Undefined helper context', function() {
var obj = {foo: undefined, bar: 'bat'};
var helpers = {
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 (this.hasOwnProperty(name)) {
return 'found';
}
}
// And to make IE happy, check for the known string as length is not enumerated.
return (this == 'bat' ? 'found' : 'not');
}
};
shouldCompileTo('{{#each obj}}{{{helper}}}{{.}}{{/each}}', [{obj: obj}, helpers], 'notfoundbat');
});
it('should support multiple levels of inline partials', function() {
var string = '{{#> layout}}{{#*inline "subcontent"}}subcontent{{/inline}}{{/layout}}';
var partials = {
doctype: 'doctype{{> content}}',
layout: '{{#> doctype}}{{#*inline "content"}}layout{{> subcontent}}{{/inline}}{{/doctype}}'
};
shouldCompileToWithPartials(string, [{}, {}, partials], true, 'doctypelayoutsubcontent');
});
it('GH-1089: should support failover content in multiple levels of inline partials', function() {
var string = '{{#> layout}}{{/layout}}';
var partials = {
doctype: 'doctype{{> content}}',
layout: '{{#> doctype}}{{#*inline "content"}}layout{{#> subcontent}}subcontent{{/subcontent}}{{/inline}}{{/doctype}}'
};
shouldCompileToWithPartials(string, [{}, {}, partials], true, 'doctypelayoutsubcontent');
});
it('GH-1099: should support greater than 3 nested levels of inline partials', function() {
var string = '{{#> layout}}Outer{{/layout}}';
var partials = {
layout: '{{#> inner}}Inner{{/inner}}{{> @partial-block }}',
inner: ''
};
shouldCompileToWithPartials(string, [{}, {}, partials], true, 'Outer');
});
it('GH-1135 : Context handling within each iteration', function() {
var obj = {array: [1], name: 'John'};
var helpers = {
myif: function(conditional, options) {
if (conditional) {
return options.fn(this);
} else {
return options.inverse(this);
}
}
};
shouldCompileTo(
'{{#each array}}\n'
+ ' 1. IF: {{#if true}}{{../name}}-{{../../name}}-{{../../../name}}{{/if}}\n'
+ ' 2. MYIF: {{#myif true}}{{../name}}={{../../name}}={{../../../name}}{{/myif}}\n'
+ '{{/each}}', [obj, helpers],
' 1. IF: John--\n'
+ ' 2. MYIF: John==\n');
});
it('GH-1186: Support block params for existing programs', function() {
var string =
'{{#*inline "test"}}{{> @partial-block }}{{/inline}}'
+ '{{#>test }}{{#each listOne as |item|}}{{ item }}{{/each}}{{/test}}'
+ '{{#>test }}{{#each listTwo as |item|}}{{ item }}{{/each}}{{/test}}';
shouldCompileTo(string, { listOne: ['a'], listTwo: ['b']}, 'ab', '');
});
});
+7 -7
View File
@@ -1,21 +1,21 @@
if (typeof(require) !== 'undefined' && require.extensions[".handlebars"]) {
if (typeof require !== 'undefined' && require.extensions['.handlebars']) {
describe('Require', function() {
it('Load .handlebars files with require()', function() {
var template = require("./artifacts/example_1");
equal(template, require("./artifacts/example_1.handlebars"));
var template = require('./artifacts/example_1');
equal(template, require('./artifacts/example_1.handlebars'));
var expected = 'foo\n';
var result = template({foo: "foo"});
var result = template({foo: 'foo'});
equal(result, expected);
});
it('Load .hbs files with require()', function() {
var template = require("./artifacts/example_2");
equal(template, require("./artifacts/example_2.hbs"));
var template = require('./artifacts/example_2');
equal(template, require('./artifacts/example_2.hbs'));
var expected = 'Hello, World!\n';
var result = template({name: "World"});
var result = template({name: 'World'});
equal(result, expected);
});
+3 -5
View File
@@ -1,5 +1,3 @@
/*globals Handlebars, shouldThrow */
describe('runtime', function() {
describe('#template', function() {
it('should throw on invalid templates', function() {
@@ -16,19 +14,19 @@ describe('runtime', function() {
it('should throw on version mismatch', function() {
shouldThrow(function() {
Handlebars.template({
main: true,
main: {},
compiler: [Handlebars.COMPILER_REVISION + 1]
});
}, Error, /Template was precompiled with a newer version of Handlebars than the current runtime/);
shouldThrow(function() {
Handlebars.template({
main: true,
main: {},
compiler: [Handlebars.COMPILER_REVISION - 1]
});
}, Error, /Template was precompiled with an older version of Handlebars than the current runtime/);
shouldThrow(function() {
Handlebars.template({
main: true
main: {}
});
}, Error, /Template was precompiled with an older version of Handlebars than the current runtime/);
});
+5 -4
View File
@@ -1,7 +1,8 @@
/*global CompilerContext, Handlebars */
try {
if (typeof define !== 'function' || !define.amd) {
var SourceMap = require('source-map'),
SourceMapConsumer = SourceMap.SourceMapConsumer;
}
} catch (err) {
/* NOP for in browser */
}
@@ -18,13 +19,13 @@ describe('source-map', function() {
equal(!!template.map, !CompilerContext.browser);
});
it('should map source properly', function() {
var source = ' b{{hello}} \n {{bar}}a {{#block arg hash=(subex 1 subval)}}{{/block}}',
template = Handlebars.precompile(source, {destName: 'dest.js', srcName: 'src.hbs'});
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 = new SourceMapConsumer(template.map),
lines = template.code.split('\n'),
srcLines = source.split('\n'),
srcLines = templateSource.split('\n'),
generated = grepLine('" b"', lines),
source = grepLine(' b', srcLines);
+5 -4
View File
@@ -5,12 +5,11 @@ describe('spec', function() {
}
var _ = require('underscore'),
Handlebars = require('../lib'),
fs = require('fs');
var specDir = __dirname + '/mustache/specs/';
var specs = _.filter(fs.readdirSync(specDir), function(name) {
return /.*\.json$/.test(name);
return (/.*\.json$/).test(name);
});
_.each(specs, function(name) {
@@ -26,8 +25,8 @@ describe('spec', function() {
// We nest the entire response from partials, not just the literals
|| (name === 'partials.json' && test.name === 'Standalone Indentation')
|| /\{\{\=/.test(test.template)
|| _.any(test.partials, function(partial) { return /\{\{\=/.test(partial); })) {
|| (/\{\{\=/).test(test.template)
|| _.any(test.partials, function(partial) { return (/\{\{\=/).test(partial); })) {
it.skip(name + ' - ' + test.name);
return;
}
@@ -35,7 +34,9 @@ describe('spec', function() {
var data = _.clone(test.data);
if (data.lambda) {
// Blergh
/* eslint-disable no-eval */
data.lambda = eval('(' + data.lambda.js + ')');
/* eslint-enable no-eval */
}
it(name + ' - ' + test.name, function() {
if (test.partials) {
+17 -1
View File
@@ -1,4 +1,3 @@
/*global CompilerContext, Handlebars, shouldThrow */
var Exception = Handlebars.Exception;
describe('strict', function() {
@@ -79,6 +78,23 @@ describe('strict', function() {
template({hello: {}});
}, Exception, /"bar" not defined in/);
});
it('should allow undefined parameters when passed to helpers', function() {
var template = CompilerContext.compile('{{#unless foo}}success{{/unless}}', {strict: true});
equals(template({}), 'success');
});
it('should allow undefined hash when passed to helpers', function() {
var template = CompilerContext.compile('{{helper value=@foo}}', {strict: true});
var helpers = {
helper: function(options) {
equals('value' in options.hash, true);
equals(options.hash.value, undefined);
return 'success';
}
};
equals(template({}, {helpers: helpers}), 'success');
});
});
describe('assume objects', function() {
+43 -44
View File
@@ -1,44 +1,43 @@
/*global CompilerContext */
describe('string params mode', function() {
it("arguments to helpers can be retrieved from options hash in string form", function() {
it('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) {
return "HELP ME MY BOSS " + passiveVoice + ' ' + noun;
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");
equals(result, 'HELP ME MY BOSS is.a slave.driver', 'String parameters output');
});
it("when using block form, arguments to helpers can be retrieved from options hash in string form", function() {
it('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 + ' ' +
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");
equals(result, 'HELP ME MY BOSS is.a slave.driver: help :(', 'String parameters output');
});
it("when inside a block in String mode, .. passes the appropriate context in the options hash", function() {
it('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;
return 'STOP ME FROM READING HACKER NEWS I ' +
options.contexts[0][desire] + ' ' + noun;
},
"with": function(context, options) {
'with': function(context, options) {
return options.fn(options.contexts[0][context]);
}
};
@@ -49,31 +48,31 @@ describe('string params mode', function() {
need: 'need-a'
}, {helpers: helpers});
equals(result, "STOP ME FROM READING HACKER NEWS I need-a dad.joke", "Proper context variable output");
equals(result, 'STOP ME FROM READING HACKER NEWS I need-a dad.joke', 'Proper context variable output');
});
it("information about the types is passed along", function() {
var template = CompilerContext.compile('{{tomdale "need" dad.joke true false}}', { stringParams: true });
it('information about the types is passed along', function() {
var template = CompilerContext.compile("{{tomdale 'need' dad.joke true false}}", { stringParams: true });
var helpers = {
tomdale: function(desire, noun, trueBool, falseBool, options) {
equal(options.types[0], 'StringLiteral', "the string type is passed");
equal(options.types[1], 'PathExpression', "the expression type is passed");
equal(options.types[2], 'BooleanLiteral', "the expression type is passed");
equal(desire, "need", "the string form is passed for strings");
equal(noun, "dad.joke", "the string form is passed for expressions");
equal(trueBool, true, "raw booleans are passed through");
equal(falseBool, false, "raw booleans are passed through");
return "Helper called";
equal(options.types[0], 'StringLiteral', 'the string type is passed');
equal(options.types[1], 'PathExpression', 'the expression type is passed');
equal(options.types[2], 'BooleanLiteral', 'the expression type is passed');
equal(desire, 'need', 'the string form is passed for strings');
equal(noun, 'dad.joke', 'the string form is passed for expressions');
equal(trueBool, true, 'raw booleans are passed through');
equal(falseBool, false, 'raw booleans are passed through');
return 'Helper called';
}
};
var result = template({}, { helpers: helpers });
equal(result, "Helper called");
equal(result, 'Helper called');
});
it("hash parameters get type information", function() {
var template = CompilerContext.compile('{{tomdale he.says desire="need" noun=dad.joke bool=true}}', { stringParams: true });
it('hash parameters get type information', function() {
var template = CompilerContext.compile("{{tomdale he.says desire='need' noun=dad.joke bool=true}}", { stringParams: true });
var helpers = {
tomdale: function(exclamation, options) {
@@ -94,43 +93,43 @@ describe('string params mode', function() {
equal(result, 'Helper called');
});
it("hash parameters get context information", function() {
var template = CompilerContext.compile('{{#with dale}}{{tomdale he.says desire="need" noun=../dad/joke bool=true}}{{/with}}', { stringParams: true });
it('hash parameters get context information', function() {
var template = CompilerContext.compile("{{#with dale}}{{tomdale he.says desire='need' noun=../dad/joke bool=true}}{{/with}}", { stringParams: true });
var context = {dale: {}};
var helpers = {
tomdale: function(exclamation, options) {
equal(exclamation, "he.says");
equal(exclamation, 'he.says');
equal(options.types[0], 'PathExpression');
equal(options.contexts.length, 1);
equal(options.hashContexts.noun, context);
equal(options.hash.desire, "need");
equal(options.hash.noun, "dad.joke");
equal(options.hash.desire, 'need');
equal(options.hash.noun, 'dad.joke');
equal(options.hash.bool, true);
return "Helper called";
return 'Helper called';
},
"with": function(context, options) {
return options.fn(options.contexts[0][context]);
'with': function(withContext, options) {
return options.fn(options.contexts[0][withContext]);
}
};
var result = template(context, { helpers: helpers });
equal(result, "Helper called");
equal(result, 'Helper called');
});
it("when inside a block in String mode, .. passes the appropriate context in the options hash to a block helper", function() {
it('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 + " " +
return 'STOP ME FROM READING HACKER NEWS I ' +
options.contexts[0][desire] + ' ' + noun + ' ' +
options.fn(this);
},
"with": function(context, options) {
'with': function(context, options) {
return options.fn(options.contexts[0][context]);
}
};
@@ -141,23 +140,23 @@ describe('string params mode', function() {
need: 'need-a'
}, {helpers: helpers});
equals(result, "STOP ME FROM READING HACKER NEWS I need-a dad.joke wot", "Proper context variable output");
equals(result, 'STOP ME FROM READING HACKER NEWS I need-a dad.joke wot', 'Proper context variable output');
});
it("with nested block ambiguous", function() {
it('with nested block ambiguous', function() {
var template = CompilerContext.compile('{{#with content}}{{#view}}{{firstName}} {{lastName}}{{/view}}{{/with}}', {stringParams: true});
var helpers = {
'with': function(options) {
return "WITH";
'with': function() {
return 'WITH';
},
view: function() {
return "VIEW";
return 'VIEW';
}
};
var result = template({}, {helpers: helpers});
equals(result, "WITH");
equals(result, 'WITH');
});
it('should handle DATA', function() {
+52 -54
View File
@@ -1,128 +1,127 @@
/*global CompilerContext, Handlebars, shouldCompileTo, shouldThrow */
describe('subexpressions', function() {
it("arg-less helper", function() {
var string = "{{foo (bar)}}!";
it('arg-less helper', function() {
var string = '{{foo (bar)}}!';
var context = {};
var helpers = {
foo: function(val) {
return val + val;
},
bar: function() {
return "LOL";
return 'LOL';
}
};
shouldCompileTo(string, [context, helpers], "LOLLOL!");
shouldCompileTo(string, [context, helpers], 'LOLLOL!');
});
it("helper w args", function() {
it('helper w args', function() {
var string = '{{blog (equal a b)}}';
var context = { bar: "LOL" };
var context = { bar: 'LOL' };
var helpers = {
blog: function(val) {
return "val is " + val;
return 'val is ' + val;
},
equal: function(x, y) {
return x === y;
}
};
shouldCompileTo(string, [context, helpers], "val is true");
shouldCompileTo(string, [context, helpers], 'val is true');
});
it("mixed paths and helpers", function() {
it('mixed paths and helpers', function() {
var string = '{{blog baz.bat (equal a b) baz.bar}}';
var context = { bar: "LOL", baz: {bat: 'foo!', bar: 'bar!'} };
var context = { bar: 'LOL', baz: {bat: 'foo!', bar: 'bar!'} };
var helpers = {
blog: function(val, that, theOther) {
return "val is " + val + ', ' + that + ' and ' + theOther;
return 'val is ' + val + ', ' + that + ' and ' + theOther;
},
equal: function(x, y) {
return x === y;
}
};
shouldCompileTo(string, [context, helpers], "val is foo!, true and bar!");
shouldCompileTo(string, [context, helpers], 'val is foo!, true and bar!');
});
it("supports much nesting", function() {
it('supports much nesting', function() {
var string = '{{blog (equal (equal true true) true)}}';
var context = { bar: "LOL" };
var context = { bar: 'LOL' };
var helpers = {
blog: function(val) {
return "val is " + val;
return 'val is ' + val;
},
equal: function(x, y) {
return x === y;
}
};
shouldCompileTo(string, [context, helpers], "val is true");
shouldCompileTo(string, [context, helpers], '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;
return a + '-' + b;
},
concat: function(a, b) {
return a + b;
}
};
shouldCompileTo('{{dash "abc" (concat a b)}}', [context, helpers], 'abc-ab');
shouldCompileTo("{{dash 'abc' (concat a b)}}", [context, helpers], 'abc-ab');
shouldCompileTo('{{dash d (concat a b)}}', [context, helpers], 'd-ab');
shouldCompileTo('{{dash c.c (concat a b)}}', [context, helpers], 'c-ab');
shouldCompileTo('{{dash (concat a b) c.c}}', [context, helpers], 'ab-c');
shouldCompileTo('{{dash (concat a e.e) c.c}}', [context, helpers], 'ae-c');
});
it("provides each nested helper invocation its own options hash", function() {
it('provides each nested helper invocation its own options hash', function() {
var string = '{{equal (equal true true) true}}';
var lastOptions = null;
var helpers = {
equal: function(x, y, options) {
if (!options || options === lastOptions) {
throw new Error("options hash was reused");
throw new Error('options hash was reused');
}
lastOptions = options;
return x === y;
}
};
shouldCompileTo(string, [{}, helpers], "true");
shouldCompileTo(string, [{}, helpers], 'true');
});
it("with hashes", function() {
var string = '{{blog (equal (equal true true) true fun="yes")}}';
it('with hashes', function() {
var string = "{{blog (equal (equal true true) true fun='yes')}}";
var context = { bar: "LOL" };
var context = { bar: 'LOL' };
var helpers = {
blog: function(val) {
return "val is " + val;
return 'val is ' + val;
},
equal: function(x, y) {
return x === y;
}
};
shouldCompileTo(string, [context, helpers], "val is true");
shouldCompileTo(string, [context, helpers], 'val is true');
});
it("as hashes", function() {
var string = '{{blog fun=(equal (blog fun=1) "val is 1")}}';
it('as hashes', function() {
var string = "{{blog fun=(equal (blog fun=1) 'val is 1')}}";
var helpers = {
blog: function(options) {
return "val is " + options.hash.fun;
return 'val is ' + options.hash.fun;
},
equal: function(x, y) {
return x === y;
}
};
shouldCompileTo(string, [{}, helpers], "val is true");
shouldCompileTo(string, [{}, helpers], 'val is true');
});
it("multiple subexpressions in a hash", function() {
it('multiple subexpressions in a hash', function() {
var string = '{{input aria-label=(t "Name") placeholder=(t "Example User")}}';
var helpers = {
@@ -139,13 +138,13 @@ describe('subexpressions', function() {
shouldCompileTo(string, [{}, helpers], '<input aria-label="Name" placeholder="Example User" />');
});
it("multiple subexpressions in a hash with context", function() {
it('multiple subexpressions in a hash with context', function() {
var string = '{{input aria-label=(t item.field) placeholder=(t item.placeholder)}}';
var context = {
item: {
field: "Name",
placeholder: "Example User"
field: 'Name',
placeholder: 'Example User'
}
};
@@ -163,21 +162,21 @@ describe('subexpressions', function() {
shouldCompileTo(string, [context, helpers], '<input aria-label="Name" placeholder="Example User" />');
});
it("in string params mode,", function() {
it('in string params mode,', function() {
var template = CompilerContext.compile('{{snog (blorg foo x=y) yeah a=b}}', {stringParams: true});
var helpers = {
snog: function(a, b, options) {
equals(a, 'foo');
equals(options.types.length, 2, "string params for outer helper processed correctly");
equals(options.types[0], 'SubExpression', "string params for outer helper processed correctly");
equals(options.types[1], 'PathExpression', "string params for outer helper processed correctly");
equals(options.types.length, 2, 'string params for outer helper processed correctly');
equals(options.types[0], 'SubExpression', 'string params for outer helper processed correctly');
equals(options.types[1], 'PathExpression', 'string params for outer helper processed correctly');
return a + b;
},
blorg: function(a, options) {
equals(options.types.length, 1, "string params for inner helper processed correctly");
equals(options.types[0], 'PathExpression', "string params for inner helper processed correctly");
equals(options.types.length, 1, 'string params for inner helper processed correctly');
equals(options.types[0], 'PathExpression', 'string params for inner helper processed correctly');
return a;
}
};
@@ -187,32 +186,31 @@ describe('subexpressions', function() {
yeah: {}
}, {helpers: helpers});
equals(result, "fooyeah");
equals(result, 'fooyeah');
});
it("as hashes in string params mode", function() {
it('as hashes in string params mode', function() {
var template = CompilerContext.compile('{{blog fun=(bork)}}', {stringParams: true});
var helpers = {
blog: function(options) {
equals(options.hashTypes.fun, 'SubExpression');
return "val is " + options.hash.fun;
return 'val is ' + options.hash.fun;
},
bork: function() {
return "BORK";
return 'BORK';
}
};
var result = template({}, {helpers: helpers});
equals(result, "val is BORK");
equals(result, 'val is BORK');
});
it("subexpression functions on the context", function() {
var string = "{{foo (bar)}}!";
it('subexpression functions on the context', function() {
var string = '{{foo (bar)}}!';
var context = {
bar: function() {
return "LOL";
return 'LOL';
}
};
var helpers = {
@@ -220,13 +218,13 @@ describe('subexpressions', function() {
return val + val;
}
};
shouldCompileTo(string, [context, helpers], "LOLLOL!");
shouldCompileTo(string, [context, helpers], 'LOLLOL!');
});
it("subexpressions can't just be property lookups", function() {
var string = "{{foo (bar)}}!";
var string = '{{foo (bar)}}!';
var context = {
bar: "LOL"
bar: 'LOL'
};
var helpers = {
foo: function(val) {
@@ -234,7 +232,7 @@ describe('subexpressions', function() {
}
};
shouldThrow(function() {
shouldCompileTo(string, [context, helpers], "LOLLOL!");
shouldCompileTo(string, [context, helpers], 'LOLLOL!');
});
});
});
+155 -131
View File
@@ -1,4 +1,3 @@
/*global Handlebars */
function shouldMatchTokens(result, tokens) {
for (var index = 0; index < result.length; index++) {
equals(result[index].name, tokens[index]);
@@ -22,7 +21,7 @@ describe('Tokenizer', function() {
var out = [],
token;
while (token = lexer.lex()) {
while ((token = lexer.lex())) {
var result = parser.terminals_[token] || token;
if (!result || result === 'EOF' || result === 'INVALID') {
break;
@@ -34,366 +33,391 @@ describe('Tokenizer', function() {
}
it('tokenizes a simple mustache as "OPEN ID CLOSE"', function() {
var result = tokenize("{{foo}}");
var result = tokenize('{{foo}}');
shouldMatchTokens(result, ['OPEN', 'ID', 'CLOSE']);
shouldBeToken(result[1], "ID", "foo");
shouldBeToken(result[1], 'ID', 'foo');
});
it('supports unescaping with &', function() {
var result = tokenize("{{&bar}}");
var result = tokenize('{{&bar}}');
shouldMatchTokens(result, ['OPEN', 'ID', 'CLOSE']);
shouldBeToken(result[0], "OPEN", "{{&");
shouldBeToken(result[1], "ID", "bar");
shouldBeToken(result[0], 'OPEN', '{{&');
shouldBeToken(result[1], 'ID', 'bar');
});
it('supports unescaping with {{{', function() {
var result = tokenize("{{{bar}}}");
var result = tokenize('{{{bar}}}');
shouldMatchTokens(result, ['OPEN_UNESCAPED', 'ID', 'CLOSE_UNESCAPED']);
shouldBeToken(result[1], "ID", "bar");
shouldBeToken(result[1], 'ID', 'bar');
});
it('supports escaping delimiters', function() {
var result = tokenize("{{foo}} \\{{bar}} {{baz}}");
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}} ");
shouldBeToken(result[3], 'CONTENT', ' ');
shouldBeToken(result[4], 'CONTENT', '{{bar}} ');
});
it('supports escaping multiple delimiters', function() {
var result = tokenize("{{foo}} \\{{bar}} \\{{baz}}");
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}}");
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}}");
var result = tokenize('{{foo}} \\{{{bar}}} {{baz}}');
shouldMatchTokens(result, ['OPEN', 'ID', 'CLOSE', 'CONTENT', 'CONTENT', 'OPEN', 'ID', 'CLOSE']);
shouldBeToken(result[4], "CONTENT", "{{{bar}}} ");
shouldBeToken(result[4], 'CONTENT', '{{{bar}}} ');
});
it('supports escaping escape character', function() {
var result = tokenize("{{foo}} \\\\{{bar}} {{baz}}");
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[3], 'CONTENT', ' \\');
shouldBeToken(result[5], 'ID', 'bar');
});
it('supports escaping multiple escape characters', function() {
var result = tokenize("{{foo}} \\\\{{bar}} \\\\{{baz}}");
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");
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}}");
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}}");
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}}");
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");
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}}");
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");
shouldBeToken(result[3], 'CONTENT', ' \\');
shouldBeToken(result[5], 'ID', 'bar');
});
it('tokenizes a simple path', function() {
var result = tokenize("{{foo/bar}}");
var result = tokenize('{{foo/bar}}');
shouldMatchTokens(result, ['OPEN', 'ID', 'SEP', 'ID', 'CLOSE']);
});
it('allows dot notation', function() {
var result = tokenize("{{foo.bar}}");
var result = tokenize('{{foo.bar}}');
shouldMatchTokens(result, ['OPEN', 'ID', 'SEP', 'ID', 'CLOSE']);
shouldMatchTokens(tokenize("{{foo.bar.baz}}"), ['OPEN', 'ID', 'SEP', '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]}}");
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]}}");
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("{{.}}");
var result = tokenize('{{.}}');
shouldMatchTokens(result, ['OPEN', 'ID', 'CLOSE']);
});
it('tokenizes a path as "OPEN (ID SEP)* ID CLOSE"', function() {
var result = tokenize("{{../foo/bar}}");
var result = tokenize('{{../foo/bar}}');
shouldMatchTokens(result, ['OPEN', 'ID', 'SEP', 'ID', 'SEP', 'ID', 'CLOSE']);
shouldBeToken(result[1], "ID", "..");
shouldBeToken(result[1], 'ID', '..');
});
it('tokenizes a path with .. as a parent path', function() {
var result = tokenize("{{../foo.bar}}");
var result = tokenize('{{../foo.bar}}');
shouldMatchTokens(result, ['OPEN', 'ID', 'SEP', 'ID', 'SEP', 'ID', 'CLOSE']);
shouldBeToken(result[1], "ID", "..");
shouldBeToken(result[1], 'ID', '..');
});
it('tokenizes a path with this/foo as OPEN ID SEP ID CLOSE', function() {
var result = tokenize("{{this/foo}}");
var result = tokenize('{{this/foo}}');
shouldMatchTokens(result, ['OPEN', 'ID', 'SEP', 'ID', 'CLOSE']);
shouldBeToken(result[1], "ID", "this");
shouldBeToken(result[3], "ID", "foo");
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 }}");
var result = tokenize('{{ foo }}');
shouldMatchTokens(result, ['OPEN', 'ID', 'CLOSE']);
shouldBeToken(result[1], "ID", "foo");
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 }}");
var result = tokenize('{{ foo \n bar }}');
shouldMatchTokens(result, ['OPEN', 'ID', 'ID', 'CLOSE']);
shouldBeToken(result[1], "ID", "foo");
shouldBeToken(result[1], 'ID', 'foo');
});
it('tokenizes raw content as "CONTENT"', function() {
var result = tokenize("foo {{ bar }} baz");
var result = tokenize('foo {{ bar }} baz');
shouldMatchTokens(result, ['CONTENT', 'OPEN', 'ID', 'CLOSE', 'CONTENT']);
shouldBeToken(result[0], "CONTENT", "foo ");
shouldBeToken(result[4], "CONTENT", " baz");
shouldBeToken(result[0], 'CONTENT', 'foo ');
shouldBeToken(result[4], 'CONTENT', ' baz');
});
it('tokenizes a partial as "OPEN_PARTIAL ID CLOSE"', function() {
var result = tokenize("{{> foo}}");
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 }}");
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}}");
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 }}");
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 }}");
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 }}");
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 }}");
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 }}");
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}} --}}");
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 }}");
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--}}");
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}}");
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']);
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}}");
var result = tokenize('{{^foo}}');
shouldMatchTokens(result, ['OPEN_INVERSE', 'ID', 'CLOSE']);
shouldBeToken(result[1], "ID", "foo");
shouldBeToken(result[1], 'ID', 'foo');
});
it('tokenizes inverse sections with ID and spaces as "OPEN_INVERSE ID CLOSE"', function() {
var result = tokenize("{{^ foo }}");
var result = tokenize('{{^ foo }}');
shouldMatchTokens(result, ['OPEN_INVERSE', 'ID', 'CLOSE']);
shouldBeToken(result[1], "ID", "foo");
shouldBeToken(result[1], 'ID', 'foo');
});
it('tokenizes mustaches with params as "OPEN ID ID ID CLOSE"', function() {
var result = tokenize("{{ foo bar baz }}");
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");
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\" }}");
var result = tokenize('{{ foo bar \"baz\" }}');
shouldMatchTokens(result, ['OPEN', 'ID', 'ID', 'STRING', 'CLOSE']);
shouldBeToken(result[3], "STRING", "baz");
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");
shouldBeToken(result[3], 'STRING', 'baz');
});
it('tokenizes String params with spaces inside as "STRING"', function() {
var result = tokenize("{{ foo bar \"baz bat\" }}");
var result = tokenize('{{ foo bar "baz bat" }}');
shouldMatchTokens(result, ['OPEN', 'ID', 'ID', 'STRING', 'CLOSE']);
shouldBeToken(result[3], "STRING", "baz bat");
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');
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");
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");
shouldBeToken(result[2], 'NUMBER', '1');
result = tokenize('{{ foo 1.1 }}');
shouldMatchTokens(result, ['OPEN', 'ID', 'NUMBER', 'CLOSE']);
shouldBeToken(result[2], "NUMBER", "1.1");
shouldBeToken(result[2], 'NUMBER', '1.1');
result = tokenize('{{ foo -1 }}');
shouldMatchTokens(result, ['OPEN', 'ID', 'NUMBER', 'CLOSE']);
shouldBeToken(result[2], "NUMBER", "-1");
shouldBeToken(result[2], 'NUMBER', '-1');
result = tokenize('{{ foo -1.1 }}');
shouldMatchTokens(result, ['OPEN', 'ID', 'NUMBER', 'CLOSE']);
shouldBeToken(result[2], "NUMBER", "-1.1");
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");
shouldBeToken(result[2], 'BOOLEAN', 'true');
result = tokenize('{{ foo false }}');
shouldMatchTokens(result, ['OPEN', 'ID', 'BOOLEAN', 'CLOSE']);
shouldBeToken(result[2], "BOOLEAN", "false");
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 }}");
var result = tokenize('{{ foo bar=baz }}');
shouldMatchTokens(result, ['OPEN', 'ID', 'ID', 'EQUALS', 'ID', 'CLOSE']);
result = tokenize("{{ foo bar baz=bat }}");
result = tokenize('{{ foo bar baz=bat }}');
shouldMatchTokens(result, ['OPEN', 'ID', 'ID', 'ID', 'EQUALS', 'ID', 'CLOSE']);
result = tokenize("{{ foo bar baz=1 }}");
result = tokenize('{{ foo bar baz=1 }}');
shouldMatchTokens(result, ['OPEN', 'ID', 'ID', 'ID', 'EQUALS', 'NUMBER', 'CLOSE']);
result = tokenize("{{ foo bar baz=true }}");
result = tokenize('{{ foo bar baz=true }}');
shouldMatchTokens(result, ['OPEN', 'ID', 'ID', 'ID', 'EQUALS', 'BOOLEAN', 'CLOSE']);
result = tokenize("{{ foo bar baz=false }}");
result = tokenize('{{ foo bar baz=false }}');
shouldMatchTokens(result, ['OPEN', 'ID', 'ID', 'ID', 'EQUALS', 'BOOLEAN', 'CLOSE']);
result = tokenize("{{ foo bar\n baz=bat }}");
result = tokenize('{{ foo bar\n baz=bat }}');
shouldMatchTokens(result, ['OPEN', 'ID', 'ID', 'ID', 'EQUALS', 'ID', 'CLOSE']);
result = tokenize("{{ foo bar baz=\"bat\" }}");
result = tokenize('{{ foo bar baz=\"bat\" }}');
shouldMatchTokens(result, ['OPEN', 'ID', 'ID', 'ID', 'EQUALS', 'STRING', 'CLOSE']);
result = tokenize("{{ foo bar baz=\"bat\" bam=wot }}");
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\"}}");
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");
shouldBeToken(result[2], 'ID', 'omg');
});
it('tokenizes special @ identifiers', function() {
var result = tokenize("{{ @foo }}");
var result = tokenize('{{ @foo }}');
shouldMatchTokens(result, ['OPEN', 'DATA', 'ID', 'CLOSE']);
shouldBeToken(result[2], "ID", "foo");
shouldBeToken(result[2], 'ID', 'foo');
result = tokenize("{{ foo @bar }}");
result = tokenize('{{ foo @bar }}');
shouldMatchTokens(result, ['OPEN', 'ID', 'DATA', 'ID', 'CLOSE']);
shouldBeToken(result[3], "ID", "bar");
shouldBeToken(result[3], 'ID', 'bar');
result = tokenize("{{ foo bar=@baz }}");
result = tokenize('{{ foo bar=@baz }}');
shouldMatchTokens(result, ['OPEN', 'ID', 'ID', 'EQUALS', 'DATA', 'ID', 'CLOSE']);
shouldBeToken(result[5], "ID", "baz");
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']);
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']);
shouldMatchTokens(tokenize('{{foo & }}'), ['OPEN', 'ID']);
});
it('tokenizes subexpressions', function() {
var result = tokenize("{{foo (bar)}}");
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");
shouldBeToken(result[1], 'ID', 'foo');
shouldBeToken(result[3], 'ID', 'bar');
result = tokenize("{{foo (a-x b-y)}}");
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");
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)}}");
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");
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() {
@@ -402,19 +426,19 @@ describe('Tokenizer', function() {
});
it('tokenizes block params', function() {
var result = tokenize("{{#foo as |bar|}}");
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|}}");
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 |}}");
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 |}}");
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|}}");
result = tokenize('{{else foo as |bar baz|}}');
shouldMatchTokens(result, ['OPEN_INVERSE_CHAIN', 'ID', 'OPEN_BLOCK_PARAMS', 'ID', 'ID', 'CLOSE_BLOCK_PARAMS', 'CLOSE']);
});
});

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