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 .DS_Store
.gitignore .gitignore
.rvmrc .rvmrc
.jshintrc .eslintrc
.travis.yml .travis.yml
.rspec .rspec
Gemfile Gemfile
@@ -10,6 +10,7 @@ Rakefile
Gruntfile.js Gruntfile.js
*.gemspec *.gemspec
*.nuspec *.nuspec
*.log
bench/* bench/*
configurations/* configurations/*
components/* components/*
+5 -1
View File
@@ -13,11 +13,15 @@ env:
- secure: Nm4AgSfsgNB21kgKrF9Tl7qVZU8YYREhouQunFracTcZZh2NZ2XH5aHuSiXCj88B13Cr/jGbJKsZ4T3QS3wWYtz6lkyVOx3H3iI+TMtqhD9RM3a7A4O+4vVN8IioB2YjhEu0OKjwgX5gp+0uF+pLEi7Hpj6fupD3AbbL5uYcKg8= - secure: Nm4AgSfsgNB21kgKrF9Tl7qVZU8YYREhouQunFracTcZZh2NZ2XH5aHuSiXCj88B13Cr/jGbJKsZ4T3QS3wWYtz6lkyVOx3H3iI+TMtqhD9RM3a7A4O+4vVN8IioB2YjhEu0OKjwgX5gp+0uF+pLEi7Hpj6fupD3AbbL5uYcKg8=
matrix: matrix:
include: include:
- node_js: '0.10' - node_js: '5'
env: env:
- PUBLISH=true - PUBLISH=true
- secure: pLTzghtVll9yGKJI0AaB0uI8GypfWxLTaIB0ZL8//yN3nAEIKMhf/RRilYTsn/rKj2NUa7vt2edYILi3lttOUlCBOwTc9amiRms1W8Lwr/3IdWPeBLvLuH1zNJRm2lBAwU4LBSqaOwhGaxOQr6KHTnWudhNhgOucxpZfvfI/dFw= - secure: pLTzghtVll9yGKJI0AaB0uI8GypfWxLTaIB0ZL8//yN3nAEIKMhf/RRilYTsn/rKj2NUa7vt2edYILi3lttOUlCBOwTc9amiRms1W8Lwr/3IdWPeBLvLuH1zNJRm2lBAwU4LBSqaOwhGaxOQr6KHTnWudhNhgOucxpZfvfI/dFw=
- secure: yERYCf7AwL11D9uMtacly/THGV8BlzsMmrt+iQVvGA3GaY6QMmfYqf6P6cCH98sH5etd1Y+1e6YrPeMjqI6lyRllT7FptoyOdHulazQe86VQN4sc0EpqMlH088kB7gGjTut9Z+X9ViooT5XEh9WA5jXEI9pXhQJNoIHkWPuwGuY= - secure: yERYCf7AwL11D9uMtacly/THGV8BlzsMmrt+iQVvGA3GaY6QMmfYqf6P6cCH98sH5etd1Y+1e6YrPeMjqI6lyRllT7FptoyOdHulazQe86VQN4sc0EpqMlH088kB7gGjTut9Z+X9ViooT5XEh9WA5jXEI9pXhQJNoIHkWPuwGuY=
- node_js: '4'
cache: cache:
directories: directories:
- node_modules - 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) 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). 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 If you notice any problems, please report them to the GitHub issue tracker at
[http://github.com/wycats/handlebars.js/issues](http://github.com/wycats/handlebars.js/issues). [http://github.com/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 ## 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. 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 [generator-release]: https://github.com/walmartlabs/generator-release
[pull-request]: https://github.com/wycats/handlebars.js/pull/new/master [pull-request]: https://github.com/wycats/handlebars.js/pull/new/master
[issue]: https://github.com/wycats/handlebars.js/issues/new [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) { module.exports = function(grunt) {
grunt.initConfig({ grunt.initConfig({
pkg: grunt.file.readJSON('package.json'), pkg: grunt.file.readJSON('package.json'),
jshint: { eslint: {
options: { options: {
jshintrc: '.jshintrc'
}, },
files: [ 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: { copy: {
dist: { dist: {
options: { options: {
processContent: function(content, path) { processContent: function(content) {
return grunt.template.process('/*!\n\n <%= pkg.name %> v<%= pkg.version %>\n\n<%= grunt.file.read("LICENSE") %>\n@license\n*/\n') return grunt.template.process('/**!\n\n @license\n <%= pkg.name %> v<%= pkg.version %>\n\n<%= grunt.file.read("LICENSE") %>\n*/\n')
+ content; + content;
} }
}, },
@@ -41,21 +43,16 @@ module.exports = function(grunt) {
} }
}, },
packager: { babel: {
global: { options: {
type: 'umd', sourceMaps: 'inline',
export: 'Handlebars', loose: ['es6.modules'],
files: [{ auxiliaryCommentBefore: 'istanbul ignore next'
cwd: 'lib/',
expand: true,
src: ['handlebars*.js'],
dest: 'dist/'
}]
}, },
amd: { amd: {
type: 'amd', options: {
anonymous: true, modules: 'amd'
},
files: [{ files: [{
expand: true, expand: true,
cwd: 'lib/', cwd: 'lib/',
@@ -65,30 +62,61 @@ module.exports = function(grunt) {
}, },
cjs: { cjs: {
type: 'cjs', options: {
modules: 'common'
},
files: [{ files: [{
expand: true,
cwd: 'lib/', cwd: 'lib/',
expand: true,
src: '**/!(index).js', src: '**/!(index).js',
dest: 'dist/cjs/' 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: { requirejs: {
options: { options: {
optimize: "none", optimize: 'none',
baseUrl: "dist/amd/" baseUrl: 'dist/amd/'
}, },
dist: { dist: {
options: { options: {
name: "handlebars", name: 'handlebars',
out: "dist/handlebars.amd.js" out: 'dist/handlebars.amd.js'
} }
}, },
runtime: { runtime: {
options: { options: {
name: "handlebars.runtime", name: 'handlebars.runtime',
out: "dist/handlebars.runtime.amd.js" out: 'dist/handlebars.runtime.amd.js'
} }
} }
}, },
@@ -97,7 +125,7 @@ module.exports = function(grunt) {
options: { options: {
mangle: true, mangle: true,
compress: true, compress: true,
preserveComments: 'some' preserveComments: /(?:^!|@(?:license|preserve|cc_on))/
}, },
dist: { dist: {
files: [{ files: [{
@@ -134,15 +162,14 @@ module.exports = function(grunt) {
build: process.env.TRAVIS_JOB_ID, build: process.env.TRAVIS_JOB_ID,
urls: ['http://localhost:9999/spec/?headless=true', 'http://localhost:9999/spec/amd.html?headless=true'], urls: ['http://localhost:9999/spec/?headless=true', 'http://localhost:9999/spec/amd.html?headless=true'],
detailedError: true, detailedError: true,
concurrency: 2, concurrency: 4,
browsers: [ browsers: [
{browserName: 'chrome'}, {browserName: 'chrome'},
{browserName: 'firefox'}, {browserName: 'firefox', platform: 'Linux'},
{browserName: 'safari', version: 7, platform: 'OS X 10.9'}, {browserName: 'safari', version: 9, platform: 'OS X 10.11'},
{browserName: 'safari', version: 6, platform: 'OS X 10.8'}, {browserName: 'safari', version: 8, platform: 'OS X 10.10'},
{browserName: 'internet explorer', version: 11, platform: 'Windows 8.1'}, {browserName: 'internet explorer', version: 11, platform: 'Windows 8.1'},
{browserName: 'internet explorer', version: 10, platform: 'Windows 8'}, {browserName: 'internet explorer', version: 10, platform: 'Windows 8'}
{browserName: 'internet explorer', version: 9, platform: 'Windows 7'}
] ]
} }
}, },
@@ -172,18 +199,18 @@ module.exports = function(grunt) {
}); });
// Build a new version of the library // 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', 'parser',
'node', 'node',
'globals', 'globals']);
'jshint']);
this.registerTask('amd', ['packager:amd', 'requirejs']); this.registerTask('amd', ['babel:amd', 'requirejs']);
this.registerTask('node', ['packager:cjs']); this.registerTask('node', ['babel:cjs']);
this.registerTask('globals', ['packager:global']); this.registerTask('globals', ['webpack']);
this.registerTask('tests', ['concat:tests']); 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 // Load tasks from npm
grunt.loadNpmTasks('grunt-contrib-clean'); grunt.loadNpmTasks('grunt-contrib-clean');
@@ -191,11 +218,12 @@ module.exports = function(grunt) {
grunt.loadNpmTasks('grunt-contrib-connect'); grunt.loadNpmTasks('grunt-contrib-connect');
grunt.loadNpmTasks('grunt-contrib-copy'); grunt.loadNpmTasks('grunt-contrib-copy');
grunt.loadNpmTasks('grunt-contrib-requirejs'); grunt.loadNpmTasks('grunt-contrib-requirejs');
grunt.loadNpmTasks('grunt-contrib-jshint');
grunt.loadNpmTasks('grunt-contrib-uglify'); grunt.loadNpmTasks('grunt-contrib-uglify');
grunt.loadNpmTasks('grunt-contrib-watch'); grunt.loadNpmTasks('grunt-contrib-watch');
grunt.loadNpmTasks('grunt-babel');
grunt.loadNpmTasks('grunt-eslint');
grunt.loadNpmTasks('grunt-saucelabs'); grunt.loadNpmTasks('grunt-saucelabs');
grunt.loadNpmTasks('es6-module-packager'); grunt.loadNpmTasks('grunt-webpack');
grunt.task.loadTasks('tasks'); 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 Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal 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. keep the view and the code separated like we all know they should be.
Checkout the official Handlebars docs site at 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
---------- ----------
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. See our [installation documentation](http://handlebarsjs.com/installation.html).
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.
Usage Usage
----- -----
@@ -57,266 +45,34 @@ var result = template(data);
// </ul> // </ul>
``` ```
Full documentation and more examples are at [handlebarsjs.com](http://handlebarsjs.com/).
Registering Helpers Precompiling Templates
------------------- ----------------------
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.
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 Differences Between Handlebars.js and Mustache
---------------------------------------------- ----------------------------------------------
Handlebars.js adds a couple of additional features to make writing Handlebars.js adds a couple of additional features to make writing
templates easier and also changes a tiny detail of how partials work. templates easier and also changes a tiny detail of how partials work.
### Paths - [Nested Paths](http://handlebarsjs.com/#paths)
- [Helpers](http://handlebarsjs.com/#helpers)
Handlebars.js supports an extended expression syntax that we call paths. - [Block Expressions](http://handlebarsjs.com/#block-expressions)
Paths are made up of typical expressions and `.` characters. Expressions - [Literal Values](http://handlebarsjs.com/#literals)
allow you to not only display data from the current context, but to - [Delimited Comments](http://handlebarsjs.com/#comments)
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>
```
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 ### Compatibility
There are a few Mustache behaviors that Handlebars does not implement. 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. - 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. - The optional Mustache-style lambdas are not supported. Instead Handlebars provides its own lambda resolution that follows the behaviors of helpers.
- Alternative delimeters are not supported. - 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 Supported Environments
---------------------- ----------------------
@@ -365,7 +121,9 @@ Handlebars in the Wild
* [Assemble](http://assemble.io), by [@jonschlinkert](https://github.com/jonschlinkert) * [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 and [@doowb](https://github.com/doowb), is a static site generator that uses Handlebars.js
as its template engine. 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 * [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 * [Ember.js](http://www.emberjs.com) makes Handlebars.js the primary way to
structure your views, also with automatic data binding support. structure your views, also with automatic data binding support.
* [Ghost](https://ghost.org/) Just a blogging platform. * [Ghost](https://ghost.org/) Just a blogging platform.
@@ -373,6 +131,7 @@ Handlebars in the Wild
from Les Hill (@leshill). from Les Hill (@leshill).
* [handlebars-helpers](https://github.com/assemble/handlebars-helpers) is an extensive library * [handlebars-helpers](https://github.com/assemble/handlebars-helpers) is an extensive library
with 100+ handlebars helpers. 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, * [hbs](http://github.com/donpark/hbs): An Express.js view engine adapter for Handlebars.js,
from Don Park. from Don Park.
* [koa-hbs](https://github.com/jwilm/koa-hbs): [koa](https://github.com/koajs/koa) generator based * [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/). Handlebars.js with [jQuery](http://jquery.com/).
* [Lumbar](http://walmartlabs.github.io/lumbar) provides easy module-based template management for * [Lumbar](http://walmartlabs.github.io/lumbar) provides easy module-based template management for
handlebars projects. 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, * [sammy.js](http://github.com/quirkey/sammy) by Aaron Quint, a.k.a. quirkey,
supports Handlebars.js as one of its template plugins. supports Handlebars.js as one of its template plugins.
* [SproutCore](http://www.sproutcore.com) uses Handlebars.js as its main * [SproutCore](http://www.sproutcore.com) uses Handlebars.js as its main
templating engine, extending it with automatic data binding support. templating engine, extending it with automatic data binding support.
* [YUI](http://yuilibrary.com/yui/docs/handlebars/) implements a port of handlebars * [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! * [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 External Resources
------------------ ------------------
@@ -402,6 +164,4 @@ License
------- -------
Handlebars.js is released under the MIT 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 [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'), var async = require('async'),
async = require('async'),
fs = require('fs'), fs = require('fs'),
zlib = require('zlib'); zlib = require('zlib');
+1 -1
View File
@@ -2,7 +2,7 @@ var fs = require('fs');
var metrics = fs.readdirSync(__dirname); var metrics = fs.readdirSync(__dirname);
metrics.forEach(function(metric) { metrics.forEach(function(metric) {
if (metric === 'index.js' || !/(.*)\.js$/.test(metric)) { if (metric === 'index.js' || !(/(.*)\.js$/.test(metric))) {
return; return;
} }
+1 -1
View File
@@ -1,6 +1,6 @@
module.exports = { module.exports = {
helpers: { helpers: {
foo: function(options) { foo: function() {
return ''; return '';
} }
}, },
+5 -5
View File
@@ -1,7 +1,7 @@
module.exports = { module.exports = {
context: { names: [{name: "Moe"}, {name: "Larry"}, {name: "Curly"}, {name: "Shemp"}] }, context: { names: [{name: 'Moe'}, {name: 'Larry'}, {name: 'Curly'}, {name: 'Shemp'}] },
handlebars: "{{#each names}}{{name}}{{/each}}", handlebars: '{{#each names}}{{name}}{{/each}}',
dust: "{#names}{name}{/names}", dust: '{#names}{name}{/names}',
mustache: "{{#names}}{{name}}{{/names}}", mustache: '{{#names}}{{name}}{{/names}}',
eco: "<% for item in @names: %><%= item.name %><% end %>" eco: '<% for item in @names: %><%= item.name %><% end %>'
}; };
+3 -3
View File
@@ -1,4 +1,4 @@
module.exports = { module.exports = {
context: { names: [{name: "Moe"}, {name: "Larry"}, {name: "Curly"}, {name: "Shemp"}] }, context: { names: [{name: 'Moe'}, {name: 'Larry'}, {name: 'Curly'}, {name: 'Shemp'}] },
handlebars: "{{#names}}{{name}}{{/names}}" handlebars: '{{#names}}{{name}}{{/names}}'
} };
+4 -4
View File
@@ -3,13 +3,13 @@ var fs = require('fs');
module.exports = { module.exports = {
context: { context: {
header: function() { header: function() {
return "Colors"; return 'Colors';
}, },
hasItems: true, // To make things fairer in mustache land due to no `{{if}}` construct on arrays hasItems: true, // To make things fairer in mustache land due to no `{{if}}` construct on arrays
items: [ items: [
{name: "red", current: true, url: "#Red"}, {name: 'red', current: true, url: '#Red'},
{name: "green", current: false, url: "#Green"}, {name: 'green', current: false, url: '#Green'},
{name: "blue", current: false, url: "#Blue"} {name: 'blue', current: false, url: '#Blue'}
] ]
}, },
+3 -3
View File
@@ -1,4 +1,4 @@
module.exports = { module.exports = {
context: { names: [{name: "Moe"}, {name: "Larry"}, {name: "Curly"}, {name: "Shemp"}] }, context: { names: [{name: 'Moe'}, {name: 'Larry'}, {name: 'Curly'}, {name: 'Shemp'}] },
handlebars: "{{#each names}}{{@index}}{{name}}{{/each}}" handlebars: '{{#each names}}{{@index}}{{name}}{{/each}}'
} };
+4 -4
View File
@@ -1,6 +1,6 @@
module.exports = { module.exports = {
context: { names: [{name: "Moe"}, {name: "Larry"}, {name: "Curly"}, {name: "Shemp"}], foo: 'bar' }, context: { names: [{name: 'Moe'}, {name: 'Larry'}, {name: 'Curly'}, {name: 'Shemp'}], foo: 'bar' },
handlebars: "{{#each names}}{{../foo}}{{/each}}", handlebars: '{{#each names}}{{../foo}}{{/each}}',
mustache: "{{#names}}{{foo}}{{/names}}", mustache: '{{#names}}{{foo}}{{/names}}',
eco: "<% for item in @names: %><%= @foo %><% end %>" eco: '<% for item in @names: %><%= @foo %><% end %>'
}; };
+4 -4
View File
@@ -1,6 +1,6 @@
module.exports = { module.exports = {
context: { names: [{bat: 'foo', name: ["Moe"]}, {bat: 'foo', name: ["Larry"]}, {bat: 'foo', name: ["Curly"]}, {bat: 'foo', name: ["Shemp"]}], foo: 'bar' }, 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}}", handlebars: '{{#each names}}{{#each name}}{{../bat}}{{../../foo}}{{/each}}{{/each}}',
mustache: "{{#names}}{{#name}}{{bat}}{{foo}}{{/name}}{{/names}}", mustache: '{{#names}}{{#name}}{{bat}}{{foo}}{{/name}}{{/names}}',
eco: "<% for item in @names: %><% for child in item.name: %><%= item.bat %><%= @foo %><% end %><% end %>" 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); var templates = fs.readdirSync(__dirname);
templates.forEach(function(template) { templates.forEach(function(template) {
if (template === 'index.js' || !/(.*)\.js$/.test(template)) { if (template === 'index.js' || !(/(.*)\.js$/.test(template))) {
return; return;
} }
module.exports[RegExp.$1] = require('./' + RegExp.$1); module.exports[RegExp.$1] = require('./' + RegExp.$1);
+2 -2
View File
@@ -1,4 +1,4 @@
module.exports = { module.exports = {
context: { person: { name: "Larry", age: 45 } }, context: { person: { name: 'Larry', age: 45 } },
handlebars: "{{#person}}{{name}}{{age}}{{/person}}" handlebars: '{{#person}}{{name}}{{age}}{{/person}}'
}; };
+5 -5
View File
@@ -1,7 +1,7 @@
module.exports = { module.exports = {
context: { person: { name: "Larry", age: 45 } }, context: { person: { name: 'Larry', age: 45 } },
handlebars: "{{#with person}}{{name}}{{age}}{{/with}}", handlebars: '{{#with person}}{{name}}{{age}}{{/with}}',
dust: "{#person}{name}{age}{/person}", dust: '{#person}{name}{age}{/person}',
eco: "<%= @person.name %><%= @person.age %>", eco: '<%= @person.name %><%= @person.age %>',
mustache: "{{#person}}{{name}}{{age}}{{/person}}" mustache: '{{#person}}{{name}}{{age}}{{/person}}'
}; };
+5 -5
View File
@@ -1,10 +1,10 @@
module.exports = { module.exports = {
context: { name: '1', kids: [{ name: '1.1', kids: [{name: '1.1.1', kids: []}] }] }, context: { name: '1', kids: [{ name: '1.1', kids: [{name: '1.1.1', kids: []}] }] },
partials: { partials: {
mustache: { recursion: "{{name}}{{#kids}}{{>recursion}}{{/kids}}" }, mustache: { recursion: '{{name}}{{#kids}}{{>recursion}}{{/kids}}' },
handlebars: { recursion: "{{name}}{{#each kids}}{{>recursion}}{{/each}}" } handlebars: { recursion: '{{name}}{{#each kids}}{{>recursion}}{{/each}}' }
}, },
handlebars: "{{name}}{{#each kids}}{{>recursion}}{{/each}}", handlebars: '{{name}}{{#each kids}}{{>recursion}}{{/each}}',
dust: "{name}{#kids}{>recursion:./}{/kids}", dust: '{name}{#kids}{>recursion:./}{/kids}',
mustache: "{{name}}{{#kids}}{{>recursion}}{{/kids}}" mustache: '{{name}}{{#kids}}{{>recursion}}{{/kids}}'
}; };
+6 -6
View File
@@ -1,11 +1,11 @@
module.exports = { 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: { partials: {
mustache: { 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: { variables: 'Hello {{name}}! You have {{count}} new messages.' }
}, },
handlebars: "{{#each peeps}}{{>variables}}{{/each}}", handlebars: '{{#each peeps}}{{>variables}}{{/each}}',
dust: "{#peeps}{>variables/}{/peeps}", dust: '{#peeps}{>variables/}{/peeps}',
mustache: "{{#peeps}}{{>variables}}{{/peeps}}" mustache: '{{#peeps}}{{>variables}}{{/peeps}}'
}; };
+5 -5
View File
@@ -1,7 +1,7 @@
module.exports = { module.exports = {
context: { person: { name: {bar: {baz: "Larry"}}, age: 45 } }, context: { person: { name: {bar: {baz: 'Larry'}}, age: 45 } },
handlebars: "{{person.name.bar.baz}}{{person.age}}{{person.foo}}{{animal.age}}", handlebars: '{{person.name.bar.baz}}{{person.age}}{{person.foo}}{{animal.age}}',
dust: "{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 %>", 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}}" mustache: '{{person.name.bar.baz}}{{person.age}}{{person.foo}}{{animal.age}}'
}; };
+4 -4
View File
@@ -1,7 +1,7 @@
module.exports = { module.exports = {
context: {}, context: {},
handlebars: "Hello world", handlebars: 'Hello world',
dust: "Hello world", dust: 'Hello world',
mustache: "Hello world", mustache: 'Hello world',
eco: "Hello world" eco: 'Hello world'
}; };
+3 -3
View File
@@ -4,11 +4,11 @@ module.exports = {
return 'foo ' + value; return 'foo ' + value;
}, },
header: function() { header: function() {
return "Colors"; return 'Colors';
} }
}, },
handlebars: "{{echo (header)}}", handlebars: '{{echo (header)}}',
eco: "<%= @echo(@header()) %>" eco: '<%= @echo(@header()) %>'
}; };
module.exports.context = module.exports.helpers; module.exports.context = module.exports.helpers;
+5 -5
View File
@@ -1,8 +1,8 @@
module.exports = { module.exports = {
context: {name: "Mick", count: 30}, context: {name: 'Mick', count: 30},
handlebars: "Hello {{name}}! You have {{count}} new messages.", handlebars: 'Hello {{name}}! You have {{count}} new messages.',
dust: "Hello {name}! You have {count} new messages.", dust: 'Hello {name}! You have {count} new messages.',
mustache: "Hello {{name}}! You have {{count}} new messages.", mustache: 'Hello {{name}}! You have {{count}} new messages.',
eco: "Hello <%= @name %>! You have <%= @count %> new messages." eco: 'Hello <%= @name %>! You have <%= @count %> new messages.'
}; };
+16 -17
View File
@@ -1,23 +1,22 @@
var _ = require('underscore'), var _ = require('underscore'),
runner = require('./util/template-runner'), runner = require('./util/template-runner'),
templates = require('./templates'),
eco, dust, Handlebars, Mustache, eco; eco, dust, Handlebars, Mustache;
try { try {
dust = require("dustjs-linkedin"); dust = require('dustjs-linkedin');
} catch (err) { /* NOP */ } } catch (err) { /* NOP */ }
try { try {
Mustache = require("mustache"); Mustache = require('mustache');
} catch (err) { /* NOP */ } } catch (err) { /* NOP */ }
try { try {
eco = require("eco"); eco = require('eco');
} catch (err) { /* NOP */ } } catch (err) { /* NOP */ }
function error() { function error() {
throw new Error("EWOT"); throw new Error('EWOT');
} }
function makeSuite(bench, name, template, handlebarsOnly) { function makeSuite(bench, name, template, handlebarsOnly) {
@@ -34,19 +33,19 @@ function makeSuite(bench, name, template, handlebarsOnly) {
mustacheOut; mustacheOut;
var handlebar = Handlebars.compile(template.handlebars, {data: false}), var handlebar = Handlebars.compile(template.handlebars, {data: false}),
compat = Handlebars.compile(template.handlebars, {data: false, compat: true}), compat = Handlebars.compile(template.handlebars, {data: false, compat: true}),
options = {helpers: template.helpers}; options = {helpers: template.helpers};
_.each(template.partials && template.partials.handlebars, function(partial, name) { _.each(template.partials && template.partials.handlebars, function(partial, partialName) {
Handlebars.registerPartial(name, Handlebars.compile(partial, {data: false})); Handlebars.registerPartial(partialName, Handlebars.compile(partial, {data: false}));
}); });
handlebarsOut = handlebar(context, options); handlebarsOut = handlebar(context, options);
bench("handlebars", function() { bench('handlebars', function() {
handlebar(context, options); handlebar(context, options);
}); });
compatOut = compat(context, options); compatOut = compat(context, options);
bench("compat", function() { bench('compat', function() {
compat(context, options); compat(context, options);
}); });
@@ -61,8 +60,8 @@ function makeSuite(bench, name, template, handlebarsOnly) {
dust.render(templateName, context, function(err, out) { dustOut = out; }); dust.render(templateName, context, function(err, out) { dustOut = out; });
bench("dust", function() { bench('dust', function() {
dust.render(templateName, context, function(err, out) { }); dust.render(templateName, context, function() {});
}); });
} else { } else {
bench('dust', error); bench('dust', error);
@@ -75,11 +74,11 @@ function makeSuite(bench, name, template, handlebarsOnly) {
ecoOut = ecoTemplate(context); ecoOut = ecoTemplate(context);
bench("eco", function() { bench('eco', function() {
ecoTemplate(context); ecoTemplate(context);
}); });
} else { } else {
bench("eco", error); bench('eco', error);
} }
} }
@@ -90,11 +89,11 @@ function makeSuite(bench, name, template, handlebarsOnly) {
if (mustacheSource) { if (mustacheSource) {
mustacheOut = Mustache.to_html(mustacheSource, context, mustachePartials); mustacheOut = Mustache.to_html(mustacheSource, context, mustachePartials);
bench("mustache", function() { bench('mustache', function() {
Mustache.to_html(mustacheSource, context, mustachePartials); Mustache.to_html(mustacheSource, context, mustachePartials);
}); });
} else { } else {
bench("mustache", error); bench('mustache', error);
} }
} }
+33 -35
View File
@@ -1,7 +1,7 @@
var _ = require('underscore'), var _ = require('underscore'),
Benchmark = require("benchmark"); Benchmark = require('benchmark');
var BenchWarmer = function(names) { function BenchWarmer() {
this.benchmarks = []; this.benchmarks = [];
this.currentBenches = []; this.currentBenches = [];
this.names = []; this.names = [];
@@ -9,9 +9,9 @@ var BenchWarmer = function(names) {
this.minimum = Infinity; this.minimum = Infinity;
this.maximum = -Infinity; this.maximum = -Infinity;
this.errors = {}; this.errors = {};
}; }
var print = require("sys").print; var print = require('sys').print;
BenchWarmer.prototype = { BenchWarmer.prototype = {
winners: function(benches) { winners: function(benches) {
@@ -29,7 +29,7 @@ BenchWarmer.prototype = {
}); });
}, },
push: function(name, fn) { push: function(name, fn) {
if(this.names.indexOf(name) == -1) { if (this.names.indexOf(name) == -1) {
this.names.push(name); this.names.push(name);
} }
@@ -37,9 +37,9 @@ BenchWarmer.prototype = {
this.first = false; this.first = false;
var bench = new Benchmark(fn, { var bench = new Benchmark(fn, {
name: this.suiteName + ": " + name, name: this.suiteName + ': ' + name,
onComplete: function() { onComplete: function() {
if(first) { self.startLine(suiteName); } if (first) { self.startLine(suiteName); }
self.writeBench(bench); self.writeBench(bench);
self.currentBenches.push(bench); self.currentBenches.push(bench);
}, onError: function() { }, onError: function() {
@@ -58,7 +58,7 @@ BenchWarmer.prototype = {
this.printHeader('ops/msec', true); this.printHeader('ops/msec', true);
Benchmark.invoke(this.benchmarks, { Benchmark.invoke(this.benchmarks, {
name: "run", name: 'run',
onComplete: function() { onComplete: function() {
self.scaleTimes(); self.scaleTimes();
@@ -76,7 +76,7 @@ BenchWarmer.prototype = {
print('\n'); print('\n');
var errors = false, prop, bench; var errors = false, prop, bench;
for(prop in self.errors) { for (prop in self.errors) {
if (self.errors.hasOwnProperty(prop) if (self.errors.hasOwnProperty(prop)
&& self.errors[prop].error.message !== 'EWOT') { && self.errors[prop].error.message !== 'EWOT') {
errors = true; errors = true;
@@ -84,18 +84,18 @@ BenchWarmer.prototype = {
} }
} }
if(errors) { if (errors) {
print("\n\nErrors:\n"); print('\n\nErrors:\n');
for(prop in self.errors) { for (prop in self.errors) {
if (self.errors.hasOwnProperty(prop) if (self.errors.hasOwnProperty(prop)
&& self.errors[prop].error.message !== 'EWOT') { && self.errors[prop].error.message !== 'EWOT') {
bench = self.errors[prop]; bench = self.errors[prop];
print("\n" + bench.name + ":\n"); print('\n' + bench.name + ':\n');
print(bench.error.message); print(bench.error.message);
if(bench.error.stack) { 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() { scaleTimes: function() {
@@ -121,10 +121,10 @@ BenchWarmer.prototype = {
printHeader: function(title, winners) { printHeader: function(title, winners) {
var benchSize = 0, names = this.names, i, l; var benchSize = 0, names = this.names, i, l;
for(i=0, l=names.length; i<l; i++) { for (i = 0, l = names.length; i < l; i++) {
var name = names[i]; var name = names[i];
if(benchSize < name.length) { benchSize = name.length; } if (benchSize < name.length) { benchSize = name.length; }
} }
this.nameSize = benchSize + 2; this.nameSize = benchSize + 2;
@@ -133,28 +133,28 @@ BenchWarmer.prototype = {
this.startLine(title); this.startLine(title);
horSize = horSize + this.benchSize; horSize = horSize + this.benchSize;
for(i=0, l=names.length; i<l; i++) { for (i = 0, l = names.length; i < l; i++) {
this.writeValue(names[i]); this.writeValue(names[i]);
horSize = horSize + this.benchSize; horSize = horSize + this.benchSize;
} }
if (winners) { if (winners) {
print("WINNER(S)"); print('WINNER(S)');
horSize = horSize + "WINNER(S)".length; horSize = horSize + 'WINNER(S)'.length;
} }
print("\n" + new Array(horSize + 1).join("-")); print('\n' + new Array(horSize + 1).join('-'));
}, },
startLine: function(name) { startLine: function(name) {
var winners = Benchmark.map(this.winners(this.currentBenches), function(bench) { var winners = Benchmark.map(this.winners(this.currentBenches), function(bench) {
return bench.name.split(": ")[1]; return bench.name.split(': ')[1];
}); });
this.currentBenches = []; this.currentBenches = [];
print(winners.join(", ")); print(winners.join(', '));
print("\n"); print('\n');
if (name) { if (name) {
this.writeValue(name); this.writeValue(name);
@@ -163,9 +163,9 @@ BenchWarmer.prototype = {
writeBench: function(bench) { writeBench: function(bench) {
var out; var out;
if(!bench.error) { if (!bench.error) {
var count = bench.hz, var count = bench.hz,
moe = count * bench.stats.rme / 100, moe = count * bench.stats.rme / 100,
minimum, minimum,
maximum; maximum;
@@ -174,24 +174,22 @@ BenchWarmer.prototype = {
minimum = count - moe; minimum = count - moe;
maximum = count + moe; maximum = count + moe;
out = count + " ±" + moe + " (" + bench.cycles + ")"; out = count + ' ±' + moe + ' (' + bench.cycles + ')';
this.times[bench.suiteName][bench.benchName] = count; this.times[bench.suiteName][bench.benchName] = count;
this.minimum = Math.min(this.minimum, minimum); this.minimum = Math.min(this.minimum, minimum);
this.maximum = Math.max(this.maximum, maximum); this.maximum = Math.max(this.maximum, maximum);
} else if (bench.error.message === 'EWOT') {
out = 'NA';
} else { } else {
if (bench.error.message === 'EWOT') { out = 'E';
out = 'NA';
} else {
out = 'E';
}
} }
this.writeValue(out); this.writeValue(out);
}, },
writeValue: function(out) { writeValue: function(out) {
var padding = this.benchSize - out.length + 1; var padding = this.benchSize - out.length + 1;
out = out + new Array(padding).join(" "); out = out + new Array(padding).join(' ');
print(out); print(out);
} }
}; };
+3 -1
View File
@@ -22,6 +22,8 @@ module.exports = function(grunt, makeSuite, callback) {
}); });
warmer.bench(function() { warmer.bench(function() {
callback && callback(warmer.times, warmer.scaled); if (callback) {
callback(warmer.times, warmer.scaled);
}
}); });
}; };
+26 -9
View File
@@ -26,7 +26,7 @@ var optimist = require('optimist')
'type': 'string', 'type': 'string',
'description': 'Path to handlebar.js (only valid for amd-style)', 'description': 'Path to handlebar.js (only valid for amd-style)',
'alias': 'handlebarPath', 'alias': 'handlebarPath',
'default': '' 'default': ''
}, },
'k': { 'k': {
'type': 'string', 'type': 'string',
@@ -54,17 +54,27 @@ var optimist = require('optimist')
'description': 'Output template function only.', 'description': 'Output template function only.',
'alias': 'simple' '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': { 'r': {
'type': 'string', 'type': 'string',
'description': 'Template root. Base value that will be stripped from template names.', 'description': 'Template root. Base value that will be stripped from template names.',
'alias': 'root' 'alias': 'root'
}, },
'p' : { 'p': {
'type': 'boolean', 'type': 'boolean',
'description': 'Compiling a partial template', 'description': 'Compiling a partial template',
'alias': 'partial' 'alias': 'partial'
}, },
'd' : { 'd': {
'type': 'boolean', 'type': 'boolean',
'description': 'Include data when compiling', 'description': 'Include data when compiling',
'alias': 'data' 'alias': 'data'
@@ -92,6 +102,7 @@ var optimist = require('optimist')
} }
}) })
.wrap(120)
.check(function(argv) { .check(function(argv) {
if (argv.version) { if (argv.version) {
return; return;
@@ -100,12 +111,18 @@ var optimist = require('optimist')
var argv = optimist.argv; var argv = optimist.argv;
argv.templates = argv._; argv.files = argv._;
delete argv._; delete argv._;
if (argv.help || !argv.templates.length) { var Precompiler = require('../dist/cjs/precompiler');
optimist.showHelp(); Precompiler.loadTemplates(argv, function(err, opts) {
return; 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", "name": "handlebars",
"version": "3.0.0", "version": "4.0.6",
"main": "handlebars.js", "main": "handlebars.js",
"license": "MIT",
"dependencies": {} "dependencies": {}
} }
+1 -1
View File
@@ -2,7 +2,7 @@
<package> <package>
<metadata> <metadata>
<id>handlebars.js</id> <id>handlebars.js</id>
<version>3.0.0</version> <version>4.0.6</version>
<authors>handlebars.js Authors</authors> <authors>handlebars.js Authors</authors>
<licenseUrl>https://github.com/wycats/handlebars.js/blob/master/LICENSE</licenseUrl> <licenseUrl>https://github.com/wycats/handlebars.js/blob/master/LICENSE</licenseUrl>
<projectUrl>https://github.com/wycats/handlebars.js/</projectUrl> <projectUrl>https://github.com/wycats/handlebars.js/</projectUrl>
+49 -1
View File
@@ -87,6 +87,19 @@ interface PartialStatement <: Statement {
indent: string; indent: string;
strip: StripFlags | null; 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. `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 ### Expressions
```java ```java
@@ -164,6 +204,14 @@ interface NumberLiteral <: Literal {
value: number; value: number;
original: 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 - `parent` is the existing code in the path resolution
- `name` is the current path component - `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. 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,20 +1,22 @@
/*globals Handlebars: true */ import runtime from './handlebars.runtime';
import Handlebars from "./handlebars.runtime";
// Compiler imports // Compiler imports
import AST from "./handlebars/compiler/ast"; import AST from './handlebars/compiler/ast';
import { parser as Parser, parse } from "./handlebars/compiler/base"; import { parser as Parser, parse } from './handlebars/compiler/base';
import { Compiler, compile, precompile } from "./handlebars/compiler/compiler"; import { Compiler, compile, precompile } from './handlebars/compiler/compiler';
import JavaScriptCompiler from "./handlebars/compiler/javascript-compiler"; import JavaScriptCompiler from './handlebars/compiler/javascript-compiler';
import Visitor from './handlebars/compiler/visitor';
var _create = Handlebars.create; import noConflict from './handlebars/no-conflict';
var create = function() {
var hb = _create(); let _create = runtime.create;
function create() {
let hb = _create();
hb.compile = function(input, options) { hb.compile = function(input, options) {
return compile(input, options, hb); return compile(input, options, hb);
}; };
hb.precompile = function (input, options) { hb.precompile = function(input, options) {
return precompile(input, options, hb); return precompile(input, options, hb);
}; };
@@ -25,22 +27,15 @@ var create = function() {
hb.parse = parse; hb.parse = parse;
return hb; return hb;
}; }
Handlebars = create(); let inst = create();
Handlebars.create = create; inst.create = create;
/*jshint -W040 */ noConflict(inst);
/* 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; inst.Visitor = Visitor;
export default Handlebars; inst['default'] = inst;
export default inst;
+15 -23
View File
@@ -1,16 +1,17 @@
/*globals Handlebars: true */ import * as base from './handlebars/base';
module base from "./handlebars/base";
// Each of these augment the Handlebars object. No need to setup here. // Each of these augment the Handlebars object. No need to setup here.
// (This is done to easily share code between commonjs and browse envs) // (This is done to easily share code between commonjs and browse envs)
import SafeString from "./handlebars/safe-string"; import SafeString from './handlebars/safe-string';
import Exception from "./handlebars/exception"; import Exception from './handlebars/exception';
module Utils from "./handlebars/utils"; import * as Utils from './handlebars/utils';
module runtime from "./handlebars/runtime"; 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 // For compatibility and usage outside of module systems, make the Handlebars object a namespace
var create = function() { function create() {
var hb = new base.HandlebarsEnvironment(); let hb = new base.HandlebarsEnvironment();
Utils.extend(hb, base); Utils.extend(hb, base);
hb.SafeString = SafeString; hb.SafeString = SafeString;
@@ -24,22 +25,13 @@ var create = function() {
}; };
return hb; return hb;
}; }
var Handlebars = create(); let inst = create();
Handlebars.create = create; inst.create = create;
/*jshint -W040 */ noConflict(inst);
/* 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; inst['default'] = inst;
export default Handlebars; export default inst;
+32 -195
View File
@@ -1,40 +1,43 @@
module Utils from "./utils"; import {createFrame, extend, toString} from './utils';
import Exception from "./exception"; import Exception from './exception';
import {registerDefaultHelpers} from './helpers';
import {registerDefaultDecorators} from './decorators';
import logger from './logger';
export var VERSION = "3.0.0"; export const VERSION = '4.0.6';
export var COMPILER_REVISION = 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 1: '<= 1.0.rc.2', // 1.0.rc.2 is actually rev2 but doesn't report it
2: '== 1.0.0-rc.3', 2: '== 1.0.0-rc.3',
3: '== 1.0.0-rc.4', 3: '== 1.0.0-rc.4',
4: '== 1.x.x', 4: '== 1.x.x',
5: '== 2.0.0-alpha.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, const objectType = '[object Object]';
isFunction = Utils.isFunction,
toString = Utils.toString,
objectType = '[object Object]';
export function HandlebarsEnvironment(helpers, partials) { export function HandlebarsEnvironment(helpers, partials, decorators) {
this.helpers = helpers || {}; this.helpers = helpers || {};
this.partials = partials || {}; this.partials = partials || {};
this.decorators = decorators || {};
registerDefaultHelpers(this); registerDefaultHelpers(this);
registerDefaultDecorators(this);
} }
HandlebarsEnvironment.prototype = { HandlebarsEnvironment.prototype = {
constructor: HandlebarsEnvironment, constructor: HandlebarsEnvironment,
logger: logger, logger: logger,
log: log, log: logger.log,
registerHelper: function(name, fn) { registerHelper: function(name, fn) {
if (toString.call(name) === objectType) { if (toString.call(name) === objectType) {
if (fn) { throw new Exception('Arg not supported with multiple helpers'); } if (fn) { throw new Exception('Arg not supported with multiple helpers'); }
Utils.extend(this.helpers, name); extend(this.helpers, name);
} else { } else {
this.helpers[name] = fn; this.helpers[name] = fn;
} }
@@ -45,197 +48,31 @@ HandlebarsEnvironment.prototype = {
registerPartial: function(name, partial) { registerPartial: function(name, partial) {
if (toString.call(name) === objectType) { if (toString.call(name) === objectType) {
Utils.extend(this.partials, name); extend(this.partials, name);
} else { } else {
if (typeof partial === 'undefined') { 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; this.partials[name] = partial;
} }
}, },
unregisterPartial: function(name) { unregisterPartial: function(name) {
delete this.partials[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) { export let log = logger.log;
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 + "'");
}
});
instance.registerHelper('blockHelperMissing', function(context, options) { export {createFrame, logger};
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;
};
+4 -117
View File
@@ -1,126 +1,13 @@
var AST = { let 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;
},
// Public API used to evaluate derived attributes regarding AST nodes // Public API used to evaluate derived attributes regarding AST nodes
helpers: { helpers: {
// a mustache is definitely a helper if: // a mustache is definitely a helper if:
// * it is an eligible helper, and // * it is an eligible helper, and
// * it has at least one parameter or hash segment // * it has at least one parameter or hash segment
// TODO: Make these public utility methods
helperExpression: function(node) { 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) { scopedId: function(path) {
+7 -8
View File
@@ -1,13 +1,12 @@
import parser from "./parser"; import parser from './parser';
import AST from "./ast"; import WhitespaceControl from './whitespace-control';
import WhitespaceControl from "./whitespace-control"; import * as Helpers from './helpers';
module Helpers from "./helpers"; import { extend } from '../utils';
import { extend } from "../utils";
export { parser }; export { parser };
var yy = {}; let yy = {};
extend(yy, Helpers, AST); extend(yy, Helpers);
export function parse(input, options) { export function parse(input, options) {
// Just return if an already-compiled AST was passed in. // 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); return new yy.SourceLocation(options && options.srcName, locInfo);
}; };
var strip = new WhitespaceControl(); let strip = new WhitespaceControl(options);
return strip.accept(parser.parse(input)); return strip.accept(parser.parse(input));
} }
+37 -23
View File
@@ -1,10 +1,22 @@
import {isArray} from "../utils"; /* global define */
import {isArray} from '../utils';
let SourceNode;
try { try {
var SourceMap = require('source-map'), /* istanbul ignore next */
SourceNode = SourceMap.SourceNode; 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) { } 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) { SourceNode = function(line, column, srcFile, chunks) {
this.src = ''; this.src = '';
if (chunks) { if (chunks) {
@@ -37,15 +49,15 @@ try {
function castChunk(chunk, codeGen, loc) { function castChunk(chunk, codeGen, loc) {
if (isArray(chunk)) { 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)); ret.push(codeGen.wrap(chunk[i], loc));
} }
return ret; return ret;
} else if (typeof chunk === 'boolean' || typeof chunk === 'number') { } else if (typeof chunk === 'boolean' || typeof chunk === 'number') {
// Handle primitives that the SourceNode will throw up on // Handle primitives that the SourceNode will throw up on
return chunk+''; return chunk + '';
} }
return chunk; return chunk;
} }
@@ -57,6 +69,9 @@ function CodeGen(srcFile) {
} }
CodeGen.prototype = { CodeGen.prototype = {
isEmpty() {
return !this.source.length;
},
prepend: function(source, loc) { prepend: function(source, loc) {
this.source.unshift(this.wrap(source, loc)); this.source.unshift(this.wrap(source, loc));
}, },
@@ -65,7 +80,7 @@ CodeGen.prototype = {
}, },
merge: function() { merge: function() {
var source = this.empty(); let source = this.empty();
this.each(function(line) { this.each(function(line) {
source.add([' ', line, '\n']); source.add([' ', line, '\n']);
}); });
@@ -73,21 +88,20 @@ CodeGen.prototype = {
}, },
each: function(iter) { 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]); iter(this.source[i]);
} }
}, },
empty: function(loc) { empty: function() {
loc = loc || this.currentLocation || {start:{}}; let loc = this.currentLocation || {start: {}};
return new SourceNode(loc.start.line, loc.start.column, this.srcFile); 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) { if (chunk instanceof SourceNode) {
return chunk; return chunk;
} }
loc = loc || this.currentLocation || {start:{}};
chunk = castChunk(chunk, this, loc); chunk = castChunk(chunk, this, loc);
return new SourceNode(loc.start.line, loc.start.column, this.srcFile, chunk); return new SourceNode(loc.start.line, loc.start.column, this.srcFile, chunk);
@@ -109,40 +123,40 @@ CodeGen.prototype = {
}, },
objectLiteral: function(obj) { objectLiteral: function(obj) {
var pairs = []; let pairs = [];
for (var key in obj) { for (let key in obj) {
if (obj.hasOwnProperty(key)) { if (obj.hasOwnProperty(key)) {
var value = castChunk(obj[key], this); let value = castChunk(obj[key], this);
if (value !== 'undefined') { if (value !== 'undefined') {
pairs.push([this.quotedString(key), ':', value]); pairs.push([this.quotedString(key), ':', value]);
} }
} }
} }
var ret = this.generateList(pairs); let ret = this.generateList(pairs);
ret.prepend('{'); ret.prepend('{');
ret.add('}'); ret.add('}');
return ret; return ret;
}, },
generateList: function(entries, loc) { generateList: function(entries) {
var ret = this.empty(loc); 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) { if (i) {
ret.add(','); ret.add(',');
} }
ret.add(castChunk(entries[i], this, loc)); ret.add(castChunk(entries[i], this));
} }
return ret; return ret;
}, },
generateArray: function(entries, loc) { generateArray: function(entries) {
var ret = this.generateList(entries, loc); let ret = this.generateList(entries);
ret.prepend('['); ret.prepend('[');
ret.add(']'); ret.add(']');
+134 -75
View File
@@ -1,9 +1,10 @@
import Exception from "../exception"; /* eslint-disable new-cap */
import {isArray, indexOf} from "../utils";
import AST from "./ast";
var slice = [].slice; import Exception from '../exception';
import {isArray, indexOf} from '../utils';
import AST from './ast';
const slice = [].slice;
export function Compiler() {} export function Compiler() {}
@@ -16,13 +17,13 @@ Compiler.prototype = {
compiler: Compiler, compiler: Compiler,
equals: function(other) { equals: function(other) {
var len = this.opcodes.length; let len = this.opcodes.length;
if (other.opcodes.length !== len) { if (other.opcodes.length !== len) {
return false; return false;
} }
for (var i = 0; i < len; i++) { for (let i = 0; i < len; i++) {
var opcode = this.opcodes[i], let opcode = this.opcodes[i],
otherOpcode = other.opcodes[i]; otherOpcode = other.opcodes[i];
if (opcode.opcode !== otherOpcode.opcode || !argEquals(opcode.args, otherOpcode.args)) { if (opcode.opcode !== otherOpcode.opcode || !argEquals(opcode.args, otherOpcode.args)) {
return false; return false;
@@ -32,7 +33,7 @@ Compiler.prototype = {
// We know that length is the same between the two arrays because they are directly tied // We know that length is the same between the two arrays because they are directly tied
// to the opcode behavior above. // to the opcode behavior above.
len = this.children.length; 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])) { if (!this.children[i].equals(other.children[i])) {
return false; return false;
} }
@@ -54,7 +55,7 @@ Compiler.prototype = {
options.blockParams = options.blockParams || []; options.blockParams = options.blockParams || [];
// These changes will propagate to the other compiler components // These changes will propagate to the other compiler components
var knownHelpers = options.knownHelpers; let knownHelpers = options.knownHelpers;
options.knownHelpers = { options.knownHelpers = {
'helperMissing': true, 'helperMissing': true,
'blockHelperMissing': true, 'blockHelperMissing': true,
@@ -66,8 +67,11 @@ Compiler.prototype = {
'lookup': true 'lookup': true
}; };
if (knownHelpers) { if (knownHelpers) {
for (var name in knownHelpers) { for (let name in knownHelpers) {
options.knownHelpers[name] = knownHelpers[name]; /* istanbul ignore else */
if (name in knownHelpers) {
options.knownHelpers[name] = knownHelpers[name];
}
} }
} }
@@ -75,8 +79,9 @@ Compiler.prototype = {
}, },
compileProgram: function(program) { compileProgram: function(program) {
var result = new this.compiler().compile(program, this.options); let childCompiler = new this.compiler(), // eslint-disable-line new-cap
var guid = this.guid++; result = childCompiler.compile(program, this.options),
guid = this.guid++;
this.usePartial = this.usePartial || result.usePartial; this.usePartial = this.usePartial || result.usePartial;
@@ -87,8 +92,13 @@ Compiler.prototype = {
}, },
accept: function(node) { accept: function(node) {
/* istanbul ignore next: Sanity code */
if (!this[node.type]) {
throw new Exception('Unknown type: ' + node.type, node);
}
this.sourceNode.unshift(node); this.sourceNode.unshift(node);
var ret = this[node.type](node); let ret = this[node.type](node);
this.sourceNode.shift(); this.sourceNode.shift();
return ret; return ret;
}, },
@@ -96,14 +106,15 @@ Compiler.prototype = {
Program: function(program) { Program: function(program) {
this.options.blockParams.unshift(program.blockParams); this.options.blockParams.unshift(program.blockParams);
var body = program.body; let body = program.body,
for(var i=0, l=body.length; i<l; i++) { bodyLength = body.length;
for (let i = 0; i < bodyLength; i++) {
this.accept(body[i]); this.accept(body[i]);
} }
this.options.blockParams.shift(); this.options.blockParams.shift();
this.isSimple = l === 1; this.isSimple = bodyLength === 1;
this.blockParams = program.blockParams ? program.blockParams.length : 0; this.blockParams = program.blockParams ? program.blockParams.length : 0;
return this; return this;
@@ -112,13 +123,13 @@ Compiler.prototype = {
BlockStatement: function(block) { BlockStatement: function(block) {
transformLiteralToPath(block); transformLiteralToPath(block);
var program = block.program, let program = block.program,
inverse = block.inverse; inverse = block.inverse;
program = program && this.compileProgram(program); program = program && this.compileProgram(program);
inverse = inverse && this.compileProgram(inverse); inverse = inverse && this.compileProgram(inverse);
var type = this.classifySexpr(block); let type = this.classifySexpr(block);
if (type === 'helper') { if (type === 'helper') {
this.helperSexpr(block, program, inverse); this.helperSexpr(block, program, inverse);
@@ -145,25 +156,43 @@ Compiler.prototype = {
this.opcode('append'); 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) { PartialStatement: function(partial) {
this.usePartial = true; 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) { if (params.length > 1) {
throw new Exception('Unsupported number of partial arguments: ' + params.length, partial); throw new Exception('Unsupported number of partial arguments: ' + params.length, partial);
} else if (!params.length) { } else if (!params.length) {
params.push({type: 'PathExpression', parts: [], depth: 0}); 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'; isDynamic = partial.name.type === 'SubExpression';
if (isDynamic) { if (isDynamic) {
this.accept(partial.name); 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) { if (this.options.preventIndent && indent) {
this.opcode('appendContent', indent); this.opcode('appendContent', indent);
indent = ''; indent = '';
@@ -172,16 +201,23 @@ Compiler.prototype = {
this.opcode('invokePartial', isDynamic, partialName, indent); this.opcode('invokePartial', isDynamic, partialName, indent);
this.opcode('append'); this.opcode('append');
}, },
PartialBlockStatement: function(partialBlock) {
this.PartialStatement(partialBlock);
},
MustacheStatement: function(mustache) { MustacheStatement: function(mustache) {
this.SubExpression(mustache); this.SubExpression(mustache);
if(mustache.escaped && !this.options.noEscape) { if (mustache.escaped && !this.options.noEscape) {
this.opcode('appendEscaped'); this.opcode('appendEscaped');
} else { } else {
this.opcode('append'); this.opcode('append');
} }
}, },
Decorator(decorator) {
this.DecoratorBlock(decorator);
},
ContentStatement: function(content) { ContentStatement: function(content) {
if (content.value) { if (content.value) {
@@ -193,7 +229,7 @@ Compiler.prototype = {
SubExpression: function(sexpr) { SubExpression: function(sexpr) {
transformLiteralToPath(sexpr); transformLiteralToPath(sexpr);
var type = this.classifySexpr(sexpr); let type = this.classifySexpr(sexpr);
if (type === 'simple') { if (type === 'simple') {
this.simpleSexpr(sexpr); this.simpleSexpr(sexpr);
@@ -204,7 +240,7 @@ Compiler.prototype = {
} }
}, },
ambiguousSexpr: function(sexpr, program, inverse) { ambiguousSexpr: function(sexpr, program, inverse) {
var path = sexpr.path, let path = sexpr.path,
name = path.parts[0], name = path.parts[0],
isBlock = program != null || inverse != null; isBlock = program != null || inverse != null;
@@ -213,26 +249,30 @@ Compiler.prototype = {
this.opcode('pushProgram', program); this.opcode('pushProgram', program);
this.opcode('pushProgram', inverse); this.opcode('pushProgram', inverse);
path.strict = true;
this.accept(path); this.accept(path);
this.opcode('invokeAmbiguous', name, isBlock); this.opcode('invokeAmbiguous', name, isBlock);
}, },
simpleSexpr: function(sexpr) { simpleSexpr: function(sexpr) {
this.accept(sexpr.path); let path = sexpr.path;
path.strict = true;
this.accept(path);
this.opcode('resolvePossibleLambda'); this.opcode('resolvePossibleLambda');
}, },
helperSexpr: function(sexpr, program, inverse) { helperSexpr: function(sexpr, program, inverse) {
var params = this.setupFullMustacheParams(sexpr, program, inverse), let params = this.setupFullMustacheParams(sexpr, program, inverse),
path = sexpr.path, path = sexpr.path,
name = path.parts[0]; name = path.parts[0];
if (this.options.knownHelpers[name]) { if (this.options.knownHelpers[name]) {
this.opcode('invokeKnownHelper', params.length, name); this.opcode('invokeKnownHelper', params.length, name);
} else if (this.options.knownHelpersOnly) { } 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 { } else {
path.strict = true;
path.falsy = true; path.falsy = true;
this.accept(path); this.accept(path);
@@ -244,20 +284,20 @@ Compiler.prototype = {
this.addDepth(path.depth); this.addDepth(path.depth);
this.opcode('getContext', path.depth); this.opcode('getContext', path.depth);
var name = path.parts[0], let name = path.parts[0],
scoped = AST.helpers.scopedId(path), scoped = AST.helpers.scopedId(path),
blockParamId = !path.depth && !scoped && this.blockParamIndex(name); blockParamId = !path.depth && !scoped && this.blockParamIndex(name);
if (blockParamId) { if (blockParamId) {
this.opcode('lookupBlockParam', blockParamId, path.parts); this.opcode('lookupBlockParam', blockParamId, path.parts);
} else if (!name) { } else if (!name) {
// Context reference, i.e. `{{foo .}}` or `{{foo ..}}` // Context reference, i.e. `{{foo .}}` or `{{foo ..}}`
this.opcode('pushContext'); this.opcode('pushContext');
} else if (path.data) { } else if (path.data) {
this.options.data = true; this.options.data = true;
this.opcode('lookupData', path.depth, path.parts); this.opcode('lookupData', path.depth, path.parts, path.strict);
} else { } 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); this.opcode('pushLiteral', bool.value);
}, },
UndefinedLiteral: function() {
this.opcode('pushLiteral', 'undefined');
},
NullLiteral: function() {
this.opcode('pushLiteral', 'null');
},
Hash: function(hash) { Hash: function(hash) {
var pairs = hash.pairs, i, l; let pairs = hash.pairs,
i = 0,
l = pairs.length;
this.opcode('pushHash'); this.opcode('pushHash');
for (i=0, l=pairs.length; i<l; i++) { for (; i < l; i++) {
this.pushParam(pairs[i].value); this.pushParam(pairs[i].value);
} }
while (i--) { while (i--) {
@@ -301,25 +351,24 @@ Compiler.prototype = {
}, },
classifySexpr: function(sexpr) { 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: // a mustache is an eligible helper if:
// * its id is simple (a single part, not `this` or `..`) // * 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 // if a mustache is an eligible helper but not a definite
// helper, it is ambiguous, and will be resolved in a later // helper, it is ambiguous, and will be resolved in a later
// pass or at runtime. // pass or at runtime.
var isEligible = !isBlockParam && (isHelper || isSimple); let isEligible = !isBlockParam && (isHelper || isSimple);
var options = this.options;
// if ambiguous, we can possibly resolve the ambiguity now // 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. // An eligible helper is one that does not have a complex path, i.e. `this.foo`, `../foo` etc.
if (isEligible && !isHelper) { if (isEligible && !isHelper) {
var name = sexpr.path.parts[0]; let name = sexpr.path.parts[0],
options = this.options;
if (options.knownHelpers[name]) { if (options.knownHelpers[name]) {
isHelper = true; isHelper = true;
@@ -328,19 +377,23 @@ Compiler.prototype = {
} }
} }
if (isHelper) { return 'helper'; } if (isHelper) {
else if (isEligible) { return 'ambiguous'; } return 'helper';
else { return 'simple'; } } else if (isEligible) {
return 'ambiguous';
} else {
return 'simple';
}
}, },
pushParams: function(params) { 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]); this.pushParam(params[i]);
} }
}, },
pushParam: function(val) { 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 (this.stringParams) {
if (value.replace) { if (value.replace) {
@@ -349,7 +402,7 @@ Compiler.prototype = {
.replace(/\//g, '.'); .replace(/\//g, '.');
} }
if(val.depth) { if (val.depth) {
this.addDepth(val.depth); this.addDepth(val.depth);
} }
this.opcode('getContext', val.depth || 0); this.opcode('getContext', val.depth || 0);
@@ -362,19 +415,20 @@ Compiler.prototype = {
} }
} else { } else {
if (this.trackIds) { if (this.trackIds) {
var blockParamIndex; let blockParamIndex;
if (val.parts && !AST.helpers.scopedId(val) && !val.depth) { if (val.parts && !AST.helpers.scopedId(val) && !val.depth) {
blockParamIndex = this.blockParamIndex(val.parts[0]); blockParamIndex = this.blockParamIndex(val.parts[0]);
} }
if (blockParamIndex) { if (blockParamIndex) {
var blockParamChild = val.parts.slice(1).join('.'); let blockParamChild = val.parts.slice(1).join('.');
this.opcode('pushId', 'BlockParam', blockParamIndex, blockParamChild); this.opcode('pushId', 'BlockParam', blockParamIndex, blockParamChild);
} else { } else {
value = val.original || value; value = val.original || value;
if (value.replace) { if (value.replace) {
value = value value = value
.replace(/^\.\//g, '') .replace(/^this(?:\.|$)/, '')
.replace(/^\.$/g, ''); .replace(/^\.\//, '')
.replace(/^\.$/, '');
} }
this.opcode('pushId', val.type, value); this.opcode('pushId', val.type, value);
@@ -385,7 +439,7 @@ Compiler.prototype = {
}, },
setupFullMustacheParams: function(sexpr, program, inverse, omitEmpty) { setupFullMustacheParams: function(sexpr, program, inverse, omitEmpty) {
var params = sexpr.params; let params = sexpr.params;
this.pushParams(params); this.pushParams(params);
this.opcode('pushProgram', program); this.opcode('pushProgram', program);
@@ -401,8 +455,8 @@ Compiler.prototype = {
}, },
blockParamIndex: function(name) { blockParamIndex: function(name) {
for (var depth = 0, len = this.options.blockParams.length; depth < len; depth++) { for (let depth = 0, len = this.options.blockParams.length; depth < len; depth++) {
var blockParams = this.options.blockParams[depth], let blockParams = this.options.blockParams[depth],
param = blockParams && indexOf(blockParams, name); param = blockParams && indexOf(blockParams, name);
if (blockParams && param >= 0) { if (blockParams && param >= 0) {
return [depth, param]; return [depth, param];
@@ -413,7 +467,7 @@ Compiler.prototype = {
export function precompile(input, options, env) { export function precompile(input, options, env) {
if (input == null || (typeof input !== 'string' && input.type !== 'Program')) { 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 || {}; options = options || {};
@@ -424,18 +478,16 @@ export function precompile(input, options, env) {
options.useDepths = true; options.useDepths = true;
} }
var ast = env.parse(input, options); let ast = env.parse(input, options),
var environment = new env.Compiler().compile(ast, options); environment = new env.Compiler().compile(ast, options);
return new env.JavaScriptCompiler().compile(environment, 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')) { 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)) { if (!('data' in options)) {
options.data = true; options.data = true;
} }
@@ -443,27 +495,27 @@ export function compile(input, options, env) {
options.useDepths = true; options.useDepths = true;
} }
var compiled; let compiled;
function compileInput() { function compileInput() {
var ast = env.parse(input, options); let ast = env.parse(input, options),
var environment = new env.Compiler().compile(ast, options); environment = new env.Compiler().compile(ast, options),
var templateSpec = new env.JavaScriptCompiler().compile(environment, options, undefined, true); templateSpec = new env.JavaScriptCompiler().compile(environment, options, undefined, true);
return env.template(templateSpec); return env.template(templateSpec);
} }
// Template is only compiled on first use and cached after that point. // Template is only compiled on first use and cached after that point.
var ret = function(context, options) { function ret(context, execOptions) {
if (!compiled) { if (!compiled) {
compiled = compileInput(); compiled = compileInput();
} }
return compiled.call(this, context, options); return compiled.call(this, context, execOptions);
}; }
ret._setup = function(options) { ret._setup = function(setupOptions) {
if (!compiled) { if (!compiled) {
compiled = compileInput(); compiled = compileInput();
} }
return compiled._setup(options); return compiled._setup(setupOptions);
}; };
ret._child = function(i, data, blockParams, depths) { ret._child = function(i, data, blockParams, depths) {
if (!compiled) { if (!compiled) {
@@ -480,7 +532,7 @@ function argEquals(a, b) {
} }
if (isArray(a) && isArray(b) && a.length === b.length) { 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])) { if (!argEquals(a[i], b[i])) {
return false; return false;
} }
@@ -491,9 +543,16 @@ function argEquals(a, b) {
function transformLiteralToPath(sexpr) { function transformLiteralToPath(sexpr) {
if (!sexpr.path.parts) { 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 // Casting to string here to make false and 0 literal values play nicely with the rest
// of the system. // 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
};
} }
} }
+135 -39
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) { export function SourceLocation(source, locInfo) {
this.source = source; this.source = source;
@@ -12,10 +22,18 @@ 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) { export function stripFlags(open, close) {
return { return {
open: open.charAt(2) === '~', open: open.charAt(2) === '~',
close: close.charAt(close.length-3) === '~' close: close.charAt(close.length - 3) === '~'
}; };
} }
@@ -24,22 +42,24 @@ export function stripComment(comment) {
.replace(/-?-?~?\}\}$/, ''); .replace(/-?-?~?\}\}$/, '');
} }
export function preparePath(data, parts, locInfo) { export function preparePath(data, parts, loc) {
/*jshint -W040 */ loc = this.locInfo(loc);
locInfo = this.locInfo(locInfo);
var original = data ? '@' : '', let original = data ? '@' : '',
dig = [], dig = [],
depth = 0, depth = 0,
depthString = ''; depthString = '';
for(var i=0,l=parts.length; i<l; i++) { for (let i = 0, l = parts.length; i < l; i++) {
var part = parts[i].part; 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; original += (parts[i].separator || '') + part;
if (part === '..' || part === '.' || part === 'this') { if (!isLiteral && (part === '..' || part === '.' || part === 'this')) {
if (dig.length > 0) { if (dig.length > 0) {
throw new Exception('Invalid path: ' + original, {loc: locInfo}); throw new Exception('Invalid path: ' + original, {loc});
} else if (part === '..') { } else if (part === '..') {
depth++; depth++;
depthString += '../'; 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) { export function prepareMustache(path, params, hash, open, strip, locInfo) {
/*jshint -W040 */
// Must use charAt to support IE pre-10 // 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 !== '&'; 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) { export function prepareRawBlock(openRawBlock, contents, close, locInfo) {
/*jshint -W040 */ validateClose(openRawBlock, close);
if (openRawBlock.path.original !== close) {
var errorNode = {loc: openRawBlock.path.loc};
throw new Exception(openRawBlock.path.original + " doesn't match " + close, errorNode);
}
locInfo = this.locInfo(locInfo); 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( return {
openRawBlock.path, openRawBlock.params, openRawBlock.hash, type: 'BlockStatement',
program, undefined, path: openRawBlock.path,
{}, {}, {}, params: openRawBlock.params,
locInfo); hash: openRawBlock.hash,
program,
openStrip: {},
inverseStrip: {},
closeStrip: {},
loc: locInfo
};
} }
export function prepareBlock(openBlock, program, inverseAndProgram, close, inverted, locInfo) { export function prepareBlock(openBlock, program, inverseAndProgram, close, inverted, locInfo) {
/*jshint -W040 */ if (close && close.path) {
// When we are chaining inverse calls, we will not have a close path validateClose(openBlock, close);
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);
} }
let decorator = (/\*/.test(openBlock.open));
program.blockParams = openBlock.blockParams; program.blockParams = openBlock.blockParams;
var inverse, let inverse,
inverseStrip; inverseStrip;
if (inverseAndProgram) { if (inverseAndProgram) {
if (decorator) {
throw new Exception('Unexpected inverse block on decorator', inverseAndProgram);
}
if (inverseAndProgram.chain) { if (inverseAndProgram.chain) {
inverseAndProgram.program.body[0].closeStrip = close.strip; inverseAndProgram.program.body[0].closeStrip = close.strip;
} }
@@ -108,9 +151,62 @@ export function prepareBlock(openBlock, program, inverseAndProgram, close, inver
program = inverted; program = inverted;
} }
return new this.BlockStatement( return {
openBlock.path, openBlock.params, openBlock.hash, type: decorator ? 'DecoratorBlock' : 'BlockStatement',
program, inverse, path: openBlock.path,
openBlock.strip, inverseStrip, close && close.strip, params: openBlock.params,
this.locInfo(locInfo)); 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)
};
}
+199 -133
View File
@@ -1,7 +1,7 @@
import { COMPILER_REVISION, REVISION_CHANGES } from "../base"; import { COMPILER_REVISION, REVISION_CHANGES } from '../base';
import Exception from "../exception"; import Exception from '../exception';
import {isArray} from "../utils"; import {isArray} from '../utils';
import CodeGen from "./code-gen"; import CodeGen from './code-gen';
function Literal(value) { function Literal(value) {
this.value = value; this.value = value;
@@ -12,20 +12,20 @@ function JavaScriptCompiler() {}
JavaScriptCompiler.prototype = { JavaScriptCompiler.prototype = {
// PUBLIC API: You can override these methods in a subclass to provide // PUBLIC API: You can override these methods in a subclass to provide
// alternative compiled forms for name lookup and buffering semantics // alternative compiled forms for name lookup and buffering semantics
nameLookup: function(parent, name /* , type*/) { nameLookup: function(parent, name/* , type*/) {
if (JavaScriptCompiler.isValidJavaScriptVariableName(name)) { if (JavaScriptCompiler.isValidJavaScriptVariableName(name)) {
return [parent, ".", name]; return [parent, '.', name];
} else { } else {
return [parent, "['", name, "']"]; return [parent, '[', JSON.stringify(name), ']'];
} }
}, },
depthedLookup: function(name) { depthedLookup: function(name) {
return [this.aliasable('this.lookup'), '(depths, "', name, '")']; return [this.aliasable('container.lookup'), '(depths, "', name, '")'];
}, },
compilerInfo: function() { compilerInfo: function() {
var revision = COMPILER_REVISION, const revision = COMPILER_REVISION,
versions = REVISION_CHANGES[revision]; versions = REVISION_CHANGES[revision];
return [revision, versions]; return [revision, versions];
}, },
@@ -50,7 +50,7 @@ JavaScriptCompiler.prototype = {
}, },
initializeBuffer: function() { initializeBuffer: function() {
return this.quotedString(""); return this.quotedString('');
}, },
// END PUBLIC API // END PUBLIC API
@@ -64,6 +64,7 @@ JavaScriptCompiler.prototype = {
this.name = this.environment.name; this.name = this.environment.name;
this.isChild = !!context; this.isChild = !!context;
this.context = context || { this.context = context || {
decorators: [],
programs: [], programs: [],
environments: [] environments: []
}; };
@@ -81,10 +82,10 @@ JavaScriptCompiler.prototype = {
this.compileChildren(environment, options); 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; this.useBlockParams = this.useBlockParams || environment.useBlockParams;
var opcodes = environment.opcodes, let opcodes = environment.opcodes,
opcode, opcode,
firstLoc, firstLoc,
i, i,
@@ -107,16 +108,43 @@ JavaScriptCompiler.prototype = {
throw new Exception('Compile completed with content left on stack'); 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) { if (!this.isChild) {
var ret = { let ret = {
compiler: this.compilerInfo(), compiler: this.compilerInfo(),
main: fn 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++) { for (i = 0, l = programs.length; i < l; i++) {
if (programs[i]) { if (programs[i]) {
ret[i] = 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 // getContext opcode when it would be a noop
this.lastContext = 0; this.lastContext = 0;
this.source = new CodeGen(this.options.srcName); this.source = new CodeGen(this.options.srcName);
this.decorators = new CodeGen(this.options.srcName);
}, },
createFunctionContext: function(asObject) { 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) { if (locals.length > 0) {
varDeclarations += ", " + locals.join(", "); varDeclarations += ', ' + locals.join(', ');
} }
// Generate minimizer alias mappings // 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, // 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 // 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. // we aren't concern about minimizing the template size.
var aliasCount = 0; let aliasCount = 0;
for (var alias in this.aliases) { for (let alias in this.aliases) { // eslint-disable-line guard-for-in
var node = this.aliases[alias]; let node = this.aliases[alias];
if (this.aliases.hasOwnProperty(alias) && node.children && node.referenceCount > 1) { if (this.aliases.hasOwnProperty(alias) && node.children && node.referenceCount > 1) {
varDeclarations += ', alias' + (++aliasCount) + '=' + alias; 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) { if (this.useBlockParams || this.useDepths) {
params.push('blockParams'); params.push('blockParams');
@@ -199,7 +228,7 @@ JavaScriptCompiler.prototype = {
} }
// Perform a second pass over the output to merge content when possible // Perform a second pass over the output to merge content when possible
var source = this.mergeSource(varDeclarations); let source = this.mergeSource(varDeclarations);
if (asObject) { if (asObject) {
params.push(source); params.push(source);
@@ -210,14 +239,14 @@ JavaScriptCompiler.prototype = {
} }
}, },
mergeSource: function(varDeclarations) { mergeSource: function(varDeclarations) {
var isSimple = this.environment.isSimple, let isSimple = this.environment.isSimple,
appendOnly = !this.forceBuffer, appendOnly = !this.forceBuffer,
appendFirst, appendFirst,
sourceSeen, sourceSeen,
bufferStart, bufferStart,
bufferEnd; bufferEnd;
this.source.each(function(line) { this.source.each((line) => {
if (line.appendToBuffer) { if (line.appendToBuffer) {
if (bufferStart) { if (bufferStart) {
line.prepend(' + '); line.prepend(' + ');
@@ -252,7 +281,7 @@ JavaScriptCompiler.prototype = {
this.source.push('return "";'); this.source.push('return "";');
} }
} else { } else {
varDeclarations += ", buffer = " + (appendFirst ? '' : this.initializeBuffer()); varDeclarations += ', buffer = ' + (appendFirst ? '' : this.initializeBuffer());
if (bufferStart) { if (bufferStart) {
bufferStart.prepend('return buffer + '); bufferStart.prepend('return buffer + ');
@@ -279,11 +308,11 @@ JavaScriptCompiler.prototype = {
// replace it on the stack with the result of properly // replace it on the stack with the result of properly
// invoking blockHelperMissing. // invoking blockHelperMissing.
blockValue: function(name) { blockValue: function(name) {
var blockHelperMissing = this.aliasable('helpers.blockHelperMissing'), let blockHelperMissing = this.aliasable('helpers.blockHelperMissing'),
params = [this.contextName(0)]; params = [this.contextName(0)];
this.setupHelperArgs(name, 0, params); this.setupHelperArgs(name, 0, params);
var blockName = this.popStack(); let blockName = this.popStack();
params.splice(1, 0, blockName); params.splice(1, 0, blockName);
this.push(this.source.functionCall(blockHelperMissing, 'call', params)); this.push(this.source.functionCall(blockHelperMissing, 'call', params));
@@ -297,13 +326,13 @@ JavaScriptCompiler.prototype = {
// On stack, after, if lastHelper: value // On stack, after, if lastHelper: value
ambiguousBlockValue: function() { ambiguousBlockValue: function() {
// We're being a bit cheeky and reusing the options value from the prior exec // 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)]; params = [this.contextName(0)];
this.setupHelperArgs('', 0, params, true); this.setupHelperArgs('', 0, params, true);
this.flushInline(); this.flushInline();
var current = this.topStack(); let current = this.topStack();
params.splice(1, 0, current); params.splice(1, 0, current);
this.pushSource([ this.pushSource([
@@ -339,13 +368,11 @@ JavaScriptCompiler.prototype = {
// Otherwise, the empty string is appended // Otherwise, the empty string is appended
append: function() { append: function() {
if (this.isInline()) { if (this.isInline()) {
this.replaceStack(function(current) { this.replaceStack((current) => [' != null ? ', current, ' : ""']);
return [' != null ? ', current, ' : ""'];
});
this.pushSource(this.appendToBuffer(this.popStack())); this.pushSource(this.appendToBuffer(this.popStack()));
} else { } else {
var local = this.popStack(); let local = this.popStack();
this.pushSource(['if (', local, ' != null) { ', this.appendToBuffer(local, undefined, true), ' }']); this.pushSource(['if (', local, ' != null) { ', this.appendToBuffer(local, undefined, true), ' }']);
if (this.environment.isSimple) { if (this.environment.isSimple) {
this.pushSource(['else { ', this.appendToBuffer("''", undefined, true), ' }']); this.pushSource(['else { ', this.appendToBuffer("''", undefined, true), ' }']);
@@ -361,7 +388,7 @@ JavaScriptCompiler.prototype = {
// Escape `value` and append it to the buffer // Escape `value` and append it to the buffer
appendEscaped: function() { appendEscaped: function() {
this.pushSource(this.appendToBuffer( this.pushSource(this.appendToBuffer(
[this.aliasable('this.escapeExpression'), '(', this.popStack(), ')'])); [this.aliasable('container.escapeExpression'), '(', this.popStack(), ')']));
}, },
// [getContext] // [getContext]
@@ -392,8 +419,8 @@ JavaScriptCompiler.prototype = {
// //
// Looks up the value of `name` on the current context and pushes // Looks up the value of `name` on the current context and pushes
// it onto the stack. // it onto the stack.
lookupOnContext: function(parts, falsy, scoped) { lookupOnContext: function(parts, falsy, strict, scoped) {
var i = 0; let i = 0;
if (!scoped && this.options.compat && !this.lastContext) { if (!scoped && this.options.compat && !this.lastContext) {
// The depthed query is expected to handle the undefined logic for the root level that // The depthed query is expected to handle the undefined logic for the root level that
@@ -403,7 +430,7 @@ JavaScriptCompiler.prototype = {
this.pushContext(); this.pushContext();
} }
this.resolvePath('context', parts, i, falsy); this.resolvePath('context', parts, i, falsy, strict);
}, },
// [lookupBlockParam] // [lookupBlockParam]
@@ -426,28 +453,27 @@ JavaScriptCompiler.prototype = {
// On stack, after: data, ... // On stack, after: data, ...
// //
// Push the data lookup operator // Push the data lookup operator
lookupData: function(depth, parts) { lookupData: function(depth, parts, strict) {
/*jshint -W083 */
if (!depth) { if (!depth) {
this.pushStackLiteral('data'); this.pushStackLiteral('data');
} else { } 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) { resolvePath: function(type, parts, i, falsy, strict) {
/*jshint -W083 */
if (this.options.strict || this.options.assumeObjects) { 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; return;
} }
var len = parts.length; let len = parts.length;
for (; i < len; i++) { for (; i < len; i++) {
this.replaceStack(function(current) { /* eslint-disable no-loop-func */
var lookup = this.nameLookup(current, parts[i], type); 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) // 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. // needs to have the special handling for these values.
if (!falsy) { if (!falsy) {
@@ -457,6 +483,7 @@ JavaScriptCompiler.prototype = {
return [' && ', lookup]; 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 // If the `value` is a lambda, replace it on the stack by
// the return value of the lambda // the return value of the lambda
resolvePossibleLambda: function() { 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] // [pushStringParam]
@@ -511,7 +538,7 @@ JavaScriptCompiler.prototype = {
this.hash = {values: [], types: [], contexts: [], ids: []}; this.hash = {values: [], types: [], contexts: [], ids: []};
}, },
popHash: function() { popHash: function() {
var hash = this.hash; let hash = this.hash;
this.hash = this.hashes.pop(); this.hash = this.hashes.pop();
if (this.trackIds) { 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] // [invokeHelper]
// //
// On stack, before: hash, inverse, program, params..., ... // On stack, before: hash, inverse, program, params..., ...
@@ -573,11 +618,11 @@ JavaScriptCompiler.prototype = {
// //
// If the helper is not found, `helperMissing` is called. // If the helper is not found, `helperMissing` is called.
invokeHelper: function(paramSize, name, isSimple) { invokeHelper: function(paramSize, name, isSimple) {
var nonHelper = this.popStack(); let nonHelper = this.popStack(),
var helper = this.setupHelper(paramSize, name); helper = this.setupHelper(paramSize, name),
var simple = isSimple ? [helper.name, ' || '] : ''; simple = isSimple ? [helper.name, ' || '] : '';
var lookup = ['('].concat(simple, nonHelper); let lookup = ['('].concat(simple, nonHelper);
if (!this.options.strict) { if (!this.options.strict) {
lookup.push(' || ', this.aliasable('helpers.helperMissing')); lookup.push(' || ', this.aliasable('helpers.helperMissing'));
} }
@@ -594,7 +639,7 @@ JavaScriptCompiler.prototype = {
// This operation is used when the helper is known to exist, // This operation is used when the helper is known to exist,
// so a `helperMissing` fallback is not required. // so a `helperMissing` fallback is not required.
invokeKnownHelper: function(paramSize, name) { 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)); this.push(this.source.functionCall(helper.name, 'call', helper.callParams));
}, },
@@ -613,14 +658,14 @@ JavaScriptCompiler.prototype = {
invokeAmbiguous: function(name, helperCall) { invokeAmbiguous: function(name, helperCall) {
this.useRegister('helper'); this.useRegister('helper');
var nonHelper = this.popStack(); let nonHelper = this.popStack();
this.emptyHash(); 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) { if (!this.options.strict) {
lookup[0] = '(helper = '; lookup[0] = '(helper = ';
lookup.push( lookup.push(
@@ -633,7 +678,7 @@ JavaScriptCompiler.prototype = {
'(', lookup, '(', lookup,
(helper.paramsInit ? ['),(', helper.paramsInit] : []), '),', (helper.paramsInit ? ['),(', helper.paramsInit] : []), '),',
'(typeof helper === ', this.aliasable('"function"'), ' ? ', '(typeof helper === ', this.aliasable('"function"'), ' ? ',
this.source.functionCall('helper','call', helper.callParams), ' : helper))' this.source.functionCall('helper', 'call', helper.callParams), ' : helper))'
]); ]);
}, },
@@ -645,8 +690,8 @@ JavaScriptCompiler.prototype = {
// This operation pops off a context, invokes a partial with that context, // This operation pops off a context, invokes a partial with that context,
// and pushes the result of the invocation back. // and pushes the result of the invocation back.
invokePartial: function(isDynamic, name, indent) { invokePartial: function(isDynamic, name, indent) {
var params = [], let params = [],
options = this.setupParams(name, 1, params, false); options = this.setupParams(name, 1, params);
if (isDynamic) { if (isDynamic) {
name = this.popStack(); name = this.popStack();
@@ -658,6 +703,7 @@ JavaScriptCompiler.prototype = {
} }
options.helpers = 'helpers'; options.helpers = 'helpers';
options.partials = 'partials'; options.partials = 'partials';
options.decorators = 'container.decorators';
if (!isDynamic) { if (!isDynamic) {
params.unshift(this.nameLookup('partials', name, 'partial')); params.unshift(this.nameLookup('partials', name, 'partial'));
@@ -671,7 +717,7 @@ JavaScriptCompiler.prototype = {
options = this.objectLiteral(options); options = this.objectLiteral(options);
params.push(options); params.push(options);
this.push(this.source.functionCall('this.invokePartial', '', params)); this.push(this.source.functionCall('container.invokePartial', '', params));
}, },
// [assignToHash] // [assignToHash]
@@ -681,7 +727,7 @@ JavaScriptCompiler.prototype = {
// //
// Pops a value off the stack and assigns it to the current hash // Pops a value off the stack and assigns it to the current hash
assignToHash: function(key) { assignToHash: function(key) {
var value = this.popStack(), let value = this.popStack(),
context, context,
type, type,
id; id;
@@ -694,7 +740,7 @@ JavaScriptCompiler.prototype = {
context = this.popStack(); context = this.popStack();
} }
var hash = this.hash; let hash = this.hash;
if (context) { if (context) {
hash.contexts[key] = context; hash.contexts[key] = context;
} }
@@ -726,44 +772,47 @@ JavaScriptCompiler.prototype = {
compiler: JavaScriptCompiler, compiler: JavaScriptCompiler,
compileChildren: function(environment, options) { 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]; 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 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.index = index;
child.name = 'program' + index; child.name = 'program' + index;
this.context.programs[index] = compiler.compile(child, options, this.context, !this.precompile); this.context.programs[index] = compiler.compile(child, options, this.context, !this.precompile);
this.context.decorators[index] = compiler.decorators;
this.context.environments[index] = child; this.context.environments[index] = child;
this.useDepths = this.useDepths || compiler.useDepths; this.useDepths = this.useDepths || compiler.useDepths;
this.useBlockParams = this.useBlockParams || compiler.useBlockParams; this.useBlockParams = this.useBlockParams || compiler.useBlockParams;
child.useDepths = this.useDepths;
child.useBlockParams = this.useBlockParams;
} else { } else {
child.index = index; child.index = existing.index;
child.name = 'program' + index; child.name = 'program' + existing.index;
this.useDepths = this.useDepths || child.useDepths; this.useDepths = this.useDepths || existing.useDepths;
this.useBlockParams = this.useBlockParams || child.useBlockParams; this.useBlockParams = this.useBlockParams || existing.useBlockParams;
} }
} }
}, },
matchExistingProgram: function(child) { matchExistingProgram: function(child) {
for (var i = 0, len = this.context.environments.length; i < len; i++) { for (let i = 0, len = this.context.environments.length; i < len; i++) {
var environment = this.context.environments[i]; let environment = this.context.environments[i];
if (environment && environment.equals(child)) { if (environment && environment.equals(child)) {
return i; return environment;
} }
} }
}, },
programExpression: function(guid) { programExpression: function(guid) {
var child = this.environment.children[guid], let child = this.environment.children[guid],
programParams = [child.index, 'data', child.blockParams]; programParams = [child.index, 'data', child.blockParams];
if (this.useBlockParams || this.useDepths) { if (this.useBlockParams || this.useDepths) {
@@ -773,11 +822,11 @@ JavaScriptCompiler.prototype = {
programParams.push('depths'); programParams.push('depths');
} }
return 'this.program(' + programParams.join(', ') + ')'; return 'container.program(' + programParams.join(', ') + ')';
}, },
useRegister: function(name) { useRegister: function(name) {
if(!this.registers[name]) { if (!this.registers[name]) {
this.registers[name] = true; this.registers[name] = true;
this.registers.list.push(name); this.registers.list.push(name);
} }
@@ -809,7 +858,7 @@ JavaScriptCompiler.prototype = {
}, },
replaceStack: function(callback) { replaceStack: function(callback) {
var prefix = ['('], let prefix = ['('],
stack, stack,
createdStack, createdStack,
usedLiteral; usedLiteral;
@@ -820,7 +869,7 @@ JavaScriptCompiler.prototype = {
} }
// We want to merge the inline statement into the replacement statement via ',' // 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) { if (top instanceof Literal) {
// Literals do not need to be inlined // Literals do not need to be inlined
@@ -830,13 +879,13 @@ JavaScriptCompiler.prototype = {
} else { } else {
// Get or create the current stack name for use by the inline // Get or create the current stack name for use by the inline
createdStack = true; createdStack = true;
var name = this.incrStack(); let name = this.incrStack();
prefix = ['((', this.push(name), ' = ', top, ')']; prefix = ['((', this.push(name), ' = ', top, ')'];
stack = this.topStack(); stack = this.topStack();
} }
var item = callback.call(this, stack); let item = callback.call(this, stack);
if (!usedLiteral) { if (!usedLiteral) {
this.popStack(); this.popStack();
@@ -849,22 +898,22 @@ JavaScriptCompiler.prototype = {
incrStack: function() { incrStack: function() {
this.stackSlot++; 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(); return this.topStackName();
}, },
topStackName: function() { topStackName: function() {
return "stack" + this.stackSlot; return 'stack' + this.stackSlot;
}, },
flushInline: function() { flushInline: function() {
var inlineStack = this.inlineStack; let inlineStack = this.inlineStack;
this.inlineStack = []; this.inlineStack = [];
for (var i = 0, len = inlineStack.length; i < len; i++) { for (let i = 0, len = inlineStack.length; i < len; i++) {
var entry = inlineStack[i]; let entry = inlineStack[i];
/* istanbul ignore if */ /* istanbul ignore if */
if (entry instanceof Literal) { if (entry instanceof Literal) {
this.compileStack.push(entry); this.compileStack.push(entry);
} else { } else {
var stack = this.incrStack(); let stack = this.incrStack();
this.pushSource([stack, ' = ', entry, ';']); this.pushSource([stack, ' = ', entry, ';']);
this.compileStack.push(stack); this.compileStack.push(stack);
} }
@@ -875,7 +924,7 @@ JavaScriptCompiler.prototype = {
}, },
popStack: function(wrapped) { popStack: function(wrapped) {
var inline = this.isInline(), let inline = this.isInline(),
item = (inline ? this.inlineStack : this.compileStack).pop(); item = (inline ? this.inlineStack : this.compileStack).pop();
if (!wrapped && (item instanceof Literal)) { if (!wrapped && (item instanceof Literal)) {
@@ -893,7 +942,7 @@ JavaScriptCompiler.prototype = {
}, },
topStack: function() { topStack: function() {
var stack = (this.isInline() ? this.inlineStack : this.compileStack), let stack = (this.isInline() ? this.inlineStack : this.compileStack),
item = stack[stack.length - 1]; item = stack[stack.length - 1];
/* istanbul ignore if */ /* istanbul ignore if */
@@ -921,7 +970,7 @@ JavaScriptCompiler.prototype = {
}, },
aliasable: function(name) { aliasable: function(name) {
var ret = this.aliases[name]; let ret = this.aliases[name];
if (ret) { if (ret) {
ret.referenceCount++; ret.referenceCount++;
return ret; return ret;
@@ -935,20 +984,30 @@ JavaScriptCompiler.prototype = {
}, },
setupHelper: function(paramSize, name, blockHelper) { setupHelper: function(paramSize, name, blockHelper) {
var params = [], let params = [],
paramsInit = this.setupHelperArgs(name, paramSize, params, blockHelper); 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 { return {
params: params, params: params,
paramsInit: paramsInit, paramsInit: paramsInit,
name: foundHelper, name: foundHelper,
callParams: [this.contextName(0)].concat(params) callParams: [callContext].concat(params)
}; };
}, },
setupParams: function(helper, paramSize, 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.name = this.quotedString(helper);
options.hash = this.popStack(); options.hash = this.popStack();
@@ -961,19 +1020,19 @@ JavaScriptCompiler.prototype = {
options.hashContexts = this.popStack(); options.hashContexts = this.popStack();
} }
var inverse = this.popStack(), let inverse = this.popStack(),
program = this.popStack(); program = this.popStack();
// Avoid setting fn and inverse if neither are set. This allows // Avoid setting fn and inverse if neither are set. This allows
// helpers to do a check for `if (options.fn)` // helpers to do a check for `if (options.fn)`
if (program || inverse) { if (program || inverse) {
options.fn = program || 'this.noop'; options.fn = program || 'container.noop';
options.inverse = inverse || 'this.noop'; options.inverse = inverse || 'container.noop';
} }
// The parameters go on to the stack in order (making sure that they are evaluated in order) // 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 // so we need to pop them off the stack in reverse order
var i = paramSize; let i = paramSize;
while (i--) { while (i--) {
param = this.popStack(); param = this.popStack();
params[i] = param; params[i] = param;
@@ -987,6 +1046,10 @@ JavaScriptCompiler.prototype = {
} }
} }
if (objectArgs) {
options.args = this.source.generateArray(params);
}
if (this.trackIds) { if (this.trackIds) {
options.ids = this.source.generateArray(ids); options.ids = this.source.generateArray(ids);
} }
@@ -1005,53 +1068,56 @@ JavaScriptCompiler.prototype = {
}, },
setupHelperArgs: function(helper, paramSize, params, useRegister) { 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); options = this.objectLiteral(options);
if (useRegister) { if (useRegister) {
this.useRegister('options'); this.useRegister('options');
params.push('options'); params.push('options');
return ['options=', options]; return ['options=', options];
} else { } else if (params) {
params.push(options); params.push(options);
return ''; return '';
} else {
return options;
} }
} }
}; };
var reservedWords = ( (function() {
"break else new var" + const reservedWords = (
" case finally return void" + 'break else new var' +
" catch for switch while" + ' case finally return void' +
" continue function this with" + ' catch for switch while' +
" default if throw" + ' continue function this with' +
" delete in try" + ' default if throw' +
" do instanceof typeof" + ' delete in try' +
" abstract enum int short" + ' do instanceof typeof' +
" boolean export interface static" + ' abstract enum int short' +
" byte extends long super" + ' boolean export interface static' +
" char final native synchronized" + ' byte extends long super' +
" class float package throws" + ' char final native synchronized' +
" const goto private transient" + ' class float package throws' +
" debugger implements protected volatile" + ' const goto private transient' +
" double import public let yield await" + ' debugger implements protected volatile' +
" null true false" ' double import public let yield await' +
).split(" "); ' 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; compilerWords[reservedWords[i]] = true;
} }
}());
JavaScriptCompiler.isValidJavaScriptVariableName = function(name) { JavaScriptCompiler.isValidJavaScriptVariableName = function(name) {
return !JavaScriptCompiler.RESERVED_WORDS[name] && /^[a-zA-Z_$][0-9a-zA-Z_$]*$/.test(name); return !JavaScriptCompiler.RESERVED_WORDS[name] && (/^[a-zA-Z_$][0-9a-zA-Z_$]*$/).test(name);
}; };
function strictLookup(requireTerminal, compiler, parts, type) { function strictLookup(requireTerminal, compiler, parts, type) {
var stack = compiler.popStack(); let stack = compiler.popStack(),
i = 0,
var i = 0,
len = parts.length; len = parts.length;
if (requireTerminal) { if (requireTerminal) {
len--; len--;
@@ -1062,7 +1128,7 @@ function strictLookup(requireTerminal, compiler, parts, type) {
} }
if (requireTerminal) { if (requireTerminal) {
return [compiler.aliasable('this.strict'), '(', stack, ', ', compiler.quotedString(parts[i]), ')']; return [compiler.aliasable('container.strict'), '(', stack, ', ', compiler.quotedString(parts[i]), ')'];
} else { } else {
return stack; return stack;
} }
+63 -31
View File
@@ -1,4 +1,5 @@
import Visitor from "./visitor"; /* eslint-disable new-cap */
import Visitor from './visitor';
export function print(ast) { export function print(ast) {
return new PrintVisitor().accept(ast); return new PrintVisitor().accept(ast);
@@ -11,32 +12,32 @@ export function PrintVisitor() {
PrintVisitor.prototype = new Visitor(); PrintVisitor.prototype = new Visitor();
PrintVisitor.prototype.pad = function(string) { PrintVisitor.prototype.pad = function(string) {
var out = ""; let out = '';
for(var i=0,l=this.padding; i<l; i++) { for (let i = 0, l = this.padding; i < l; i++) {
out = out + " "; out += ' ';
} }
out = out + string + "\n"; out += string + '\n';
return out; return out;
}; };
PrintVisitor.prototype.Program = function(program) { PrintVisitor.prototype.Program = function(program) {
var out = '', let out = '',
body = program.body, body = program.body,
i, l; i, l;
if (program.blockParams) { if (program.blockParams) {
var blockParams = 'BLOCK PARAMS: ['; let blockParams = 'BLOCK PARAMS: [';
for(i=0, l=program.blockParams.length; i<l; i++) { for (i = 0, l = program.blockParams.length; i < l; i++) {
blockParams += ' ' + program.blockParams[i]; blockParams += ' ' + program.blockParams[i];
} }
blockParams += ' ]'; blockParams += ' ]';
out += this.pad(blockParams); out += this.pad(blockParams);
} }
for(i=0, l=body.length; i<l; i++) { for (i = 0, l = body.length; i < l; i++) {
out = out + this.accept(body[i]); out += this.accept(body[i]);
} }
this.padding--; this.padding--;
@@ -47,24 +48,28 @@ PrintVisitor.prototype.Program = function(program) {
PrintVisitor.prototype.MustacheStatement = function(mustache) { PrintVisitor.prototype.MustacheStatement = function(mustache) {
return this.pad('{{ ' + this.SubExpression(mustache) + ' }}'); return this.pad('{{ ' + this.SubExpression(mustache) + ' }}');
}; };
PrintVisitor.prototype.Decorator = function(mustache) {
return this.pad('{{ DIRECTIVE ' + this.SubExpression(mustache) + ' }}');
};
PrintVisitor.prototype.BlockStatement = function(block) { PrintVisitor.prototype.BlockStatement =
var out = ""; PrintVisitor.prototype.DecoratorBlock = function(block) {
let out = '';
out = out + this.pad('BLOCK:'); out += this.pad((block.type === 'DecoratorBlock' ? 'DIRECTIVE ' : '') + 'BLOCK:');
this.padding++; this.padding++;
out = out + this.pad(this.SubExpression(block)); out += this.pad(this.SubExpression(block));
if (block.program) { if (block.program) {
out = out + this.pad('PROGRAM:'); out += this.pad('PROGRAM:');
this.padding++; this.padding++;
out = out + this.accept(block.program); out += this.accept(block.program);
this.padding--; this.padding--;
} }
if (block.inverse) { if (block.inverse) {
if (block.program) { this.padding++; } if (block.program) { this.padding++; }
out = out + this.pad('{{^}}'); out += this.pad('{{^}}');
this.padding++; this.padding++;
out = out + this.accept(block.inverse); out += this.accept(block.inverse);
this.padding--; this.padding--;
if (block.program) { this.padding--; } if (block.program) { this.padding--; }
} }
@@ -74,8 +79,8 @@ PrintVisitor.prototype.BlockStatement = function(block) {
}; };
PrintVisitor.prototype.PartialStatement = function(partial) { PrintVisitor.prototype.PartialStatement = function(partial) {
var content = 'PARTIAL:' + partial.name.original; let content = 'PARTIAL:' + partial.name.original;
if(partial.params[0]) { if (partial.params[0]) {
content += ' ' + this.accept(partial.params[0]); content += ' ' + this.accept(partial.params[0]);
} }
if (partial.hash) { if (partial.hash) {
@@ -83,6 +88,22 @@ PrintVisitor.prototype.PartialStatement = function(partial) {
} }
return this.pad('{{> ' + content + ' }}'); 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) { PrintVisitor.prototype.ContentStatement = function(content) {
return this.pad("CONTENT[ '" + content.value + "' ]"); return this.pad("CONTENT[ '" + content.value + "' ]");
@@ -93,21 +114,23 @@ PrintVisitor.prototype.CommentStatement = function(comment) {
}; };
PrintVisitor.prototype.SubExpression = function(sexpr) { 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])); 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) { PrintVisitor.prototype.PathExpression = function(id) {
var path = id.parts.join('/'); let path = id.parts.join('/');
return (id.data ? '@' : '') + 'PATH:' + path; return (id.data ? '@' : '') + 'PATH:' + path;
}; };
@@ -117,18 +140,26 @@ PrintVisitor.prototype.StringLiteral = function(string) {
}; };
PrintVisitor.prototype.NumberLiteral = function(number) { PrintVisitor.prototype.NumberLiteral = function(number) {
return "NUMBER{" + number.value + "}"; return 'NUMBER{' + number.value + '}';
}; };
PrintVisitor.prototype.BooleanLiteral = function(bool) { 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) { PrintVisitor.prototype.Hash = function(hash) {
var pairs = hash.pairs; let pairs = hash.pairs,
var joinedPairs = []; 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])); joinedPairs.push(this.accept(pairs[i]));
} }
@@ -137,3 +168,4 @@ PrintVisitor.prototype.Hash = function(hash) {
PrintVisitor.prototype.HashPair = function(pair) { PrintVisitor.prototype.HashPair = function(pair) {
return pair.key + '=' + this.accept(pair.value); return pair.key + '=' + this.accept(pair.value);
}; };
/* eslint-enable new-cap */
+40 -33
View File
@@ -1,5 +1,4 @@
import Exception from "../exception"; import Exception from '../exception';
import AST from "./ast";
function Visitor() { function Visitor() {
this.parents = []; this.parents = [];
@@ -11,10 +10,11 @@ Visitor.prototype = {
// Visits a given value. If mutating, will replace the value if necessary. // Visits a given value. If mutating, will replace the value if necessary.
acceptKey: function(node, name) { acceptKey: function(node, name) {
var value = this.accept(node[name]); let value = this.accept(node[name]);
if (this.mutating) { if (this.mutating) {
// Hacky sanity check: // Hacky sanity check: This may have a few false positives for type for the helper
if (value && (!value.type || !AST[value.type])) { // methods but will generally do the right thing without a lot of overhead.
if (value && !Visitor.prototype[value.type]) {
throw new Exception('Unexpected node type "' + value.type + '" found when accepting ' + name + ' on ' + node.type); throw new Exception('Unexpected node type "' + value.type + '" found when accepting ' + name + ' on ' + node.type);
} }
node[name] = value; node[name] = value;
@@ -34,7 +34,7 @@ Visitor.prototype = {
// Traverses a given array. If mutating, empty respnses will be removed // Traverses a given array. If mutating, empty respnses will be removed
// for child elements. // for child elements.
acceptArray: function(array) { 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); this.acceptKey(array, i);
if (!array[i]) { if (!array[i]) {
@@ -50,12 +50,17 @@ Visitor.prototype = {
return; return;
} }
/* istanbul ignore next: Sanity code */
if (!this[object.type]) {
throw new Exception('Unknown type: ' + object.type, object);
}
if (this.current) { if (this.current) {
this.parents.unshift(this.current); this.parents.unshift(this.current);
} }
this.current = object; this.current = object;
var ret = this[object.type](object); let ret = this[object.type](object);
this.current = this.parents.shift(); this.current = this.parents.shift();
@@ -70,46 +75,31 @@ Visitor.prototype = {
this.acceptArray(program.body); this.acceptArray(program.body);
}, },
MustacheStatement: function(mustache) { MustacheStatement: visitSubExpression,
this.acceptRequired(mustache, 'path'); Decorator: visitSubExpression,
this.acceptArray(mustache.params);
this.acceptKey(mustache, 'hash');
},
BlockStatement: function(block) { BlockStatement: visitBlock,
this.acceptRequired(block, 'path'); DecoratorBlock: visitBlock,
this.acceptArray(block.params);
this.acceptKey(block, 'hash');
this.acceptKey(block, 'program'); PartialStatement: visitPartial,
this.acceptKey(block, 'inverse'); PartialBlockStatement: function(partial) {
}, visitPartial.call(this, partial);
PartialStatement: function(partial) { this.acceptKey(partial, 'program');
this.acceptRequired(partial, 'name');
this.acceptArray(partial.params);
this.acceptKey(partial, 'hash');
}, },
ContentStatement: function(/* content */) {}, ContentStatement: function(/* content */) {},
CommentStatement: function(/* comment */) {}, CommentStatement: function(/* comment */) {},
SubExpression: function(sexpr) { SubExpression: visitSubExpression,
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');
},
PathExpression: function(/* path */) {}, PathExpression: function(/* path */) {},
StringLiteral: function(/* string */) {}, StringLiteral: function(/* string */) {},
NumberLiteral: function(/* number */) {}, NumberLiteral: function(/* number */) {},
BooleanLiteral: function(/* bool */) {}, BooleanLiteral: function(/* bool */) {},
UndefinedLiteral: function(/* literal */) {},
NullLiteral: function(/* literal */) {},
Hash: function(hash) { Hash: function(hash) {
this.acceptArray(hash.pairs); 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; export default Visitor;
+36 -31
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 = new Visitor();
WhitespaceControl.prototype.Program = function(program) { WhitespaceControl.prototype.Program = function(program) {
var isRoot = !this.isRootSeen; const doStandalone = !this.options.ignoreStandalone;
let isRoot = !this.isRootSeen;
this.isRootSeen = true; this.isRootSeen = true;
var body = program.body; let body = program.body;
for (var i = 0, l = body.length; i < l; i++) { for (let i = 0, l = body.length; i < l; i++) {
var current = body[i], let current = body[i],
strip = this.accept(current); strip = this.accept(current);
if (!strip) { if (!strip) {
continue; continue;
} }
var _isPrevWhitespace = isPrevWhitespace(body, i, isRoot), let _isPrevWhitespace = isPrevWhitespace(body, i, isRoot),
_isNextWhitespace = isNextWhitespace(body, i, isRoot), _isNextWhitespace = isNextWhitespace(body, i, isRoot),
openStandalone = strip.openStandalone && _isPrevWhitespace, openStandalone = strip.openStandalone && _isPrevWhitespace,
@@ -31,24 +34,24 @@ WhitespaceControl.prototype.Program = function(program) {
omitLeft(body, i, true); omitLeft(body, i, true);
} }
if (inlineStandalone) { if (doStandalone && inlineStandalone) {
omitRight(body, i); omitRight(body, i);
if (omitLeft(body, i)) { if (omitLeft(body, i)) {
// If we are on a standalone node, save the indent info for partials // If we are on a standalone node, save the indent info for partials
if (current.type === 'PartialStatement') { if (current.type === 'PartialStatement') {
// Pull out the whitespace from the final line // Pull out the whitespace from the final line
current.indent = (/([ \t]+$)/).exec(body[i-1].original)[1]; current.indent = (/([ \t]+$)/).exec(body[i - 1].original)[1];
} }
} }
} }
if (openStandalone) { if (doStandalone && openStandalone) {
omitRight((current.program || current.inverse).body); omitRight((current.program || current.inverse).body);
// Strip out the previous content node if it's whitespace only // Strip out the previous content node if it's whitespace only
omitLeft(body, i); omitLeft(body, i);
} }
if (closeStandalone) { if (doStandalone && closeStandalone) {
// Always strip the next node // Always strip the next node
omitRight(body, i); omitRight(body, i);
@@ -58,12 +61,15 @@ WhitespaceControl.prototype.Program = function(program) {
return 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.program);
this.accept(block.inverse); this.accept(block.inverse);
// Find the inverse program that is involed with whitespace stripping. // 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, inverse = block.program && block.inverse,
firstInverse = inverse, firstInverse = inverse,
lastInverse = inverse; lastInverse = inverse;
@@ -73,11 +79,11 @@ WhitespaceControl.prototype.BlockStatement = function(block) {
// Walk the inverse chain to find the last inverse that is actually in the chain. // Walk the inverse chain to find the last inverse that is actually in the chain.
while (lastInverse.chained) { while (lastInverse.chained) {
lastInverse = lastInverse.body[lastInverse.body.length-1].program; lastInverse = lastInverse.body[lastInverse.body.length - 1].program;
} }
} }
var strip = { let strip = {
open: block.openStrip.open, open: block.openStrip.open,
close: block.closeStrip.close, close: block.closeStrip.close,
@@ -92,7 +98,7 @@ WhitespaceControl.prototype.BlockStatement = function(block) {
} }
if (inverse) { if (inverse) {
var inverseStrip = block.inverseStrip; let inverseStrip = block.inverseStrip;
if (inverseStrip.open) { if (inverseStrip.open) {
omitLeft(program.body, null, true); omitLeft(program.body, null, true);
@@ -106,21 +112,20 @@ WhitespaceControl.prototype.BlockStatement = function(block) {
} }
// Find standalone else statments // Find standalone else statments
if (isPrevWhitespace(program.body) if (!this.options.ignoreStandalone
&& isPrevWhitespace(program.body)
&& isNextWhitespace(firstInverse.body)) { && isNextWhitespace(firstInverse.body)) {
omitLeft(program.body); omitLeft(program.body);
omitRight(firstInverse.body); omitRight(firstInverse.body);
} }
} else { } else if (block.closeStrip.open) {
if (block.closeStrip.open) { omitLeft(program.body, null, true);
omitLeft(program.body, null, true);
}
} }
return strip; return strip;
}; };
WhitespaceControl.prototype.Decorator =
WhitespaceControl.prototype.MustacheStatement = function(mustache) { WhitespaceControl.prototype.MustacheStatement = function(mustache) {
return mustache.strip; return mustache.strip;
}; };
@@ -128,7 +133,7 @@ WhitespaceControl.prototype.MustacheStatement = function(mustache) {
WhitespaceControl.prototype.PartialStatement = WhitespaceControl.prototype.PartialStatement =
WhitespaceControl.prototype.CommentStatement = function(node) { WhitespaceControl.prototype.CommentStatement = function(node) {
/* istanbul ignore next */ /* istanbul ignore next */
var strip = node.strip || {}; let strip = node.strip || {};
return { return {
inlineStandalone: true, inlineStandalone: true,
open: strip.open, open: strip.open,
@@ -144,8 +149,8 @@ function isPrevWhitespace(body, i, isRoot) {
// Nodes that end with newlines are considered whitespace (but are special // Nodes that end with newlines are considered whitespace (but are special
// cased for strip operations) // cased for strip operations)
var prev = body[i-1], let prev = body[i - 1],
sibling = body[i-2]; sibling = body[i - 2];
if (!prev) { if (!prev) {
return isRoot; return isRoot;
} }
@@ -159,8 +164,8 @@ function isNextWhitespace(body, i, isRoot) {
i = -1; i = -1;
} }
var next = body[i+1], let next = body[i + 1],
sibling = body[i+2]; sibling = body[i + 2];
if (!next) { if (!next) {
return isRoot; 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 // If mulitple is truthy then all whitespace will be stripped out until non-whitespace
// content is met. // content is met.
function omitRight(body, i, multiple) { 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)) { if (!current || current.type !== 'ContentStatement' || (!multiple && current.rightStripped)) {
return; return;
} }
var original = current.value; let original = current.value;
current.value = current.value.replace(multiple ? (/^\s+/) : (/^[ \t]*\r?\n?/), ''); current.value = current.value.replace(multiple ? (/^\s+/) : (/^[ \t]*\r?\n?/), '');
current.rightStripped = current.value !== original; 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 // If mulitple is truthy then all whitespace will be stripped out until non-whitespace
// content is met. // content is met.
function omitLeft(body, i, multiple) { 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)) { if (!current || current.type !== 'ContentStatement' || (!multiple && current.leftStripped)) {
return; return;
} }
// We omit the last node if it's whitespace only and not preceeded by a non-content node. // 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.value = current.value.replace(multiple ? (/\s+$/) : (/[ \t]+$/), '');
current.leftStripped = current.value !== original; current.leftStripped = current.value !== original;
return current.leftStripped; 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;
});
}
+26 -7
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) { function Exception(message, node) {
var loc = node && node.loc, let loc = node && node.loc,
line, line,
column; column;
if (loc) { if (loc) {
@@ -12,16 +12,35 @@ function Exception(message, node) {
message += ' - ' + line + ':' + column; 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. // 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]]; this[errorProps[idx]] = tmp[errorProps[idx]];
} }
if (loc) { /* istanbul ignore else */
this.lineNumber = line; if (Error.captureStackTrace) {
this.column = column; 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 */
} }
} }
+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;
};
}
+114 -53
View File
@@ -1,47 +1,50 @@
module Utils from "./utils"; import * as Utils from './utils';
import Exception from "./exception"; import Exception from './exception';
import { COMPILER_REVISION, REVISION_CHANGES, createFrame } from "./base"; import { COMPILER_REVISION, REVISION_CHANGES, createFrame } from './base';
export function checkRevision(compilerInfo) { export function checkRevision(compilerInfo) {
var compilerRevision = compilerInfo && compilerInfo[0] || 1, const compilerRevision = compilerInfo && compilerInfo[0] || 1,
currentRevision = COMPILER_REVISION; currentRevision = COMPILER_REVISION;
if (compilerRevision !== currentRevision) { if (compilerRevision !== currentRevision) {
if (compilerRevision < currentRevision) { if (compilerRevision < currentRevision) {
var runtimeVersions = REVISION_CHANGES[currentRevision], const runtimeVersions = REVISION_CHANGES[currentRevision],
compilerVersions = REVISION_CHANGES[compilerRevision]; compilerVersions = REVISION_CHANGES[compilerRevision];
throw new Exception("Template was precompiled with an older version of Handlebars than the current runtime. "+ 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+")."); 'Please update your precompiler to a newer version (' + runtimeVersions + ') or downgrade your runtime to an older version (' + compilerVersions + ').');
} else { } else {
// Use the embedded version info since the runtime doesn't know about this revision yet // 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. "+ 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]+")."); 'Please update your runtime to a newer version (' + compilerInfo[1] + ').');
} }
} }
} }
// TODO: Remove this line and break up compilePartial
export function template(templateSpec, env) { export function template(templateSpec, env) {
/* istanbul ignore next */ /* istanbul ignore next */
if (!env) { if (!env) {
throw new Exception("No environment passed to template"); throw new Exception('No environment passed to template');
} }
if (!templateSpec || !templateSpec.main) { if (!templateSpec || !templateSpec.main) {
throw new Exception('Unknown template object: ' + typeof templateSpec); 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 // 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. // for external users to override these as psuedo-supported APIs.
env.VM.checkRevision(templateSpec.compiler); env.VM.checkRevision(templateSpec.compiler);
var invokePartialWrapper = function(partial, context, options) { function invokePartialWrapper(partial, context, options) {
if (options.hash) { if (options.hash) {
context = Utils.extend({}, context, options.hash); context = Utils.extend({}, context, options.hash);
if (options.ids) {
options.ids[0] = true;
}
} }
partial = env.VM.resolvePartial.call(this, partial, context, options); 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) { if (result == null && env.compile) {
options.partials[options.name] = env.compile(partial, templateSpec.compilerOptions, env); options.partials[options.name] = env.compile(partial, templateSpec.compilerOptions, env);
@@ -49,8 +52,8 @@ export function template(templateSpec, env) {
} }
if (result != null) { if (result != null) {
if (options.indent) { if (options.indent) {
var lines = result.split('\n'); let lines = result.split('\n');
for (var i = 0, l = lines.length; i < l; i++) { for (let i = 0, l = lines.length; i < l; i++) {
if (!lines[i] && i + 1 === l) { if (!lines[i] && i + 1 === l) {
break; break;
} }
@@ -61,12 +64,12 @@ export function template(templateSpec, env) {
} }
return result; return result;
} else { } 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 // Just add water
var container = { let container = {
strict: function(obj, name) { strict: function(obj, name) {
if (!(name in obj)) { if (!(name in obj)) {
throw new Exception('"' + name + '" not defined in ' + obj); throw new Exception('"' + name + '" not defined in ' + obj);
@@ -74,8 +77,8 @@ export function template(templateSpec, env) {
return obj[name]; return obj[name];
}, },
lookup: function(depths, name) { lookup: function(depths, name) {
var len = depths.length; const len = depths.length;
for (var i = 0; i < len; i++) { for (let i = 0; i < len; i++) {
if (depths[i] && depths[i][name] != null) { if (depths[i] && depths[i][name] != null) {
return depths[i][name]; return depths[i][name];
} }
@@ -89,57 +92,66 @@ export function template(templateSpec, env) {
invokePartial: invokePartialWrapper, invokePartial: invokePartialWrapper,
fn: function(i) { fn: function(i) {
return templateSpec[i]; let ret = templateSpec[i];
ret.decorator = templateSpec[i + '_d'];
return ret;
}, },
programs: [], programs: [],
program: function(i, data, declaredBlockParams, blockParams, depths) { program: function(i, data, declaredBlockParams, blockParams, depths) {
var programWrapper = this.programs[i], let programWrapper = this.programs[i],
fn = this.fn(i); fn = this.fn(i);
if (data || depths || blockParams || declaredBlockParams) { 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) { } else if (!programWrapper) {
programWrapper = this.programs[i] = program(this, i, fn); programWrapper = this.programs[i] = wrapProgram(this, i, fn);
} }
return programWrapper; return programWrapper;
}, },
data: function(data, depth) { data: function(value, depth) {
while (data && depth--) { while (value && depth--) {
data = data._parent; value = value._parent;
} }
return data; return value;
}, },
merge: function(param, common) { merge: function(param, common) {
var ret = param || common; let obj = param || common;
if (param && common && (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, noop: env.VM.noop,
compilerInfo: templateSpec.compiler compilerInfo: templateSpec.compiler
}; };
var ret = function(context, options) { function ret(context, options = {}) {
options = options || {}; let data = options.data;
var data = options.data;
ret._setup(options); ret._setup(options);
if (!options.partial && templateSpec.useData) { if (!options.partial && templateSpec.useData) {
data = initData(context, data); data = initData(context, data);
} }
var depths, let depths,
blockParams = templateSpec.useBlockParams ? [] : undefined; blockParams = templateSpec.useBlockParams ? [] : undefined;
if (templateSpec.useDepths) { 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.isTop = true;
ret._setup = function(options) { ret._setup = function(options) {
@@ -149,9 +161,13 @@ export function template(templateSpec, env) {
if (templateSpec.usePartial) { if (templateSpec.usePartial) {
container.partials = container.merge(options.partials, env.partials); container.partials = container.merge(options.partials, env.partials);
} }
if (templateSpec.usePartial || templateSpec.useDecorators) {
container.decorators = container.merge(options.decorators, env.decorators);
}
} else { } else {
container.helpers = options.helpers; container.helpers = options.helpers;
container.partials = options.partials; container.partials = options.partials;
container.decorators = options.decorators;
} }
}; };
@@ -163,22 +179,28 @@ export function template(templateSpec, env) {
throw new Exception('must pass parent depths'); 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; return ret;
} }
export function program(container, i, fn, data, declaredBlockParams, blockParams, depths) { export function wrapProgram(container, i, fn, data, declaredBlockParams, blockParams, depths) {
var prog = function(context, options) { function prog(context, options = {}) {
options = options || {}; let currentDepths = depths;
if (depths && context != depths[0]) {
currentDepths = [context].concat(depths);
}
return fn.call(container, return fn(container,
context, context,
container.helpers, container.partials, container.helpers, container.partials,
options.data || data, options.data || data,
blockParams && [options.blockParams].concat(blockParams), blockParams && [options.blockParams].concat(blockParams),
depths && [context].concat(depths)); currentDepths);
}; }
prog = executeDecorators(fn, prog, container, depths, data, blockParams);
prog.program = i; prog.program = i;
prog.depth = depths ? depths.length : 0; prog.depth = depths ? depths.length : 0;
prog.blockParams = declaredBlockParams || 0; prog.blockParams = declaredBlockParams || 0;
@@ -187,7 +209,11 @@ export function program(container, i, fn, data, declaredBlockParams, blockParams
export function resolvePartial(partial, context, options) { export function resolvePartial(partial, context, options) {
if (!partial) { if (!partial) {
partial = options.partials[options.name]; if (options.name === '@partial-block') {
partial = options.data['partial-block'];
} else {
partial = options.partials[options.name];
}
} else if (!partial.call && !options.name) { } else if (!partial.call && !options.name) {
// This is a dynamic partial that returned a string // This is a dynamic partial that returned a string
options.name = partial; options.name = partial;
@@ -197,16 +223,42 @@ export function resolvePartial(partial, context, options) {
} }
export function invokePartial(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; options.partial = true;
if (options.ids) {
options.data.contextPath = options.ids[0] || options.data.contextPath;
}
if(partial === undefined) { let partialBlock;
throw new Exception("The partial " + options.name + " could not be found"); if (options.fn && options.fn !== noop) {
} else if(partial instanceof Function) { 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');
} else if (partial instanceof Function) {
return partial(context, options); return partial(context, options);
} }
} }
export function noop() { return ""; } export function noop() { return ''; }
function initData(context, data) { function initData(context, data) {
if (!data || !('root' in data)) { if (!data || !('root' in data)) {
@@ -215,3 +267,12 @@ function initData(context, data) {
} }
return 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() { SafeString.prototype.toString = SafeString.prototype.toHTML = function() {
return "" + this.string; return '' + this.string;
}; };
export default SafeString; export default SafeString;
+41 -31
View File
@@ -1,23 +1,23 @@
/*jshint -W004 */ const escape = {
var escape = { '&': '&amp;',
"&": "&amp;", '<': '&lt;',
"<": "&lt;", '>': '&gt;',
">": "&gt;", '"': '&quot;',
'"': "&quot;", "'": '&#x27;',
"'": "&#x27;", '`': '&#x60;',
"`": "&#x60;" '=': '&#x3D;'
}; };
var badChars = /[&<>"'`]/g; const badChars = /[&<>"'`=]/g,
var possible = /[&<>"'`]/; possible = /[&<>"'`=]/;
function escapeChar(chr) { function escapeChar(chr) {
return escape[chr]; return escape[chr];
} }
export function extend(obj /* , ...source */) { export function extend(obj/* , ...source */) {
for (var i = 1; i < arguments.length; i++) { for (let i = 1; i < arguments.length; i++) {
for (var key in arguments[i]) { for (let key in arguments[i]) {
if (Object.prototype.hasOwnProperty.call(arguments[i], key)) { if (Object.prototype.hasOwnProperty.call(arguments[i], key)) {
obj[key] = arguments[i][key]; obj[key] = arguments[i][key];
} }
@@ -27,11 +27,12 @@ export function extend(obj /* , ...source */) {
return obj; return obj;
} }
export var toString = Object.prototype.toString; export let toString = Object.prototype.toString;
// Sourced from lodash // Sourced from lodash
// https://github.com/bestiejs/lodash/blob/master/LICENSE.txt // 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'; return typeof value === 'function';
}; };
// fallback for older versions of Chrome and Safari // fallback for older versions of Chrome and Safari
@@ -41,16 +42,17 @@ if (isFunction(/x/)) {
return typeof value === 'function' && toString.call(value) === '[object Function]'; return typeof value === 'function' && toString.call(value) === '[object Function]';
}; };
} }
export var isFunction; export {isFunction};
/* eslint-enable func-style */
/* istanbul ignore next */ /* 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; 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. // Older IE versions do not directly support indexOf so we must implement our own, sadly.
export function indexOf(array, value) { 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) { if (array[i] === value) {
return i; return i;
} }
@@ -60,21 +62,23 @@ export function indexOf(array, value) {
export function escapeExpression(string) { export function escapeExpression(string) {
// don't escape SafeStrings, since they're already safe if (typeof string !== 'string') {
if (string && string.toHTML) { // don't escape SafeStrings, since they're already safe
return string.toHTML(); if (string && string.toHTML) {
} else if (string == null) { return string.toHTML();
return ""; } else if (string == null) {
} else if (!string) { return '';
return string + ''; } else if (!string) {
return 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;
} }
// Force a string conversion as this will be done by the append regardless and if (!possible.test(string)) { return string; }
// 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;
if(!possible.test(string)) { return string; }
return string.replace(badChars, escapeChar); 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) { export function blockParams(params, ids) {
params.path = ids; params.path = ids;
return params; return params;
+9 -10
View File
@@ -1,11 +1,10 @@
// USAGE: // USAGE:
// var handlebars = require('handlebars'); // var handlebars = require('handlebars');
/* eslint-disable no-var */
// var local = handlebars.create(); // var local = handlebars.create();
var handlebars = require('../dist/cjs/handlebars')["default"]; var handlebars = require('../dist/cjs/handlebars')['default'];
handlebars.Visitor = require('../dist/cjs/handlebars/compiler/visitor')["default"];
var printer = require('../dist/cjs/handlebars/compiler/printer'); var printer = require('../dist/cjs/handlebars/compiler/printer');
handlebars.PrintVisitor = printer.PrintVisitor; handlebars.PrintVisitor = printer.PrintVisitor;
@@ -14,13 +13,13 @@ handlebars.print = printer.print;
module.exports = handlebars; module.exports = handlebars;
// Publish a Node.js require() handler for .handlebars and .hbs files // 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 */ /* istanbul ignore else */
if (typeof require !== 'undefined' && require.extensions) { if (typeof require !== 'undefined' && require.extensions) {
var extension = function(module, filename) { require.extensions['.handlebars'] = extension;
var fs = require("fs"); require.extensions['.hbs'] = extension;
var templateString = fs.readFileSync(filename, "utf8");
module.exports = handlebars.compile(templateString);
};
require.extensions[".handlebars"] = extension;
require.extensions[".hbs"] = extension;
} }
+187 -91
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'), module.exports.loadTemplates = function(opts, callback) {
Handlebars = require('./index'), loadStrings(opts, function(err, strings) {
basename = require('path').basename, if (err) {
SourceMap = require('source-map'), callback(err);
SourceMapConsumer = SourceMap.SourceMapConsumer, } else {
SourceNode = SourceMap.SourceNode, loadFiles(opts, function(err, files) {
uglify = require('uglify-js'); 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) { module.exports.cli = function(opts) {
if (opts.version) { if (opts.version) {
@@ -13,41 +137,39 @@ module.exports.cli = function(opts) {
return; return;
} }
if (!opts.templates.length) { if (!opts.templates.length && !opts.hasDirectory) {
throw new Handlebars.Exception('Must define at least one template or directory.'); throw new Handlebars.Exception('Must define at least one template or directory.');
} }
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) { if (opts.simple && opts.min) {
throw new Handlebars.Exception('Unable to minimize simple output'); 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'); 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 // Convert the known list into a hash
var known = {}; let known = {};
if (opts.known && !Array.isArray(opts.known)) { if (opts.known && !Array.isArray(opts.known)) {
opts.known = [opts.known]; opts.known = [opts.known];
} }
if (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; known[opts.known[i]] = true;
} }
} }
// Build file extension pattern const objectName = opts.partial ? 'Handlebars.partials' : 'templates';
var extension = opts.extension.replace(/[\\^$*+?.():=!|{}\-\[\]]/g, function(arg) { return '\\' + arg; });
extension = new RegExp('\\.' + extension + '$');
var output = new SourceNode(); let output = new SourceNode();
if (!opts.simple) { if (!opts.simple) {
if (opts.amd) { if (opts.amd) {
output.add('define([\'' + opts.handlebarPath + 'handlebars.runtime\'], function(Handlebars) {\n Handlebars = Handlebars["default"];'); output.add('define([\'' + opts.handlebarPath + 'handlebars.runtime\'], function(Handlebars) {\n Handlebars = Handlebars["default"];');
@@ -65,81 +187,47 @@ module.exports.cli = function(opts) {
} }
output.add('{};\n'); 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 = {
knownHelpers: known,
knownHelpersOnly: opts.o
};
if (opts.map) {
options.srcName = 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);
// If we are generating a source map, we have to reconstruct the SourceNode object
if (opts.map) {
var 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']);
}
}
}
opts.templates.forEach(function(template) { opts.templates.forEach(function(template) {
processTemplate(template, opts.root); let options = {
knownHelpers: known,
knownHelpersOnly: opts.o
};
if (opts.map) {
options.srcName = template.path;
}
if (opts.data) {
options.data = true;
}
let precompiled = Handlebars.precompile(template.source, options);
// If we are generating a source map, we have to reconstruct the SourceNode object
if (opts.map) {
let consumer = new SourceMapConsumer(precompiled.map);
precompiled = SourceNode.fromStringWithSourceMap(precompiled.code, consumer);
}
if (opts.simple) {
output.add([precompiled, '\n']);
} else {
if (!template.name) {
throw new Handlebars.Exception('Name missing for template');
}
if (opts.amd && !multiple) {
output.add('return ');
}
output.add([objectName, '[\'', template.name, '\'] = template(', precompiled, ');\n']);
}
}); });
// Output the content // Output the content
if (!opts.simple) { if (!opts.simple) {
if (opts.amd) { if (opts.amd) {
if(opts.templates.length > 1 || (opts.templates.length == 1 && fs.statSync(opts.templates[0]).isDirectory())) { if (multiple) {
if(opts.partial){ output.add(['return ', objectName, ';\n']);
output.add('return Handlebars.partials;\n');
} else {
output.add('return templates;\n');
}
} }
output.add('});'); output.add('});');
} else if (!opts.commonjs) { } else if (!opts.commonjs) {
@@ -178,3 +266,11 @@ module.exports.cli = function(opts) {
console.log(output); 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", "name": "handlebars",
"barename": "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", "description": "Handlebars provides the power necessary to let you build semantic templates effectively with no frustration",
"homepage": "http://www.handlebarsjs.com/", "homepage": "http://www.handlebarsjs.com/",
"keywords": [ "keywords": [
@@ -21,37 +21,42 @@
"node": ">=0.4.7" "node": ">=0.4.7"
}, },
"dependencies": { "dependencies": {
"async": "^1.4.0",
"optimist": "^0.6.1", "optimist": "^0.6.1",
"source-map": "^0.1.40" "source-map": "^0.4.4"
}, },
"optionalDependencies": { "optionalDependencies": {
"uglify-js": "~2.3" "uglify-js": "^2.6"
}, },
"devDependencies": { "devDependencies": {
"async": "^0.9.0", "aws-sdk": "^2.1.49",
"aws-sdk": "~1.5.0", "babel-loader": "^5.0.0",
"babel-runtime": "^5.1.10",
"benchmark": "~1.0", "benchmark": "~1.0",
"dustjs-linkedin": "^2.0.2", "dustjs-linkedin": "^2.0.2",
"eco": "~1.1.0-rc-3", "eco": "~1.1.0-rc-3",
"es6-module-packager": "^2.0.0",
"grunt": "~0.4.1", "grunt": "~0.4.1",
"grunt-babel": "^5.0.0",
"grunt-cli": "~0.1.10", "grunt-cli": "~0.1.10",
"grunt-contrib-clean": "0.x", "grunt-contrib-clean": "0.x",
"grunt-contrib-concat": "0.x", "grunt-contrib-concat": "0.x",
"grunt-contrib-connect": "0.x", "grunt-contrib-connect": "0.x",
"grunt-contrib-copy": "0.x", "grunt-contrib-copy": "0.x",
"grunt-contrib-jshint": "0.x",
"grunt-contrib-requirejs": "0.x", "grunt-contrib-requirejs": "0.x",
"grunt-contrib-uglify": "0.x", "grunt-contrib-uglify": "0.x",
"grunt-contrib-watch": "0.x", "grunt-contrib-watch": "0.x",
"grunt-eslint": "^17.1.0",
"grunt-saucelabs": "8.x", "grunt-saucelabs": "8.x",
"grunt-webpack": "^1.0.8",
"istanbul": "^0.3.0", "istanbul": "^0.3.0",
"jison": "~0.3.0", "jison": "~0.3.0",
"keen.io": "0.0.3",
"mocha": "~1.20.0", "mocha": "~1.20.0",
"mustache": "0.x", "mock-stdin": "^0.3.0",
"semver": "^4.0.0", "mustache": "^2.1.3",
"underscore": "^1.5.1" "semver": "^5.0.1",
"underscore": "^1.5.1",
"webpack": "^1.12.6",
"webpack-dev-server": "^1.12.1"
}, },
"main": "lib/index.js", "main": "lib/index.js",
"bin": { "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 ## 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 ## 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)) - [#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 - INTEGER -> NUMBER
- Additional PartialNode hash parameter - Additional PartialNode hash parameter
- New RawBlockNode type - 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) [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 // Create a simple path alias to allow browserify to resolve
// the runtime on a supported path. // 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 src="/spec/env/common.js"></script>
<script> <script>
var requireFailure;
requirejs.config({ requirejs.config({
paths: { paths: {
'handlebars.runtime': '/dist/handlebars.runtime.amd' 'handlebars.runtime': '/dist/handlebars.runtime.amd'
} }
}); });
requirejs.onError = function (err) {
requireFailure = err;
};
</script> </script>
<script> <script>
onload = function(){ onload = function(){
@@ -60,6 +65,17 @@
runner.on('fail', logFailure); runner.on('fail', logFailure);
// Inject any require initilizer failures into the first test so they are properly
// reported.
if (requireFailure) {
runner.on('hook end', function(hook){
if (requireFailure) {
runner.uncaught(requireFailure);
requireFailure = undefined;
}
});
}
function logFailure(test, err){ function logFailure(test, err){
var flattenTitles = function(test){ var flattenTitles = function(test){
+20 -4
View File
@@ -1,3 +1,4 @@
<!doctype html>
<html> <html>
<head> <head>
<title>Mocha</title> <title>Mocha</title>
@@ -28,12 +29,17 @@
<script src="/spec/env/common.js"></script> <script src="/spec/env/common.js"></script>
<script> <script>
var requireFailure;
requirejs.config({ requirejs.config({
paths: { paths: {
handlebars: '/dist/handlebars.amd', handlebars: '/dist/handlebars.amd',
tests: '/tmp/tests' tests: '/tmp/tests'
} }
}); });
requirejs.onError = function (err) {
requireFailure = err;
};
var CompilerContext = { var CompilerContext = {
compile: function(template, options) { compile: function(template, options) {
@@ -79,16 +85,26 @@
runner.on('fail', logFailure); 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 = []; var titles = [];
while (test.parent.title){ while (test.parent.title){
titles.push(test.parent.title); titles.push(test.parent.title);
test = test.parent; test = test.parent;
} }
return titles.reverse(); return titles.reverse();
}; }
failedTests.push({ failedTests.push({
name: test.title, name: test.title,
@@ -97,7 +113,7 @@
stack: err.stack, stack: err.stack,
titles: flattenTitles(test) titles: flattenTitles(test)
}); });
}; }
}); });
}); });
}; };
+69 -117
View File
@@ -1,175 +1,127 @@
/*global Handlebars, handlebarsEnv, shouldThrow */
describe('ast', function() { describe('ast', function() {
if (!Handlebars.AST) { if (!Handlebars.AST) {
return; return;
} }
var LOCATION_INFO = { var AST = Handlebars.AST;
start: {
line: 1,
column: 1
},
end: {
line: 1,
column: 1
}
};
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() { describe('BlockStatement', function() {
it('should throw on mustache mismatch', function() { it('should throw on mustache mismatch', function() {
shouldThrow(function() { shouldThrow(function() {
handlebarsEnv.parse("\n {{#foo}}{{/bar}}"); handlebarsEnv.parse('\n {{#foo}}{{/bar}}');
}, Handlebars.Exception, "foo doesn't match bar - 2:5"); }, 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('PathExpression', function() {
it('stores location info', function(){ describe('helpers', function() {
var idNode = new handlebarsEnv.AST.PathExpression(false, 0, [], 'foo', LOCATION_INFO); describe('#helperExpression', function() {
testLocationInfoStorage(idNode); 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('Hash', function(){ describe('Line Numbers', 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(){
var ast, body; var ast, body;
function testColumns(node, firstLine, lastLine, firstColumn, lastColumn){ function testColumns(node, firstLine, lastLine, firstColumn, lastColumn) {
equals(node.loc.start.line, firstLine); equals(node.loc.start.line, firstLine);
equals(node.loc.start.column, firstColumn); equals(node.loc.start.column, firstColumn);
equals(node.loc.end.line, lastLine); equals(node.loc.end.line, lastLine);
equals(node.loc.end.column, lastColumn); equals(node.loc.end.column, lastColumn);
} }
ast = Handlebars.parse("line 1 {{line1Token}}\n line 2 {{line2token}}\n line 3 {{#blockHelperOnLine3}}\nline 4{{line4token}}\n" + ast = Handlebars.parse(
"line5{{else}}\n{{line6Token}}\n{{/blockHelperOnLine3}}"); '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; body = ast.body;
it('gets ContentNode line numbers', function(){ it('gets ContentNode line numbers', function() {
var contentNode = body[0]; var contentNode = body[0];
testColumns(contentNode, 1, 1, 0, 7); testColumns(contentNode, 1, 1, 0, 7);
}); });
it('gets MustacheStatement line numbers', function(){ it('gets MustacheStatement line numbers', function() {
var mustacheNode = body[1]; var mustacheNode = body[1];
testColumns(mustacheNode, 1, 1, 7, 21); testColumns(mustacheNode, 1, 1, 7, 21);
}); });
it('gets line numbers correct when newlines appear', function(){ it('gets line numbers correct when newlines appear', function() {
testColumns(body[2], 1, 2, 21, 8); testColumns(body[2], 1, 2, 21, 8);
}); });
it('gets MustacheStatement line numbers correct across newlines', function(){ it('gets MustacheStatement line numbers correct across newlines', function() {
var secondMustacheStatement = body[3]; var secondMustacheStatement = body[3];
testColumns(secondMustacheStatement, 2, 2, 8, 22); testColumns(secondMustacheStatement, 2, 2, 8, 22);
}); });
it('gets the block helper information correct', function(){ it('gets the block helper information correct', function() {
var blockHelperNode = body[5]; var blockHelperNode = body[5];
testColumns(blockHelperNode, 3, 7, 8, 23); testColumns(blockHelperNode, 3, 7, 8, 23);
}); });
it('correctly records the line numbers the program of a block helper', function(){ it('correctly records the line numbers the program of a block helper', function() {
var blockHelperNode = body[5], var blockHelperNode = body[5],
program = blockHelperNode.program; 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(){ it('correctly records the line numbers of an inverse of a block helper', function() {
var blockHelperNode = body[5], var blockHelperNode = body[5],
inverse = blockHelperNode.inverse; 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);
}); });
}); });
describe('standalone flags', function(){ describe('standalone flags', function() {
describe('mustache', function() { describe('mustache', function() {
it('does not mark mustaches as standalone', function() { it('does not mark mustaches as standalone', function() {
var ast = Handlebars.parse(' {{comment}} '); var ast = Handlebars.parse(' {{comment}} ');
+167 -123
View File
@@ -1,38 +1,41 @@
/*global CompilerContext, Handlebars, beforeEach, shouldCompileTo, shouldThrow */
global.handlebarsEnv = null; global.handlebarsEnv = null;
beforeEach(function() { beforeEach(function() {
global.handlebarsEnv = Handlebars.create(); global.handlebarsEnv = Handlebars.create();
}); });
describe("basic context", function() { describe('basic context', function() {
it("most basic", function() { it('most basic', function() {
shouldCompileTo("{{foo}}", { foo: "foo" }, "foo"); shouldCompileTo('{{foo}}', { foo: 'foo' }, 'foo');
}); });
it("escaping", function() { it('escaping', function() {
shouldCompileTo("\\{{foo}}", { foo: "food" }, "{{foo}}"); shouldCompileTo('\\{{foo}}', { foo: 'food' }, '{{foo}}');
shouldCompileTo("content \\{{foo}}", { foo: "food" }, "content {{foo}}"); shouldCompileTo('content \\{{foo}}', { foo: 'food' }, 'content {{foo}}');
shouldCompileTo("\\\\{{foo}}", { foo: "food" }, "\\food"); shouldCompileTo('\\\\{{foo}}', { foo: 'food' }, '\\food');
shouldCompileTo("content \\\\{{foo}}", { foo: "food" }, "content \\food"); shouldCompileTo('content \\\\{{foo}}', { foo: 'food' }, 'content \\food');
shouldCompileTo("\\\\ {{foo}}", { foo: "food" }, "\\\\ food"); shouldCompileTo('\\\\ {{foo}}', { foo: 'food' }, '\\\\ food');
}); });
it("compiling with a basic context", function() { it('compiling with a basic context', function() {
shouldCompileTo("Goodbye\n{{cruel}}\n{{world}}!", {cruel: "cruel", world: "world"}, "Goodbye\ncruel\nworld!", shouldCompileTo('Goodbye\n{{cruel}}\n{{world}}!', {cruel: 'cruel', world: 'world'}, 'Goodbye\ncruel\nworld!',
"It works if all the required keys are provided"); 'It works if all the required keys are provided');
}); });
it("compiling with an undefined context", function() { it('compiling with a string context', function() {
shouldCompileTo("Goodbye\n{{cruel}}\n{{world.bar}}!", undefined, "Goodbye\n\n!"); shouldCompileTo('{{.}}{{length}}', 'bye', 'bye3');
shouldCompileTo("{{#unless foo}}Goodbye{{../test}}{{test2}}{{/unless}}", undefined, "Goodbye");
}); });
it("comments", function() { it('compiling with an undefined context', function() {
shouldCompileTo("{{! Goodbye}}Goodbye\n{{cruel}}\n{{world}}!", shouldCompileTo('Goodbye\n{{cruel}}\n{{world.bar}}!', undefined, 'Goodbye\n\n!');
{cruel: "cruel", world: "world"}, "Goodbye\ncruel\nworld!",
"comments are ignored"); 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(' {{~! comment ~}} blah', {}, 'blah');
shouldCompileTo(' {{~!-- long-comment --~}} blah', {}, 'blah'); shouldCompileTo(' {{~!-- long-comment --~}} blah', {}, 'blah');
@@ -42,192 +45,233 @@ describe("basic context", function() {
shouldCompileTo(' {{~!-- long-comment --}} blah', {}, ' blah'); shouldCompileTo(' {{~!-- long-comment --}} blah', {}, ' blah');
}); });
it("boolean", function() { it('boolean', function() {
var string = "{{#goodbye}}GOODBYE {{/goodbye}}cruel {{world}}!"; var string = '{{#goodbye}}GOODBYE {{/goodbye}}cruel {{world}}!';
shouldCompileTo(string, {goodbye: true, world: "world"}, "GOODBYE cruel world!", shouldCompileTo(string, {goodbye: true, world: 'world'}, 'GOODBYE cruel world!',
"booleans show the contents when true"); 'booleans show the contents when true');
shouldCompileTo(string, {goodbye: false, world: "world"}, "cruel world!", shouldCompileTo(string, {goodbye: false, world: 'world'}, 'cruel world!',
"booleans do not show the contents when false"); 'booleans do not show the contents when false');
}); });
it("zeros", function() { it('zeros', function() {
shouldCompileTo("num1: {{num1}}, num2: {{num2}}", {num1: 42, num2: 0}, shouldCompileTo('num1: {{num1}}, num2: {{num2}}', {num1: 42, num2: 0},
"num1: 42, num2: 0"); 'num1: 42, num2: 0');
shouldCompileTo("num: {{.}}", 0, "num: 0"); shouldCompileTo('num: {{.}}', 0, 'num: 0');
shouldCompileTo("num: {{num1/num2}}", {num1: {num2: 0}}, "num: 0"); shouldCompileTo('num: {{num1/num2}}', {num1: {num2: 0}}, 'num: 0');
}); });
it('false', function() { it('false', function() {
/* eslint-disable no-new-wrappers */
shouldCompileTo('val1: {{val1}}, val2: {{val2}}', {val1: false, val2: new Boolean(false)}, 'val1: false, val2: false'); shouldCompileTo('val1: {{val1}}, val2: {{val2}}', {val1: false, val2: new Boolean(false)}, 'val1: false, val2: false');
shouldCompileTo('val: {{.}}', false, 'val: false'); shouldCompileTo('val: {{.}}', false, 'val: false');
shouldCompileTo('val: {{val1/val2}}', {val1: {val2: 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('val1: {{{val1}}}, val2: {{{val2}}}', {val1: false, val2: new Boolean(false)}, 'val1: false, val2: false');
shouldCompileTo('val: {{{val1/val2}}}', {val1: {val2: false}}, 'val: 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\nTest", {}, "Alan's\nTest");
shouldCompileTo("Alan's\rTest", {}, "Alan's\rTest"); 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'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\\', {}, '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', {}, '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('Awesome {{foo}}', {foo: '\\'}, 'Awesome \\', "text is escaped so that it doesn't mess up backslashes");
shouldCompileTo(' " " ', {}, ' " " ', "double quotes never produce invalid javascript"); shouldCompileTo(" ' ' ", {}, " ' ' ", 'double quotes never produce invalid javascript');
}); });
it("escaping expressions", function() { it('escaping expressions', function() {
shouldCompileTo("{{{awesome}}}", {awesome: "&\"\\<>"}, '&\"\\<>', shouldCompileTo('{{{awesome}}}', {awesome: '&\'\\<>'}, '&\'\\<>',
"expressions with 3 handlebars aren't escaped"); "expressions with 3 handlebars aren't escaped");
shouldCompileTo("{{&awesome}}", {awesome: "&\"\\<>"}, '&\"\\<>', shouldCompileTo('{{&awesome}}', {awesome: '&\'\\<>'}, '&\'\\<>',
"expressions with {{& handlebars aren't escaped"); "expressions with {{& handlebars aren't escaped");
shouldCompileTo("{{awesome}}", {awesome: "&\"'`\\<>"}, '&amp;&quot;&#x27;&#x60;\\&lt;&gt;', shouldCompileTo('{{awesome}}', {awesome: "&\"'`\\<>"}, '&amp;&quot;&#x27;&#x60;\\&lt;&gt;',
"by default expressions should be escaped"); '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;', 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"); 'escaping should properly handle amperstands');
}); });
it("functions returning safestrings shouldn't be escaped", function() { it("functions returning safestrings shouldn't be escaped", function() {
var hash = {awesome: function() { return new Handlebars.SafeString("&\"\\<>"); }}; var hash = {awesome: function() { return new Handlebars.SafeString('&\'\\<>'); }};
shouldCompileTo("{{awesome}}", hash, '&\"\\<>', shouldCompileTo('{{awesome}}', hash, '&\'\\<>',
"functions returning safestrings aren't escaped"); "functions returning safestrings aren't escaped");
}); });
it("functions", function() { it('functions', function() {
shouldCompileTo("{{awesome}}", {awesome: function() { return "Awesome"; }}, "Awesome", shouldCompileTo('{{awesome}}', {awesome: function() { return 'Awesome'; }}, 'Awesome',
"functions are called and render their output"); 'functions are called and render their output');
shouldCompileTo("{{awesome}}", {awesome: function() { return this.more; }, more: "More awesome"}, "More awesome", shouldCompileTo('{{awesome}}', {awesome: function() { return this.more; }, more: 'More awesome'}, 'More awesome',
"functions are bound to the context"); 'functions are bound to the context');
}); });
it("functions with context argument", function() { it('functions with context argument', function() {
shouldCompileTo("{{awesome frank}}", shouldCompileTo('{{awesome frank}}',
{awesome: function(context) { return context; }, {awesome: function(context) { return context; },
frank: "Frank"}, frank: 'Frank'},
"Frank", "functions are called with context arguments"); 'Frank', 'functions are called with context arguments');
}); });
it("pathed functions with context argument", function() { it('pathed functions with context argument', function() {
shouldCompileTo("{{bar.awesome frank}}", shouldCompileTo('{{bar.awesome frank}}',
{bar: {awesome: function(context) { return context; }}, {bar: {awesome: function(context) { return context; }},
frank: "Frank"}, frank: 'Frank'},
"Frank", "functions are called with context arguments"); 'Frank', 'functions are called with context arguments');
}); });
it("depthed functions with context argument", function() { it('depthed functions with context argument', function() {
shouldCompileTo("{{#with frank}}{{../awesome .}}{{/with}}", shouldCompileTo('{{#with frank}}{{../awesome .}}{{/with}}',
{awesome: function(context) { return context; }, {awesome: function(context) { return context; },
frank: "Frank"}, frank: 'Frank'},
"Frank", "functions are called with context arguments"); 'Frank', 'functions are called with context arguments');
}); });
it("block functions with context argument", function() { it('block functions with context argument', function() {
shouldCompileTo("{{#awesome 1}}inner {{.}}{{/awesome}}", shouldCompileTo('{{#awesome 1}}inner {{.}}{{/awesome}}',
{awesome: function(context, options) { return options.fn(context); }}, {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() { it('depthed block functions with context argument', function() {
shouldCompileTo("{{#with value}}{{#../awesome 1}}inner {{.}}{{/../awesome}}{{/with}}", shouldCompileTo('{{#with value}}{{#../awesome 1}}inner {{.}}{{/../awesome}}{{/with}}',
{value: true, awesome: function(context, options) { return options.fn(context); }}, {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() { it('block functions without context argument', function() {
shouldCompileTo("{{#awesome}}inner{{/awesome}}", shouldCompileTo('{{#awesome}}inner{{/awesome}}',
{awesome: function(options) { return options.fn(this); }}, {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() { it('pathed block functions without context argument', function() {
shouldCompileTo("{{#foo.awesome}}inner{{/foo.awesome}}", shouldCompileTo('{{#foo.awesome}}inner{{/foo.awesome}}',
{foo: {awesome: function() { return this; }}}, {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() { it('depthed block functions without context argument', function() {
shouldCompileTo("{{#with value}}{{#../awesome}}inner{{/../awesome}}{{/with}}", shouldCompileTo('{{#with value}}{{#../awesome}}inner{{/../awesome}}{{/with}}',
{value: true, awesome: function() { return this; }}, {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() { it('paths with hyphens', function() {
shouldCompileTo("{{foo-bar}}", {"foo-bar": "baz"}, "baz", "Paths can contain hyphens (-)"); 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 (-)');
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() { it('nested paths', function() {
shouldCompileTo("Goodbye {{alan/expression}} world!", {alan: {expression: "beautiful"}}, shouldCompileTo('Goodbye {{alan/expression}} world!', {alan: {expression: 'beautiful'}},
"Goodbye beautiful world!", "Nested paths access nested objects"); 'Goodbye beautiful world!', 'Nested paths access nested objects');
}); });
it("nested paths with empty string value", function() { it('nested paths with empty string value', function() {
shouldCompileTo("Goodbye {{alan/expression}} world!", {alan: {expression: ""}}, shouldCompileTo('Goodbye {{alan/expression}} world!', {alan: {expression: ''}},
"Goodbye world!", "Nested paths access nested objects with empty string"); 'Goodbye world!', 'Nested paths access nested objects with empty string');
}); });
it("literal paths", function() { it('literal paths', function() {
shouldCompileTo("Goodbye {{[@alan]/expression}} world!", {"@alan": {expression: "beautiful"}}, shouldCompileTo('Goodbye {{[@alan]/expression}} world!', {'@alan': {expression: 'beautiful'}},
"Goodbye beautiful world!", "Literal paths can be used"); 'Goodbye beautiful world!', 'Literal paths can be used');
shouldCompileTo("Goodbye {{[foo bar]/expression}} world!", {"foo bar": {expression: "beautiful"}}, shouldCompileTo('Goodbye {{[foo bar]/expression}} world!', {'foo bar': {expression: 'beautiful'}},
"Goodbye beautiful world!", "Literal paths can be used"); 'Goodbye beautiful world!', 'Literal paths can be used');
}); });
it('literal references', function() { it('literal references', function() {
shouldCompileTo("Goodbye {{[foo bar]}} world!", {"foo bar": "beautiful"}, shouldCompileTo('Goodbye {{[foo bar]}} world!', {'foo bar': 'beautiful'}, 'Goodbye beautiful world!');
"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!');
}); });
it("that current context path ({{.}}) doesn't hit helpers", function() { 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() { it('complex but empty paths', function() {
shouldCompileTo("{{person/name}}", {person: {name: null}}, ""); shouldCompileTo('{{person/name}}', {person: {name: null}}, '');
shouldCompileTo("{{person/name}}", {person: {}}, ""); shouldCompileTo('{{person/name}}', {person: {}}, '');
}); });
it("this keyword in paths", function() { it('this keyword in paths', function() {
var string = "{{#goodbyes}}{{this}}{{/goodbyes}}"; var string = '{{#goodbyes}}{{this}}{{/goodbyes}}';
var hash = {goodbyes: ["goodbye", "Goodbye", "GOODBYE"]}; var hash = {goodbyes: ['goodbye', 'Goodbye', 'GOODBYE']};
shouldCompileTo(string, hash, "goodbyeGoodbyeGOODBYE", shouldCompileTo(string, hash, 'goodbyeGoodbyeGOODBYE',
"This keyword in paths evaluates to current context"); 'This keyword in paths evaluates to current context');
string = "{{#hellos}}{{this/text}}{{/hellos}}"; string = '{{#hellos}}{{this/text}}{{/hellos}}';
hash = {hellos: [{text: "hello"}, {text: "Hello"}, {text: "HELLO"}]}; hash = {hellos: [{text: 'hello'}, {text: 'Hello'}, {text: 'HELLO'}]};
shouldCompileTo(string, hash, "helloHelloHELLO", "This keyword evaluates in more complex paths"); shouldCompileTo(string, hash, 'helloHelloHELLO', 'This keyword evaluates in more complex paths');
}); });
it("this keyword nested inside path", function() { it('this keyword nested inside path', function() {
var string = "{{#hellos}}{{text/this/foo}}{{/hellos}}";
shouldThrow(function() { shouldThrow(function() {
CompilerContext.compile(string); CompilerContext.compile('{{#hellos}}{{text/this/foo}}{{/hellos}}');
}, Error); }, 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) { var helpers = {foo: function(value) {
return 'bar ' + value; return 'bar ' + value;
}}; }};
var string = "{{#goodbyes}}{{foo this}}{{/goodbyes}}"; var string = '{{#goodbyes}}{{foo this}}{{/goodbyes}}';
var hash = {goodbyes: ["goodbye", "Goodbye", "GOODBYE"]}; var hash = {goodbyes: ['goodbye', 'Goodbye', 'GOODBYE']};
shouldCompileTo(string, [hash, helpers], "bar goodbyebar Goodbyebar GOODBYE", shouldCompileTo(string, [hash, helpers], 'bar goodbyebar Goodbyebar GOODBYE',
"This keyword in paths evaluates to current context"); 'This keyword in paths evaluates to current context');
string = "{{#hellos}}{{foo this/text}}{{/hellos}}"; string = '{{#hellos}}{{foo this/text}}{{/hellos}}';
hash = {hellos: [{text: "hello"}, {text: "Hello"}, {text: "HELLO"}]}; hash = {hellos: [{text: 'hello'}, {text: 'Hello'}, {text: 'HELLO'}]};
shouldCompileTo(string, [hash, helpers], "bar hellobar Hellobar HELLO", "This keyword evaluates in more complex paths"); shouldCompileTo(string, [hash, helpers], 'bar hellobar Hellobar HELLO', 'This keyword evaluates in more complex paths');
}); });
it("this keyword nested inside helpers param", function() { it('this keyword nested inside helpers param', function() {
var string = "{{#hellos}}{{foo text/this/foo}}{{/hellos}}"; var string = '{{#hellos}}{{foo text/this/foo}}{{/hellos}}';
shouldThrow(function() { shouldThrow(function() {
CompilerContext.compile(string); 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() { it('pass string literals', function() {
+265 -71
View File
@@ -1,112 +1,118 @@
/*global CompilerContext, shouldCompileTo, shouldThrow */
describe('blocks', function() { describe('blocks', function() {
it("array", function() { it('array', function() {
var string = "{{#goodbyes}}{{text}}! {{/goodbyes}}cruel {{world}}!"; var string = '{{#goodbyes}}{{text}}! {{/goodbyes}}cruel {{world}}!';
var hash = {goodbyes: [{text: "goodbye"}, {text: "Goodbye"}, {text: "GOODBYE"}], world: "world"}; var hash = {goodbyes: [{text: 'goodbye'}, {text: 'Goodbye'}, {text: 'GOODBYE'}], world: 'world'};
shouldCompileTo(string, hash, "goodbye! Goodbye! GOODBYE! cruel world!", shouldCompileTo(string, hash, 'goodbye! Goodbye! GOODBYE! cruel world!',
"Arrays iterate over the contents when not empty"); 'Arrays iterate over the contents when not empty');
shouldCompileTo(string, {goodbyes: [], world: "world"}, "cruel world!", shouldCompileTo(string, {goodbyes: [], world: 'world'}, 'cruel world!',
"Arrays ignore the contents when empty"); 'Arrays ignore the contents when empty');
}); });
it('array without data', function() { it('array without data', function() {
var string = '{{#goodbyes}}{{text}}{{/goodbyes}} {{#goodbyes}}{{text}}{{/goodbyes}}'; var string = '{{#goodbyes}}{{text}}{{/goodbyes}} {{#goodbyes}}{{text}}{{/goodbyes}}';
var hash = {goodbyes: [{text: 'goodbye'}, {text: 'Goodbye'}, {text: 'GOODBYE'}], world: 'world'}; 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() { it('array with @index', function() {
var string = "{{#goodbyes}}{{@index}}. {{text}}! {{/goodbyes}}cruel {{world}}!"; var string = '{{#goodbyes}}{{@index}}. {{text}}! {{/goodbyes}}cruel {{world}}!';
var hash = {goodbyes: [{text: "goodbye"}, {text: "Goodbye"}, {text: "GOODBYE"}], world: "world"}; var hash = {goodbyes: [{text: 'goodbye'}, {text: 'Goodbye'}, {text: 'GOODBYE'}], world: 'world'};
var template = CompilerContext.compile(string); var template = CompilerContext.compile(string);
var result = template(hash); 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() { it('empty block', function() {
var string = "{{#goodbyes}}{{/goodbyes}}cruel {{world}}!"; var string = '{{#goodbyes}}{{/goodbyes}}cruel {{world}}!';
var hash = {goodbyes: [{text: "goodbye"}, {text: "Goodbye"}, {text: "GOODBYE"}], world: "world"}; var hash = {goodbyes: [{text: 'goodbye'}, {text: 'Goodbye'}, {text: 'GOODBYE'}], world: 'world'};
shouldCompileTo(string, hash, "cruel world!", shouldCompileTo(string, hash, 'cruel world!',
"Arrays iterate over the contents when not empty"); 'Arrays iterate over the contents when not empty');
shouldCompileTo(string, {goodbyes: [], world: "world"}, "cruel world!", shouldCompileTo(string, {goodbyes: [], world: 'world'}, 'cruel world!',
"Arrays ignore the contents when empty"); 'Arrays ignore the contents when empty');
}); });
it("block with complex lookup", function() { it('block with complex lookup', function() {
var string = "{{#goodbyes}}{{text}} cruel {{../name}}! {{/goodbyes}}"; var string = '{{#goodbyes}}{{text}} cruel {{../name}}! {{/goodbyes}}';
var hash = {name: "Alan", goodbyes: [{text: "goodbye"}, {text: "Goodbye"}, {text: "GOODBYE"}]}; var hash = {name: 'Alan', goodbyes: [{text: 'goodbye'}, {text: 'Goodbye'}, {text: 'GOODBYE'}]};
shouldCompileTo(string, hash, "goodbye cruel Alan! Goodbye cruel Alan! GOODBYE cruel Alan! ", 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"); 'Templates can access variables in contexts up the stack with relative path syntax');
}); });
it('multiple blocks with complex lookup', function() { it('multiple blocks with complex lookup', function() {
var string = '{{#goodbyes}}{{../name}}{{../name}}{{/goodbyes}}'; var string = '{{#goodbyes}}{{../name}}{{../name}}{{/goodbyes}}';
var hash = {name: 'Alan', goodbyes: [{text: 'goodbye'}, {text: 'Goodbye'}, {text: 'GOODBYE'}]}; var hash = {name: 'Alan', goodbyes: [{text: 'goodbye'}, {text: 'Goodbye'}, {text: 'GOODBYE'}]};
shouldCompileTo(string, hash, 'AlanAlanAlanAlanAlanAlan'); shouldCompileTo(string, hash, 'AlanAlanAlanAlanAlanAlan');
}); });
it("block with complex lookup using nested context", function() { it('block with complex lookup using nested context', function() {
var string = "{{#goodbyes}}{{text}} cruel {{foo/../name}}! {{/goodbyes}}"; var string = '{{#goodbyes}}{{text}} cruel {{foo/../name}}! {{/goodbyes}}';
shouldThrow(function() { shouldThrow(function() {
CompilerContext.compile(string); CompilerContext.compile(string);
}, Error); }, Error);
}); });
it("block with deep nested complex lookup", function() { it('block with deep nested complex lookup', function() {
var string = "{{#outer}}Goodbye {{#inner}}cruel {{../sibling}} {{../../omg}}{{/inner}}{{/outer}}"; var string = '{{#outer}}Goodbye {{#inner}}cruel {{../sibling}} {{../../omg}}{{/inner}}{{/outer}}';
var hash = {omg: "OMG!", outer: [{ sibling: 'sad', inner: [{ text: "goodbye" }] }] }; 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() { describe('inverted sections', function() {
it("inverted sections with unset value", function() { it('inverted sections with unset value', function() {
var string = "{{#goodbyes}}{{this}}{{/goodbyes}}{{^goodbyes}}Right On!{{/goodbyes}}"; var string = '{{#goodbyes}}{{this}}{{/goodbyes}}{{^goodbyes}}Right On!{{/goodbyes}}';
var hash = {}; 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() { it('inverted section with false value', function() {
var string = "{{#goodbyes}}{{this}}{{/goodbyes}}{{^goodbyes}}Right On!{{/goodbyes}}"; var string = '{{#goodbyes}}{{this}}{{/goodbyes}}{{^goodbyes}}Right On!{{/goodbyes}}';
var hash = {goodbyes: false}; 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() { it('inverted section with empty set', function() {
var string = "{{#goodbyes}}{{this}}{{/goodbyes}}{{^goodbyes}}Right On!{{/goodbyes}}"; var string = '{{#goodbyes}}{{this}}{{/goodbyes}}{{^goodbyes}}Right On!{{/goodbyes}}';
var hash = {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() { it('block inverted sections', function() {
shouldCompileTo("{{#people}}{{name}}{{^}}{{none}}{{/people}}", {none: "No people"}, shouldCompileTo('{{#people}}{{name}}{{^}}{{none}}{{/people}}', {none: 'No people'},
"No people"); 'No people');
}); });
it("chained inverted sections", function() { it('chained inverted sections', function() {
shouldCompileTo("{{#people}}{{name}}{{else if none}}{{none}}{{/people}}", {none: "No people"}, shouldCompileTo('{{#people}}{{name}}{{else if none}}{{none}}{{/people}}', {none: 'No people'},
"No people"); 'No people');
shouldCompileTo("{{#people}}{{name}}{{else if nothere}}fail{{else unless nothere}}{{none}}{{/people}}", {none: "No people"}, shouldCompileTo('{{#people}}{{name}}{{else if nothere}}fail{{else unless nothere}}{{none}}{{/people}}', {none: 'No people'},
"No people"); 'No people');
shouldCompileTo("{{#people}}{{name}}{{else if none}}{{none}}{{else}}fail{{/people}}", {none: "No people"}, shouldCompileTo('{{#people}}{{name}}{{else if none}}{{none}}{{else}}fail{{/people}}', {none: 'No people'},
"No people"); 'No people');
}); });
it("chained inverted sections with mismatch", function() { it('chained inverted sections with mismatch', function() {
shouldThrow(function() { shouldThrow(function() {
shouldCompileTo("{{#people}}{{name}}{{else if none}}{{none}}{{/if}}", {none: "No people"}, shouldCompileTo('{{#people}}{{name}}{{else if none}}{{none}}{{/if}}', {none: 'No people'},
"No people"); 'No people');
}, Error); }, Error);
}); });
it("block inverted sections with empty arrays", function() { it('block inverted sections with empty arrays', function() {
shouldCompileTo("{{#people}}{{name}}{{^}}{{none}}{{/people}}", {none: "No people", people: []}, shouldCompileTo('{{#people}}{{name}}{{^}}{{none}}{{/people}}', {none: 'No people', people: []},
"No people"); 'No people');
}); });
}); });
@@ -119,6 +125,16 @@ describe('blocks', function() {
shouldCompileTo('{{#people}}\n{{name}}\n{{^}}\n{{none}}\n{{/people}}\n', {none: 'No people'}, shouldCompileTo('{{#people}}\n{{name}}\n{{^}}\n{{none}}\n{{/people}}\n', {none: 'No people'},
'No people\n'); '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() { it('block standalone chained else sections', function() {
shouldCompileTo('{{#people}}\n{{name}}\n{{else if none}}\n{{none}}\n{{/people}}\n', {none: 'No people'}, shouldCompileTo('{{#people}}\n{{name}}\n{{else if none}}\n{{none}}\n{{/people}}\n', {none: 'No people'},
'No people\n'); 'No people\n');
@@ -131,23 +147,201 @@ describe('blocks', function() {
}); });
describe('compat mode', function() { describe('compat mode', function() {
it("block with deep recursive lookup lookup", function() { it('block with deep recursive lookup lookup', function() {
var string = "{{#outer}}Goodbye {{#inner}}cruel {{omg}}{{/inner}}{{/outer}}"; var string = '{{#outer}}Goodbye {{#inner}}cruel {{omg}}{{/inner}}{{/outer}}';
var hash = {omg: "OMG!", outer: [{ inner: [{ text: "goodbye" }] }] }; 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() { it('block with deep recursive pathed lookup', function() {
var string = "{{#outer}}Goodbye {{#inner}}cruel {{omg.yes}}{{/inner}}{{/outer}}"; var string = '{{#outer}}Goodbye {{#inner}}cruel {{omg.yes}}{{/inner}}{{/outer}}';
var hash = {omg: {yes: "OMG!"}, outer: [{ inner: [{ yes: 'no', text: "goodbye" }] }] }; 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() { it('block with missed recursive lookup', function() {
var string = "{{#outer}}Goodbye {{#inner}}cruel {{omg.yes}}{{/inner}}{{/outer}}"; var string = '{{#outer}}Goodbye {{#inner}}cruel {{omg.yes}}{{/inner}}{{/outer}}';
var hash = {omg: {no: "OMG!"}, outer: [{ inner: [{ yes: 'no', text: "goodbye" }] }] }; 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');
});
}); });
}); });
}); });
+235 -143
View File
@@ -1,52 +1,66 @@
/*global CompilerContext, shouldCompileTo, shouldThrow, compileWithPartials, handlebarsEnv */
describe('builtin helpers', function() { describe('builtin helpers', function() {
describe('#if', function() { describe('#if', function() {
it("if", function() { it('if', function() {
var string = "{{#if goodbye}}GOODBYE {{/if}}cruel {{world}}!"; var string = '{{#if goodbye}}GOODBYE {{/if}}cruel {{world}}!';
shouldCompileTo(string, {goodbye: true, world: "world"}, "GOODBYE cruel world!", shouldCompileTo(string, {goodbye: true, world: 'world'}, 'GOODBYE cruel world!',
"if with boolean argument shows the contents when true"); 'if with boolean argument shows the contents when true');
shouldCompileTo(string, {goodbye: "dummy", world: "world"}, "GOODBYE cruel world!", shouldCompileTo(string, {goodbye: 'dummy', world: 'world'}, 'GOODBYE cruel world!',
"if with string argument shows the contents"); 'if with string argument shows the contents');
shouldCompileTo(string, {goodbye: false, world: "world"}, "cruel world!", shouldCompileTo(string, {goodbye: false, world: 'world'}, 'cruel world!',
"if with boolean argument does not show the contents when false"); 'if with boolean argument does not show the contents when false');
shouldCompileTo(string, {world: "world"}, "cruel world!", shouldCompileTo(string, {world: 'world'}, 'cruel world!',
"if with undefined does not show the contents"); 'if with undefined does not show the contents');
shouldCompileTo(string, {goodbye: ['foo'], world: "world"}, "GOODBYE cruel world!", shouldCompileTo(string, {goodbye: ['foo'], world: 'world'}, 'GOODBYE cruel world!',
"if with non-empty array shows the contents"); 'if with non-empty array shows the contents');
shouldCompileTo(string, {goodbye: [], world: "world"}, "cruel world!", shouldCompileTo(string, {goodbye: [], world: 'world'}, 'cruel world!',
"if with empty array does not show the contents"); 'if with empty array does not show the contents');
shouldCompileTo(string, {goodbye: 0, world: "world"}, "cruel world!", shouldCompileTo(string, {goodbye: 0, world: 'world'}, 'cruel world!',
"if with zero does not show the contents"); 'if with zero does not show the contents');
shouldCompileTo("{{#if goodbye includeZero=true}}GOODBYE {{/if}}cruel {{world}}!", shouldCompileTo('{{#if goodbye includeZero=true}}GOODBYE {{/if}}cruel {{world}}!',
{goodbye: 0, world: "world"}, "GOODBYE cruel world!", {goodbye: 0, world: 'world'}, 'GOODBYE cruel world!',
"if with zero does not show the contents"); 'if with zero does not show the contents');
}); });
it("if with function argument", function() { it('if with function argument', function() {
var string = "{{#if goodbye}}GOODBYE {{/if}}cruel {{world}}!"; var string = '{{#if goodbye}}GOODBYE {{/if}}cruel {{world}}!';
shouldCompileTo(string, {goodbye: function() {return true;}, world: "world"}, "GOODBYE cruel world!", shouldCompileTo(string, {goodbye: function() {return true; }, world: 'world'}, 'GOODBYE cruel world!',
"if with function shows the contents when function returns true"); 'if with function shows the contents when function returns true');
shouldCompileTo(string, {goodbye: function() {return this.world;}, world: "world"}, "GOODBYE cruel world!", shouldCompileTo(string, {goodbye: function() {return this.world; }, world: 'world'}, 'GOODBYE cruel world!',
"if with function shows the contents when function returns string"); 'if with function shows the contents when function returns string');
shouldCompileTo(string, {goodbye: function() {return false;}, world: "world"}, "cruel world!", shouldCompileTo(string, {goodbye: function() {return false; }, world: 'world'}, 'cruel world!',
"if with function does not show the contents when returns false"); 'if with function does not show the contents when returns false');
shouldCompileTo(string, {goodbye: function() {return this.foo;}, world: "world"}, "cruel world!", shouldCompileTo(string, {goodbye: function() {return this.foo; }, world: 'world'}, 'cruel world!',
"if with function does not show the contents when returns undefined"); '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() { describe('#with', function() {
it("with", function() { it('with', function() {
var string = "{{#with person}}{{first}} {{last}}{{/with}}"; var string = '{{#with person}}{{first}} {{last}}{{/with}}';
shouldCompileTo(string, {person: {first: "Alan", last: "Johnson"}}, "Alan Johnson"); shouldCompileTo(string, {person: {first: 'Alan', last: 'Johnson'}}, 'Alan Johnson');
}); });
it("with with function argument", function() { it('with with function argument', function() {
var string = "{{#with person}}{{first}} {{last}}{{/with}}"; var string = '{{#with person}}{{first}} {{last}}{{/with}}';
shouldCompileTo(string, {person: function() { return {first: "Alan", last: "Johnson"};}}, "Alan Johnson"); shouldCompileTo(string, {person: function() { return {first: 'Alan', last: 'Johnson'}; }}, 'Alan Johnson');
}); });
it("with with else", function() { it('with with else', function() {
var string = "{{#with person}}Person is present{{else}}Person is not present{{/with}}"; var string = '{{#with person}}Person is present{{else}}Person is not present{{/with}}';
shouldCompileTo(string, {}, "Person is not present"); 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,73 +71,73 @@ describe('builtin helpers', function() {
}); });
}); });
it("each", function() { it('each', function() {
var string = "{{#each goodbyes}}{{text}}! {{/each}}cruel {{world}}!"; var string = '{{#each goodbyes}}{{text}}! {{/each}}cruel {{world}}!';
var hash = {goodbyes: [{text: "goodbye"}, {text: "Goodbye"}, {text: "GOODBYE"}], world: "world"}; var hash = {goodbyes: [{text: 'goodbye'}, {text: 'Goodbye'}, {text: 'GOODBYE'}], world: 'world'};
shouldCompileTo(string, hash, "goodbye! Goodbye! GOODBYE! cruel world!", shouldCompileTo(string, hash, 'goodbye! Goodbye! GOODBYE! cruel world!',
"each with array argument iterates over the contents when not empty"); 'each with array argument iterates over the contents when not empty');
shouldCompileTo(string, {goodbyes: [], world: "world"}, "cruel world!", shouldCompileTo(string, {goodbyes: [], world: 'world'}, 'cruel world!',
"each with array argument ignores the contents when empty"); 'each with array argument ignores the contents when empty');
}); });
it('each without data', function() { it('each without data', function() {
var string = '{{#each goodbyes}}{{text}}! {{/each}}cruel {{world}}!'; var string = '{{#each goodbyes}}{{text}}! {{/each}}cruel {{world}}!';
var hash = {goodbyes: [{text: 'goodbye'}, {text: 'Goodbye'}, {text: 'GOODBYE'}], world: 'world'}; var hash = {goodbyes: [{text: 'goodbye'}, {text: 'Goodbye'}, {text: 'GOODBYE'}], world: 'world'};
shouldCompileTo(string, [hash,,,,false], 'goodbye! Goodbye! GOODBYE! cruel world!'); shouldCompileTo(string, [hash,,,, false], 'goodbye! Goodbye! GOODBYE! cruel world!');
hash = {goodbyes: 'cruel', world: 'world'}; hash = {goodbyes: 'cruel', world: 'world'};
shouldCompileTo('{{#each .}}{{.}}{{/each}}', [hash,,,,false], 'cruelworld'); shouldCompileTo('{{#each .}}{{.}}{{/each}}', [hash,,,, false], 'cruelworld');
}); });
it('each without context', function() { it('each without context', function() {
var string = '{{#each goodbyes}}{{text}}! {{/each}}cruel {{world}}!'; var string = '{{#each goodbyes}}{{text}}! {{/each}}cruel {{world}}!';
shouldCompileTo(string, [,,,,], 'cruel !'); shouldCompileTo(string, [,,,, ], 'cruel !');
}); });
it("each with an object and @key", function() { it('each with an object and @key', function() {
var string = "{{#each goodbyes}}{{@key}}. {{text}}! {{/each}}cruel {{world}}!"; var string = '{{#each goodbyes}}{{@key}}. {{text}}! {{/each}}cruel {{world}}!';
function Clazz() { function Clazz() {
this['<b>#1</b>'] = {text: 'goodbye'}; this['<b>#1</b>'] = {text: 'goodbye'};
this[2] = {text: 'GOODBYE'}; this[2] = {text: 'GOODBYE'};
} }
Clazz.prototype.foo = 'fail'; Clazz.prototype.foo = 'fail';
var hash = {goodbyes: new Clazz(), world: 'world'}; var hash = {goodbyes: new Clazz(), world: 'world'};
// Object property iteration order is undefined according to ECMA spec, // Object property iteration order is undefined according to ECMA spec,
// so we need to check both possible orders // so we need to check both possible orders
// @see http://stackoverflow.com/questions/280713/elements-order-in-a-for-in-loop // @see http://stackoverflow.com/questions/280713/elements-order-in-a-for-in-loop
var actual = compileWithPartials(string, hash); var actual = compileWithPartials(string, hash);
var expected1 = "&lt;b&gt;#1&lt;/b&gt;. goodbye! 2. 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!"; 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!'); shouldCompileTo(string, {goodbyes: {}, world: 'world'}, 'cruel world!');
}); });
it("each with @index", function() { it('each with @index', function() {
var string = "{{#each goodbyes}}{{@index}}. {{text}}! {{/each}}cruel {{world}}!"; var string = '{{#each goodbyes}}{{@index}}. {{text}}! {{/each}}cruel {{world}}!';
var hash = {goodbyes: [{text: "goodbye"}, {text: "Goodbye"}, {text: "GOODBYE"}], world: "world"}; var hash = {goodbyes: [{text: 'goodbye'}, {text: 'Goodbye'}, {text: 'GOODBYE'}], world: 'world'};
var template = CompilerContext.compile(string); var template = CompilerContext.compile(string);
var result = template(hash); 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() { it('each with nested @index', function() {
var string = "{{#each goodbyes}}{{@index}}. {{text}}! {{#each ../goodbyes}}{{@index}} {{/each}}After {{@index}} {{/each}}{{@index}}cruel {{world}}!"; 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 hash = {goodbyes: [{text: 'goodbye'}, {text: 'Goodbye'}, {text: 'GOODBYE'}], world: 'world'};
var template = CompilerContext.compile(string); var template = CompilerContext.compile(string);
var result = template(hash); 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() { it('each with block params', function() {
var string = '{{#each goodbyes as |value index|}}{{index}}. {{value.text}}! {{#each ../goodbyes as |childValue childIndex|}} {{index}} {{childIndex}}{{/each}} After {{index}} {{/each}}{{index}}cruel {{world}}!'; var string = '{{#each goodbyes as |value index|}}{{index}}. {{value.text}}! {{#each ../goodbyes as |childValue childIndex|}} {{index}} {{childIndex}}{{/each}} After {{index}} {{/each}}{{index}}cruel {{world}}!';
var hash = {goodbyes: [{text: 'goodbye'}, {text: 'Goodbye'}], world: 'world'}; var hash = {goodbyes: [{text: 'goodbye'}, {text: 'Goodbye'}], world: 'world'};
var template = CompilerContext.compile(string); var template = CompilerContext.compile(string);
var result = template(hash); var result = template(hash);
@@ -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!'); 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() { it('each object with @index', function() {
var string = "{{#each goodbyes}}{{@index}}. {{text}}! {{/each}}cruel {{world}}!"; var string = '{{#each goodbyes}}{{@index}}. {{text}}! {{/each}}cruel {{world}}!';
var hash = {goodbyes: {'a': {text: "goodbye"}, b: {text: "Goodbye"}, c: {text: "GOODBYE"}}, world: "world"}; var hash = {goodbyes: {'a': {text: 'goodbye'}, b: {text: 'Goodbye'}, c: {text: 'GOODBYE'}}, world: 'world'};
var template = CompilerContext.compile(string); var template = CompilerContext.compile(string);
var result = template(hash); 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() { it('each with @first', function() {
var string = "{{#each goodbyes}}{{#if @first}}{{text}}! {{/if}}{{/each}}cruel {{world}}!"; var string = '{{#each goodbyes}}{{#if @first}}{{text}}! {{/if}}{{/each}}cruel {{world}}!';
var hash = {goodbyes: [{text: "goodbye"}, {text: "Goodbye"}, {text: "GOODBYE"}], world: "world"}; var hash = {goodbyes: [{text: 'goodbye'}, {text: 'Goodbye'}, {text: 'GOODBYE'}], world: 'world'};
var template = CompilerContext.compile(string); var template = CompilerContext.compile(string);
var result = template(hash); 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() { 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 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 hash = {goodbyes: [{text: 'goodbye'}, {text: 'Goodbye'}, {text: 'GOODBYE'}], world: 'world'};
var template = CompilerContext.compile(string); var template = CompilerContext.compile(string);
var result = template(hash); 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() { it('each object with @first', function() {
var string = "{{#each goodbyes}}{{#if @first}}{{text}}! {{/if}}{{/each}}cruel {{world}}!"; var string = '{{#each goodbyes}}{{#if @first}}{{text}}! {{/if}}{{/each}}cruel {{world}}!';
var hash = {goodbyes: {'foo': {text: "goodbye"}, bar: {text: "Goodbye"}}, world: "world"}; var hash = {goodbyes: {'foo': {text: 'goodbye'}, bar: {text: 'Goodbye'}}, world: 'world'};
var template = CompilerContext.compile(string); var template = CompilerContext.compile(string);
var result = template(hash); 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() { it('each with @last', function() {
var string = "{{#each goodbyes}}{{#if @last}}{{text}}! {{/if}}{{/each}}cruel {{world}}!"; var string = '{{#each goodbyes}}{{#if @last}}{{text}}! {{/if}}{{/each}}cruel {{world}}!';
var hash = {goodbyes: [{text: "goodbye"}, {text: "Goodbye"}, {text: "GOODBYE"}], world: "world"}; var hash = {goodbyes: [{text: 'goodbye'}, {text: 'Goodbye'}, {text: 'GOODBYE'}], world: 'world'};
var template = CompilerContext.compile(string); var template = CompilerContext.compile(string);
var result = template(hash); 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() { it('each object with @last', function() {
var string = "{{#each goodbyes}}{{#if @last}}{{text}}! {{/if}}{{/each}}cruel {{world}}!"; var string = '{{#each goodbyes}}{{#if @last}}{{text}}! {{/if}}{{/each}}cruel {{world}}!';
var hash = {goodbyes: {'foo': {text: "goodbye"}, bar: {text: "Goodbye"}}, world: "world"}; var hash = {goodbyes: {'foo': {text: 'goodbye'}, bar: {text: 'Goodbye'}}, world: 'world'};
var template = CompilerContext.compile(string); var template = CompilerContext.compile(string);
var result = template(hash); 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() { 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 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 hash = {goodbyes: [{text: 'goodbye'}, {text: 'Goodbye'}, {text: 'GOODBYE'}], world: 'world'};
var template = CompilerContext.compile(string); var template = CompilerContext.compile(string);
var result = template(hash); 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() { it('each with function argument', function() {
var string = "{{#each goodbyes}}{{text}}! {{/each}}cruel {{world}}!"; var string = '{{#each goodbyes}}{{text}}! {{/each}}cruel {{world}}!';
var hash = {goodbyes: function () { return [{text: "goodbye"}, {text: "Goodbye"}, {text: "GOODBYE"}];}, world: "world"}; var hash = {goodbyes: function() { return [{text: 'goodbye'}, {text: 'Goodbye'}, {text: 'GOODBYE'}]; }, world: 'world'};
shouldCompileTo(string, hash, "goodbye! Goodbye! GOODBYE! cruel world!", shouldCompileTo(string, hash, 'goodbye! Goodbye! GOODBYE! cruel world!',
"each with array function argument iterates over the contents when not empty"); 'each with array function argument iterates over the contents when not empty');
shouldCompileTo(string, {goodbyes: [], world: "world"}, "cruel world!", shouldCompileTo(string, {goodbyes: [], world: 'world'}, 'cruel world!',
"each with array function argument ignores the contents when empty"); 'each with array function argument ignores the contents when empty');
}); });
it("data passed to helpers", function() { it('each object when last key is an empty string', function() {
var string = "{{#each letters}}{{this}}{{detectDataInsideEach}}{{/each}}"; 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 hash = {letters: ['a', 'b', 'c']};
var template = CompilerContext.compile(string); var template = CompilerContext.compile(string);
@@ -224,106 +248,174 @@ describe('builtin helpers', function() {
equal(result, 'a!b!c!', 'should output data'); equal(result, 'a!b!c!', 'should output data');
}); });
it("each on implicit context", function() { it('each on implicit context', function() {
shouldThrow(function() { shouldThrow(function() {
var template = CompilerContext.compile("{{#each}}{{text}}! {{/each}}cruel world!"); var template = CompilerContext.compile('{{#each}}{{text}}! {{/each}}cruel world!');
template({}); template({});
}, handlebarsEnv.Exception, 'Must pass iterator to #each'); }, handlebarsEnv.Exception, 'Must pass iterator to #each');
}); });
}); });
describe("#log", function() { describe('#log', function() {
/* eslint-disable no-console */
if (typeof console === 'undefined') { if (typeof console === 'undefined') {
return; return;
} }
var log, var $log,
info, $info,
error; $error;
beforeEach(function() { beforeEach(function() {
log = console.log; $log = console.log;
info = console.info; $info = console.info;
error = console.error; $error = console.error;
}); });
afterEach(function() { afterEach(function() {
console.log = log; console.log = $log;
console.info = info; console.info = $info;
console.error = error; console.error = $error;
}); });
it('should call logger at default level', function() { it('should call logger at default level', function() {
var string = "{{log blah}}"; var string = '{{log blah}}';
var hash = { blah: "whee" }; var hash = { blah: 'whee' };
var levelArg, logArg; var levelArg, logArg;
handlebarsEnv.log = function(level, arg){ handlebarsEnv.log = function(level, arg) {
levelArg = level; levelArg = level;
logArg = arg; logArg = arg;
}; };
shouldCompileTo(string, hash, "", "log should not display"); shouldCompileTo(string, hash, '', 'log should not display');
equals(1, levelArg, "should call log with 1"); equals(1, levelArg, 'should call log with 1');
equals("whee", logArg, "should call log with 'whee'"); equals('whee', logArg, "should call log with 'whee'");
}); });
it('should call logger at data level', function() { it('should call logger at data level', function() {
var string = "{{log blah}}"; var string = '{{log blah}}';
var hash = { blah: "whee" }; var hash = { blah: 'whee' };
var levelArg, logArg; var levelArg, logArg;
handlebarsEnv.log = function(level, arg){ handlebarsEnv.log = function(level, arg) {
levelArg = level; levelArg = level;
logArg = arg; logArg = arg;
}; };
shouldCompileTo(string, [hash,,,,{level: '03'}], ""); shouldCompileTo(string, [hash,,,, {level: '03'}], '');
equals(3, levelArg); equals('03', levelArg);
equals("whee", logArg); equals('whee', logArg);
}); });
it('should output to info', function() { it('should output to info', function() {
var string = "{{log blah}}"; var string = '{{log blah}}';
var hash = { blah: "whee" }; var hash = { blah: 'whee' };
var called; var called;
console.info = function(log) { console.info = function(info) {
equals("whee", log); equals('whee', info);
called = true; called = true;
}; };
console.log = function(log) { console.log = function(log) {
equals("whee", log); equals('whee', log);
called = true; called = true;
}; };
shouldCompileTo(string, hash, ""); shouldCompileTo(string, hash, '');
equals(true, called); equals(true, called);
}); });
it('should log at data level', function() { it('should log at data level', function() {
var string = "{{log blah}}"; var string = '{{log blah}}';
var hash = { blah: "whee" }; var hash = { blah: 'whee' };
var called; var called;
console.error = function(log) { console.error = function(log) {
equals("whee", log); equals('whee', log);
called = true; called = true;
}; };
shouldCompileTo(string, [hash,,,,{level: '03'}], ""); shouldCompileTo(string, [hash,,,, {level: '03'}], '');
equals(true, called); equals(true, called);
}); });
it('should handle missing logger', function() { it('should handle missing logger', function() {
var string = "{{log blah}}"; var string = '{{log blah}}';
var hash = { blah: "whee" }; var hash = { blah: 'whee' },
called = false;
console.error = undefined; 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 */
}); });
describe('#lookup', function() { describe('#lookup', function() {
it('should lookup arbitrary content', function() { it('should lookup arbitrary content', function() {
var string = '{{#each goodbyes}}{{lookup ../data .}}{{/each}}', var string = '{{#each goodbyes}}{{lookup ../data .}}{{/each}}',
hash = {goodbyes: [0, 1], data: ['foo', 'bar']}; hash = {goodbyes: [0, 1], data: ['foo', 'bar']};
var template = CompilerContext.compile(string); var template = CompilerContext.compile(string);
var result = template(hash); var result = template(hash);
@@ -332,7 +424,7 @@ describe('builtin helpers', function() {
}); });
it('should not fail on undefined value', function() { it('should not fail on undefined value', function() {
var string = '{{#each goodbyes}}{{lookup ../bar .}}{{/each}}', var string = '{{#each goodbyes}}{{lookup ../bar .}}{{/each}}',
hash = {goodbyes: [0, 1], data: ['foo', 'bar']}; hash = {goodbyes: [0, 1], data: ['foo', 'bar']};
var template = CompilerContext.compile(string); var template = CompilerContext.compile(string);
var result = template(hash); var result = template(hash);
+36 -7
View File
@@ -1,5 +1,3 @@
/*global Handlebars, shouldThrow */
describe('compiler', function() { describe('compiler', function() {
if (!Handlebars.compile) { if (!Handlebars.compile) {
return; return;
@@ -40,11 +38,39 @@ describe('compiler', function() {
}, Error, 'You must pass a string or Handlebars AST to Handlebars.compile. You passed [object Object]'); }, Error, 'You must pass a string or Handlebars AST to Handlebars.compile. You passed [object Object]');
}); });
it('can utilize AST instance', function() { it('should include the location in the error (row and column)', function() {
equal(Handlebars.compile(new Handlebars.AST.Program([ new Handlebars.AST.ContentStatement("Hello")], null, {}))(), 'Hello'); 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('')(), ''); equal(Handlebars.compile('')(), '');
}); });
}); });
@@ -60,10 +86,13 @@ describe('compiler', function() {
}); });
it('can utilize AST instance', 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); equal(/return ""/.test(Handlebars.precompile('')), true);
}); });
}); });
+83 -80
View File
@@ -1,25 +1,24 @@
/*global CompilerContext, Handlebars, handlebarsEnv, shouldThrow */
describe('data', function() { describe('data', function() {
it("passing in data to a compiled function that expects data - works with helpers", function() { it('passing in data to a compiled function that expects data - works with helpers', function() {
var template = CompilerContext.compile("{{hello}}", {data: true}); var template = CompilerContext.compile('{{hello}}', {data: true});
var helpers = { var helpers = {
hello: function(options) { 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"}}); var result = template({noun: 'cat'}, {helpers: helpers, data: {adjective: 'happy'}});
equals("happy cat", result, "Data output by helper"); equals('happy cat', result, 'Data output by helper');
}); });
it("data can be looked up via @foo", function() { it('data can be looked up via @foo', function() {
var template = CompilerContext.compile("{{@hello}}"); var template = CompilerContext.compile('{{@hello}}');
var result = template({}, { data: { hello: "hello" } }); var result = template({}, { data: { hello: 'hello' } });
equals("hello", result, "@foo retrieves template data"); 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 template = CompilerContext.compile('{{#let world="world"}}{{#if foo}}{{#if foo}}Hello {{@world}}{{/if}}{{/if}}{{/let}}');
var helpers = Handlebars.createFrame(handlebarsEnv.helpers); var helpers = Handlebars.createFrame(handlebarsEnv.helpers);
@@ -28,65 +27,67 @@ describe('data', function() {
var frame = Handlebars.createFrame(options.data); var frame = Handlebars.createFrame(options.data);
for (var prop in options.hash) { for (var prop in options.hash) {
frame[prop] = options.hash[prop]; if (prop in options.hash) {
frame[prop] = options.hash[prop];
}
} }
return options.fn(this, { data: frame }); return options.fn(this, { data: frame });
}; };
var result = template({ foo: true }, { helpers: helpers }); 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() { it('parameter data can be looked up via @foo', function() {
var template = CompilerContext.compile("{{hello @world}}"); var template = CompilerContext.compile('{{hello @world}}');
var helpers = { var helpers = {
hello: function(noun) { hello: function(noun) {
return "Hello " + noun; return 'Hello ' + noun;
} }
}; };
var result = template({}, { helpers: helpers, data: { world: "world" } }); var result = template({}, { helpers: helpers, data: { world: 'world' } });
equals("Hello world", result, "@foo as a parameter retrieves template data"); equals('Hello world', result, '@foo as a parameter retrieves template data');
}); });
it("hash values can be looked up via @foo", function() { it('hash values can be looked up via @foo', function() {
var template = CompilerContext.compile("{{hello noun=@world}}"); var template = CompilerContext.compile('{{hello noun=@world}}');
var helpers = { var helpers = {
hello: function(options) { hello: function(options) {
return "Hello " + options.hash.noun; return 'Hello ' + options.hash.noun;
} }
}; };
var result = template({}, { helpers: helpers, data: { world: "world" } }); var result = template({}, { helpers: helpers, data: { world: 'world' } });
equals("Hello world", result, "@foo as a parameter retrieves template data"); equals('Hello world', result, '@foo as a parameter retrieves template data');
}); });
it("nested parameter data can be looked up via @foo.bar", function() { it('nested parameter data can be looked up via @foo.bar', function() {
var template = CompilerContext.compile("{{hello @world.bar}}"); var template = CompilerContext.compile('{{hello @world.bar}}');
var helpers = { var helpers = {
hello: function(noun) { hello: function(noun) {
return "Hello " + noun; return 'Hello ' + noun;
} }
}; };
var result = template({}, { helpers: helpers, data: { world: {bar: "world" } } }); var result = template({}, { helpers: helpers, data: { world: {bar: 'world' } } });
equals("Hello world", result, "@foo as a parameter retrieves template data"); equals('Hello world', result, '@foo as a parameter retrieves template data');
}); });
it("nested parameter data does not fail with @world.bar", function() { it('nested parameter data does not fail with @world.bar', function() {
var template = CompilerContext.compile("{{hello @world.bar}}"); var template = CompilerContext.compile('{{hello @world.bar}}');
var helpers = { var helpers = {
hello: function(noun) { hello: function(noun) {
return "Hello " + noun; return 'Hello ' + noun;
} }
}; };
var result = template({}, { helpers: helpers, data: { foo: {bar: "world" } } }); var result = template({}, { helpers: helpers, data: { foo: {bar: 'world' } } });
equals("Hello undefined", result, "@foo as a parameter retrieves template data"); equals('Hello undefined', result, '@foo as a parameter retrieves template data');
}); });
it("parameter data throws when using complex scope references", function() { it('parameter data throws when using complex scope references', function() {
var string = "{{#goodbyes}}{{text}} cruel {{@foo/../name}}! {{/goodbyes}}"; var string = '{{#goodbyes}}{{text}} cruel {{@foo/../name}}! {{/goodbyes}}';
shouldThrow(function() { shouldThrow(function() {
CompilerContext.compile(string); CompilerContext.compile(string);
@@ -104,131 +105,133 @@ describe('data', function() {
equals('hello', result); equals('hello', result);
}); });
it("data is inherited downstream", function() { 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 template = CompilerContext.compile('{{#let foo=1 bar=2}}{{#let foo=bar.baz}}{{@bar}}{{@foo}}{{/let}}{{@foo}}{{/let}}', { data: true });
var helpers = { var helpers = {
let: function(options) { let: function(options) {
var frame = Handlebars.createFrame(options.data); var frame = Handlebars.createFrame(options.data);
for (var prop in options.hash) { for (var prop in options.hash) {
frame[prop] = options.hash[prop]; if (prop in options.hash) {
frame[prop] = options.hash[prop];
}
} }
return options.fn(this, {data: frame}); return options.fn(this, {data: frame});
} }
}; };
var result = template({ bar: { baz: "hello world" } }, { helpers: helpers, data: {} }); var result = template({ bar: { baz: 'hello world' } }, { helpers: helpers, data: {} });
equals("2hello world1", result, "data variables are inherited downstream"); 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() { 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}); var template = CompilerContext.compile('{{>myPartial}}', {data: true});
var partials = { var partials = {
my_partial: CompilerContext.compile("{{hello}}", {data: true}) myPartial: CompilerContext.compile('{{hello}}', {data: true})
}; };
var helpers = { var helpers = {
hello: function(options) { 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"}}); var result = template({noun: 'cat'}, {helpers: helpers, partials: partials, data: {adjective: 'happy'}});
equals("happy cat", result, "Data output by helper inside partial"); 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() { 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 template = CompilerContext.compile('{{hello world}}', {data: true});
var helpers = { var helpers = {
hello: function(noun, options) { 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"}}); var result = template({exclaim: true, world: 'world'}, {helpers: helpers, data: {adjective: 'happy'}});
equals("happy world!", result, "Data output by helper"); equals('happy world!', result, 'Data output by helper');
}); });
it("passing in data to a compiled function that expects data - works with block helpers", function() { it('passing in data to a compiled function that expects data - works with block helpers', function() {
var template = CompilerContext.compile("{{#hello}}{{world}}{{/hello}}", {data: true}); var template = CompilerContext.compile('{{#hello}}{{world}}{{/hello}}', {data: true});
var helpers = { var helpers = {
hello: function(options) { hello: function(options) {
return options.fn(this); return options.fn(this);
}, },
world: function(options) { 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"}}); var result = template({exclaim: true}, {helpers: helpers, data: {adjective: 'happy'}});
equals("happy world!", result, "Data output by helper"); 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() { 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 template = CompilerContext.compile('{{#hello}}{{world ../zomg}}{{/hello}}', {data: true});
var helpers = { var helpers = {
hello: function(options) { hello: function(options) {
return options.fn({exclaim: "?"}); return options.fn({exclaim: '?'});
}, },
world: function(thing, options) { 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"}}); var result = template({exclaim: true, zomg: 'world'}, {helpers: helpers, data: {adjective: 'happy'}});
equals("happy world?", result, "Data output by helper"); 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() { 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 template = CompilerContext.compile('{{#hello}}{{world ../zomg}}{{/hello}}', {data: true});
var helpers = { var helpers = {
hello: function(options) { hello: function(options) {
return options.data.accessData + " " + options.fn({exclaim: "?"}); return options.data.accessData + ' ' + options.fn({exclaim: '?'});
}, },
world: function(thing, options) { 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"}}); var result = template({exclaim: true, zomg: 'world'}, {helpers: helpers, data: {adjective: 'happy', accessData: '#win'}});
equals("#win happy world?", result, "Data output by helper"); equals('#win happy world?', result, 'Data output by helper');
}); });
it("you can override inherited data when invoking a helper", function() { it('you can override inherited data when invoking a helper', function() {
var template = CompilerContext.compile("{{#hello}}{{world zomg}}{{/hello}}", {data: true}); var template = CompilerContext.compile('{{#hello}}{{world zomg}}{{/hello}}', {data: true});
var helpers = { var helpers = {
hello: function(options) { 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) { 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"}}); var result = template({exclaim: true, zomg: 'planet'}, {helpers: helpers, data: {adjective: 'happy'}});
equals("sad world?", result, "Overriden data output by helper"); equals('sad world?', result, 'Overriden data output by helper');
}); });
it("you can override inherited data when invoking a helper with depth", function() { it('you can override inherited data when invoking a helper with depth', function() {
var template = CompilerContext.compile("{{#hello}}{{world ../zomg}}{{/hello}}", {data: true}); var template = CompilerContext.compile('{{#hello}}{{world ../zomg}}{{/hello}}', {data: true});
var helpers = { var helpers = {
hello: function(options) { hello: function(options) {
return options.fn({exclaim: "?"}, { data: {adjective: "sad"} }); return options.fn({exclaim: '?'}, { data: {adjective: 'sad'} });
}, },
world: function(thing, options) { 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"}}); var result = template({exclaim: true, zomg: 'world'}, {helpers: helpers, data: {adjective: 'happy'}});
equals("sad world?", result, "Overriden data output by helper"); equals('sad world?', result, 'Overriden data output by helper');
}); });
describe('@root', function() { describe('@root', function() {
+9 -4
View File
@@ -1,12 +1,15 @@
/*global handlebarsEnv */
require('./common'); require('./common');
var _ = require('underscore'), var fs = require('fs'),
fs = require('fs'),
vm = require('vm'); vm = require('vm');
global.Handlebars = 'no-conflict'; 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 = { global.CompilerContext = {
browser: true, browser: true,
@@ -21,10 +24,12 @@ global.CompilerContext = {
}; };
function safeEval(templateSpec) { function safeEval(templateSpec) {
/* eslint-disable no-eval, no-console */
try { try {
return eval('(' + templateSpec + ')'); return eval('(' + templateSpec + ')');
} catch (err) { } catch (err) {
console.error(templateSpec); console.error(templateSpec);
throw err; 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) { global.shouldCompileTo = function(string, hashOrArray, expected, message) {
shouldCompileToWithPartials(string, hashOrArray, false, 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); var result = compileWithPartials(string, hashOrArray, partials);
if (result !== expected) { 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, var template,
ary, ary,
options; 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 = [];
ary.push(hashOrArray[0]); ary.push(hashOrArray[0]);
ary.push({ helpers: hashOrArray[1], partials: hashOrArray[2] }); 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) { 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 { try {
callback(); callback();
failed = true; failed = true;
} catch (err) { } catch (caught) {
if (type && !(err instanceof type)) { if (type && !(caught instanceof type)) {
throw new Error('Type failure: ' + err); throw new AssertError('Type failure: ' + caught);
} }
if (msg && !(msg.test ? msg.test(err.message) : msg === err.message)) { if (msg && !(msg.test ? msg.test(caught.message) : msg === caught.message)) {
equal(msg, err.message); throw new AssertError('Throw mismatch: Expected ' + caught.message + ' to match ' + msg + '\n\n' + caught.stack, shouldThrow);
} }
} }
if (failed) { 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'); require('./common');
global.Handlebars = require('../../lib'); global.Handlebars = require('../../lib');
@@ -14,10 +13,12 @@ global.CompilerContext = {
}; };
function safeEval(templateSpec) { function safeEval(templateSpec) {
/* eslint-disable no-eval, no-console */
try { try {
return eval('(' + templateSpec + ')'); return eval('(' + templateSpec + ')');
} catch (err) { } catch (err) {
console.error(templateSpec); console.error(templateSpec);
throw err; throw err;
} }
/* eslint-enable no-eval, no-console */
} }
+22 -5
View File
@@ -1,3 +1,4 @@
/* eslint-disable no-console */
var fs = require('fs'), var fs = require('fs'),
Mocha = require('mocha'), Mocha = require('mocha'),
path = require('path'); path = require('path');
@@ -6,19 +7,35 @@ var errors = 0,
testDir = path.dirname(__dirname), testDir = path.dirname(__dirname),
grep = process.argv[2]; 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) var files = fs.readdirSync(testDir)
.filter(function(name) { return (/.*\.js$/).test(name); }) .filter(function(name) { return (/.*\.js$/).test(name); })
.map(function(name) { return testDir + '/' + name; }); .map(function(name) { return testDir + '/' + name; });
run('./node', function() { if (global.minimizedTest) {
run('./browser', function() { run('./runtime', function() {
run('./runtime', function() { run('./browser', function() {
/* eslint-disable no-process-exit */
process.exit(errors); 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) { function run(env, callback) {
+14 -9
View File
@@ -1,24 +1,27 @@
/*global handlebarsEnv */
require('./common'); require('./common');
var _ = require('underscore'), var fs = require('fs'),
fs = require('fs'),
vm = require('vm'); vm = require('vm');
global.Handlebars = 'no-conflict'; 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 parse = require('../../dist/cjs/handlebars/compiler/base').parse;
var compiler = require('../../dist/cjs/handlebars/compiler/compiler'); 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 = { global.CompilerContext = {
browser: true, browser: true,
compile: function(template, options) { compile: function(template, options) {
// Hack the compiler on to the environment for these specific tests // Hack the compiler on to the environment for these specific tests
handlebarsEnv.precompile = function(template, options) { handlebarsEnv.precompile = function(precompileTemplate, precompileOptions) {
return compiler.precompile(template, options, handlebarsEnv); return compiler.precompile(precompileTemplate, precompileOptions, handlebarsEnv);
}; };
handlebarsEnv.parse = parse; handlebarsEnv.parse = parse;
handlebarsEnv.Compiler = compiler.Compiler; handlebarsEnv.Compiler = compiler.Compiler;
@@ -29,8 +32,8 @@ global.CompilerContext = {
}, },
compileWithPartial: function(template, options) { compileWithPartial: function(template, options) {
// Hack the compiler on to the environment for these specific tests // Hack the compiler on to the environment for these specific tests
handlebarsEnv.compile = function(template, options) { handlebarsEnv.compile = function(compileTemplate, compileOptions) {
return compiler.compile(template, options, handlebarsEnv); return compiler.compile(compileTemplate, compileOptions, handlebarsEnv);
}; };
handlebarsEnv.parse = parse; handlebarsEnv.parse = parse;
handlebarsEnv.Compiler = compiler.Compiler; handlebarsEnv.Compiler = compiler.Compiler;
@@ -41,10 +44,12 @@ global.CompilerContext = {
}; };
function safeEval(templateSpec) { function safeEval(templateSpec) {
/* eslint-disable no-eval, no-console */
try { try {
return eval('(' + templateSpec + ')'); return eval('(' + templateSpec + ')');
} catch (err) { } catch (err) {
console.error(templateSpec); console.error(templateSpec);
throw err; throw err;
} }
/* eslint-enable no-eval, no-console */
} }
+1 -1
View File
@@ -1,6 +1,6 @@
define(['handlebars.runtime'], function(Handlebars) { define(['handlebars.runtime'], function(Handlebars) {
Handlebars = Handlebars["default"]; var template = Handlebars.template, templates = Handlebars.templates = Handlebars.templates || {}; 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 ""; return "";
},"useData":true}); },"useData":true});
}); });
+285 -265
View File
@@ -1,165 +1,187 @@
/*global CompilerContext, Handlebars, shouldCompileTo, shouldCompileToWithPartials, shouldThrow, handlebarsEnv */
describe('helpers', function() { describe('helpers', function() {
it("helper with complex lookup$", function() { it('helper with complex lookup$', function() {
var string = "{{#goodbyes}}{{{link ../prefix}}}{{/goodbyes}}"; var string = '{{#goodbyes}}{{{link ../prefix}}}{{/goodbyes}}';
var hash = {prefix: "/root", goodbyes: [{text: "Goodbye", url: "goodbye"}]}; var hash = {prefix: '/root', goodbyes: [{text: 'Goodbye', url: 'goodbye'}]};
var helpers = {link: function(prefix) { 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() { it('helper for raw block gets raw content', function() {
var string = "{{{{raw}}}} {{test}} {{{{/raw}}}}"; var string = '{{{{raw}}}} {{test}} {{{{/raw}}}}';
var hash = { test: "hello" }; var hash = { test: 'hello' };
var helpers = { raw: function(options) { var helpers = { raw: function(options) {
return options.fn(); return options.fn();
} }; } };
shouldCompileTo(string, [hash, helpers], " {{test}} ", shouldCompileTo(string, [hash, helpers], ' {{test}} ',
"raw block helper gets raw content"); 'raw block helper gets raw content');
}); });
it("helper for raw block gets parameters", function() { it('helper for raw block gets parameters', function() {
var string = "{{{{raw 1 2 3}}}} {{test}} {{{{/raw}}}}"; var string = '{{{{raw 1 2 3}}}} {{test}} {{{{/raw}}}}';
var hash = { test: "hello" }; var hash = { test: 'hello' };
var helpers = { raw: function(a, b, c, options) { var helpers = { raw: function(a, b, c, options) {
return options.fn() + a + b + c; return options.fn() + a + b + c;
} }; } };
shouldCompileTo(string, [hash, helpers], " {{test}} 123", shouldCompileTo(string, [hash, helpers], ' {{test}} 123',
"raw block helper gets raw content"); 'raw block helper gets raw content');
}); });
it("helper block with complex lookup expression", function() { it('helper for nested raw block gets raw content', function() {
var string = "{{#goodbyes}}{{../name}}{{/goodbyes}}"; var string = '{{{{a}}}} {{{{b}}}} {{{{/b}}}} {{{{/a}}}}';
var hash = {name: "Alan"}; 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 helpers = {goodbyes: function(options) {
var out = ""; var out = '';
var byes = ["Goodbye", "goodbye", "GOODBYE"]; var byes = ['Goodbye', 'goodbye', 'GOODBYE'];
for (var i = 0,j = byes.length; i < j; i++) { for (var i = 0, j = byes.length; i < j; i++) {
out += byes[i] + " " + options.fn(this) + "! "; out += byes[i] + ' ' + options.fn(this) + '! ';
} }
return out; 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() { it('helper with complex lookup and nested template', function() {
var string = "{{#goodbyes}}{{#link ../prefix}}{{text}}{{/link}}{{/goodbyes}}"; var string = '{{#goodbyes}}{{#link ../prefix}}{{text}}{{/link}}{{/goodbyes}}';
var hash = {prefix: '/root', goodbyes: [{text: "Goodbye", url: "goodbye"}]}; var hash = {prefix: '/root', goodbyes: [{text: 'Goodbye', url: 'goodbye'}]};
var helpers = {link: function (prefix, options) { 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() { it('helper with complex lookup and nested template in VM+Compiler', function() {
var string = "{{#goodbyes}}{{#link ../prefix}}{{text}}{{/link}}{{/goodbyes}}"; var string = '{{#goodbyes}}{{#link ../prefix}}{{text}}{{/link}}{{/goodbyes}}';
var hash = {prefix: '/root', goodbyes: [{text: "Goodbye", url: "goodbye"}]}; var hash = {prefix: '/root', goodbyes: [{text: 'Goodbye', url: 'goodbye'}]};
var helpers = {link: function (prefix, options) { 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() { it('helper returning undefined value', function() {
shouldCompileTo(' {{nothere}}', [{}, {nothere: function() {}}], ' '); shouldCompileTo(' {{nothere}}', [{}, {nothere: function() {}}], ' ');
shouldCompileTo(' {{#nothere}}{{/nothere}}', [{}, {nothere: function() {}}], ' '); shouldCompileTo(' {{#nothere}}{{/nothere}}', [{}, {nothere: function() {}}], ' ');
}); });
it("block helper", function() { it('block helper', function() {
var string = "{{#goodbyes}}{{text}}! {{/goodbyes}}cruel {{world}}!"; var string = '{{#goodbyes}}{{text}}! {{/goodbyes}}cruel {{world}}!';
var template = CompilerContext.compile(string); var template = CompilerContext.compile(string);
var result = template({world: "world"}, { helpers: {goodbyes: function(options) { return options.fn({text: "GOODBYE"}); }}}); var result = template({world: 'world'}, { helpers: {goodbyes: function(options) { return options.fn({text: 'GOODBYE'}); }}});
equal(result, "GOODBYE! cruel world!", "Block helper executed"); equal(result, 'GOODBYE! cruel world!', 'Block helper executed');
}); });
it("block helper staying in the same context", function() { it('block helper staying in the same context', function() {
var string = "{{#form}}<p>{{name}}</p>{{/form}}"; var string = '{{#form}}<p>{{name}}</p>{{/form}}';
var template = CompilerContext.compile(string); var template = CompilerContext.compile(string);
var result = template({name: "Yehuda"}, {helpers: {form: function(options) { return "<form>" + options.fn(this) + "</form>"; } }}); 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"); equal(result, '<form><p>Yehuda</p></form>', 'Block helper executed with current context');
}); });
it("block helper should have context in this", function() { it('block helper should have context in this', function() {
var source = "<ul>{{#people}}<li>{{#link}}{{name}}{{/link}}</li>{{/people}}</ul>"; var source = '<ul>{{#people}}<li>{{#link}}{{name}}{{/link}}</li>{{/people}}</ul>';
var link = function(options) { function link(options) {
return '<a href="/people/' + this.id + '">' + options.fn(this) + '</a>'; return '<a href="/people/' + this.id + '">' + options.fn(this) + '</a>';
}; }
var data = { "people": [ var data = { 'people': [
{ "name": "Alan", "id": 1 }, { 'name': 'Alan', 'id': 1 },
{ "name": "Yehuda", "id": 2 } { '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() { it('block helper for undefined value', function() {
shouldCompileTo("{{#empty}}shouldn't render{{/empty}}", {}, ""); shouldCompileTo("{{#empty}}shouldn't render{{/empty}}", {}, '');
}); });
it("block helper passing a new context", function() { it('block helper passing a new context', function() {
var string = "{{#form yehuda}}<p>{{name}}</p>{{/form}}"; var string = '{{#form yehuda}}<p>{{name}}</p>{{/form}}';
var template = CompilerContext.compile(string); var template = CompilerContext.compile(string);
var result = template({yehuda: {name: "Yehuda"}}, { helpers: {form: function(context, options) { return "<form>" + options.fn(context) + "</form>"; }}}); 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"); equal(result, '<form><p>Yehuda</p></form>', 'Context variable resolved');
}); });
it("block helper passing a complex path context", function() { it('block helper passing a complex path context', function() {
var string = "{{#form yehuda/cat}}<p>{{name}}</p>{{/form}}"; var string = '{{#form yehuda/cat}}<p>{{name}}</p>{{/form}}';
var template = CompilerContext.compile(string); 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>"; }}}); 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"); equal(result, '<form><p>Harold</p></form>', 'Complex path variable resolved');
}); });
it("nested block helpers", function() { it('nested block helpers', function() {
var string = "{{#form yehuda}}<p>{{name}}</p>{{#link}}Hello{{/link}}{{/form}}"; var string = '{{#form yehuda}}<p>{{name}}</p>{{#link}}Hello{{/link}}{{/form}}';
var template = CompilerContext.compile(string); var template = CompilerContext.compile(string);
var result = template({ var result = template({
yehuda: {name: "Yehuda" } yehuda: {name: 'Yehuda' }
}, { }, {
helpers: { helpers: {
link: function(options) { return "<a href='" + this.name + "'>" + options.fn(this) + "</a>"; }, link: function(options) { return '<a href="' + this.name + '">' + options.fn(this) + '</a>'; },
form: function(context, options) { return "<form>" + options.fn(context) + "</form>"; } 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() { it('block helper inverted sections', function() {
var string = "{{#list people}}{{name}}{{^}}<em>Nobody's here</em>{{/list}}"; var string = '{{#list people}}{{name}}{{^}}<em>Nobody\'s here</em>{{/list}}';
var list = function(context, options) { function list(context, options) {
if (context.length > 0) { if (context.length > 0) {
var out = "<ul>"; var out = '<ul>';
for(var i = 0,j=context.length; i < j; i++) { for (var i = 0, j = context.length; i < j; i++) {
out += "<li>"; out += '<li>';
out += options.fn(context[i]); out += options.fn(context[i]);
out += "</li>"; out += '</li>';
} }
out += "</ul>"; out += '</ul>';
return out; return out;
} else { } 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 empty = {people: []};
var rootMessage = { var rootMessage = {
people: [], 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, // the meaning here may be kind of hard to catch, but list.not is always called,
// so we should see the output of both // 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, [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(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(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() { it('pathed lambas with parameters', function() {
@@ -178,43 +200,47 @@ describe('helpers', function() {
shouldCompileTo('{{hash/helper 1}}', [hash, helpers], 'winning'); shouldCompileTo('{{hash/helper 1}}', [hash, helpers], 'winning');
}); });
describe("helpers hash", function() { describe('helpers hash', function() {
it("providing a helpers hash", function() { it('providing a helpers hash', function() {
shouldCompileTo("Goodbye {{cruel}} {{world}}!", [{cruel: "cruel"}, {world: function() { return "world"; }}], "Goodbye cruel world!", shouldCompileTo('Goodbye {{cruel}} {{world}}!', [{cruel: 'cruel'}, {world: function() { return 'world'; }}], 'Goodbye cruel world!',
"helpers hash is available"); 'helpers hash is available');
shouldCompileTo("Goodbye {{#iter}}{{cruel}} {{world}}{{/iter}}!", [{iter: [{cruel: "cruel"}]}, {world: function() { return "world"; }}], shouldCompileTo('Goodbye {{#iter}}{{cruel}} {{world}}{{/iter}}!', [{iter: [{cruel: 'cruel'}]}, {world: function() { return 'world'; }}],
"Goodbye cruel world!", "helpers hash is available inside other blocks"); 'Goodbye cruel world!', 'helpers hash is available inside other blocks');
}); });
it("in cases of conflict, helpers win", function() { it('in cases of conflict, helpers win', function() {
shouldCompileTo("{{{lookup}}}", [{lookup: 'Explicit'}, {lookup: function() { return 'helpers'; }}], "helpers", shouldCompileTo('{{{lookup}}}', [{lookup: 'Explicit'}, {lookup: function() { return 'helpers'; }}], 'helpers',
"helpers hash has precedence escaped expansion"); 'helpers hash has precedence escaped expansion');
shouldCompileTo("{{lookup}}", [{lookup: 'Explicit'}, {lookup: function() { return 'helpers'; }}], "helpers", shouldCompileTo('{{lookup}}', [{lookup: 'Explicit'}, {lookup: function() { return 'helpers'; }}], 'helpers',
"helpers hash has precedence simple expansion"); 'helpers hash has precedence simple expansion');
}); });
it("the helpers hash is available is nested contexts", function() { it('the helpers hash is available is nested contexts', function() {
shouldCompileTo("{{#outer}}{{#inner}}{{helper}}{{/inner}}{{/outer}}", shouldCompileTo(
[{'outer': {'inner': {'unused':[]}}}, {'helper': function() { return 'helper'; }}], "helper", '{{#outer}}{{#inner}}{{helper}}{{/inner}}{{/outer}}',
"helpers hash is available in nested contexts."); [
{'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!'; }); handlebarsEnv.registerHelper('test_helper', function() { return 'found it!'; });
shouldCompileTo( shouldCompileTo(
"{{test_helper}} {{#if cruel}}Goodbye {{cruel}} {{world}}!{{/if}}", [ '{{test_helper}} {{#if cruel}}Goodbye {{cruel}} {{world}}!{{/if}}', [
{cruel: "cruel"}, {cruel: 'cruel'},
{world: function() { return "world!"; }} {world: function() { return 'world!'; }}
], ],
"found it! Goodbye cruel world!!"); 'found it! Goodbye cruel world!!');
}); });
}); });
describe('registration', function() { describe('registration', function() {
it('unregisters', function() { it('unregisters', function() {
var helpers = handlebarsEnv.helpers;
handlebarsEnv.helpers = {}; handlebarsEnv.helpers = {};
handlebarsEnv.registerHelper('foo', function() { handlebarsEnv.registerHelper('foo', function() {
@@ -230,134 +256,128 @@ describe('helpers', function() {
handlebarsEnv.registerHelper({ handlebarsEnv.registerHelper({
'if': helpers['if'], 'if': helpers['if'],
world: function() { return "world!"; }, world: function() { return 'world!'; },
test_helper: function() { return 'found it!'; } testHelper: function() { return 'found it!'; }
}); });
shouldCompileTo( shouldCompileTo(
"{{test_helper}} {{#if cruel}}Goodbye {{cruel}} {{world}}!{{/if}}", '{{testHelper}} {{#if cruel}}Goodbye {{cruel}} {{world}}!{{/if}}',
[{cruel: "cruel"}], [{cruel: 'cruel'}],
"found it! Goodbye cruel world!!"); 'found it! Goodbye cruel world!!');
}); });
it('fails with multiple and args', function() { it('fails with multiple and args', function() {
shouldThrow(function() { shouldThrow(function() {
handlebarsEnv.registerHelper({ handlebarsEnv.registerHelper({
world: function() { return "world!"; }, world: function() { return 'world!'; },
test_helper: function() { return 'found it!'; } testHelper: function() { return 'found it!'; }
}, {}); }, {});
}, Error, 'Arg not supported with multiple helpers'); }, 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 string = 'Message: {{hello -1.2 1.2}}';
var hash = {}; var helpers = {hello: function(times, times2) {
var helpers = {hello: function(times, times2) { if (typeof times !== 'number') { times = 'NaN'; }
if(typeof times !== 'number') { times = "NaN"; } if (typeof times2 !== 'number') { times2 = 'NaN'; }
if(typeof times2 !== 'number') { times2 = "NaN"; } return 'Hello ' + times + ' ' + times2 + ' times';
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 string = 'Message: {{hello -12}}';
var hash = {}; var helpers = {hello: function(times) {
var helpers = {hello: function(times) { if (typeof times !== 'number') { times = 'NaN'; }
if(typeof times !== 'number') { times = "NaN"; } return 'Hello ' + times + ' times';
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() { describe('String literal parameters', function() {
it("simple literals work", function() { it('simple literals work', function() {
var string = 'Message: {{hello "world" 12 true false}}'; var string = 'Message: {{hello "world" 12 true false}}';
var hash = {}; var helpers = {hello: function(param, times, bool1, bool2) {
var helpers = {hello: function(param, times, bool1, bool2) { if (typeof times !== 'number') { times = 'NaN'; }
if(typeof times !== 'number') { times = "NaN"; } if (typeof bool1 !== 'boolean') { bool1 = 'NaB'; }
if(typeof bool1 !== 'boolean') { bool1 = "NaB"; } if (typeof bool2 !== 'boolean') { bool2 = 'NaB'; }
if(typeof bool2 !== 'boolean') { bool2 = "NaB"; } return 'Hello ' + param + ' ' + times + ' times: ' + bool1 + ' ' + bool2;
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"}}'; var string = 'Message: {{hello wo"rld"}}';
shouldThrow(function() { shouldThrow(function() {
CompilerContext.compile(string); CompilerContext.compile(string);
}, Error); }, Error);
}); });
it("escaping a String is possible", function(){ it('escaping a String is possible', function() {
var string = 'Message: {{{hello "\\"world\\""}}}'; var string = 'Message: {{{hello "\\"world\\""}}}';
var hash = {}; var helpers = {hello: function(param) { return 'Hello ' + param; }};
var helpers = {hello: function(param) { return "Hello " + param; }}; shouldCompileTo(string, [{}, helpers], 'Message: Hello "world"', 'template with an escaped String literal');
shouldCompileTo(string, [hash, helpers], "Message: Hello \"world\"", "template with an escaped String literal");
}); });
it("it works with ' marks", function() { it("it works with ' marks", function() {
var string = 'Message: {{{hello "Alan\'s world"}}}'; var string = 'Message: {{{hello "Alan\'s world"}}}';
var hash = {}; var helpers = {hello: function(param) { return 'Hello ' + param; }};
var helpers = {hello: function(param) { return "Hello " + param; }}; shouldCompileTo(string, [{}, helpers], "Message: Hello Alan's world", "template with a ' mark");
shouldCompileTo(string, [hash, 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 string = 'Message: {{hello -12}}';
var hash = {}; var helpers = {hello: function(times) {
var helpers = {hello: function(times) { if (typeof times !== 'number') { times = 'NaN'; }
if(typeof times !== 'number') { times = "NaN"; } return 'Hello ' + times + ' times';
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() { describe('multiple parameters', function() {
it("simple multi-params work", function() { it('simple multi-params work', function() {
var string = 'Message: {{goodbye cruel world}}'; var string = 'Message: {{goodbye cruel world}}';
var hash = {cruel: "cruel", world: "world"}; var hash = {cruel: 'cruel', world: 'world'};
var helpers = {goodbye: function(cruel, world) { return "Goodbye " + cruel + " " + world; }}; var helpers = {goodbye: function(cruel, world) { return 'Goodbye ' + cruel + ' ' + world; }};
shouldCompileTo(string, [hash, helpers], "Message: Goodbye cruel world", "regular helpers with multiple params"); 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 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) { 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() { 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 template = CompilerContext.compile('{{goodbye cruel="CRUEL" world="WORLD" times=12}}');
var helpers = { var helpers = {
goodbye: function(options) { 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 context = {};
var result = template(context, {helpers: helpers}); 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 = { var helpers = {
goodbye: function(options) { goodbye: function(options) {
if (options.hash.print === true) { 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) { } else if (options.hash.print === false) {
return "NOT PRINTING"; return 'NOT PRINTING';
} else { } 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 template = CompilerContext.compile('{{goodbye cruel="CRUEL" world="WORLD" print=true}}');
var result = template(context, {helpers: helpers}); 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}}'); template = CompilerContext.compile('{{goodbye cruel="CRUEL" world="WORLD" print=false}}');
result = template(context, {helpers: helpers}); 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 template = CompilerContext.compile('{{#goodbye cruel="CRUEL" times=12}}world{{/goodbye}}');
var helpers = { var helpers = {
goodbye: function(options) { 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}); 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() { 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 template = CompilerContext.compile('{{#goodbye cruel="CRUEL" times=12}}world{{/goodbye}}');
var helpers = { var helpers = {
goodbye: function(options) { 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}); 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 = { var helpers = {
goodbye: function(options) { goodbye: function(options) {
if (options.hash.print === true) { 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) { } else if (options.hash.print === false) {
return "NOT PRINTING"; return 'NOT PRINTING';
} else { } else {
return "THIS SHOULD NOT HAPPEN"; return 'THIS SHOULD NOT HAPPEN';
} }
} }
}; };
var template = CompilerContext.compile('{{#goodbye cruel="CRUEL" print=true}}world{{/goodbye}}'); var template = CompilerContext.compile('{{#goodbye cruel="CRUEL" print=true}}world{{/goodbye}}');
var result = template({}, {helpers: helpers}); 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}}'); template = CompilerContext.compile('{{#goodbye cruel="CRUEL" print=false}}world{{/goodbye}}');
result = template({}, {helpers: helpers}); result = template({}, {helpers: helpers});
equals(result, "NOT PRINTING", "Boolean hash parameter honored"); equals(result, 'NOT PRINTING', 'Boolean hash parameter honored');
}); });
}); });
describe("helperMissing", function() { describe('helperMissing', function() {
it("if a context is not found, helperMissing is used", function() { it('if a context is not found, helperMissing is used', function() {
shouldThrow(function() { shouldThrow(function() {
var template = CompilerContext.compile("{{hello}} {{link_to world}}"); var template = CompilerContext.compile('{{hello}} {{link_to world}}');
template({}); template({});
}, undefined, /Missing helper: 'link_to'/); }, undefined, /Missing helper: "link_to"/);
}); });
it("if a context is not found, custom helperMissing is used", function() { it('if a context is not found, custom helperMissing is used', function() {
var string = "{{hello}} {{link_to world}}"; var string = '{{hello}} {{link_to world}}';
var context = { hello: "Hello", world: "world" }; var context = { hello: 'Hello', world: 'world' };
var helpers = { var helpers = {
helperMissing: function(mesg, options) { helperMissing: function(mesg, options) {
if(options.name === "link_to") { if (options.name === 'link_to') {
return new Handlebars.SafeString("<a>" + mesg + "</a>"); 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() { it('if a value is not found, custom helperMissing is used', function() {
var string = "{{hello}} {{link_to}}"; var string = '{{hello}} {{link_to}}';
var context = { hello: "Hello", world: "world" }; var context = { hello: 'Hello', world: 'world' };
var helpers = { var helpers = {
helperMissing: function(options) { helperMissing: function(options) {
if(options.name === "link_to") { if (options.name === 'link_to') {
return new Handlebars.SafeString("<a>winning</a>"); 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() { describe('knownHelpers', function() {
it("Known helper should render helper", function() { it('Known helper should render helper', function() {
var template = CompilerContext.compile("{{hello}}", {knownHelpers: {"hello" : true}}); var template = CompilerContext.compile('{{hello}}', {knownHelpers: {hello: true}});
var result = template({}, {helpers: {hello: function() { return "foo"; }}}); var result = template({}, {helpers: {hello: function() { return 'foo'; }}});
equal(result, "foo", "'foo' should === '" + result); equal(result, 'foo', "'foo' should === '" + result);
}); });
it("Unknown helper in knownHelpers only mode should be passed as undefined", function() { 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 template = CompilerContext.compile('{{typeof hello}}', {knownHelpers: {'typeof': true}, knownHelpersOnly: true});
var result = template({}, {helpers: {'typeof': function(arg) { return typeof arg; }, hello: function() { return "foo"; }}}); var result = template({}, {helpers: {'typeof': function(arg) { return typeof arg; }, hello: function() { return 'foo'; }}});
equal(result, "undefined", "'undefined' should === '" + result); equal(result, 'undefined', "'undefined' should === '" + result);
}); });
it("Builtin helpers available in knownHelpers only mode", function() { it('Builtin helpers available in knownHelpers only mode', function() {
var template = CompilerContext.compile("{{#unless foo}}bar{{/unless}}", {knownHelpersOnly: true}); var template = CompilerContext.compile('{{#unless foo}}bar{{/unless}}', {knownHelpersOnly: true});
var result = template({}); var result = template({});
equal(result, "bar", "'bar' should === '" + result); equal(result, 'bar', "'bar' should === '" + result);
}); });
it("Field lookup works in knownHelpers only mode", function() { it('Field lookup works in knownHelpers only mode', function() {
var template = CompilerContext.compile("{{foo}}", {knownHelpersOnly: true}); var template = CompilerContext.compile('{{foo}}', {knownHelpersOnly: true});
var result = template({foo: 'bar'}); 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() { it('Conditional blocks work in knownHelpers only mode', function() {
var template = CompilerContext.compile("{{#foo}}bar{{/foo}}", {knownHelpersOnly: true}); var template = CompilerContext.compile('{{#foo}}bar{{/foo}}', {knownHelpersOnly: true});
var result = template({foo: 'baz'}); 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() { it('Invert blocks work in knownHelpers only mode', function() {
var template = CompilerContext.compile("{{^foo}}bar{{/foo}}", {knownHelpersOnly: true}); var template = CompilerContext.compile('{{^foo}}bar{{/foo}}', {knownHelpersOnly: true});
var result = template({foo: false}); 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() { it('Functions are bound to the context in knownHelpers only mode', function() {
var template = CompilerContext.compile("{{foo}}", {knownHelpersOnly: true}); var template = CompilerContext.compile('{{foo}}', {knownHelpersOnly: true});
var result = template({foo: function() { return this.bar; }, bar: 'bar'}); 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() { shouldThrow(function() {
CompilerContext.compile("{{typeof hello}}", {knownHelpersOnly: true}); CompilerContext.compile('{{typeof hello}}', {knownHelpersOnly: true});
}, Error); }, Error);
}); });
}); });
describe("blockHelperMissing", function() { describe('blockHelperMissing', function() {
it("lambdas are resolved by blockHelperMissing, not handlebars proper", function() { it('lambdas are resolved by blockHelperMissing, not handlebars proper', function() {
var string = "{{#truthy}}yep{{/truthy}}"; var string = '{{#truthy}}yep{{/truthy}}';
var data = { truthy: function() { return true; } }; 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() { it('lambdas resolved by blockHelperMissing are bound to the context', function() {
var string = "{{#truthy}}yep{{/truthy}}"; var string = '{{#truthy}}yep{{/truthy}}';
var boundData = { truthy: function() { return this.truthiness(); }, truthiness: function() { return false; } }; var boundData = { truthy: function() { return this.truthiness(); }, truthiness: function() { return false; } };
shouldCompileTo(string, boundData, ""); shouldCompileTo(string, boundData, '');
}); });
}); });
@@ -528,13 +548,13 @@ describe('helpers', function() {
var context = {}; var context = {};
var helpers = { var helpers = {
blockHelperMissing: function() { blockHelperMissing: function() {
return 'missing: ' + arguments[arguments.length-1].name; return 'missing: ' + arguments[arguments.length - 1].name;
}, },
helperMissing: function() { helperMissing: function() {
return 'helper missing: ' + arguments[arguments.length-1].name; return 'helper missing: ' + arguments[arguments.length - 1].name;
}, },
helper: function() { helper: function() {
return 'ran: ' + arguments[arguments.length-1].name; return 'ran: ' + arguments[arguments.length - 1].name;
} }
}; };
@@ -554,7 +574,7 @@ describe('helpers', function() {
shouldCompileTo('{{#helper 1}}{{/helper}}', [context, helpers], 'ran: helper'); shouldCompileTo('{{#helper 1}}{{/helper}}', [context, helpers], 'ran: helper');
}); });
it('should include in known helper calls', function() { 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'); equal(template({}, {helpers: helpers}), 'ran: helper');
}); });
@@ -569,8 +589,8 @@ describe('helpers', function() {
}); });
describe('name conflicts', function() { describe('name conflicts', function() {
it("helpers take precedence over same-named context properties", function() { it('helpers take precedence over same-named context properties', function() {
var template = CompilerContext.compile("{{goodbye}} {{cruel world}}"); var template = CompilerContext.compile('{{goodbye}} {{cruel world}}');
var helpers = { var helpers = {
goodbye: function() { goodbye: function() {
@@ -578,21 +598,21 @@ describe('helpers', function() {
}, },
cruel: function(world) { cruel: function(world) {
return "cruel " + world.toUpperCase(); return 'cruel ' + world.toUpperCase();
} }
}; };
var context = { var context = {
goodbye: "goodbye", goodbye: 'goodbye',
world: "world" world: 'world'
}; };
var result = template(context, {helpers: helpers}); 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() { it('helpers take precedence over same-named context properties$', function() {
var template = CompilerContext.compile("{{#goodbye}} {{cruel world}}{{/goodbye}}"); var template = CompilerContext.compile('{{#goodbye}} {{cruel world}}{{/goodbye}}');
var helpers = { var helpers = {
goodbye: function(options) { goodbye: function(options) {
@@ -600,21 +620,21 @@ describe('helpers', function() {
}, },
cruel: function(world) { cruel: function(world) {
return "cruel " + world.toUpperCase(); return 'cruel ' + world.toUpperCase();
} }
}; };
var context = { var context = {
goodbye: "goodbye", goodbye: 'goodbye',
world: "world" world: 'world'
}; };
var result = template(context, {helpers: helpers}); 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() { it('Scoped names take precedence over helpers', function() {
var template = CompilerContext.compile("{{this.goodbye}} {{cruel world}} {{cruel this.goodbye}}"); var template = CompilerContext.compile('{{this.goodbye}} {{cruel world}} {{cruel this.goodbye}}');
var helpers = { var helpers = {
goodbye: function() { goodbye: function() {
@@ -622,21 +642,21 @@ describe('helpers', function() {
}, },
cruel: function(world) { cruel: function(world) {
return "cruel " + world.toUpperCase(); return 'cruel ' + world.toUpperCase();
} }
}; };
var context = { var context = {
goodbye: "goodbye", goodbye: 'goodbye',
world: "world" world: 'world'
}; };
var result = template(context, {helpers: helpers}); 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() { it('Scoped names take precedence over block helpers', function() {
var template = CompilerContext.compile("{{#goodbye}} {{cruel world}}{{/goodbye}} {{this.goodbye}}"); var template = CompilerContext.compile('{{#goodbye}} {{cruel world}}{{/goodbye}} {{this.goodbye}}');
var helpers = { var helpers = {
goodbye: function(options) { goodbye: function(options) {
@@ -644,17 +664,17 @@ describe('helpers', function() {
}, },
cruel: function(world) { cruel: function(world) {
return "cruel " + world.toUpperCase(); return 'cruel ' + world.toUpperCase();
} }
}; };
var context = { var context = {
goodbye: "goodbye", goodbye: 'goodbye',
world: "world" world: 'world'
}; };
var result = template(context, {helpers: helpers}); 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> <html>
<head> <head>
<title>Mocha</title> <title>Mocha</title>
+7 -6
View File
@@ -1,4 +1,3 @@
/*global Handlebars, beforeEach, handlebarsEnv, shouldCompileTo */
describe('javascript-compiler api', function() { describe('javascript-compiler api', function() {
if (!Handlebars.JavaScriptCompiler) { if (!Handlebars.JavaScriptCompiler) {
return; return;
@@ -17,13 +16,15 @@ describe('javascript-compiler api', function() {
handlebarsEnv.JavaScriptCompiler.prototype.nameLookup = function(parent, name) { handlebarsEnv.JavaScriptCompiler.prototype.nameLookup = function(parent, name) {
return parent + '.bar_' + 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 // Tests nameLookup dot vs. bracket behavior. Bracket is required in certain cases
// to avoid errors in older browsers. // to avoid errors in older browsers.
it('should handle reserved words', function() { it('should handle reserved words', function() {
shouldCompileTo("{{foo}} {{~null~}}", { foo: "food" }, "food"); shouldCompileTo('{{foo}} {{~null~}}', { foo: 'food' }, 'food');
}); });
}); });
describe('#compilerInfo', function() { describe('#compilerInfo', function() {
@@ -45,7 +46,7 @@ describe('javascript-compiler api', function() {
throw new Error('It didn\'t work'); throw new Error('It didn\'t work');
} }
}; };
shouldCompileTo("{{foo}} ", { foo: "food" }, "food "); shouldCompileTo('{{foo}} ', { foo: 'food' }, 'food ');
}); });
}); });
describe('buffer', function() { describe('buffer', function() {
@@ -65,13 +66,13 @@ describe('javascript-compiler api', function() {
handlebarsEnv.JavaScriptCompiler.prototype.initializeBuffer = function() { handlebarsEnv.JavaScriptCompiler.prototype.initializeBuffer = function() {
return this.quotedString('foo_'); return this.quotedString('foo_');
}; };
shouldCompileTo("{{foo}} ", { foo: "food" }, "foo_food "); shouldCompileTo('{{foo}} ', { foo: 'food' }, 'foo_food ');
}); });
it('should allow append buffer override', function() { it('should allow append buffer override', function() {
handlebarsEnv.JavaScriptCompiler.prototype.appendToBuffer = function(string) { handlebarsEnv.JavaScriptCompiler.prototype.appendToBuffer = function(string) {
return $superAppend.call(this, [string, ' + "_foo"']); 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() { describe('parser', function() {
if (!Handlebars.print) { if (!Handlebars.print) {
return; return;
} }
function ast_for(template) { function astFor(template) {
var ast = Handlebars.parse(template); var ast = Handlebars.parse(template);
return Handlebars.print(ast); return Handlebars.print(ast);
} }
it('parses simple mustaches', function() { it('parses simple mustaches', function() {
equals(ast_for('{{123}}'), "{{ NUMBER{123} [] }}\n"); equals(astFor('{{123}}'), '{{ NUMBER{123} [] }}\n');
equals(ast_for('{{"foo"}}'), '{{ "foo" [] }}\n'); equals(astFor('{{"foo"}}'), '{{ "foo" [] }}\n');
equals(ast_for('{{false}}'), '{{ BOOLEAN{false} [] }}\n'); equals(astFor('{{false}}'), '{{ BOOLEAN{false} [] }}\n');
equals(ast_for('{{true}}'), '{{ BOOLEAN{true} [] }}\n'); equals(astFor('{{true}}'), '{{ BOOLEAN{true} [] }}\n');
equals(ast_for('{{foo}}'), "{{ PATH:foo [] }}\n"); equals(astFor('{{foo}}'), '{{ PATH:foo [] }}\n');
equals(ast_for('{{foo?}}'), "{{ PATH:foo? [] }}\n"); equals(astFor('{{foo?}}'), '{{ PATH:foo? [] }}\n');
equals(ast_for('{{foo_}}'), "{{ PATH:foo_ [] }}\n"); equals(astFor('{{foo_}}'), '{{ PATH:foo_ [] }}\n');
equals(ast_for('{{foo-}}'), "{{ PATH:foo- [] }}\n"); equals(astFor('{{foo-}}'), '{{ PATH:foo- [] }}\n');
equals(ast_for('{{foo:}}'), "{{ PATH:foo: [] }}\n"); equals(astFor('{{foo:}}'), '{{ PATH:foo: [] }}\n');
}); });
it('parses simple mustaches with data', function() { 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() { 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() { 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() { 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() { 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() { 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() { 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() { 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() { it('parses mustaches with BOOLEAN parameters', function() {
equals(ast_for("{{foo true}}"), "{{ PATH:foo [BOOLEAN{true}] }}\n"); equals(astFor('{{foo true}}'), '{{ PATH:foo [BOOLEAN{true}] }}\n');
equals(ast_for("{{foo false}}"), "{{ PATH:foo [BOOLEAN{false}] }}\n"); equals(astFor('{{foo false}}'), '{{ PATH:foo [BOOLEAN{false}] }}\n');
}); });
it('parses mutaches with DATA parameters', function() { it('parses mustaches with undefined and null paths', function() {
equals(ast_for("{{foo @bar}}"), "{{ PATH:foo [@PATH:bar] }}\n"); 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() { it('parses mustaches with hash arguments', function() {
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(ast_for("{{foo bar=1}}"), "{{ PATH:foo [] HASH{bar=NUMBER{1}} }}\n"); equals(astFor('{{foo bar=1}}'), '{{ PATH:foo [] HASH{bar=NUMBER{1}} }}\n');
equals(ast_for("{{foo bar=true}}"), "{{ PATH:foo [] HASH{bar=BOOLEAN{true}} }}\n"); equals(astFor('{{foo bar=true}}'), '{{ PATH:foo [] HASH{bar=BOOLEAN{true}} }}\n');
equals(ast_for("{{foo bar=false}}"), "{{ PATH:foo [] HASH{bar=BOOLEAN{false}} }}\n"); equals(astFor('{{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(ast_for("{{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=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="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(astFor('{{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(astFor('{{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(astFor('{{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\" baz=false}}'), '{{ PATH:foo [PATH:omg] HASH{bar=PATH:baz, bat="bam", baz=BOOLEAN{false}} }}\n');
}); });
it('parses contents followed by a mustache', function() { 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() { it('parses a partial', function() {
equals(ast_for("{{> foo }}"), "{{> PARTIAL:foo }}\n"); equals(astFor('{{> foo }}'), '{{> PARTIAL:foo }}\n');
equals(ast_for("{{> 'foo' }}"), "{{> PARTIAL:foo }}\n"); equals(astFor('{{> "foo" }}'), '{{> PARTIAL:foo }}\n');
equals(ast_for("{{> 1 }}"), "{{> PARTIAL:1 }}\n"); equals(astFor('{{> 1 }}'), '{{> PARTIAL:1 }}\n');
}); });
it('parses a partial with context', function() { 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() { 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() { 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() { 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() { 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() { 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() { 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() { 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() { 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() { 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() { 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() { 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() { 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() { 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() { 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() { 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() { 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() { it('throws on old inverse section', function() {
shouldThrow(function() { shouldThrow(function() {
ast_for("{{else foo}}bar{{/foo}}"); astFor('{{else foo}}bar{{/foo}}');
}, Error); }, Error);
}); });
it('parses block with block params', function() { 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() { 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() { 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() { shouldThrow(function() {
ast_for("foo{{^}}bar"); astFor('foo{{^}}bar');
}, Error, /Parse error on line 1/); }, Error, /Parse error on line 1/);
shouldThrow(function() { shouldThrow(function() {
ast_for("{{foo}"); astFor('{{foo}');
}, Error, /Parse error on line 1/); }, Error, /Parse error on line 1/);
shouldThrow(function() { shouldThrow(function() {
ast_for("{{foo &}}"); astFor('{{foo &}}');
}, Error, /Parse error on line 1/); }, Error, /Parse error on line 1/);
shouldThrow(function() { shouldThrow(function() {
ast_for("{{#goodbyes}}{{/hellos}}"); astFor('{{#goodbyes}}{{/hellos}}');
}, Error, /goodbyes doesn't match hellos/); }, Error, /goodbyes doesn't match hellos/);
shouldThrow(function() { shouldThrow(function() {
ast_for("{{{{goodbyes}}}} {{{{/hellos}}}}"); astFor('{{{{goodbyes}}}} {{{{/hellos}}}}');
}, Error, /goodbyes doesn't match hellos/); }, Error, /goodbyes doesn't match hellos/);
}); });
it('should handle invalid paths', function() { it('should handle invalid paths', function() {
shouldThrow(function() { shouldThrow(function() {
ast_for("{{foo/../bar}}"); astFor('{{foo/../bar}}');
}, Error, /Invalid path: foo\/\.\. - 1:2/); }, Error, /Invalid path: foo\/\.\. - 1:2/);
shouldThrow(function() { shouldThrow(function() {
ast_for("{{foo/./bar}}"); astFor('{{foo/./bar}}');
}, Error, /Invalid path: foo\/\. - 1:2/); }, Error, /Invalid path: foo\/\. - 1:2/);
shouldThrow(function() { shouldThrow(function() {
ast_for("{{foo/this/bar}}"); astFor('{{foo/this/bar}}');
}, Error, /Invalid path: foo\/this - 1:2/); }, Error, /Invalid path: foo\/this - 1:2/);
}); });
it('knows how to report the correct line number in errors', function() { it('knows how to report the correct line number in errors', function() {
shouldThrow(function() { shouldThrow(function() {
ast_for("hello\nmy\n{{foo}"); astFor('hello\nmy\n{{foo}');
}, Error, /Parse error on line 3/); }, Error, /Parse error on line 3/);
shouldThrow(function() { shouldThrow(function() {
ast_for("hello\n\nmy\n\n{{foo}"); astFor('hello\n\nmy\n\n{{foo}');
}, Error, /Parse error on line 5/); }, 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() { it('knows how to report the correct line number in errors when the first character is a newline', function() {
shouldThrow(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/); }, Error, /Parse error on line 7/);
}); });
describe('externally compiled AST', function() { describe('externally compiled AST', function() {
it('can pass through an already-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 }
}));
});
}); });
+353 -108
View File
@@ -1,11 +1,10 @@
/*global CompilerContext, Handlebars, handlebarsEnv, shouldCompileTo, shouldCompileToWithPartials, shouldThrow */
describe('partials', function() { describe('partials', function() {
it('basic partials', function() { it('basic partials', function() {
var string = 'Dudes: {{#dudes}}{{> dude}}{{/dudes}}'; var string = 'Dudes: {{#dudes}}{{> dude}}{{/dudes}}';
var partial = '{{name}} ({{url}}) '; var partial = '{{name}} ({{url}}) ';
var hash = {dudes: [{name: 'Yehuda', url: 'http://yehuda'}, {name: 'Alan', url: 'http://alan'}]}; 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) '); shouldCompileToWithPartials(string, [hash, {}, {dude: partial}], true, 'Dudes: Yehuda (http://yehuda) Alan (http://alan) ');
shouldCompileToWithPartials(string, [hash, {}, {dude: partial},,false], true, 'Dudes: Yehuda (http://yehuda) Alan (http://alan) '); shouldCompileToWithPartials(string, [hash, {}, {dude: partial},, false], true, 'Dudes: Yehuda (http://yehuda) Alan (http://alan) ');
}); });
it('dynamic partials', function() { it('dynamic partials', function() {
@@ -18,7 +17,7 @@ describe('partials', function() {
} }
}; };
shouldCompileToWithPartials(string, [hash, helpers, {dude: partial}], true, 'Dudes: Yehuda (http://yehuda) Alan (http://alan) '); shouldCompileToWithPartials(string, [hash, helpers, {dude: partial}], true, 'Dudes: Yehuda (http://yehuda) Alan (http://alan) ');
shouldCompileToWithPartials(string, [hash, helpers, {dude: partial},,false], true, 'Dudes: Yehuda (http://yehuda) Alan (http://alan) '); shouldCompileToWithPartials(string, [hash, helpers, {dude: partial},, false], true, 'Dudes: Yehuda (http://yehuda) Alan (http://alan) ');
}); });
it('failing dynamic partials', function() { it('failing dynamic partials', function() {
var string = 'Dudes: {{#dudes}}{{> (partial)}}{{/dudes}}'; var string = 'Dudes: {{#dudes}}{{> (partial)}}{{/dudes}}';
@@ -34,19 +33,41 @@ describe('partials', function() {
}, Handlebars.Exception, 'The partial missing could not be found'); }, Handlebars.Exception, 'The partial missing could not be found');
}); });
it("partials with context", function() { it('partials with context', function() {
var string = "Dudes: {{>dude dudes}}"; var string = 'Dudes: {{>dude dudes}}';
var partial = "{{#this}}{{name}} ({{url}}) {{/this}}"; var partial = '{{#this}}{{name}} ({{url}}) {{/this}}';
var hash = {dudes: [{name: "Yehuda", url: "http://yehuda"}, {name: "Alan", url: "http://alan"}]}; 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) ", shouldCompileToWithPartials(string, [hash, {}, {dude: partial}], true, 'Dudes: Yehuda (http://yehuda) Alan (http://alan) ',
"Partials can be passed a context"); 'Partials can be passed a context');
}); });
it("partials with undefined context", function() { it('partials with no context', function() {
var string = "Dudes: {{>dude dudes}}"; var partial = '{{name}} ({{url}}) ';
var partial = "{{foo}} Empty"; 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 = {}; 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() { it('partials with duplicate parameters', function() {
@@ -55,133 +76,133 @@ describe('partials', function() {
}, Error, 'Unsupported number of partial arguments: 2 - 1:7'); }, Error, 'Unsupported number of partial arguments: 2 - 1:7');
}); });
it("partials with parameters", function() { it('partials with parameters', function() {
var string = "Dudes: {{#dudes}}{{> dude others=..}}{{/dudes}}"; var string = 'Dudes: {{#dudes}}{{> dude others=..}}{{/dudes}}';
var partial = "{{others.foo}}{{name}} ({{url}}) "; var partial = '{{others.foo}}{{name}} ({{url}}) ';
var hash = {foo: 'bar', dudes: [{name: "Yehuda", url: "http://yehuda"}, {name: "Alan", url: "http://alan"}]}; 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) ", shouldCompileToWithPartials(string, [hash, {}, {dude: partial}], true, 'Dudes: barYehuda (http://yehuda) barAlan (http://alan) ',
"Basic partials output based on current context."); 'Basic partials output based on current context.');
}); });
it("partial in a partial", function() { it('partial in a partial', function() {
var string = "Dudes: {{#dudes}}{{>dude}}{{/dudes}}"; var string = 'Dudes: {{#dudes}}{{>dude}}{{/dudes}}';
var dude = "{{name}} {{> url}} "; var dude = '{{name}} {{> url}} ';
var url = "<a href='{{url}}'>{{url}}</a>"; var url = '<a href="{{url}}">{{url}}</a>';
var hash = {dudes: [{name: "Yehuda", url: "http://yehuda"}, {name: "Alan", url: "http://alan"}]}; 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"); 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() { shouldThrow(function() {
var template = CompilerContext.compile("{{> whatever}}"); var template = CompilerContext.compile('{{> whatever}}');
template(); template();
}, Handlebars.Exception, 'The partial whatever could not be found'); }, 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() { shouldThrow(function() {
var undef; var undef;
handlebarsEnv.registerPartial('undefined_test', 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() { shouldThrow(function() {
var template = CompilerContext.compile("{{> whatever}}"); var template = CompilerContext.compile('{{> whatever}}');
template(); template();
}, Handlebars.Exception, 'The partial whatever could not be found'); }, Handlebars.Exception, 'The partial whatever could not be found');
}); });
it("rendering function partial in vm mode", function() { it('rendering function partial in vm mode', function() {
var string = "Dudes: {{#dudes}}{{> dude}}{{/dudes}}"; var string = 'Dudes: {{#dudes}}{{> dude}}{{/dudes}}';
var partial = function(context) { function partial(context) {
return context.name + ' (' + context.url + ') '; return context.name + ' (' + context.url + ') ';
}; }
var hash = {dudes: [{name: "Yehuda", url: "http://yehuda"}, {name: "Alan", url: "http://alan"}]}; 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) ", shouldCompileTo(string, [hash, {}, {dude: partial}], 'Dudes: Yehuda (http://yehuda) Alan (http://alan) ',
"Function partials output based in VM."); 'Function partials output based in VM.');
}); });
it("GH-14: a partial preceding a selector", function() { it('GH-14: a partial preceding a selector', function() {
var string = "Dudes: {{>dude}} {{another_dude}}"; var string = 'Dudes: {{>dude}} {{anotherDude}}';
var dude = "{{name}}"; var dude = '{{name}}';
var hash = {name:"Jeepers", another_dude:"Creepers"}; var hash = {name: 'Jeepers', anotherDude: 'Creepers'};
shouldCompileToWithPartials(string, [hash, {}, {dude:dude}], true, "Dudes: Jeepers Creepers", "Regular selectors can follow a partial"); shouldCompileToWithPartials(string, [hash, {}, {dude: dude}], true, 'Dudes: Jeepers Creepers', 'Regular selectors can follow a partial');
}); });
it("Partials with slash paths", function() { it('Partials with slash paths', function() {
var string = "Dudes: {{> shared/dude}}"; var string = 'Dudes: {{> shared/dude}}';
var dude = "{{name}}"; var dude = '{{name}}';
var hash = {name:"Jeepers", another_dude:"Creepers"}; var hash = {name: 'Jeepers', anotherDude: 'Creepers'};
shouldCompileToWithPartials(string, [hash, {}, {'shared/dude':dude}], true, "Dudes: Jeepers", "Partials can use literal paths"); shouldCompileToWithPartials(string, [hash, {}, {'shared/dude': dude}], true, 'Dudes: Jeepers', 'Partials can use literal paths');
}); });
it("Partials with slash and point paths", function() { it('Partials with slash and point paths', function() {
var string = "Dudes: {{> shared/dude.thing}}"; var string = 'Dudes: {{> shared/dude.thing}}';
var dude = "{{name}}"; var dude = '{{name}}';
var hash = {name:"Jeepers", another_dude:"Creepers"}; var hash = {name: 'Jeepers', anotherDude: 'Creepers'};
shouldCompileToWithPartials(string, [hash, {}, {'shared/dude.thing':dude}], true, "Dudes: Jeepers", "Partials can use literal with points in paths"); shouldCompileToWithPartials(string, [hash, {}, {'shared/dude.thing': dude}], true, 'Dudes: Jeepers', 'Partials can use literal with points in paths');
}); });
it("Global Partials", function() { it('Global Partials', function() {
handlebarsEnv.registerPartial('global_test', '{{another_dude}}'); handlebarsEnv.registerPartial('globalTest', '{{anotherDude}}');
var string = "Dudes: {{> shared/dude}} {{> global_test}}"; var string = 'Dudes: {{> shared/dude}} {{> globalTest}}';
var dude = "{{name}}"; var dude = '{{name}}';
var hash = {name:"Jeepers", another_dude:"Creepers"}; var hash = {name: 'Jeepers', anotherDude: 'Creepers'};
shouldCompileToWithPartials(string, [hash, {}, {'shared/dude':dude}], true, "Dudes: Jeepers Creepers", "Partials can use globals or passed"); shouldCompileToWithPartials(string, [hash, {}, {'shared/dude': dude}], true, 'Dudes: Jeepers Creepers', 'Partials can use globals or passed');
handlebarsEnv.unregisterPartial('global_test'); handlebarsEnv.unregisterPartial('globalTest');
equals(handlebarsEnv.partials.global_test, undefined); equals(handlebarsEnv.partials.globalTest, undefined);
}); });
it("Multiple partial registration", function() { it('Multiple partial registration', function() {
handlebarsEnv.registerPartial({ handlebarsEnv.registerPartial({
'shared/dude': '{{name}}', 'shared/dude': '{{name}}',
global_test: '{{another_dude}}' globalTest: '{{anotherDude}}'
}); });
var string = "Dudes: {{> shared/dude}} {{> global_test}}"; var string = 'Dudes: {{> shared/dude}} {{> globalTest}}';
var hash = {name:"Jeepers", another_dude:"Creepers"}; var hash = {name: 'Jeepers', anotherDude: 'Creepers'};
shouldCompileToWithPartials(string, [hash], true, "Dudes: Jeepers Creepers", "Partials can use globals or passed"); shouldCompileToWithPartials(string, [hash], true, 'Dudes: Jeepers Creepers', 'Partials can use globals or passed');
}); });
it("Partials with integer path", function() { it('Partials with integer path', function() {
var string = "Dudes: {{> 404}}"; var string = 'Dudes: {{> 404}}';
var dude = "{{name}}"; var dude = '{{name}}';
var hash = {name:"Jeepers", another_dude:"Creepers"}; var hash = {name: 'Jeepers', anotherDude: 'Creepers'};
shouldCompileToWithPartials(string, [hash, {}, {404:dude}], true, "Dudes: Jeepers", "Partials can use literal paths"); shouldCompileToWithPartials(string, [hash, {}, {404: dude}], true, 'Dudes: Jeepers', 'Partials can use literal paths');
}); });
it("Partials with complex path", function() { it('Partials with complex path', function() {
var string = "Dudes: {{> 404/asdf?.bar}}"; var string = 'Dudes: {{> 404/asdf?.bar}}';
var dude = "{{name}}"; var dude = '{{name}}';
var hash = {name:"Jeepers", another_dude:"Creepers"}; var hash = {name: 'Jeepers', anotherDude: 'Creepers'};
shouldCompileToWithPartials(string, [hash, {}, {'404/asdf?.bar':dude}], true, "Dudes: Jeepers", "Partials can use literal paths"); shouldCompileToWithPartials(string, [hash, {}, {'404/asdf?.bar': dude}], true, 'Dudes: Jeepers', 'Partials can use literal paths');
}); });
it("Partials with escaped", function() { it('Partials with escaped', function() {
var string = "Dudes: {{> [+404/asdf?.bar]}}"; var string = 'Dudes: {{> [+404/asdf?.bar]}}';
var dude = "{{name}}"; var dude = '{{name}}';
var hash = {name:"Jeepers", another_dude:"Creepers"}; var hash = {name: 'Jeepers', anotherDude: 'Creepers'};
shouldCompileToWithPartials(string, [hash, {}, {'+404/asdf?.bar':dude}], true, "Dudes: Jeepers", "Partials can use literal paths"); shouldCompileToWithPartials(string, [hash, {}, {'+404/asdf?.bar': dude}], true, 'Dudes: Jeepers', 'Partials can use literal paths');
}); });
it("Partials with string", function() { it('Partials with string', function() {
var string = "Dudes: {{> \"+404/asdf?.bar\"}}"; var string = 'Dudes: {{> \'+404/asdf?.bar\'}}';
var dude = "{{name}}"; var dude = '{{name}}';
var hash = {name:"Jeepers", another_dude:"Creepers"}; var hash = {name: 'Jeepers', anotherDude: 'Creepers'};
shouldCompileToWithPartials(string, [hash, {}, {'+404/asdf?.bar':dude}], true, "Dudes: Jeepers", "Partials can use literal paths"); shouldCompileToWithPartials(string, [hash, {}, {'+404/asdf?.bar': dude}], true, 'Dudes: Jeepers', 'Partials can use literal paths');
}); });
it('should handle empty partial', function() { it('should handle empty partial', function() {
var string = "Dudes: {{#dudes}}{{> dude}}{{/dudes}}"; var string = 'Dudes: {{#dudes}}{{> dude}}{{/dudes}}';
var partial = ""; var partial = '';
var hash = {dudes: [{name: "Yehuda", url: "http://yehuda"}, {name: "Alan", url: "http://alan"}]}; var hash = {dudes: [{name: 'Yehuda', url: 'http://yehuda'}, {name: 'Alan', url: 'http://alan'}]};
shouldCompileToWithPartials(string, [hash, {}, {dude: partial}], true, "Dudes: "); shouldCompileToWithPartials(string, [hash, {}, {dude: partial}], true, 'Dudes: ');
}); });
it("throw on missing partial", function() { it('throw on missing partial', function() {
var compile = handlebarsEnv.compile; var compile = handlebarsEnv.compile;
handlebarsEnv.compile = undefined; handlebarsEnv.compile = undefined;
shouldThrow(function() { shouldThrow(function() {
@@ -190,6 +211,224 @@ describe('partials', function() {
handlebarsEnv.compile = compile; 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() { it('should pass compiler flags', function() {
if (Handlebars.compile) { if (Handlebars.compile) {
var env = Handlebars.create(); var env = Handlebars.create();
@@ -200,28 +439,28 @@ describe('partials', function() {
}); });
describe('standalone partials', function() { describe('standalone partials', function() {
it("indented partials", function() { it('indented partials', function() {
var string = "Dudes:\n{{#dudes}}\n {{>dude}}\n{{/dudes}}"; var string = 'Dudes:\n{{#dudes}}\n {{>dude}}\n{{/dudes}}';
var dude = "{{name}}\n"; var dude = '{{name}}\n';
var hash = {dudes: [{name: "Yehuda", url: "http://yehuda"}, {name: "Alan", url: "http://alan"}]}; var hash = {dudes: [{name: 'Yehuda', url: 'http://yehuda'}, {name: 'Alan', url: 'http://alan'}]};
shouldCompileToWithPartials(string, [hash, {}, {dude: dude}], true, shouldCompileToWithPartials(string, [hash, {}, {dude: dude}], true,
"Dudes:\n Yehuda\n Alan\n"); 'Dudes:\n Yehuda\n Alan\n');
}); });
it("nested indented partials", function() { it('nested indented partials', function() {
var string = "Dudes:\n{{#dudes}}\n {{>dude}}\n{{/dudes}}"; var string = 'Dudes:\n{{#dudes}}\n {{>dude}}\n{{/dudes}}';
var dude = "{{name}}\n {{> url}}"; var dude = '{{name}}\n {{> url}}';
var url = "{{url}}!\n"; var url = '{{url}}!\n';
var hash = {dudes: [{name: "Yehuda", url: "http://yehuda"}, {name: "Alan", url: "http://alan"}]}; var hash = {dudes: [{name: 'Yehuda', url: 'http://yehuda'}, {name: 'Alan', url: 'http://alan'}]};
shouldCompileToWithPartials(string, [hash, {}, {dude: dude, url: url}], true, 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() { it('prevent nested indented partials', function() {
var string = "Dudes:\n{{#dudes}}\n {{>dude}}\n{{/dudes}}"; var string = 'Dudes:\n{{#dudes}}\n {{>dude}}\n{{/dudes}}';
var dude = "{{name}}\n {{> url}}"; var dude = '{{name}}\n {{> url}}';
var url = "{{url}}!\n"; var url = '{{url}}!\n';
var hash = {dudes: [{name: "Yehuda", url: "http://yehuda"}, {name: "Alan", url: "http://alan"}]}; var hash = {dudes: [{name: 'Yehuda', url: 'http://yehuda'}, {name: 'Alan', url: 'http://alan'}]};
shouldCompileToWithPartials(string, [hash, {}, {dude: dude, url: url}, {preventIndent: true}], true, 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'}]}; 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 '); 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() { it('partials can access parents without data', function() {
var string = 'Dudes: {{#dudes}}{{> dude}}{{/dudes}}'; var string = 'Dudes: {{#dudes}}{{> dude}}{{/dudes}}';
var partial = '{{name}} ({{url}}) {{root}} '; var partial = '{{name}} ({{url}}) {{root}} ';
+123 -44
View File
@@ -1,6 +1,4 @@
/*global shouldThrow */ /* eslint-disable no-console */
describe('precompiler', function() { describe('precompiler', function() {
// NOP Under non-node environments // NOP Under non-node environments
if (typeof process === 'undefined') { if (typeof process === 'undefined') {
@@ -8,7 +6,7 @@ describe('precompiler', function() {
} }
var Handlebars = require('../lib'), var Handlebars = require('../lib'),
Precompiler = require('../lib/precompiler'), Precompiler = require('../dist/cjs/precompiler'),
fs = require('fs'), fs = require('fs'),
uglify = require('uglify-js'); uglify = require('uglify-js');
@@ -18,6 +16,12 @@ describe('precompiler', function() {
precompile, precompile,
minify, minify,
emptyTemplate = {
path: __dirname + '/artifacts/empty.handlebars',
name: 'empty',
source: ''
},
file, file,
content, content,
writeFileSync; writeFileSync;
@@ -53,10 +57,9 @@ describe('precompiler', function() {
Precompiler.cli({templates: []}); Precompiler.cli({templates: []});
}, Handlebars.Exception, 'Must define at least one template or directory.'); }, Handlebars.Exception, 'Must define at least one template or directory.');
}); });
it('should throw on missing template', function() { it('should handle empty/filtered directories', function() {
shouldThrow(function() { Precompiler.cli({hasDirectory: true, templates: []});
Precompiler.cli({templates: ['foo']}); // Success is not throwing
}, Handlebars.Exception, 'Unable to open template file "foo"');
}); });
it('should throw when combining simple and minimized', function() { it('should throw when combining simple and minimized', function() {
shouldThrow(function() { shouldThrow(function() {
@@ -68,107 +71,183 @@ describe('precompiler', function() {
Precompiler.cli({templates: [__dirname + '/artifacts/empty.handlebars', __dirname + '/artifacts/empty.handlebars'], simple: true}); Precompiler.cli({templates: [__dirname + '/artifacts/empty.handlebars', __dirname + '/artifacts/empty.handlebars'], simple: true});
}, Handlebars.Exception, 'Unable to output multiple templates in simple mode'); }, 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() { it('should throw when combining simple and directories', function() {
shouldThrow(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'); }, 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() { it('should output simple templates', function() {
Handlebars.precompile = function() { return 'simple'; }; 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'); equal(log, 'simple\n');
}); });
it('should output amd templates', function() { it('should output amd templates', function() {
Handlebars.precompile = function() { return 'amd'; }; 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); equal(/template\(amd\)/.test(log), true);
}); });
it('should output multiple amd', function() { it('should output multiple amd', function() {
Handlebars.precompile = function() { return 'amd'; }; 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(/templates = foo = foo \|\|/.test(log), true);
equal(/return templates/.test(log), true); equal(/return templates/.test(log), true);
equal(/template\(amd\)/.test(log), true); equal(/template\(amd\)/.test(log), true);
}); });
it('should output amd partials', function() { it('should output amd partials', function() {
Handlebars.precompile = function() { return 'amd'; }; 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(/return Handlebars\.partials\['empty'\]/.test(log), true);
equal(/template\(amd\)/.test(log), true); equal(/template\(amd\)/.test(log), true);
}); });
it('should output multiple amd partials', function() { it('should output multiple amd partials', function() {
Handlebars.precompile = function() { return 'amd'; }; 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(/return Handlebars\.partials\[/.test(log), false);
equal(/template\(amd\)/.test(log), true); equal(/template\(amd\)/.test(log), true);
}); });
it('should output commonjs templates', function() { it('should output commonjs templates', function() {
Handlebars.precompile = function() { return 'commonjs'; }; 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); equal(/template\(commonjs\)/.test(log), true);
}); });
it('should set data flag', function() { it('should set data flag', function() {
Handlebars.precompile = function(data, options) { equal(options.data, true); return 'simple'; }; 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'); equal(log, 'simple\n');
}); });
it('should set known helpers', function() { it('should set known helpers', function() {
Handlebars.precompile = function(data, options) { equal(options.knownHelpers.foo, true); return 'simple'; }; 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'}); Precompiler.cli({templates: [emptyTemplate], simple: true, 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/'});
equal(log, 'simple\n'); equal(log, 'simple\n');
}); });
it('should output to file system', function() { it('should output to file system', function() {
Handlebars.precompile = function() { return 'simple'; }; 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(file, 'file!');
equal(content, 'simple\n'); equal(content, 'simple\n');
equal(log, ''); 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() { it('should output minimized templates', function() {
Handlebars.precompile = function() { return 'amd'; }; Handlebars.precompile = function() { return 'amd'; };
uglify.minify = function() { return {code: 'min'}; }; 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'); equal(log, 'min');
}); });
it('should output map', function() { 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(file, 'foo.js.map');
equal(/sourceMappingURL=/.test(log), true); equal(/sourceMappingURL=/.test(log), true);
}); });
it('should output map', function() { 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(file, 'foo.js.map');
equal(/sourceMappingURL=/.test(log), true); 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() { describe('Regressions', function() {
it("GH-94: Cannot read property of undefined", 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 data = {
var string = "{{#books}}{{title}}{{author.name}}{{/books}}"; 'books': [{
shouldCompileTo(string, data, "The origin of speciesCharles DarwinLazarillo de Tormes", 'title': 'The origin of species',
"Renders without an undefined property error"); '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() { 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, {}, '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: 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: 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, {set: true}, ' :: set', "inverted sections don't run when property is true");
}); });
it("GH-158: Using array index twice, breaks the template", function() { it('GH-158: Using array index twice, breaks the template', function() {
var string = "{{arr.[0]}}, {{arr.[1]}}"; var string = '{{arr.[0]}}, {{arr.[1]}}';
var data = { "arr": [1,2] }; 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() { it("bug reported by @fat where lambdas weren't being properly resolved", function() {
@@ -39,12 +47,12 @@ describe('Regressions', function() {
+ '{{/hasThings}}'; + '{{/hasThings}}';
var data = { var data = {
thing: function() { thing: function() {
return "blah"; return 'blah';
}, },
things: [ things: [
{className: "one", word: "@fat"}, {className: 'one', word: '@fat'},
{className: "two", word: "@dhg"}, {className: 'two', word: '@dhg'},
{className: "three", word:"@sayrer"} {className: 'three', word: '@sayrer'}
], ],
hasThings: function() { hasThings: function() {
return true; return true;
@@ -63,14 +71,14 @@ describe('Regressions', function() {
it('GH-408: Multiple loops fail', function() { it('GH-408: Multiple loops fail', function() {
var context = [ var context = [
{ name: "John Doe", location: { city: "Chicago" } }, { name: 'John Doe', location: { city: 'Chicago' } },
{ name: "Jane Doe", location: { city: "New York"} } { name: 'Jane Doe', location: { city: 'New York'} }
]; ];
var template = CompilerContext.compile('{{#.}}{{name}}{{/.}}{{#.}}{{name}}{{/.}}{{#.}}{{name}}{{/.}}'); var template = CompilerContext.compile('{{#.}}{{name}}{{/.}}{{#.}}{{name}}{{/.}}{{#.}}{{name}}{{/.}}');
var result = template(context); 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() { it('GS-428: Nested if else rendering', function() {
@@ -95,11 +103,13 @@ describe('Regressions', function() {
}); });
it('GH-534: Object prototype aliases', function() { it('GH-534: Object prototype aliases', function() {
/* eslint-disable no-extend-native */
Object.prototype[0xD834] = true; Object.prototype[0xD834] = true;
shouldCompileTo('{{foo}}', { foo: 'bar' }, 'bar'); shouldCompileTo('{{foo}}', { foo: 'bar' }, 'bar');
delete Object.prototype[0xD834]; delete Object.prototype[0xD834];
/* eslint-enable no-extend-native */
}); });
it('GH-437: Matching escaping', function() { it('GH-437: Matching escaping', function() {
@@ -111,23 +121,23 @@ describe('Regressions', function() {
}, Error); }, Error);
}); });
it("GH-676: Using array in escaping mustache fails", function() { it('GH-676: Using array in escaping mustache fails', function() {
var string = "{{arr}}"; var string = '{{arr}}';
var data = { "arr": [1,2] }; 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() { it('Mustache man page', function() {
var string = "Hello {{name}}. You have just won ${{value}}!{{#in_ca}} Well, ${{taxed_value}}, after taxes.{{/in_ca}}"; var string = 'Hello {{name}}. You have just won ${{value}}!{{#in_ca}} Well, ${{taxed_value}}, after taxes.{{/in_ca}}';
var data = { var data = {
"name": "Chris", 'name': 'Chris',
"value": 10000, 'value': 10000,
"taxed_value": 10000 - (10000 * 0.4), 'taxed_value': 10000 - (10000 * 0.4),
"in_ca": true '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() { it('GH-731: zero context rendering', function() {
@@ -162,4 +172,109 @@ describe('Regressions', function() {
var result = template(context); var result = template(context);
equals(result, 'foo'); 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() { describe('Require', function() {
it('Load .handlebars files with require()', function() { it('Load .handlebars files with require()', function() {
var template = require("./artifacts/example_1"); var template = require('./artifacts/example_1');
equal(template, require("./artifacts/example_1.handlebars")); equal(template, require('./artifacts/example_1.handlebars'));
var expected = 'foo\n'; var expected = 'foo\n';
var result = template({foo: "foo"}); var result = template({foo: 'foo'});
equal(result, expected); equal(result, expected);
}); });
it('Load .hbs files with require()', function() { it('Load .hbs files with require()', function() {
var template = require("./artifacts/example_2"); var template = require('./artifacts/example_2');
equal(template, require("./artifacts/example_2.hbs")); equal(template, require('./artifacts/example_2.hbs'));
var expected = 'Hello, World!\n'; var expected = 'Hello, World!\n';
var result = template({name: "World"}); var result = template({name: 'World'});
equal(result, expected); equal(result, expected);
}); });
+3 -5
View File
@@ -1,5 +1,3 @@
/*globals Handlebars, shouldThrow */
describe('runtime', function() { describe('runtime', function() {
describe('#template', function() { describe('#template', function() {
it('should throw on invalid templates', function() { it('should throw on invalid templates', function() {
@@ -16,19 +14,19 @@ describe('runtime', function() {
it('should throw on version mismatch', function() { it('should throw on version mismatch', function() {
shouldThrow(function() { shouldThrow(function() {
Handlebars.template({ Handlebars.template({
main: true, main: {},
compiler: [Handlebars.COMPILER_REVISION + 1] compiler: [Handlebars.COMPILER_REVISION + 1]
}); });
}, Error, /Template was precompiled with a newer version of Handlebars than the current runtime/); }, Error, /Template was precompiled with a newer version of Handlebars than the current runtime/);
shouldThrow(function() { shouldThrow(function() {
Handlebars.template({ Handlebars.template({
main: true, main: {},
compiler: [Handlebars.COMPILER_REVISION - 1] compiler: [Handlebars.COMPILER_REVISION - 1]
}); });
}, Error, /Template was precompiled with an older version of Handlebars than the current runtime/); }, Error, /Template was precompiled with an older version of Handlebars than the current runtime/);
shouldThrow(function() { shouldThrow(function() {
Handlebars.template({ Handlebars.template({
main: true main: {}
}); });
}, Error, /Template was precompiled with an older version of Handlebars than the current runtime/); }, Error, /Template was precompiled with an older version of Handlebars than the current runtime/);
}); });
+8 -7
View File
@@ -1,7 +1,8 @@
/*global CompilerContext, Handlebars */
try { try {
var SourceMap = require('source-map'), if (typeof define !== 'function' || !define.amd) {
SourceMapConsumer = SourceMap.SourceMapConsumer; var SourceMap = require('source-map'),
SourceMapConsumer = SourceMap.SourceMapConsumer;
}
} catch (err) { } catch (err) {
/* NOP for in browser */ /* NOP for in browser */
} }
@@ -18,13 +19,13 @@ describe('source-map', function() {
equal(!!template.map, !CompilerContext.browser); equal(!!template.map, !CompilerContext.browser);
}); });
it('should map source properly', function() { it('should map source properly', function() {
var source = ' b{{hello}} \n {{bar}}a {{#block arg hash=(subex 1 subval)}}{{/block}}', var templateSource = ' b{{hello}} \n {{bar}}a {{#block arg hash=(subex 1 subval)}}{{/block}}',
template = Handlebars.precompile(source, {destName: 'dest.js', srcName: 'src.hbs'}); template = Handlebars.precompile(templateSource, {destName: 'dest.js', srcName: 'src.hbs'});
if (template.map) { if (template.map) {
var consumer = new SourceMapConsumer(template.map), var consumer = new SourceMapConsumer(template.map),
lines = template.code.split('\n'), lines = template.code.split('\n'),
srcLines = source.split('\n'), srcLines = templateSource.split('\n'),
generated = grepLine('" b"', lines), generated = grepLine('" b"', lines),
source = grepLine(' b', srcLines); source = grepLine(' b', srcLines);
@@ -41,7 +42,7 @@ function grepLine(token, lines) {
var column = lines[i].indexOf(token); var column = lines[i].indexOf(token);
if (column >= 0) { if (column >= 0) {
return { return {
line: i+1, line: i + 1,
column: column column: column
}; };
} }
+6 -5
View File
@@ -5,12 +5,11 @@ describe('spec', function() {
} }
var _ = require('underscore'), var _ = require('underscore'),
Handlebars = require('../lib'),
fs = require('fs'); fs = require('fs');
var specDir =__dirname + '/mustache/specs/'; var specDir = __dirname + '/mustache/specs/';
var specs = _.filter(fs.readdirSync(specDir), function(name) { var specs = _.filter(fs.readdirSync(specDir), function(name) {
return /.*\.json$/.test(name); return (/.*\.json$/).test(name);
}); });
_.each(specs, function(name) { _.each(specs, function(name) {
@@ -26,8 +25,8 @@ describe('spec', function() {
// We nest the entire response from partials, not just the literals // We nest the entire response from partials, not just the literals
|| (name === 'partials.json' && test.name === 'Standalone Indentation') || (name === 'partials.json' && test.name === 'Standalone Indentation')
|| /\{\{\=/.test(test.template) || (/\{\{\=/).test(test.template)
|| _.any(test.partials, function(partial) { return /\{\{\=/.test(partial); })) { || _.any(test.partials, function(partial) { return (/\{\{\=/).test(partial); })) {
it.skip(name + ' - ' + test.name); it.skip(name + ' - ' + test.name);
return; return;
} }
@@ -35,7 +34,9 @@ describe('spec', function() {
var data = _.clone(test.data); var data = _.clone(test.data);
if (data.lambda) { if (data.lambda) {
// Blergh // Blergh
/* eslint-disable no-eval */
data.lambda = eval('(' + data.lambda.js + ')'); data.lambda = eval('(' + data.lambda.js + ')');
/* eslint-enable no-eval */
} }
it(name + ' - ' + test.name, function() { it(name + ' - ' + test.name, function() {
if (test.partials) { if (test.partials) {
+17 -1
View File
@@ -1,4 +1,3 @@
/*global CompilerContext, Handlebars, shouldThrow */
var Exception = Handlebars.Exception; var Exception = Handlebars.Exception;
describe('strict', function() { describe('strict', function() {
@@ -79,6 +78,23 @@ describe('strict', function() {
template({hello: {}}); template({hello: {}});
}, Exception, /"bar" not defined in/); }, 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() { describe('assume objects', function() {
+44 -45
View File
@@ -1,44 +1,43 @@
/*global CompilerContext */
describe('string params mode', function() { 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 template = CompilerContext.compile('{{wycats is.a slave.driver}}', {stringParams: true});
var helpers = { var helpers = {
wycats: function(passiveVoice, noun) { wycats: function(passiveVoice, noun) {
return "HELP ME MY BOSS " + passiveVoice + ' ' + noun; return 'HELP ME MY BOSS ' + passiveVoice + ' ' + noun;
} }
}; };
var result = template({}, {helpers: helpers}); 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 template = CompilerContext.compile('{{#wycats is.a slave.driver}}help :({{/wycats}}', {stringParams: true});
var helpers = { var helpers = {
wycats: function(passiveVoice, noun, options) { wycats: function(passiveVoice, noun, options) {
return "HELP ME MY BOSS " + passiveVoice + ' ' + return 'HELP ME MY BOSS ' + passiveVoice + ' ' +
noun + ': ' + options.fn(this); noun + ': ' + options.fn(this);
} }
}; };
var result = template({}, {helpers: helpers}); 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 template = CompilerContext.compile('{{#with dale}}{{tomdale ../need dad.joke}}{{/with}}', {stringParams: true});
var helpers = { var helpers = {
tomdale: function(desire, noun, options) { tomdale: function(desire, noun, options) {
return "STOP ME FROM READING HACKER NEWS I " + return 'STOP ME FROM READING HACKER NEWS I ' +
options.contexts[0][desire] + " " + noun; options.contexts[0][desire] + ' ' + noun;
}, },
"with": function(context, options) { 'with': function(context, options) {
return options.fn(options.contexts[0][context]); return options.fn(options.contexts[0][context]);
} }
}; };
@@ -49,31 +48,31 @@ describe('string params mode', function() {
need: 'need-a' need: 'need-a'
}, {helpers: helpers}); }, {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() { it('information about the types is passed along', function() {
var template = CompilerContext.compile('{{tomdale "need" dad.joke true false}}', { stringParams: true }); var template = CompilerContext.compile("{{tomdale 'need' dad.joke true false}}", { stringParams: true });
var helpers = { var helpers = {
tomdale: function(desire, noun, trueBool, falseBool, options) { tomdale: function(desire, noun, trueBool, falseBool, options) {
equal(options.types[0], 'StringLiteral', "the string type is passed"); equal(options.types[0], 'StringLiteral', 'the string type is passed');
equal(options.types[1], 'PathExpression', "the expression type is passed"); equal(options.types[1], 'PathExpression', 'the expression type is passed');
equal(options.types[2], 'BooleanLiteral', "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(desire, 'need', 'the string form is passed for strings');
equal(noun, "dad.joke", "the string form is passed for expressions"); equal(noun, 'dad.joke', 'the string form is passed for expressions');
equal(trueBool, true, "raw booleans are passed through"); equal(trueBool, true, 'raw booleans are passed through');
equal(falseBool, false, "raw booleans are passed through"); equal(falseBool, false, 'raw booleans are passed through');
return "Helper called"; return 'Helper called';
} }
}; };
var result = template({}, { helpers: helpers }); var result = template({}, { helpers: helpers });
equal(result, "Helper called"); equal(result, 'Helper called');
}); });
it("hash parameters get type information", function() { it('hash parameters get type information', function() {
var template = CompilerContext.compile('{{tomdale he.says desire="need" noun=dad.joke bool=true}}', { stringParams: true }); var template = CompilerContext.compile("{{tomdale he.says desire='need' noun=dad.joke bool=true}}", { stringParams: true });
var helpers = { var helpers = {
tomdale: function(exclamation, options) { tomdale: function(exclamation, options) {
@@ -94,43 +93,43 @@ describe('string params mode', function() {
equal(result, 'Helper called'); equal(result, 'Helper called');
}); });
it("hash parameters get context information", function() { 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 template = CompilerContext.compile("{{#with dale}}{{tomdale he.says desire='need' noun=../dad/joke bool=true}}{{/with}}", { stringParams: true });
var context = {dale: {}}; var context = {dale: {}};
var helpers = { var helpers = {
tomdale: function(exclamation, options) { tomdale: function(exclamation, options) {
equal(exclamation, "he.says"); equal(exclamation, 'he.says');
equal(options.types[0], 'PathExpression'); equal(options.types[0], 'PathExpression');
equal(options.contexts.length, 1); equal(options.contexts.length, 1);
equal(options.hashContexts.noun, context); equal(options.hashContexts.noun, context);
equal(options.hash.desire, "need"); equal(options.hash.desire, 'need');
equal(options.hash.noun, "dad.joke"); equal(options.hash.noun, 'dad.joke');
equal(options.hash.bool, true); equal(options.hash.bool, true);
return "Helper called"; return 'Helper called';
}, },
"with": function(context, options) { 'with': function(withContext, options) {
return options.fn(options.contexts[0][context]); return options.fn(options.contexts[0][withContext]);
} }
}; };
var result = template(context, { helpers: helpers }); 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 template = CompilerContext.compile('{{#with dale}}{{#tomdale ../need dad.joke}}wot{{/tomdale}}{{/with}}', {stringParams: true});
var helpers = { var helpers = {
tomdale: function(desire, noun, options) { tomdale: function(desire, noun, options) {
return "STOP ME FROM READING HACKER NEWS I " + return 'STOP ME FROM READING HACKER NEWS I ' +
options.contexts[0][desire] + " " + noun + " " + options.contexts[0][desire] + ' ' + noun + ' ' +
options.fn(this); options.fn(this);
}, },
"with": function(context, options) { 'with': function(context, options) {
return options.fn(options.contexts[0][context]); return options.fn(options.contexts[0][context]);
} }
}; };
@@ -141,23 +140,23 @@ describe('string params mode', function() {
need: 'need-a' need: 'need-a'
}, {helpers: helpers}); }, {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 template = CompilerContext.compile('{{#with content}}{{#view}}{{firstName}} {{lastName}}{{/view}}{{/with}}', {stringParams: true});
var helpers = { var helpers = {
'with': function(options) { 'with': function() {
return "WITH"; return 'WITH';
}, },
view: function() { view: function() {
return "VIEW"; return 'VIEW';
} }
}; };
var result = template({}, {helpers: helpers}); var result = template({}, {helpers: helpers});
equals(result, "WITH"); equals(result, 'WITH');
}); });
it('should handle DATA', function() { it('should handle DATA', function() {
+77 -79
View File
@@ -1,134 +1,133 @@
/*global CompilerContext, Handlebars, shouldCompileTo, shouldThrow */
describe('subexpressions', function() { describe('subexpressions', function() {
it("arg-less helper", function() { it('arg-less helper', function() {
var string = "{{foo (bar)}}!"; var string = '{{foo (bar)}}!';
var context = {}; var context = {};
var helpers = { var helpers = {
foo: function(val) { foo: function(val) {
return val+val; return val + val;
}, },
bar: function() { 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 string = '{{blog (equal a b)}}';
var context = { bar: "LOL" }; var context = { bar: 'LOL' };
var helpers = { var helpers = {
blog: function(val) { blog: function(val) {
return "val is " + val; return 'val is ' + val;
}, },
equal: function(x, y) { equal: function(x, y) {
return 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 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 = { var helpers = {
blog: function(val, that, theOther) { blog: function(val, that, theOther) {
return "val is " + val + ', ' + that + ' and ' + theOther; return 'val is ' + val + ', ' + that + ' and ' + theOther;
}, },
equal: function(x, y) { equal: function(x, y) {
return 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 string = '{{blog (equal (equal true true) true)}}';
var context = { bar: "LOL" }; var context = { bar: 'LOL' };
var helpers = { var helpers = {
blog: function(val) { blog: function(val) {
return "val is " + val; return 'val is ' + val;
}, },
equal: function(x, y) { equal: function(x, y) {
return 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() { it('GH-800 : Complex subexpressions', function() {
var context = {a: 'a', b:'b', c:{c:'c'}, d:'d', e: {e: 'e'}}; var context = {a: 'a', b: 'b', c: {c: 'c'}, d: 'd', e: {e: 'e'}};
var helpers = { var helpers = {
dash: function(a, b) { dash: function(a, b) {
return a + "-" + b; return a + '-' + b;
}, },
concat: function(a, b) { concat: function(a, b) {
return 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 d (concat a b)}}', [context, helpers], 'd-ab');
shouldCompileTo('{{dash c.c (concat a b)}}', [context, helpers], 'c-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 b) c.c}}', [context, helpers], 'ab-c');
shouldCompileTo('{{dash (concat a e.e) c.c}}', [context, helpers], 'ae-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 string = '{{equal (equal true true) true}}';
var lastOptions = null; var lastOptions = null;
var helpers = { var helpers = {
equal: function(x, y, options) { equal: function(x, y, options) {
if (!options || options === lastOptions) { if (!options || options === lastOptions) {
throw new Error("options hash was reused"); throw new Error('options hash was reused');
} }
lastOptions = options; lastOptions = options;
return x === y; return x === y;
} }
}; };
shouldCompileTo(string, [{}, helpers], "true"); shouldCompileTo(string, [{}, helpers], 'true');
}); });
it("with hashes", function() { it('with hashes', function() {
var string = '{{blog (equal (equal true true) true fun="yes")}}'; var string = "{{blog (equal (equal true true) true fun='yes')}}";
var context = { bar: "LOL" }; var context = { bar: 'LOL' };
var helpers = { var helpers = {
blog: function(val) { blog: function(val) {
return "val is " + val; return 'val is ' + val;
}, },
equal: function(x, y) { equal: function(x, y) {
return x === y; return x === y;
} }
}; };
shouldCompileTo(string, [context, helpers], "val is true"); shouldCompileTo(string, [context, helpers], 'val is true');
}); });
it("as hashes", function() { it('as hashes', function() {
var string = '{{blog fun=(equal (blog fun=1) "val is 1")}}'; var string = "{{blog fun=(equal (blog fun=1) 'val is 1')}}";
var helpers = { var helpers = {
blog: function(options) { blog: function(options) {
return "val is " + options.hash.fun; return 'val is ' + options.hash.fun;
}, },
equal: function(x, y) { equal: function(x, y) {
return 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 string = '{{input aria-label=(t "Name") placeholder=(t "Example User")}}';
var helpers = { var helpers = {
input: function(options) { input: function(options) {
var hash = options.hash; var hash = options.hash;
var ariaLabel = Handlebars.Utils.escapeExpression(hash['aria-label']); var ariaLabel = Handlebars.Utils.escapeExpression(hash['aria-label']);
var placeholder = Handlebars.Utils.escapeExpression(hash.placeholder); var placeholder = Handlebars.Utils.escapeExpression(hash.placeholder);
return new Handlebars.SafeString('<input aria-label="' + ariaLabel + '" placeholder="' + placeholder + '" />'); return new Handlebars.SafeString('<input aria-label="' + ariaLabel + '" placeholder="' + placeholder + '" />');
}, },
@@ -139,20 +138,20 @@ describe('subexpressions', function() {
shouldCompileTo(string, [{}, helpers], '<input aria-label="Name" placeholder="Example User" />'); 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 string = '{{input aria-label=(t item.field) placeholder=(t item.placeholder)}}';
var context = { var context = {
item: { item: {
field: "Name", field: 'Name',
placeholder: "Example User" placeholder: 'Example User'
} }
}; };
var helpers = { var helpers = {
input: function(options) { input: function(options) {
var hash = options.hash; var hash = options.hash;
var ariaLabel = Handlebars.Utils.escapeExpression(hash['aria-label']); var ariaLabel = Handlebars.Utils.escapeExpression(hash['aria-label']);
var placeholder = Handlebars.Utils.escapeExpression(hash.placeholder); var placeholder = Handlebars.Utils.escapeExpression(hash.placeholder);
return new Handlebars.SafeString('<input aria-label="' + ariaLabel + '" placeholder="' + placeholder + '" />'); return new Handlebars.SafeString('<input aria-label="' + ariaLabel + '" placeholder="' + placeholder + '" />');
}, },
@@ -163,21 +162,21 @@ describe('subexpressions', function() {
shouldCompileTo(string, [context, helpers], '<input aria-label="Name" placeholder="Example User" />'); 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 template = CompilerContext.compile('{{snog (blorg foo x=y) yeah a=b}}', {stringParams: true});
var helpers = { var helpers = {
snog: function(a, b, options) { snog: function(a, b, options) {
equals(a, 'foo'); equals(a, 'foo');
equals(options.types.length, 2, "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[0], 'SubExpression', 'string params for outer helper processed correctly');
equals(options.types[1], 'PathExpression', "string params for outer helper processed correctly"); equals(options.types[1], 'PathExpression', 'string params for outer helper processed correctly');
return a + b; return a + b;
}, },
blorg: function(a, options) { blorg: function(a, options) {
equals(options.types.length, 1, "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"); equals(options.types[0], 'PathExpression', 'string params for inner helper processed correctly');
return a; return a;
} }
}; };
@@ -187,54 +186,53 @@ describe('subexpressions', function() {
yeah: {} yeah: {}
}, {helpers: helpers}); }, {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 template = CompilerContext.compile('{{blog fun=(bork)}}', {stringParams: true});
var helpers = { var helpers = {
blog: function(options) { blog: function(options) {
equals(options.hashTypes.fun, 'SubExpression'); equals(options.hashTypes.fun, 'SubExpression');
return "val is " + options.hash.fun; return 'val is ' + options.hash.fun;
}, },
bork: function() { bork: function() {
return "BORK"; return 'BORK';
} }
}; };
var result = template({}, {helpers: helpers}); var result = template({}, {helpers: helpers});
equals(result, "val is BORK"); equals(result, 'val is BORK');
}); });
it("subexpression functions on the context", function() { it('subexpression functions on the context', function() {
var string = "{{foo (bar)}}!"; var string = '{{foo (bar)}}!';
var context = { var context = {
bar: function() { bar: function() {
return "LOL"; return 'LOL';
} }
}; };
var helpers = { var helpers = {
foo: function(val) { foo: function(val) {
return val+val; return val + val;
} }
}; };
shouldCompileTo(string, [context, helpers], "LOLLOL!"); shouldCompileTo(string, [context, helpers], 'LOLLOL!');
}); });
it("subexpressions can't just be property lookups", function() { it("subexpressions can't just be property lookups", function() {
var string = "{{foo (bar)}}!"; var string = '{{foo (bar)}}!';
var context = { var context = {
bar: "LOL" bar: 'LOL'
}; };
var helpers = { var helpers = {
foo: function(val) { foo: function(val) {
return val+val; return val + val;
} }
}; };
shouldThrow(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) { function shouldMatchTokens(result, tokens) {
for (var index = 0; index < result.length; index++) { for (var index = 0; index < result.length; index++) {
equals(result[index].name, tokens[index]); equals(result[index].name, tokens[index]);
@@ -22,7 +21,7 @@ describe('Tokenizer', function() {
var out = [], var out = [],
token; token;
while (token = lexer.lex()) { while ((token = lexer.lex())) {
var result = parser.terminals_[token] || token; var result = parser.terminals_[token] || token;
if (!result || result === 'EOF' || result === 'INVALID') { if (!result || result === 'EOF' || result === 'INVALID') {
break; break;
@@ -34,366 +33,391 @@ describe('Tokenizer', function() {
} }
it('tokenizes a simple mustache as "OPEN ID CLOSE"', function() { it('tokenizes a simple mustache as "OPEN ID CLOSE"', function() {
var result = tokenize("{{foo}}"); var result = tokenize('{{foo}}');
shouldMatchTokens(result, ['OPEN', 'ID', 'CLOSE']); shouldMatchTokens(result, ['OPEN', 'ID', 'CLOSE']);
shouldBeToken(result[1], "ID", "foo"); shouldBeToken(result[1], 'ID', 'foo');
}); });
it('supports unescaping with &', function() { it('supports unescaping with &', function() {
var result = tokenize("{{&bar}}"); var result = tokenize('{{&bar}}');
shouldMatchTokens(result, ['OPEN', 'ID', 'CLOSE']); shouldMatchTokens(result, ['OPEN', 'ID', 'CLOSE']);
shouldBeToken(result[0], "OPEN", "{{&"); shouldBeToken(result[0], 'OPEN', '{{&');
shouldBeToken(result[1], "ID", "bar"); shouldBeToken(result[1], 'ID', 'bar');
}); });
it('supports unescaping with {{{', function() { it('supports unescaping with {{{', function() {
var result = tokenize("{{{bar}}}"); var result = tokenize('{{{bar}}}');
shouldMatchTokens(result, ['OPEN_UNESCAPED', 'ID', 'CLOSE_UNESCAPED']); shouldMatchTokens(result, ['OPEN_UNESCAPED', 'ID', 'CLOSE_UNESCAPED']);
shouldBeToken(result[1], "ID", "bar"); shouldBeToken(result[1], 'ID', 'bar');
}); });
it('supports escaping delimiters', function() { 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']); shouldMatchTokens(result, ['OPEN', 'ID', 'CLOSE', 'CONTENT', 'CONTENT', 'OPEN', 'ID', 'CLOSE']);
shouldBeToken(result[3], "CONTENT", " "); shouldBeToken(result[3], 'CONTENT', ' ');
shouldBeToken(result[4], "CONTENT", "{{bar}} "); shouldBeToken(result[4], 'CONTENT', '{{bar}} ');
}); });
it('supports escaping multiple delimiters', function() { 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']); shouldMatchTokens(result, ['OPEN', 'ID', 'CLOSE', 'CONTENT', 'CONTENT', 'CONTENT']);
shouldBeToken(result[3], "CONTENT", " "); shouldBeToken(result[3], 'CONTENT', ' ');
shouldBeToken(result[4], "CONTENT", "{{bar}} "); shouldBeToken(result[4], 'CONTENT', '{{bar}} ');
shouldBeToken(result[5], "CONTENT", "{{baz}}"); shouldBeToken(result[5], 'CONTENT', '{{baz}}');
}); });
it('supports escaping a triple stash', function() { 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']); 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() { 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']); shouldMatchTokens(result, ['OPEN', 'ID', 'CLOSE', 'CONTENT', 'OPEN', 'ID', 'CLOSE', 'CONTENT', 'OPEN', 'ID', 'CLOSE']);
shouldBeToken(result[3], "CONTENT", " \\"); shouldBeToken(result[3], 'CONTENT', ' \\');
shouldBeToken(result[5], "ID", "bar"); shouldBeToken(result[5], 'ID', 'bar');
}); });
it('supports escaping multiple escape characters', function() { 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']); shouldMatchTokens(result, ['OPEN', 'ID', 'CLOSE', 'CONTENT', 'OPEN', 'ID', 'CLOSE', 'CONTENT', 'OPEN', 'ID', 'CLOSE']);
shouldBeToken(result[3], "CONTENT", " \\"); shouldBeToken(result[3], 'CONTENT', ' \\');
shouldBeToken(result[5], "ID", "bar"); shouldBeToken(result[5], 'ID', 'bar');
shouldBeToken(result[7], "CONTENT", " \\"); shouldBeToken(result[7], 'CONTENT', ' \\');
shouldBeToken(result[9], "ID", "baz"); shouldBeToken(result[9], 'ID', 'baz');
}); });
it('supports escaped mustaches after escaped escape characters', function() { 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']); shouldMatchTokens(result, ['OPEN', 'ID', 'CLOSE', 'CONTENT', 'OPEN', 'ID', 'CLOSE', 'CONTENT', 'CONTENT', 'CONTENT']);
shouldBeToken(result[3], "CONTENT", " \\"); shouldBeToken(result[3], 'CONTENT', ' \\');
shouldBeToken(result[4], "OPEN", "{{"); shouldBeToken(result[4], 'OPEN', '{{');
shouldBeToken(result[5], "ID", "bar"); shouldBeToken(result[5], 'ID', 'bar');
shouldBeToken(result[7], "CONTENT", " "); shouldBeToken(result[7], 'CONTENT', ' ');
shouldBeToken(result[8], "CONTENT", "{{baz}}"); shouldBeToken(result[8], 'CONTENT', '{{baz}}');
}); });
it('supports escaped escape characters after escaped mustaches', function() { 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']); shouldMatchTokens(result, ['OPEN', 'ID', 'CLOSE', 'CONTENT', 'CONTENT', 'CONTENT', 'OPEN', 'ID', 'CLOSE']);
shouldBeToken(result[4], "CONTENT", "{{bar}} "); shouldBeToken(result[4], 'CONTENT', '{{bar}} ');
shouldBeToken(result[5], "CONTENT", "\\"); shouldBeToken(result[5], 'CONTENT', '\\');
shouldBeToken(result[6], "OPEN", "{{"); shouldBeToken(result[6], 'OPEN', '{{');
shouldBeToken(result[7], "ID", "baz"); shouldBeToken(result[7], 'ID', 'baz');
}); });
it('supports escaped escape character on a triple stash', function() { 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']); shouldMatchTokens(result, ['OPEN', 'ID', 'CLOSE', 'CONTENT', 'OPEN_UNESCAPED', 'ID', 'CLOSE_UNESCAPED', 'CONTENT', 'OPEN', 'ID', 'CLOSE']);
shouldBeToken(result[3], "CONTENT", " \\"); shouldBeToken(result[3], 'CONTENT', ' \\');
shouldBeToken(result[5], "ID", "bar"); shouldBeToken(result[5], 'ID', 'bar');
}); });
it('tokenizes a simple path', function() { it('tokenizes a simple path', function() {
var result = tokenize("{{foo/bar}}"); var result = tokenize('{{foo/bar}}');
shouldMatchTokens(result, ['OPEN', 'ID', 'SEP', 'ID', 'CLOSE']); shouldMatchTokens(result, ['OPEN', 'ID', 'SEP', 'ID', 'CLOSE']);
}); });
it('allows dot notation', function() { it('allows dot notation', function() {
var result = tokenize("{{foo.bar}}"); var result = tokenize('{{foo.bar}}');
shouldMatchTokens(result, ['OPEN', 'ID', 'SEP', 'ID', 'CLOSE']); 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() { it('allows path literals with []', function() {
var result = tokenize("{{foo.[bar]}}"); var result = tokenize('{{foo.[bar]}}');
shouldMatchTokens(result, ['OPEN', 'ID', 'SEP', 'ID', 'CLOSE']); shouldMatchTokens(result, ['OPEN', 'ID', 'SEP', 'ID', 'CLOSE']);
}); });
it('allows multiple path literals on a line with []', function() { 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']); 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() { it('tokenizes {{.}} as OPEN ID CLOSE', function() {
var result = tokenize("{{.}}"); var result = tokenize('{{.}}');
shouldMatchTokens(result, ['OPEN', 'ID', 'CLOSE']); shouldMatchTokens(result, ['OPEN', 'ID', 'CLOSE']);
}); });
it('tokenizes a path as "OPEN (ID SEP)* ID CLOSE"', function() { 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']); 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() { 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']); 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() { 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']); shouldMatchTokens(result, ['OPEN', 'ID', 'SEP', 'ID', 'CLOSE']);
shouldBeToken(result[1], "ID", "this"); shouldBeToken(result[1], 'ID', 'this');
shouldBeToken(result[3], "ID", "foo"); shouldBeToken(result[3], 'ID', 'foo');
}); });
it('tokenizes a simple mustache with spaces as "OPEN ID CLOSE"', function() { 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']); 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() { 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']); shouldMatchTokens(result, ['OPEN', 'ID', 'ID', 'CLOSE']);
shouldBeToken(result[1], "ID", "foo"); shouldBeToken(result[1], 'ID', 'foo');
}); });
it('tokenizes raw content as "CONTENT"', function() { 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']); shouldMatchTokens(result, ['CONTENT', 'OPEN', 'ID', 'CLOSE', 'CONTENT']);
shouldBeToken(result[0], "CONTENT", "foo "); shouldBeToken(result[0], 'CONTENT', 'foo ');
shouldBeToken(result[4], "CONTENT", " baz"); shouldBeToken(result[4], 'CONTENT', ' baz');
}); });
it('tokenizes a partial as "OPEN_PARTIAL ID CLOSE"', function() { it('tokenizes a partial as "OPEN_PARTIAL ID CLOSE"', function() {
var result = tokenize("{{> foo}}"); var result = tokenize('{{> foo}}');
shouldMatchTokens(result, ['OPEN_PARTIAL', 'ID', 'CLOSE']); shouldMatchTokens(result, ['OPEN_PARTIAL', 'ID', 'CLOSE']);
}); });
it('tokenizes a partial with context as "OPEN_PARTIAL ID ID CLOSE"', function() { 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']); shouldMatchTokens(result, ['OPEN_PARTIAL', 'ID', 'ID', 'CLOSE']);
}); });
it('tokenizes a partial without spaces as "OPEN_PARTIAL ID CLOSE"', function() { 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']); shouldMatchTokens(result, ['OPEN_PARTIAL', 'ID', 'CLOSE']);
}); });
it('tokenizes a partial space at the }); as "OPEN_PARTIAL ID CLOSE"', function() { 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']); shouldMatchTokens(result, ['OPEN_PARTIAL', 'ID', 'CLOSE']);
}); });
it('tokenizes a partial space at the }); as "OPEN_PARTIAL ID CLOSE"', function() { 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']); 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() { 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']); 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() { 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']); 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() { 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']); 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() { 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']); 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() { it('tokenizes inverse sections as "INVERSE"', function() {
shouldMatchTokens(tokenize("{{^}}"), ['INVERSE']); shouldMatchTokens(tokenize('{{^}}'), ['INVERSE']);
shouldMatchTokens(tokenize("{{else}}"), ['INVERSE']); shouldMatchTokens(tokenize('{{else}}'), ['INVERSE']);
shouldMatchTokens(tokenize("{{ else }}"), ['INVERSE']); shouldMatchTokens(tokenize('{{ else }}'), ['INVERSE']);
}); });
it('tokenizes inverse sections with ID as "OPEN_INVERSE ID CLOSE"', function() { 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']); 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() { 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']); 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() { 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']); shouldMatchTokens(result, ['OPEN', 'ID', 'ID', 'ID', 'CLOSE']);
shouldBeToken(result[1], "ID", "foo"); shouldBeToken(result[1], 'ID', 'foo');
shouldBeToken(result[2], "ID", "bar"); shouldBeToken(result[2], 'ID', 'bar');
shouldBeToken(result[3], "ID", "baz"); shouldBeToken(result[3], 'ID', 'baz');
}); });
it('tokenizes mustaches with String params as "OPEN ID ID STRING CLOSE"', function() { 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']); 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() { it('tokenizes mustaches with String params using single quotes 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']); 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() { 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']); 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() { it('tokenizes String params with escapes quotes as STRING', function() {
var result = tokenize('{{ foo "bar\\"baz" }}'); var result = tokenize('{{ foo "bar\\"baz" }}');
shouldMatchTokens(result, ['OPEN', 'ID', 'STRING', 'CLOSE']); 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() { it('tokenizes String params using single quotes with escapes quotes as STRING', function() {
var result = tokenize("{{ foo 'bar\\'baz' }}"); var result = tokenize("{{ foo 'bar\\'baz' }}");
shouldMatchTokens(result, ['OPEN', 'ID', 'STRING', 'CLOSE']); shouldMatchTokens(result, ['OPEN', 'ID', 'STRING', 'CLOSE']);
shouldBeToken(result[2], "STRING", "bar'baz"); shouldBeToken(result[2], 'STRING', "bar'baz");
}); });
it('tokenizes numbers', function() { it('tokenizes numbers', function() {
var result = tokenize('{{ foo 1 }}'); var result = tokenize('{{ foo 1 }}');
shouldMatchTokens(result, ['OPEN', 'ID', 'NUMBER', 'CLOSE']); shouldMatchTokens(result, ['OPEN', 'ID', 'NUMBER', 'CLOSE']);
shouldBeToken(result[2], "NUMBER", "1"); shouldBeToken(result[2], 'NUMBER', '1');
result = tokenize('{{ foo 1.1 }}'); result = tokenize('{{ foo 1.1 }}');
shouldMatchTokens(result, ['OPEN', 'ID', 'NUMBER', 'CLOSE']); shouldMatchTokens(result, ['OPEN', 'ID', 'NUMBER', 'CLOSE']);
shouldBeToken(result[2], "NUMBER", "1.1"); shouldBeToken(result[2], 'NUMBER', '1.1');
result = tokenize('{{ foo -1 }}'); result = tokenize('{{ foo -1 }}');
shouldMatchTokens(result, ['OPEN', 'ID', 'NUMBER', 'CLOSE']); shouldMatchTokens(result, ['OPEN', 'ID', 'NUMBER', 'CLOSE']);
shouldBeToken(result[2], "NUMBER", "-1"); shouldBeToken(result[2], 'NUMBER', '-1');
result = tokenize('{{ foo -1.1 }}'); result = tokenize('{{ foo -1.1 }}');
shouldMatchTokens(result, ['OPEN', 'ID', 'NUMBER', 'CLOSE']); shouldMatchTokens(result, ['OPEN', 'ID', 'NUMBER', 'CLOSE']);
shouldBeToken(result[2], "NUMBER", "-1.1"); shouldBeToken(result[2], 'NUMBER', '-1.1');
}); });
it('tokenizes booleans', function() { it('tokenizes booleans', function() {
var result = tokenize('{{ foo true }}'); var result = tokenize('{{ foo true }}');
shouldMatchTokens(result, ['OPEN', 'ID', 'BOOLEAN', 'CLOSE']); shouldMatchTokens(result, ['OPEN', 'ID', 'BOOLEAN', 'CLOSE']);
shouldBeToken(result[2], "BOOLEAN", "true"); shouldBeToken(result[2], 'BOOLEAN', 'true');
result = tokenize('{{ foo false }}'); result = tokenize('{{ foo false }}');
shouldMatchTokens(result, ['OPEN', 'ID', 'BOOLEAN', 'CLOSE']); 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() { 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']); 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']); 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']); 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']); 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']); 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']); 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']); 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']); 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']); 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() { it('tokenizes special @ identifiers', function() {
var result = tokenize("{{ @foo }}"); var result = tokenize('{{ @foo }}');
shouldMatchTokens(result, ['OPEN', 'DATA', 'ID', 'CLOSE']); 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']); 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']); 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() { 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() { 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() { it('tokenizes subexpressions', function() {
var result = tokenize("{{foo (bar)}}"); var result = tokenize('{{foo (bar)}}');
shouldMatchTokens(result, ['OPEN', 'ID', 'OPEN_SEXPR', 'ID', 'CLOSE_SEXPR', 'CLOSE']); shouldMatchTokens(result, ['OPEN', 'ID', 'OPEN_SEXPR', 'ID', 'CLOSE_SEXPR', 'CLOSE']);
shouldBeToken(result[1], "ID", "foo"); shouldBeToken(result[1], 'ID', 'foo');
shouldBeToken(result[3], "ID", "bar"); 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']); shouldMatchTokens(result, ['OPEN', 'ID', 'OPEN_SEXPR', 'ID', 'ID', 'CLOSE_SEXPR', 'CLOSE']);
shouldBeToken(result[1], "ID", "foo"); shouldBeToken(result[1], 'ID', 'foo');
shouldBeToken(result[3], "ID", "a-x"); shouldBeToken(result[3], 'ID', 'a-x');
shouldBeToken(result[4], "ID", "b-y"); shouldBeToken(result[4], 'ID', 'b-y');
}); });
it('tokenizes nested subexpressions', function() { 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']); 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[3], 'ID', 'bar');
shouldBeToken(result[5], "ID", "lol"); shouldBeToken(result[5], 'ID', 'lol');
shouldBeToken(result[6], "ID", "rofl"); shouldBeToken(result[6], 'ID', 'rofl');
shouldBeToken(result[10], "ID", "baz"); shouldBeToken(result[10], 'ID', 'baz');
}); });
it('tokenizes nested subexpressions: literals', function() { it('tokenizes nested subexpressions: literals', function() {
@@ -402,19 +426,19 @@ describe('Tokenizer', function() {
}); });
it('tokenizes block params', 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']); 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']); 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']); 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']); 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']); 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