Compare commits

...

178 Commits

Author SHA1 Message Date
Nils Knappmeier 64254b604b v3.0.5 2018-12-15 14:16:13 +01:00
Nils Knappmeier 878e980c0f Update release notes 2018-12-15 14:15:54 +01:00
Nils Knappmeier f6b5a288b4 docs: bump node version used in release 2018-12-15 14:13:57 +01:00
Nils Knappmeier 4ed0a625e3 chore: use node 10.x in travis-build 2018-12-15 14:08:55 +01:00
Nils Knappmeier e97741f9ab docs: add notes about required tools and versions for a release 2018-12-15 14:02:14 +01:00
Nils Knappmeier 698c8a93a4 v3.0.4 2018-12-15 13:55:00 +01:00
Nils Knappmeier 12cd4ef010 Update release notes 2018-12-15 13:54:17 +01:00
Nils Knappmeier ed9e301eb8 chore: add wepback@1.x to devDependencies
This is required by some other dependencies as
peerDependencies, which are not automatically installed
anymore since npm@3
2018-12-15 13:39:45 +01:00
Nils Knappmeier bbe684b7c7 chore: remove TODO comment line
This comment is not relevant for 3.x anymore, since this
release will only get bugfixes and not major refactorings.
It breaks the build though.
2018-12-15 13:35:26 +01:00
Nils Knappmeier f8d7b381dc docs: update CONTRIBUTING.md from master and add text about 3.x 2018-12-15 13:20:20 +01:00
kpdecker 4cd5305c7f Further relax uglify dependency 2016-04-11 13:22:17 +01:00
Paul Lynch d97c2e6dc0 Update uglify-js to avoid vulnerability 2016-04-11 13:22:04 +01:00
kpdecker 1c863e34ab Escape = in HTML content
There was a potential XSS exploit when using unquoted attributes that this should help reduce.

Fixes #1083
2016-02-08 18:56:04 +00: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
kpdecker 0099e16a01 v3.0.0 2015-02-10 00:14:55 -06:00
kpdecker a8848d0af9 Update release notes 2015-02-10 00:14:42 -06:00
kpdecker ca6152d2f7 Merge branch 'marcioj-boolean-literals' 2015-02-09 23:55:25 -06:00
kpdecker 55dc82d938 Update AST docs for literal helper names 2015-02-09 23:55:01 -06:00
kpdecker 39121cf8f5 Handle all potential literal values
Adds support for literal helper names in a few missing cases such as block expressions and subexpressions.
2015-02-09 23:54:46 -06:00
Marcio Junior 07f27843dc Transform literals to path expressions in mustache nodes 2015-02-08 19:31:38 -02:00
Marcio Junior 5cc326d425 Test compilation of literal values in mustaches 2015-02-08 19:31:38 -02:00
Marcio Junior f857471cc8 Boolean literals can be passed to mustaches 2015-02-08 19:31:38 -02:00
kpdecker fd8484798c Fix console.log cleanup 2015-02-07 14:31:54 -06:00
kpdecker 810d86d474 Fix global leak in test 2015-02-07 13:28:27 -06:00
kpdecker cfbef2585d Fail over to console.log if lacking console method
This improves logger resiliency under older browsers.
2015-02-07 13:23:10 -06:00
kpdecker 95421271e3 Change default log level to info
Eventually we want to fix this API so that you can provide per-instance levels, etc but for now this is a quick fix.

Fixes wycats/handlebars-site#91
2015-02-07 12:31:19 -06:00
Kevin Decker 752a28f24c Merge pull request #943 from ogennadi/patch-2
Properly formatted dot
2015-01-21 06:16:21 -06:00
Kevin Decker 05bda7ed0c Merge pull request #944 from ogennadi/patch-3
Clarified a long sentence
2015-01-21 06:15:58 -06:00
Oge Nnadi 1f6a94424d Clarified a long sentence 2015-01-20 11:01:11 -08:00
Oge Nnadi 8626fa8bbe Properly formatted dot 2015-01-20 10:59:38 -08:00
Kevin Decker 9f8daf9076 Merge pull request #941 from wycats/dynamic-partial
Add support for dynamic partial names
2015-01-18 20:13:25 -06:00
kpdecker 884bf15536 Avoid direct references to sexpr in statements
This allows us to avoid creating unnecessary AST nodes and avoids things like isHelper.

Side effect of these changes is that @data functions can now have data parameters passed to them.
2015-01-18 17:27:27 -06:00
kpdecker 999da739a6 Update for proposed SubExpression dependency break
Avoids parsing against SubExpressions and instead inlines the content that a subexpression otherwise would have. This can still be based via duck typing so should not add much overhead to the compiler.
2015-01-18 14:21:04 -06:00
kpdecker d567d9c345 Update PartialExpression AST docs 2015-01-18 13:29:15 -06:00
kpdecker cb51b82b8e Add support for dynamic partial names
Uses the subexpression syntax to allow for dynamic partial lookups. Ex:

```
{{> (helper) }}
```

Fixes #933
2015-01-18 13:23:45 -06:00
kpdecker b0b522b4f8 Add tests for partial comment whitespace control 2015-01-17 19:19:30 -06:00
kpdecker 3cf47abe19 Include error info when throwing type error 2015-01-17 19:19:30 -06:00
Kevin Decker b7488acf89 Merge pull request #940 from mattflaschen/addMissingReservedWords
Add missing reserved words so compiler knows to use array syntax:
2015-01-17 18:32:58 -06:00
Matthew Flaschen c9d723e0fb Add missing reserved words so compiler knows to use array syntax:
* await
* null
* true
* false

IE 8 was failing to compile Handlebars-generated source code
because it had helpers.null.

I came up with this list by diffing
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Lexical_grammar#Keywords
against the ones Handlebars already had.

I added two corresponding tests for nameLookup.
2015-01-17 19:04:38 -05:00
kpdecker c5fe2527b3 Ignore coverage dir in npm package
Fixes #937
2015-01-17 14:51:49 -06:00
Kevin Decker 9c40c07941 Merge pull request #938 from diwo/patch-1
Fix example using #with helper
2015-01-15 10:57:23 -08:00
Dickson Wong 9c7641f63f Fix example using #with helper
Example using #with helper should close tag using {{/with}}
2015-01-14 15:46:54 -08:00
Kevin Decker 96a5cf5ba1 Merge pull request #932 from wycats/strict
Implement strict and assumeObject modes
2014-12-31 19:28:05 -06:00
Kevin Decker 4b2146b12d Merge pull request #930 from wycats/visitor-update
Add parent tracking and mutation to AST visitors
2014-12-29 16:59:21 -06:00
kpdecker 6650957f79 Implement strict and assumeObject modes
Causes templates to throw when lookup fields are not defined within the context. Strict mode will throw when any field is omitted. assumeObjects mode maintains the existing behavior of outputting an empty response when fields are not defined but allows for performance/size optimizations by not doing safety checks for intermediate objects in the lookup chain.

Strict mode effectively disables the helperMissing and inverse handling for blockHelperMissing as templates will throw rather than call those features for missing data fields.

Fixes #651
Fixes #805
2014-12-27 14:59:36 -06:00
kpdecker ec798a7c44 Merge branch 'master' into visitor-update 2014-12-27 13:30:20 -06:00
kpdecker b764fb1ded Fix sourcemap test 2014-12-27 13:17:55 -06:00
kpdecker 0a9fc171b0 Fix block param evaluation under older IE 2014-12-27 13:13:52 -06:00
kpdecker 9c9a972a0c Safely handle source map in browser tests 2014-12-27 13:01:13 -06:00
kpdecker b474630c7d Strip unnecessary whitespace in template output 2014-12-27 12:58:28 -06:00
kpdecker f147050513 Update dependencies 2014-12-27 12:12:21 -06:00
kpdecker daa8f3dd05 Fix duplicate return statement on simple templates 2014-12-27 10:05:05 -06:00
kpdecker be0ba75925 Fix case-insensitive file system compile issue 2014-12-26 11:34:48 -06:00
kpdecker 9ff3daf785 Add parent tracking and mutation to AST visitors
Fixes #916
2014-12-26 11:32:30 -06:00
kpdecker 8babe05953 Bump code coverage 2014-12-26 11:12:47 -06:00
kpdecker 396795c983 Implement block parameters
Fixes #907
2014-12-26 00:31:57 -06:00
kpdecker 9e907e6785 Expose AST helpers in public API 2014-12-21 22:32:50 -06:00
kpdecker 6d2239d8ac Update fix for #926 for 3.0 codeline 2014-12-17 00:40:28 -06:00
kpdecker 59e80c5103 Track depths on deduped children
Fixes a very specific error case where deduped children won’t receive the depths object due to it being omitted by the caller when optimizing.

Fixes #926
2014-12-16 17:59:32 -06:00
kpdecker 69b1bdd32e Avoid unnecessary this. reference 2014-12-16 12:57:46 -06:00
kpdecker c05fad8b18 Fix remaining depth arg 2014-12-16 12:57:46 -06:00
kpdecker 203df9d5b7 Remove unused vars and add jshint checking 2014-12-16 12:57:46 -06:00
kpdecker 0d396ccd30 Remove Compiler.depths parameter
This is no longer necessary with the depths array tracking and adds compiler overhead.
2014-12-16 12:57:46 -06:00
Kevin Decker ae8c1d229f Merge pull request #918 from oneeman/submodule-instr-in-contributing
Add instructions for 'spec/mustache' to CONTRIBUTING.md, fix a few typos
2014-12-08 11:39:22 -08:00
Or Neeman e7f51c2248 Add instructions for 'spec/mustache' to CONTRIBUTING.md, fix a few typos elsewere 2014-12-07 09:00:56 -06:00
kpdecker 3238645f26 Allow blockParams on chained inverse statements 2014-11-29 18:38:22 -06:00
kpdecker f84f76f006 Add basic docs for JavaScriptCompiler override API 2014-11-29 18:20:11 -06:00
kpdecker 96c1300f15 Drop unused JavaScriptCompiler.namespace param 2014-11-29 18:11:58 -06:00
kpdecker b6ba2dcdb0 Move compileProgram out of visitor methods 2014-11-29 18:02:58 -06:00
kpdecker 5b0efc7e91 Track source node via accept call rather vs opcode 2014-11-29 18:02:58 -06:00
Kevin Decker d4070c3667 Merge pull request #915 from wycats/ast-update
Ast update
2014-11-29 18:02:12 -06:00
kpdecker a655aedb5c s/Subexpression/SubExpression/ 2014-11-29 17:59:09 -06:00
kpdecker 928ba56b95 Rework strip flags to make clearer at in AST level
Rather than keeping state in the AST, which requires some gymnastics, we create a separate visitor flow which does the top down iteration necessary to calculate all of the state needed for proper whitespace control evaluation.
2014-11-28 23:13:06 -06:00
kpdecker 8a6796e5c0 Move Jison parsing out of AST into helpers 2014-11-28 17:26:52 -06:00
kpdecker 95b23095c0 First crack at compiler API docs 2014-11-28 16:55:53 -06:00
kpdecker ffc9fb5007 Add original to BooleanLiteral 2014-11-28 16:55:41 -06:00
kpdecker 6a7a8c803d Rename AST objects to match type names 2014-11-28 14:42:24 -06:00
kpdecker f990cf0064 Treat partial exec in a manner closer to helpers
This helps unify the code handling and will also be needed to support string/id tracking on partials.
2014-11-27 09:11:03 -06:00
kpdecker 1124908d2a Update subexpression and hash AST constructs 2014-11-27 08:37:48 -06:00
kpdecker e1cba432a6 Simplify Path and Sexpr calculated flags 2014-11-27 07:32:15 -06:00
kpdecker 5c921cafeb Replace DataNode and IdNode with PathNode
This is a breaking change for string mode users as there is no longer a distinct type for data parameters. Instead data consumers should look for the @ prefix value.
2014-11-26 20:36:36 -06:00
kpdecker 697bbe59ca Update literal ast nodes for new spec 2014-11-26 19:22:09 -06:00
kpdecker 2a4819d75c Update statement node ASTs 2014-11-26 19:04:18 -06:00
kpdecker 5cfe6a0be1 Update MustacheNode for new AST structure 2014-11-26 09:31:46 -06:00
kpdecker df421f1e4b Update ProgramNode to better match SpiderMonkey 2014-11-26 09:18:33 -06:00
kpdecker 61dd721ca2 Update AST location info to match SpiderMonkey
Part of #889
2014-11-26 09:01:03 -06:00
kpdecker 3a9440f954 Make each helper data uniform
Provide @key and @last value for all forms of iteration.

Fixes #910
2014-11-26 08:02:55 -06:00
kpdecker e4c874d1af Update to latest es6-module-packager 2014-11-26 08:00:23 -06:00
kpdecker 5594416121 Export the default object for handlebars/runtime
Maintains consistency with the prior export pattern, while avoids the knarly default loading issues.
2014-11-26 07:49:29 -06:00
kpdecker c4b3c901a5 Provide Handlebars.noConflict implementation
Allows for users who are loading via a global pattern to avoid conflicting with other instances of the library.

Fixes #887
2014-11-26 07:48:45 -06:00
kpdecker 617dd57162 Lookup partials when undefined 2014-11-25 23:15:11 -06:00
kpdecker d044a8c99d Optimize variable declaration 2014-11-25 23:15:11 -06:00
Kevin Decker bf993528d1 Merge pull request #906 from mmun/block-params
Add parser support for block params
2014-11-12 21:36:25 -06:00
Martin Muñoz b8a9f7264d Add parser support for block params 2014-11-11 21:35:10 -05:00
Kevin Decker b3b5b35889 Merge pull request #856 from MajorBreakfast/patch-1
jspm compatibility
2014-11-11 15:05:10 -06:00
MajorBreakfast 80b5b24cce jspm compatibility
This will enable compatibilty with jspm as soon as https://github.com/wycats/handlebars.js/issues/855 has been addressed.
2014-11-10 15:12:58 +01:00
kpdecker 4a2afa9d02 Propagate options for client compiled partials
Fixes #901
2014-11-08 19:06:40 -06:00
kpdecker 4415f3151d Merge branch 'mmun-remove-block-mustache' 2014-11-08 18:57:19 -06:00
kpdecker bf8cc50798 Merge branch 'remove-block-mustache' of github.com:mmun/handlebars.js into mmun-remove-block-mustache
Conflicts:
	lib/handlebars/compiler/helpers.js
	spec/parser.js
2014-11-08 18:57:06 -06:00
kpdecker ac208b1bcd Only provide aliases for multiple use calls
Fixes #903
2014-11-08 18:53:43 -06:00
kpdecker 8dfeabdd16 Fix missing coverage 2014-11-08 18:11:29 -06:00
kpdecker 6d7696ef66 Remove unnecessary test stub 2014-11-08 18:09:15 -06:00
Kevin Decker 8e6e9fbc60 Merge pull request #902 from wycats/source-map
Generate Source Maps
2014-11-08 18:04:56 -06:00
kpdecker 1429587dd4 Provide default mapping for boilerplate code 2014-11-08 17:50:01 -06:00
kpdecker 0dbeeb4503 Add source map output support to the CLI 2014-11-08 17:50:01 -06:00
kpdecker 3ee0682247 Generate source maps
Allow the precompiler to generate source maps when the srcFile parameter is passed.

This refactors large chunks of the code generation pipeline, allowing metadata to be associated with code chunks as well as breaking out much of the code generation logic into a separate helper.
2014-11-08 17:50:01 -06:00
kpdecker 249f559104 Include location information in all opcodes 2014-11-08 17:50:01 -06:00
Kevin Decker ea6b0be910 Merge pull request #892 from wycats/else-if
Implement parser for else chaining of helpers
2014-11-08 17:49:14 -06:00
Kevin Decker e779ecf12d Merge pull request #890 from wycats/to-html-safe
Use toHTML vs. instanceof checks for SafeString
2014-11-08 17:47:25 -06:00
kpdecker 01a22e61df Use toHTML vs. instanceof checks for SafeString
Allows for us to play nicely in environments such as Node that could have multiple versions of the library loaded. Also allows for implementors to provide their own behavior, provided they know what they are doing.

Fixes #886
2014-11-08 17:46:53 -06:00
Kevin Decker c8af90f697 Merge pull request #863 from tomByrer/patch-1
+ jsDelivr CDN info
2014-11-08 17:44:22 -06:00
Kevin Decker b5f03c95ab Merge pull request #870 from max-b/master
Registering undefined partial throws exception.
2014-11-08 17:43:41 -06:00
kpdecker d595ebc0e2 Simplify program generation logic 2014-11-08 14:36:17 -06:00
kpdecker d47e4dd1f7 Bump test coverage 2014-11-08 14:35:23 -06:00
kpdecker 83bcbee222 Ignore unused code paths
We don’t want to remove these as the generic code may need it in the future, but these code paths are not triggered through the existing language constructs.
2014-11-08 14:35:23 -06:00
kpdecker 30034c75e7 Merge setupParams and setupOptions 2014-11-06 09:43:43 -06:00
kpdecker d3bd1a523d Simplify flushInline implementation 2014-11-05 23:23:43 -06:00
kpdecker 9665379cdc Use terinary operator for inline appends
Allows for append operations to avoid breaking inline chain.
2014-11-05 22:28:12 -06:00
kpdecker a7000f8129 Provide stubbed visitor implementation
Part of #889
2014-11-05 21:57:09 -06:00
Martin Muñoz a5d0491d57 Remove RawBlockNode in favor of prepareRawBlock helper 2014-11-05 11:00:39 -05:00
Martin Muñoz 12e3d210e8 Simplify BlockNode by removing intermediate MustacheNode 2014-11-02 18:19:20 -05:00
kpdecker 632fadcea3 Add preventIndent option for partials
This disables the standalone partial indent behavior required by the Mustache spec and allows for users to utilize partials in the same manner as under 1.x.

Fixes #858
2014-11-02 12:19:56 -06:00
kpdecker 7c220b9af5 Add test for whitespace control w/ partial indent
Verifies the behaviors discussed in #858
2014-11-02 12:09:37 -06:00
kpdecker f30b3ea329 Allow whitespace control on comments
This changes the call signature for the CommentNode constructor, which is a potentially breaking change for AST users.

Fixes #866
2014-11-02 11:55:52 -06:00
kpdecker 9fbf7aa753 Fix help message for CLI
Fixes #895
2014-11-02 11:08:42 -06:00
Kevin Decker 3e1840a75d Merge pull request #874 from ogwiz2/patch-1
Capitalization change in comments for consistency
2014-10-27 23:12:25 -05:00
kpdecker 4282668d47 Implement parser for else chaining of helpers
Allows users to chain multiple helpers together using their inverse callbacks. I.e.

```
{{#if foo}}
{{else if bar}}
{{else}}
{{/if}}
```

The control flow here effectively causes the helpers to be nested. The above is actually syntactic sugar for this:

```
{{#if foo}}
{{else}}
  {{#if bar}}
  {{else}}
  {{/if}}
{{/if}}
```

Any helper may be used in this manner, the only requirement is they support normal calls and inverse calls.

Introduces a breaking change in that `{{else foo}}` may no longer be used as a root level operator. Instead `{{^foo}}` must be used.

Fixes #72.
2014-10-27 19:17:16 -05:00
Alex Jeng 4aab967796 Capitalization change in comments for consistency 2014-09-23 11:59:30 -07:00
Maxb b87e601063 Fixing indentation in spec/partials.js 2014-09-19 11:46:02 -07:00
Maxb a0e2e200d9 Registering undefined partial throws exception. 2014-09-19 11:35:16 -07:00
Kevin Decker 3133af2a21 Merge pull request #869 from jonsadka/patch-1
Update FAQ.md
2014-09-19 01:02:40 -05:00
Jon Sadka 5fe7ef63d8 Update FAQ.md
Fixed misspelling of sufficent to sufficient on Line 60
2014-09-18 22:08:51 -07:00
tomByrer 62ed555b2c + jsDelivr CDN info
Thanks to libgrabber, every time a [new release is posted](https://github.com/components/handlebars.js/releases), jsDelivr will find it & automatically upload it.  If you change your file structure, please updated the [include section](https://github.com/jsdelivr/jsdelivr/blob/master/files/handlebarsjs/update.json) or ask us to do it.

I was going to link CDNJS, but they [haven't uploaded to v2.0.0](http://cdnjs.com/libraries/handlebars.js).  Nothing against those cats; we [use their network](http://www.jsdelivr.com/network.php).
2014-09-06 08:08:23 -06:00
kpdecker 8aa2a34a8d Update jsfiddle link 2014-09-02 00:14:10 -05:00
kpdecker aaa590d846 Fix non-prerelease handling for gemspec 2014-09-01 21:42:45 -05:00
kpdecker 1eb2b04aa1 v2.0.0 2014-09-01 21:28:59 -05:00
kpdecker 2e75199664 Update release notes 2014-09-01 21:28:51 -05:00
kpdecker 0670f656fb Update jsfiddle to 2.0.0-beta.1 2014-08-27 09:27:57 -05:00
kpdecker 4d17e3c209 Add contrib note regarding handlebarsjs.com docs 2014-08-27 01:49:52 -05:00
kpdecker 58fb258016 Cleanup var names slightly 2014-08-27 01:49:23 -05:00
kpdecker ed3ae9af59 Drop cdnjs release step 2014-08-26 19:38:40 -05:00
kpdecker 64d5481c00 Play nice with gemspec version numbers 2014-08-26 19:38:32 -05:00
74 changed files with 4865 additions and 2763 deletions
+199
View File
@@ -0,0 +1,199 @@
{
"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,
"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-class": 2,
"no-ex-assign": 2,
"no-extra-boolean-cast": 2,
"no-extra-parens": 0,
"no-extra-semi": 2,
"no-func-assign": 2,
// 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,
"no-reserved-keys": 2, // Important for IE
"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": 2,
"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-wrap-func": 2,
"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-line-comment": 2,
"wrap-regex": 1,
"no-var": 1
}
}
-51
View File
@@ -1,51 +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,
"sub": true,
"strict": false,
"white": false
}
+3 -1
View File
@@ -1,7 +1,7 @@
.DS_Store
.gitignore
.rvmrc
.jshintrc
.eslintrc
.travis.yml
.rspec
Gemfile
@@ -10,9 +10,11 @@ Rakefile
Gruntfile.js
*.gemspec
*.nuspec
*.log
bench/*
configurations/*
components/*
coverage/*
dist/cdnjs/*
dist/components/*
spec/*
+1 -1
View File
@@ -13,7 +13,7 @@ env:
- secure: Nm4AgSfsgNB21kgKrF9Tl7qVZU8YYREhouQunFracTcZZh2NZ2XH5aHuSiXCj88B13Cr/jGbJKsZ4T3QS3wWYtz6lkyVOx3H3iI+TMtqhD9RM3a7A4O+4vVN8IioB2YjhEu0OKjwgX5gp+0uF+pLEi7Hpj6fupD3AbbL5uYcKg8=
matrix:
include:
- node_js: '0.10'
- node_js: '10'
env:
- PUBLISH=true
- secure: pLTzghtVll9yGKJI0AaB0uI8GypfWxLTaIB0ZL8//yN3nAEIKMhf/RRilYTsn/rKj2NUa7vt2edYILi3lttOUlCBOwTc9amiRms1W8Lwr/3IdWPeBLvLuH1zNJRm2lBAwU4LBSqaOwhGaxOQr6KHTnWudhNhgOucxpZfvfI/dFw=
+41 -3
View File
@@ -6,7 +6,17 @@ Please see our [FAQ](https://github.com/wycats/handlebars.js/blob/master/FAQ.md)
Should you run into other issues with the project, please don't hesitate to let us know by filing an [issue][issue]! In general we are going to ask for an example of the problem failing, which can be as simple as a jsfiddle/jsbin/etc. We've put together a jsfiddle [template][jsfiddle] to ease this. (We will keep this link up to date as new releases occur, so feel free to check back here)
Pull requests containing only failing thats demonstrating the issue are welcomed and this also helps ensure that your issue won't regress in the future once it's fixed.
Pull requests containing only failing tests demonstrating the issue are welcomed and this also helps ensure that your issue won't regress in the future once it's fixed.
Documentation issues on the handlebarsjs.com site should be reported on [handlebars-site](https://github.com/wycats/handlebars-site).
## Branches
* The branch `4.x` contains the currently released version. Bugfixes should be made in this branch.
* The branch `master` contains the next version. A release date is not yet specified. Maintainers
should merge the branch `4.x` into the master branch regularly.
* The branch `3.x` contains the previous release. Relevant bugfixes may be cherry-picked into this
branch from the `4.x` branch. There should be no merges anymore between this branch and `4.x` or `master`.
## Pull Requests
@@ -26,6 +36,8 @@ To build Handlebars.js you'll need a few things installed.
* Node.js
* [Grunt](http://gruntjs.com/getting-started)
Before building, you need to make sure that the Git submodule `spec/mustache` is included (i.e. the directory `spec/mustache` should not be empty). To include it, if using Git version 1.6.5 or newer, use `git clone --recursive` rather than `git clone`. Or, if you already cloned without `--recursive`, use `git submodule update --init`.
Project dependencies may be installed via `npm install`.
To build Handlebars.js from scratch, you'll want to run `grunt`
@@ -38,6 +50,25 @@ The `grunt dev` implements watching for tests and allows for in browser testing
If you notice any problems, please report them to the GitHub issue tracker at
[http://github.com/wycats/handlebars.js/issues](http://github.com/wycats/handlebars.js/issues).
## Running Tests
To run tests locally, first install all dependencies.
```sh
npm install
```
Clone the mustache specs into the spec/mustache folder.
```sh
cd spec
rm -r mustache
git clone https://github.com/mustache/spec.git mustache
```
From the root directory, run the tests.
```sh
npm test
```
## Ember testing
The current ember distribution should be tested as part of the handlebars release process. This requires building the `handlebars-source` gem locally and then executing the ember test script.
@@ -56,12 +87,19 @@ npm test
Handlebars utilizes the [release yeoman generator][generator-release] to perform most release tasks.
Before starting the release, make sure that the `yo`, `generator-release` and `grunt` are installed globally.
The last release was successfully performed with `yo@2.0.5`, `generator-release@2.6.1` and `grunt@1.0.3` and `node@10.11.0`.
If the latest versions of the tools don't work you may want to revert to those versions.
```bash
npm install -g yo generator-release grunt
```
A full release may be completed with the following:
```
yo release
npm publish
yo release:publish cdnjs handlebars.js dist/cdnjs/
yo release:publish components handlebars.js dist/components/
cd dist/components/
@@ -74,4 +112,4 @@ After this point the handlebars site needs to be updated to point to the new ver
[generator-release]: https://github.com/walmartlabs/generator-release
[pull-request]: https://github.com/wycats/handlebars.js/pull/new/master
[issue]: https://github.com/wycats/handlebars.js/issues/new
[jsfiddle]: http://jsfiddle.net/9D88g/11/
[jsfiddle]: http://jsfiddle.net/9D88g/26/
+1 -1
View File
@@ -57,4 +57,4 @@
The other option is to load the `handlebars.runtime.js` UMD build, which might not require path configuration and exposes the library as both the module root and the `default` field for compatibility.
If not using ES6 transpilers or accessing submodules in the build the former option should be sufficent for most use cases.
If not using ES6 transpilers or accessing submodules in the build the former option should be sufficient for most use cases.
+61 -36
View File
@@ -1,16 +1,16 @@
var childProcess = require('child_process');
/*eslint-disable no-process-env */
module.exports = function(grunt) {
grunt.initConfig({
pkg: grunt.file.readJSON('package.json'),
jshint: {
eslint: {
options: {
jshintrc: '.jshintrc'
},
files: [
'dist/cjs/**/!(*.min|parser).js'
'*.js',
'lib/**/!(*.min|parser).js',
'spec/**/!(*.amd|json2|require).js'
]
},
@@ -19,7 +19,7 @@ module.exports = function(grunt) {
copy: {
dist: {
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')
+ content;
}
@@ -41,21 +41,14 @@ module.exports = function(grunt) {
}
},
packager: {
global: {
type: 'umd',
export: 'Handlebars',
files: [{
cwd: 'lib/',
expand: true,
src: ['handlebars*.js'],
dest: 'dist/'
}]
babel: {
options: {
loose: ['es6.modules']
},
amd: {
type: 'amd',
anonymous: true,
options: {
modules: 'amd'
},
files: [{
expand: true,
cwd: 'lib/',
@@ -65,30 +58,61 @@ module.exports = function(grunt) {
},
cjs: {
type: 'cjs',
options: {
modules: 'common'
},
files: [{
expand: true,
cwd: 'lib/',
expand: true,
src: '**/!(index).js',
dest: 'dist/cjs/'
}]
}
},
webpack: {
options: {
context: __dirname,
module: {
loaders: [
// the optional 'runtime' transformer tells babel to require the runtime instead of inlining it.
{ test: /\.jsx?$/, exclude: /node_modules/, loader: 'babel-loader?optional=runtime&loose=es6.modules' }
]
},
output: {
path: 'dist/',
library: 'Handlebars',
libraryTarget: 'umd'
}
},
handlebars: {
entry: './lib/handlebars.js',
output: {
filename: 'handlebars.js'
}
},
runtime: {
entry: './lib/handlebars.runtime.js',
output: {
filename: 'handlebars.runtime.js'
}
}
},
requirejs: {
options: {
optimize: "none",
baseUrl: "dist/amd/"
optimize: 'none',
baseUrl: 'dist/amd/'
},
dist: {
options: {
name: "handlebars",
out: "dist/handlebars.amd.js"
name: 'handlebars',
out: 'dist/handlebars.amd.js'
}
},
runtime: {
options: {
name: "handlebars.runtime",
out: "dist/handlebars.runtime.amd.js"
name: 'handlebars.runtime',
out: 'dist/handlebars.runtime.amd.js'
}
}
},
@@ -172,18 +196,18 @@ module.exports = function(grunt) {
});
// Build a new version of the library
this.registerTask('build', "Builds a distributable version of the current project", [
this.registerTask('build', 'Builds a distributable version of the current project', [
'eslint',
'parser',
'node',
'globals',
'jshint']);
'globals']);
this.registerTask('amd', ['packager:amd', 'requirejs']);
this.registerTask('node', ['packager:cjs']);
this.registerTask('globals', ['packager:global']);
this.registerTask('amd', ['babel:amd', 'requirejs']);
this.registerTask('node', ['babel:cjs']);
this.registerTask('globals', ['webpack']);
this.registerTask('tests', ['concat:tests']);
this.registerTask('release', 'Build final packages', ['amd', 'jshint', 'uglify', 'copy:dist', 'copy:components', 'copy:cdnjs']);
this.registerTask('release', 'Build final packages', ['eslint', 'amd', 'uglify', 'copy:dist', 'copy:components', 'copy:cdnjs']);
// Load tasks from npm
grunt.loadNpmTasks('grunt-contrib-clean');
@@ -191,11 +215,12 @@ module.exports = function(grunt) {
grunt.loadNpmTasks('grunt-contrib-connect');
grunt.loadNpmTasks('grunt-contrib-copy');
grunt.loadNpmTasks('grunt-contrib-requirejs');
grunt.loadNpmTasks('grunt-contrib-jshint');
grunt.loadNpmTasks('grunt-contrib-uglify');
grunt.loadNpmTasks('grunt-contrib-watch');
grunt.loadNpmTasks('grunt-babel');
grunt.loadNpmTasks('grunt-eslint');
grunt.loadNpmTasks('grunt-saucelabs');
grunt.loadNpmTasks('es6-module-packager');
grunt.loadNpmTasks('grunt-webpack');
grunt.task.loadTasks('tasks');
+36 -3
View File
@@ -16,6 +16,8 @@ Installing
----------
Installing Handlebars is easy. Simply download the package [from the official site](http://handlebarsjs.com/) or the [bower repository][bower-repo] and add it to your web pages (you should usually use the most recent version).
For web browsers, a free CDN is available at [jsDelivr](http://www.jsdelivr.com/#!handlebarsjs). Advanced usage, such as [version aliasing & concocting](https://github.com/jsdelivr/jsdelivr#usage), is available.
Alternatively, if you prefer having the latest version of handlebars from
the 'master' branch, passing builds of the 'master' branch are automatically
published to S3. You may download the latest passing master build by grabbing
@@ -108,7 +110,7 @@ templates easier and also changes a tiny detail of how partials work.
### Paths
Handlebars.js supports an extended expression syntax that we call paths.
Paths are made up of typical expressions and . characters. Expressions
Paths are made up of typical expressions and `.` characters. Expressions
allow you to not only display data from the current context, but to
display data from contexts that are descendants and ancestors of the
current context.
@@ -132,7 +134,7 @@ into the person object you could still display the company's name with
an expression like `{{../company.name}}`, so:
```
{{#with person}}{{name}} - {{../company.name}}{{/person}}
{{#with person}}{{name}} - {{../company.name}}{{/with}}
```
would render:
@@ -195,7 +197,7 @@ template(data);
```
Whenever the block helper is called it is given one or more parameters,
any arguments that are passed in the helper in the call and an `options`
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`.
@@ -234,6 +236,35 @@ template(data);
// </ul>
```
Partials can also accept parameters
```js
var source = "<div>{{> roster rosterProperties people=listOfPeople}}</div>";
Handlebars.registerPartial('roster', '<h2>{{rosterName}}</h2>{{#people}}<span>{{id}}: {{name}}</span>{{/people}}')
var template = Handlebars.compile(source);
var data = {
"listOfPeople": [
{ "name": "Alan", "id": 1 },
{ "name": "Yehuda", "id": 2 }
],
"rosterProperties": {
"rosterName": "Cool People"
}
};
template(data);
// Should render:
// <div>
// <h2>Cool People</h2>
// <span>1: Alan</span>
// <span>2: Yehuda</span>
// </div>
```
### Comments
You can add comments to your templates with the following syntax:
@@ -364,6 +395,7 @@ Handlebars in the Wild
and [@doowb](https://github.com/doowb), is a static site generator that uses Handlebars.js
as its template engine.
* [CoSchedule](http://coschedule.com) An editorial calendar for WordPress that uses Handlebars.js
* [dashbars](https://github.com/pismute/dashbars) A modern helper library for Handlebars.js.
* [Ember.js](http://www.emberjs.com) makes Handlebars.js the primary way to
structure your views, also with automatic data binding support.
* [Ghost](https://ghost.org/) Just a blogging platform.
@@ -371,6 +403,7 @@ Handlebars in the Wild
from Les Hill (@leshill).
* [handlebars-helpers](https://github.com/assemble/handlebars-helpers) is an extensive library
with 100+ handlebars helpers.
* [handlebars-layouts](https://github.com/shannonmoeller/handlebars-layouts) is a set of helpers which implement extendible and embeddable layout blocks as seen in other popular templating languages.
* [hbs](http://github.com/donpark/hbs): An Express.js view engine adapter for Handlebars.js,
from Don Park.
* [koa-hbs](https://github.com/jwilm/koa-hbs): [koa](https://github.com/koajs/koa) generator based
+18 -2
View File
@@ -1,12 +1,16 @@
#!/usr/bin/env node
var optimist = require('optimist')
.usage('Precompile handlebar templates.\nUsage: $0 template...', {
.usage('Precompile handlebar templates.\nUsage: $0 [template|directory]...', {
'f': {
'type': 'string',
'description': 'Output File',
'alias': 'output'
},
'map': {
'type': 'string',
'description': 'Source Map File'
},
'a': {
'type': 'boolean',
'description': 'Exports amd style (require.js)',
@@ -80,6 +84,11 @@ var optimist = require('optimist')
'type': 'boolean',
'description': 'Prints the current compiler version',
'alias': 'version'
},
'help': {
'type': 'boolean',
'description': 'Outputs this message'
}
})
@@ -89,7 +98,14 @@ var optimist = require('optimist')
}
});
var argv = optimist.argv;
argv.templates = argv._;
delete argv._;
return require('../lib/precompiler').cli(argv);
if (argv.help || (!argv.templates.length && !argv.version)) {
optimist.showHelp();
return;
}
return require('../dist/cjs/precompiler').cli(argv);
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "handlebars",
"version": "2.0.0-beta.1",
"version": "3.0.5",
"main": "handlebars.js",
"dependencies": {}
}
+1 -1
View File
@@ -11,7 +11,7 @@ Gem::Specification.new do |gem|
gem.description = %q{Handlebars.js source code wrapper for (pre)compilation gems.}
gem.summary = %q{Handlebars.js source code wrapper}
gem.homepage = "https://github.com/wycats/handlebars.js/"
gem.version = package["version"]
gem.version = package["version"].sub "-", "."
gem.license = "MIT"
gem.files = [
+1 -1
View File
@@ -2,7 +2,7 @@
<package>
<metadata>
<id>handlebars.js</id>
<version>2.0.0-beta.1</version>
<version>3.0.5</version>
<authors>handlebars.js Authors</authors>
<licenseUrl>https://github.com/wycats/handlebars.js/blob/master/LICENSE</licenseUrl>
<projectUrl>https://github.com/wycats/handlebars.js/</projectUrl>
+276
View File
@@ -0,0 +1,276 @@
# Handlebars Compiler APIs
There are a number of formal APIs that tool implementors may interact with.
## AST
Other tools may interact with the formal AST as defined below. Any JSON structure matching this pattern may be used and passed into the `compile` and `precompile` methods in the same way as the text for a template.
AST structures may be generated either with the `Handlebars.parse` method and then manipulated, via the `Handlebars.AST` objects of the same name, or constructed manually as a generic JavaScript object matching the structure defined below.
```javascript
var ast = Handlebars.parse(myTemplate);
// Modify ast
Handlebars.precompile(ast);
```
### Basic
```java
interface Node {
type: string;
loc: SourceLocation | null;
}
interface SourceLocation {
source: string | null;
start: Position;
end: Position;
}
interface Position {
line: uint >= 1;
column: uint >= 0;
}
```
### Programs
```java
interface Program <: Node {
type: "Program";
body: [ Statement ];
blockParams: [ string ];
}
```
### Statements
```java
interface Statement <: Node { }
interface MustacheStatement <: Statement {
type: "MustacheStatement";
path: PathExpression | Literal;
params: [ Expression ];
hash: Hash;
escaped: boolean;
strip: StripFlags | null;
}
interface BlockStatement <: Statement {
type: "BlockStatement";
path: PathExpression;
params: [ Expression ];
hash: Hash;
program: Program | null;
inverse: Program | null;
openStrip: StripFlags | null;
inverseStrip: StripFlags | null;
closeStrip: StripFlags | null;
}
interface PartialStatement <: Statement {
type: "PartialStatement";
name: PathExpression | SubExpression;
params: [ Expression ];
hash: Hash;
indent: string;
strip: 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.
```java
interface ContentStatement <: Statement {
type: "ContentStatement";
value: string;
original: string;
}
interface CommentStatement <: Statement {
type: "CommentStatement";
value: string;
strip: StripFlags | null;
}
```
### Expressions
```java
interface Expression <: Node { }
```
##### SubExpressions
```java
interface SubExpression <: Expression {
type: "SubExpression";
path: PathExpression;
params: [ Expression ];
hash: Hash;
}
```
##### Paths
```java
interface PathExpression <: Expression {
type: "PathExpression";
data: boolean;
depth: uint >= 0;
parts: [ string ];
original: string;
}
```
- `data` is true when the given expression is a `@data` reference.
- `depth` is an integer representation of which context the expression references. `0` represents the current context, `1` would be `../`, etc.
- `parts` is an array of the names in the path. `foo.bar` would be `['foo', 'bar']`. Scope references, `.`, `..`, and `this` should be omitted from this array.
- `original` is the path as entered by the user. Separator and scope references are left untouched.
##### Literals
```java
interface Literal <: Expression { }
interface StringLiteral <: Literal {
type: "StringLiteral";
value: string;
original: string;
}
interface BooleanLiteral <: Literal {
type: "BooleanLiteral";
value: boolean;
original: boolean;
}
interface NumberLiteral <: Literal {
type: "NumberLiteral";
value: number;
original: number;
}
interface UndefinedLiteral <: Literal {
type: "UndefinedLiteral";
}
interface NullLiteral <: Literal {
type: "NullLiteral";
}
```
### Miscellaneous
```java
interface Hash <: Node {
type: "Hash";
pairs: [ HashPair ];
}
interface HashPair <: Node {
type: "HashPair";
key: string;
value: Expression;
}
interface StripFlags {
open: boolean;
close: boolean;
}
```
`StripFlags` are used to signify whitespace control character that may have been entered on a given statement.
## AST Visitor
`Handlebars.Visitor` is available as a base class for general interaction with AST structures. This will by default traverse the entire tree and individual methods may be overridden to provide specific responses to particular nodes.
Recording all referenced partial names:
```javascript
var Visitor = Handlebars.Visitor;
function ImportScanner() {
this.partials = [];
}
ImportScanner.prototype = new Visitor();
ImportScanner.prototype.PartialStatement = function(partial) {
this.partials.push({request: partial.name.original});
Visitor.prototype.PartialStatement.call(this, partial);
};
var scanner = new ImportScanner();
scanner.accept(ast);
```
The current node's ancestors will be maintained in the `parents` array, with the most recent parent listed first.
The visitor may also be configured to operate in mutation mode by setting the `mutation` field to true. When in this mode, handler methods may return any valid AST node and it will replace the one they are currently operating on. Returning `false` will remove the given value (if valid) and returning `undefined` will leave the node in tact. This return structure only apply to mutation mode and non-mutation mode visitors are free to return whatever values they wish.
Implementors that may need to support mutation mode are encouraged to utilize the `acceptKey`, `acceptRequired` and `acceptArray` helpers which provide the conditional overwrite behavior as well as implement sanity checks where pertinent.
## JavaScript Compiler
The `Handlebars.JavaScriptCompiler` object has a number of methods that may be customized to alter the output of the compiler:
- `nameLookup(parent, name, type)`
Used to generate the code to resolve a give path component.
- `parent` is the existing code in the path resolution
- `name` is the current path component
- `type` is the type of name being evaluated. May be one of `context`, `data`, `helper`, or `partial`.
Note that this does not impact dynamic partials, which implementors need to be aware of. Overriding `VM.resolvePartial` may be required to support dynamic cases.
- `depthedLookup(name)`
Used to generate code that resolves parameters within any context in the stack. Is only used in `compat` mode.
- `compilerInfo()`
Allows for custom compiler flags used in the runtime version checking logic.
- `appendToBuffer(source, location, explicit)`
Allows for code buffer emitting code. Defaults behavior is string concatenation.
- `source` is the source code whose result is to be appending
- `location` is the location of the source in the source map.
- `explicit` is a flag signaling that the emit operation must occur, vs. the lazy evaled options otherwise.
- `initializeBuffer()`
Allows for buffers other than the default string buffer to be used. Generally needs to be paired with a custom `appendToBuffer` implementation.
```javascript
function MyCompiler() {
Handlebars.JavaScriptCompiler.apply(this, arguments);
}
MyCompiler.prototype = Object.create(Handlebars.JavaScriptCompiler);
MyCompiler.nameLookup = function(parent, name, type) {
if (type === 'partial') {
return 'MyPartialList[' + JSON.stringify(name) ']';
} else {
return Handlebars.JavaScriptCompiler.prototype.nameLookup.call(this, parent, name, type);
}
};
var env = Handlebars.create();
env.JavaScriptCompiler = MyCompiler;
env.compile('my template');
```
+21 -15
View File
@@ -1,20 +1,22 @@
/*globals Handlebars: true */
import Handlebars from "./handlebars.runtime";
import runtime from './handlebars.runtime';
// Compiler imports
import AST from "./handlebars/compiler/ast";
import { parser as Parser, parse } from "./handlebars/compiler/base";
import { Compiler, compile, precompile } from "./handlebars/compiler/compiler";
import JavaScriptCompiler from "./handlebars/compiler/javascript-compiler";
import AST from './handlebars/compiler/ast';
import { parser as Parser, parse } from './handlebars/compiler/base';
import { Compiler, compile, precompile } from './handlebars/compiler/compiler';
import JavaScriptCompiler from './handlebars/compiler/javascript-compiler';
import Visitor from './handlebars/compiler/visitor';
var _create = Handlebars.create;
var create = function() {
var hb = _create();
import noConflict from './handlebars/no-conflict';
let _create = runtime.create;
function create() {
let hb = _create();
hb.compile = function(input, options) {
return compile(input, options, hb);
};
hb.precompile = function (input, options) {
hb.precompile = function(input, options) {
return precompile(input, options, hb);
};
@@ -25,11 +27,15 @@ var create = function() {
hb.parse = parse;
return hb;
};
}
Handlebars = create();
Handlebars.create = create;
let inst = create();
inst.create = create;
Handlebars['default'] = Handlebars;
noConflict(inst);
export default Handlebars;
inst.Visitor = Visitor;
inst['default'] = inst;
export default inst;
+16 -13
View File
@@ -1,16 +1,17 @@
/*globals Handlebars: true */
module base from "./handlebars/base";
import * as base from './handlebars/base';
// Each of these augment the Handlebars object. No need to setup here.
// (This is done to easily share code between commonjs and browse envs)
import SafeString from "./handlebars/safe-string";
import Exception from "./handlebars/exception";
module Utils from "./handlebars/utils";
module runtime from "./handlebars/runtime";
import SafeString from './handlebars/safe-string';
import Exception from './handlebars/exception';
import * as Utils from './handlebars/utils';
import * as runtime from './handlebars/runtime';
import noConflict from './handlebars/no-conflict';
// For compatibility and usage outside of module systems, make the Handlebars object a namespace
var create = function() {
var hb = new base.HandlebarsEnvironment();
function create() {
let hb = new base.HandlebarsEnvironment();
Utils.extend(hb, base);
hb.SafeString = SafeString;
@@ -24,11 +25,13 @@ var create = function() {
};
return hb;
};
}
var Handlebars = create();
Handlebars.create = create;
let inst = create();
inst.create = create;
Handlebars['default'] = Handlebars;
noConflict(inst);
export default Handlebars;
inst['default'] = inst;
export default inst;
+79 -64
View File
@@ -1,10 +1,10 @@
module Utils from "./utils";
import Exception from "./exception";
import * as Utils from './utils';
import Exception from './exception';
export var VERSION = "2.0.0-beta.1";
export var COMPILER_REVISION = 6;
export const VERSION = '3.0.1';
export const COMPILER_REVISION = 6;
export var REVISION_CHANGES = {
export const REVISION_CHANGES = {
1: '<= 1.0.rc.2', // 1.0.rc.2 is actually rev2 but doesn't report it
2: '== 1.0.0-rc.3',
3: '== 1.0.0-rc.4',
@@ -13,10 +13,10 @@ export var REVISION_CHANGES = {
6: '>= 2.0.0-beta.1'
};
var isArray = Utils.isArray,
isFunction = Utils.isFunction,
toString = Utils.toString,
objectType = '[object Object]';
const isArray = Utils.isArray,
isFunction = Utils.isFunction,
toString = Utils.toString,
objectType = '[object Object]';
export function HandlebarsEnvironment(helpers, partials) {
this.helpers = helpers || {};
@@ -43,11 +43,14 @@ HandlebarsEnvironment.prototype = {
delete this.helpers[name];
},
registerPartial: function(name, str) {
registerPartial: function(name, partial) {
if (toString.call(name) === objectType) {
Utils.extend(this.partials, name);
Utils.extend(this.partials, name);
} else {
this.partials[name] = str;
if (typeof partial === 'undefined') {
throw new Exception('Attempting to register a partial as undefined');
}
this.partials[name] = partial;
}
},
unregisterPartial: function(name) {
@@ -57,25 +60,25 @@ HandlebarsEnvironment.prototype = {
function registerDefaultHelpers(instance) {
instance.registerHelper('helperMissing', function(/* [args, ]options */) {
if(arguments.length === 1) {
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 + "'");
throw new Exception('Missing helper: "' + arguments[arguments.length - 1].name + '"');
}
});
instance.registerHelper('blockHelperMissing', function(context, options) {
var inverse = options.inverse,
let inverse = options.inverse,
fn = options.fn;
if(context === true) {
if (context === true) {
return fn(this);
} else if(context === false || context == null) {
} else if (context === false || context == null) {
return inverse(this);
} else if (isArray(context)) {
if(context.length > 0) {
if (context.length > 0) {
if (options.ids) {
options.ids = [options.name];
}
@@ -86,7 +89,7 @@ function registerDefaultHelpers(instance) {
}
} else {
if (options.data && options.ids) {
var data = createFrame(options.data);
let data = createFrame(options.data);
data.contextPath = Utils.appendContextPath(options.data.contextPath, options.name);
options = {data: data};
}
@@ -100,10 +103,13 @@ function registerDefaultHelpers(instance) {
throw new Exception('Must pass iterator to #each');
}
var fn = options.fn, inverse = options.inverse;
var i = 0, ret = "", data;
let fn = options.fn,
inverse = options.inverse,
i = 0,
ret = '',
data,
contextPath;
var contextPath;
if (options.data && options.ids) {
contextPath = Utils.appendContextPath(options.data.contextPath, options.ids[0]) + '.';
}
@@ -114,40 +120,51 @@ function registerDefaultHelpers(instance) {
data = createFrame(options.data);
}
if(context && typeof context === 'object') {
if (isArray(context)) {
for(var j = context.length; i<j; i++) {
if (data) {
data.index = i;
data.first = (i === 0);
data.last = (i === (context.length-1));
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 + i;
}
}
ret = ret + fn(context[i], { data: data });
if (contextPath) {
data.contextPath = contextPath + field;
}
}
ret = ret + fn(context[field], {
data: data,
blockParams: Utils.blockParams([context[field], field], [contextPath + field, null])
});
}
if (context && typeof context === 'object') {
if (isArray(context)) {
for (let j = context.length; i < j; i++) {
execIteration(i, i, i === context.length - 1);
}
} else {
for(var key in context) {
if(context.hasOwnProperty(key)) {
if(data) {
data.key = key;
data.index = i;
data.first = (i === 0);
let priorKey;
if (contextPath) {
data.contextPath = contextPath + key;
}
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) {
execIteration(priorKey, i - 1);
}
ret = ret + fn(context[key], {data: data});
priorKey = key;
i++;
}
}
if (priorKey) {
execIteration(priorKey, i - 1, true);
}
}
}
if(i === 0){
if (i === 0) {
ret = inverse(this);
}
@@ -174,13 +191,13 @@ function registerDefaultHelpers(instance) {
instance.registerHelper('with', function(context, options) {
if (isFunction(context)) { context = context.call(this); }
var fn = options.fn;
let fn = options.fn;
if (!Utils.isEmpty(context)) {
if (options.data && options.ids) {
var data = createFrame(options.data);
let data = createFrame(options.data);
data.contextPath = Utils.appendContextPath(options.data.contextPath, options.ids[0]);
options = {data:data};
options = {data: data};
}
return fn(context, options);
@@ -189,9 +206,9 @@ function registerDefaultHelpers(instance) {
}
});
instance.registerHelper('log', function(context, options) {
var level = options.data && options.data.level != null ? parseInt(options.data.level, 10) : 1;
instance.log(level, context);
instance.registerHelper('log', function(message, options) {
let level = options.data && options.data.level != null ? parseInt(options.data.level, 10) : 1;
instance.log(level, message);
});
instance.registerHelper('lookup', function(obj, field) {
@@ -199,7 +216,7 @@ function registerDefaultHelpers(instance) {
});
}
export var logger = {
export let logger = {
methodMap: { 0: 'debug', 1: 'info', 2: 'warn', 3: 'error' },
// State enum
@@ -207,23 +224,21 @@ export var logger = {
INFO: 1,
WARN: 2,
ERROR: 3,
level: 3,
level: 1,
// can be overridden in the host environment
log: function(level, obj) {
if (logger.level <= level) {
var method = logger.methodMap[level];
if (typeof console !== 'undefined' && console[method]) {
console[method].call(console, obj);
}
// Can be overridden in the host environment
log: function(level, message) {
if (typeof console !== 'undefined' && logger.level <= level) {
let method = logger.methodMap[level];
(console[method] || console.log).call(console, message); // eslint-disable-line no-console
}
}
};
export var log = logger.log;
export let log = logger.log;
export var createFrame = function(object) {
var frame = Utils.extend({}, object);
export function createFrame(object) {
let frame = Utils.extend({}, object);
frame._parent = object;
return frame;
};
}
+128 -189
View File
@@ -1,213 +1,152 @@
import Exception from "../exception";
let AST = {
Program: function(statements, blockParams, strip, locInfo) {
this.loc = locInfo;
this.type = 'Program';
this.body = statements;
function LocationInfo(locInfo) {
locInfo = locInfo || {};
this.firstLine = locInfo.first_line;
this.firstColumn = locInfo.first_column;
this.lastColumn = locInfo.last_column;
this.lastLine = locInfo.last_line;
}
var AST = {
ProgramNode: function(statements, strip, locInfo) {
LocationInfo.call(this, locInfo);
this.type = "program";
this.statements = statements;
this.blockParams = blockParams;
this.strip = strip;
},
MustacheNode: function(rawParams, hash, open, strip, locInfo) {
LocationInfo.call(this, locInfo);
this.type = "mustache";
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;
// Open may be a string parsed from the parser or a passed boolean flag
if (open != null && open.charAt) {
// Must use charAt to support IE pre-10
var escapeFlag = open.charAt(3) || open.charAt(2);
this.escaped = escapeFlag !== '{' && escapeFlag !== '&';
} else {
this.escaped = !!open;
}
if (rawParams instanceof AST.SexprNode) {
this.sexpr = rawParams;
} else {
// Support old AST API
this.sexpr = new AST.SexprNode(rawParams, hash);
}
// Support old AST API that stored this info in MustacheNode
this.id = this.sexpr.id;
this.params = this.sexpr.params;
this.hash = this.sexpr.hash;
this.eligibleHelper = this.sexpr.eligibleHelper;
this.isHelper = this.sexpr.isHelper;
},
SexprNode: function(rawParams, hash, locInfo) {
LocationInfo.call(this, locInfo);
BlockStatement: function(path, params, hash, program, inverse, openStrip, inverseStrip, closeStrip, locInfo) {
this.loc = locInfo;
this.type = 'BlockStatement';
this.type = "sexpr";
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;
var id = this.id = rawParams[0];
var params = this.params = rawParams.slice(1);
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';
},
UndefinedLiteral: function(locInfo) {
this.loc = locInfo;
this.type = 'UndefinedLiteral';
this.original = this.value = undefined;
},
NullLiteral: function(locInfo) {
this.loc = locInfo;
this.type = 'NullLiteral';
this.original = this.value = null;
},
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
helpers: {
// a mustache is definitely a helper if:
// * it is an eligible helper, and
// * it has at least one parameter or hash segment
this.isHelper = !!(params.length || hash);
helperExpression: function(node) {
return !!(node.type === 'SubExpression' || node.params.length || node.hash);
},
// a mustache is an eligible helper if:
// * its id is simple (a single part, not `this` or `..`)
this.eligibleHelper = this.isHelper || id.isSimple;
// if a mustache is an eligible helper but not a definite
// helper, it is ambiguous, and will be resolved in a later
// pass or at runtime.
},
PartialNode: function(partialName, context, hash, strip, locInfo) {
LocationInfo.call(this, locInfo);
this.type = "partial";
this.partialName = partialName;
this.context = context;
this.hash = hash;
this.strip = strip;
this.strip.inlineStandalone = true;
},
BlockNode: function(mustache, program, inverse, strip, locInfo) {
LocationInfo.call(this, locInfo);
this.type = 'block';
this.mustache = mustache;
this.program = program;
this.inverse = inverse;
this.strip = strip;
if (inverse && !program) {
this.isInverse = true;
}
},
RawBlockNode: function(mustache, content, close, locInfo) {
LocationInfo.call(this, locInfo);
if (mustache.sexpr.id.original !== close) {
throw new Exception(mustache.sexpr.id.original + " doesn't match " + close, this);
}
content = new AST.ContentNode(content, locInfo);
this.type = 'block';
this.mustache = mustache;
this.program = new AST.ProgramNode([content], {}, locInfo);
},
ContentNode: function(string, locInfo) {
LocationInfo.call(this, locInfo);
this.type = "content";
this.original = this.string = string;
},
HashNode: function(pairs, locInfo) {
LocationInfo.call(this, locInfo);
this.type = "hash";
this.pairs = pairs;
},
IdNode: function(parts, locInfo) {
LocationInfo.call(this, locInfo);
this.type = "ID";
var original = "",
dig = [],
depth = 0,
depthString = '';
for(var i=0,l=parts.length; i<l; i++) {
var part = parts[i].part;
original += (parts[i].separator || '') + part;
if (part === ".." || part === "." || part === "this") {
if (dig.length > 0) {
throw new Exception("Invalid path: " + original, this);
} else if (part === "..") {
depth++;
depthString += '../';
} else {
this.isScoped = true;
}
} else {
dig.push(part);
}
}
this.original = original;
this.parts = dig;
this.string = dig.join('.');
this.depth = depth;
this.idName = depthString + this.string;
scopedId: function(path) {
return (/^\.|this\b/).test(path.original);
},
// an ID is simple if it only has one part, and that part is not
// `..` or `this`.
this.isSimple = parts.length === 1 && !this.isScoped && depth === 0;
this.stringModeValue = this.string;
},
PartialNameNode: function(name, locInfo) {
LocationInfo.call(this, locInfo);
this.type = "PARTIAL_NAME";
this.name = name.original;
},
DataNode: function(id, locInfo) {
LocationInfo.call(this, locInfo);
this.type = "DATA";
this.id = id;
this.stringModeValue = id.stringModeValue;
this.idName = '@' + id.stringModeValue;
},
StringNode: function(string, locInfo) {
LocationInfo.call(this, locInfo);
this.type = "STRING";
this.original =
this.string =
this.stringModeValue = string;
},
NumberNode: function(number, locInfo) {
LocationInfo.call(this, locInfo);
this.type = "NUMBER";
this.original =
this.number = number;
this.stringModeValue = Number(number);
},
BooleanNode: function(bool, locInfo) {
LocationInfo.call(this, locInfo);
this.type = "BOOLEAN";
this.bool = bool;
this.stringModeValue = bool === "true";
},
CommentNode: function(comment, locInfo) {
LocationInfo.call(this, locInfo);
this.type = "comment";
this.comment = comment;
this.strip = {
inlineStandalone: true
};
simpleId: function(path) {
return path.parts.length === 1 && !AST.helpers.scopedId(path) && !path.depth;
}
}
};
// Must be exported as an object rather than the root of the module as the jison lexer
// most modify the object to operate properly.
// must modify the object to operate properly.
export default AST;
+16 -9
View File
@@ -1,18 +1,25 @@
import parser from "./parser";
import AST from "./ast";
module Helpers from "./helpers";
import { extend } from "../utils";
import parser from './parser';
import AST from './ast';
import WhitespaceControl from './whitespace-control';
import * as Helpers from './helpers';
import { extend } from '../utils';
export { parser };
var yy = {};
let yy = {};
extend(yy, Helpers, AST);
export function parse(input) {
// Just return if an already-compile AST was passed in.
if (input.constructor === AST.ProgramNode) { return input; }
export function parse(input, options) {
// Just return if an already-compiled AST was passed in.
if (input.type === 'Program') { return input; }
parser.yy = yy;
return parser.parse(input);
// Altering the shared object here, but this is ok as parser is a sync operation
yy.locInfo = function(locInfo) {
return new yy.SourceLocation(options && options.srcName, locInfo);
};
let strip = new WhitespaceControl();
return strip.accept(parser.parse(input));
}
+164
View File
@@ -0,0 +1,164 @@
/*global define */
import {isArray} from '../utils';
let SourceNode;
try {
/* istanbul ignore next */
if (typeof define !== 'function' || !define.amd) {
// We don't support this in AMD environments. For these environments, we asusme that
// they are running on the browser and thus have no need for the source-map library.
let SourceMap = require('source-map');
SourceNode = SourceMap.SourceNode;
}
} catch (err) {
/* NOP */
}
/* istanbul ignore if: tested but not covered in istanbul due to dist build */
if (!SourceNode) {
SourceNode = function(line, column, srcFile, chunks) {
this.src = '';
if (chunks) {
this.add(chunks);
}
};
/* istanbul ignore next */
SourceNode.prototype = {
add: function(chunks) {
if (isArray(chunks)) {
chunks = chunks.join('');
}
this.src += chunks;
},
prepend: function(chunks) {
if (isArray(chunks)) {
chunks = chunks.join('');
}
this.src = chunks + this.src;
},
toStringWithSourceMap: function() {
return {code: this.toString()};
},
toString: function() {
return this.src;
}
};
}
function castChunk(chunk, codeGen, loc) {
if (isArray(chunk)) {
let ret = [];
for (let i = 0, len = chunk.length; i < len; i++) {
ret.push(codeGen.wrap(chunk[i], loc));
}
return ret;
} else if (typeof chunk === 'boolean' || typeof chunk === 'number') {
// Handle primitives that the SourceNode will throw up on
return chunk + '';
}
return chunk;
}
function CodeGen(srcFile) {
this.srcFile = srcFile;
this.source = [];
}
CodeGen.prototype = {
prepend: function(source, loc) {
this.source.unshift(this.wrap(source, loc));
},
push: function(source, loc) {
this.source.push(this.wrap(source, loc));
},
merge: function() {
let source = this.empty();
this.each(function(line) {
source.add([' ', line, '\n']);
});
return source;
},
each: function(iter) {
for (let i = 0, len = this.source.length; i < len; i++) {
iter(this.source[i]);
}
},
empty: function(loc = this.currentLocation || {start: {}}) {
return new SourceNode(loc.start.line, loc.start.column, this.srcFile);
},
wrap: function(chunk, loc = this.currentLocation || {start: {}}) {
if (chunk instanceof SourceNode) {
return chunk;
}
chunk = castChunk(chunk, this, loc);
return new SourceNode(loc.start.line, loc.start.column, this.srcFile, chunk);
},
functionCall: function(fn, type, params) {
params = this.generateList(params);
return this.wrap([fn, type ? '.' + type + '(' : '(', params, ')']);
},
quotedString: function(str) {
return '"' + (str + '')
.replace(/\\/g, '\\\\')
.replace(/"/g, '\\"')
.replace(/\n/g, '\\n')
.replace(/\r/g, '\\r')
.replace(/\u2028/g, '\\u2028') // Per Ecma-262 7.3 + 7.8.4
.replace(/\u2029/g, '\\u2029') + '"';
},
objectLiteral: function(obj) {
let pairs = [];
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
let value = castChunk(obj[key], this);
if (value !== 'undefined') {
pairs.push([this.quotedString(key), ':', value]);
}
}
}
let ret = this.generateList(pairs);
ret.prepend('{');
ret.add('}');
return ret;
},
generateList: function(entries, loc) {
let ret = this.empty(loc);
for (let i = 0, len = entries.length; i < len; i++) {
if (i) {
ret.add(',');
}
ret.add(castChunk(entries[i], this, loc));
}
return ret;
},
generateArray: function(entries, loc) {
let ret = this.generateList(entries, loc);
ret.prepend('[');
ret.add(']');
return ret;
}
};
export default CodeGen;
+271 -209
View File
@@ -1,7 +1,8 @@
import Exception from "../exception";
import {isArray} from "../utils";
import Exception from '../exception';
import {isArray, indexOf} from '../utils';
import AST from './ast';
var slice = [].slice;
const slice = [].slice;
export function Compiler() {}
@@ -14,13 +15,13 @@ Compiler.prototype = {
compiler: Compiler,
equals: function(other) {
var len = this.opcodes.length;
let len = this.opcodes.length;
if (other.opcodes.length !== len) {
return false;
}
for (var i = 0; i < len; i++) {
var opcode = this.opcodes[i],
for (let i = 0; i < len; i++) {
let opcode = this.opcodes[i],
otherOpcode = other.opcodes[i];
if (opcode.opcode !== otherOpcode.opcode || !argEquals(opcode.args, otherOpcode.args)) {
return false;
@@ -30,7 +31,7 @@ Compiler.prototype = {
// We know that length is the same between the two arrays because they are directly tied
// to the opcode behavior above.
len = this.children.length;
for (i = 0; i < len; i++) {
for (let i = 0; i < len; i++) {
if (!this.children[i].equals(other.children[i])) {
return false;
}
@@ -42,16 +43,18 @@ Compiler.prototype = {
guid: 0,
compile: function(program, options) {
this.sourceNode = [];
this.opcodes = [];
this.children = [];
this.depths = {list: []};
this.options = options;
this.stringParams = options.stringParams;
this.trackIds = options.trackIds;
options.blockParams = options.blockParams || [];
// These changes will propagate to the other compiler components
var knownHelpers = this.options.knownHelpers;
this.options.knownHelpers = {
let knownHelpers = options.knownHelpers;
options.knownHelpers = {
'helperMissing': true,
'blockHelperMissing': true,
'each': true,
@@ -62,80 +65,77 @@ Compiler.prototype = {
'lookup': true
};
if (knownHelpers) {
for (var name in knownHelpers) {
this.options.knownHelpers[name] = knownHelpers[name];
for (let name in knownHelpers) {
if (name in knownHelpers) {
options.knownHelpers[name] = knownHelpers[name];
}
}
}
return this.accept(program);
},
accept: function(node) {
return this[node.type](node);
},
program: function(program) {
var statements = program.statements;
for(var i=0, l=statements.length; i<l; i++) {
this.accept(statements[i]);
}
this.isSimple = l === 1;
this.depths.list = this.depths.list.sort(function(a, b) {
return a - b;
});
return this;
},
compileProgram: function(program) {
var result = new this.compiler().compile(program, this.options);
var guid = this.guid++, depth;
let childCompiler = new this.compiler(), // eslint-disable-line new-cap
result = childCompiler.compile(program, this.options),
guid = this.guid++;
this.usePartial = this.usePartial || result.usePartial;
this.children[guid] = result;
for(var i=0, l=result.depths.list.length; i<l; i++) {
depth = result.depths.list[i];
if(depth < 2) { continue; }
else { this.addDepth(depth - 1); }
}
this.useDepths = this.useDepths || result.useDepths;
return guid;
},
block: function(block) {
var mustache = block.mustache,
program = block.program,
accept: function(node) {
this.sourceNode.unshift(node);
let ret = this[node.type](node);
this.sourceNode.shift();
return ret;
},
Program: function(program) {
this.options.blockParams.unshift(program.blockParams);
let body = program.body,
bodyLength = body.length;
for (let i = 0; i < bodyLength; i++) {
this.accept(body[i]);
}
this.options.blockParams.shift();
this.isSimple = bodyLength === 1;
this.blockParams = program.blockParams ? program.blockParams.length : 0;
return this;
},
BlockStatement: function(block) {
transformLiteralToPath(block);
let program = block.program,
inverse = block.inverse;
if (program) {
program = this.compileProgram(program);
}
program = program && this.compileProgram(program);
inverse = inverse && this.compileProgram(inverse);
if (inverse) {
inverse = this.compileProgram(inverse);
}
let type = this.classifySexpr(block);
var sexpr = mustache.sexpr;
var type = this.classifySexpr(sexpr);
if (type === "helper") {
this.helperSexpr(sexpr, program, inverse);
} else if (type === "simple") {
this.simpleSexpr(sexpr);
if (type === 'helper') {
this.helperSexpr(block, program, inverse);
} else if (type === 'simple') {
this.simpleSexpr(block);
// now that the simple mustache is resolved, we need to
// evaluate it by executing `blockHelperMissing`
this.opcode('pushProgram', program);
this.opcode('pushProgram', inverse);
this.opcode('emptyHash');
this.opcode('blockValue', sexpr.id.original);
this.opcode('blockValue', block.path.original);
} else {
this.ambiguousSexpr(sexpr, program, inverse);
this.ambiguousSexpr(block, program, inverse);
// now that the simple mustache is resolved, we need to
// evaluate it by executing `blockHelperMissing`
@@ -148,173 +148,190 @@ Compiler.prototype = {
this.opcode('append');
},
hash: function(hash) {
var pairs = hash.pairs, i, l;
this.opcode('pushHash');
for(i=0, l=pairs.length; i<l; i++) {
this.pushParam(pairs[i][1]);
}
while(i--) {
this.opcode('assignToHash', pairs[i][0]);
}
this.opcode('popHash');
},
partial: function(partial) {
var partialName = partial.partialName;
PartialStatement: function(partial) {
this.usePartial = true;
if (partial.hash) {
this.accept(partial.hash);
} else {
this.opcode('push', 'undefined');
let params = partial.params;
if (params.length > 1) {
throw new Exception('Unsupported number of partial arguments: ' + params.length, partial);
} else if (!params.length) {
params.push({type: 'PathExpression', parts: [], depth: 0});
}
if (partial.context) {
this.accept(partial.context);
} else {
this.opcode('getContext', 0);
this.opcode('pushContext');
let partialName = partial.name.original,
isDynamic = partial.name.type === 'SubExpression';
if (isDynamic) {
this.accept(partial.name);
}
this.opcode('invokePartial', partialName.name, partial.indent || '');
this.setupFullMustacheParams(partial, undefined, undefined, true);
let indent = partial.indent || '';
if (this.options.preventIndent && indent) {
this.opcode('appendContent', indent);
indent = '';
}
this.opcode('invokePartial', isDynamic, partialName, indent);
this.opcode('append');
},
content: function(content) {
if (content.string) {
this.opcode('appendContent', content.string);
}
},
MustacheStatement: function(mustache) {
this.SubExpression(mustache); // eslint-disable-line new-cap
mustache: function(mustache) {
this.sexpr(mustache.sexpr);
if(mustache.escaped && !this.options.noEscape) {
if (mustache.escaped && !this.options.noEscape) {
this.opcode('appendEscaped');
} else {
this.opcode('append');
}
},
ambiguousSexpr: function(sexpr, program, inverse) {
var id = sexpr.id,
name = id.parts[0],
isBlock = program != null || inverse != null;
this.opcode('getContext', id.depth);
this.opcode('pushProgram', program);
this.opcode('pushProgram', inverse);
this.ID(id);
this.opcode('invokeAmbiguous', name, isBlock);
},
simpleSexpr: function(sexpr) {
var id = sexpr.id;
if (id.type === 'DATA') {
this.DATA(id);
} else if (id.parts.length) {
this.ID(id);
} else {
// Simplified ID for `this`
this.addDepth(id.depth);
this.opcode('getContext', id.depth);
this.opcode('pushContext');
}
this.opcode('resolvePossibleLambda');
},
helperSexpr: function(sexpr, program, inverse) {
var params = this.setupFullMustacheParams(sexpr, program, inverse),
id = sexpr.id,
name = id.parts[0];
if (this.options.knownHelpers[name]) {
this.opcode('invokeKnownHelper', params.length, name);
} else if (this.options.knownHelpersOnly) {
throw new Exception("You specified knownHelpersOnly, but used the unknown helper " + name, sexpr);
} else {
id.falsy = true;
this.ID(id);
this.opcode('invokeHelper', params.length, id.original, id.isSimple);
ContentStatement: function(content) {
if (content.value) {
this.opcode('appendContent', content.value);
}
},
sexpr: function(sexpr) {
var type = this.classifySexpr(sexpr);
CommentStatement: function() {},
if (type === "simple") {
SubExpression: function(sexpr) {
transformLiteralToPath(sexpr);
let type = this.classifySexpr(sexpr);
if (type === 'simple') {
this.simpleSexpr(sexpr);
} else if (type === "helper") {
} else if (type === 'helper') {
this.helperSexpr(sexpr);
} else {
this.ambiguousSexpr(sexpr);
}
},
ambiguousSexpr: function(sexpr, program, inverse) {
let path = sexpr.path,
name = path.parts[0],
isBlock = program != null || inverse != null;
ID: function(id) {
this.addDepth(id.depth);
this.opcode('getContext', id.depth);
this.opcode('getContext', path.depth);
var name = id.parts[0];
if (!name) {
// Context reference, i.e. `{{foo .}}` or `{{foo ..}}`
this.opcode('pushContext');
this.opcode('pushProgram', program);
this.opcode('pushProgram', inverse);
this.accept(path);
this.opcode('invokeAmbiguous', name, isBlock);
},
simpleSexpr: function(sexpr) {
this.accept(sexpr.path);
this.opcode('resolvePossibleLambda');
},
helperSexpr: function(sexpr, program, inverse) {
let params = this.setupFullMustacheParams(sexpr, program, inverse),
path = sexpr.path,
name = path.parts[0];
if (this.options.knownHelpers[name]) {
this.opcode('invokeKnownHelper', params.length, name);
} else if (this.options.knownHelpersOnly) {
throw new Exception('You specified knownHelpersOnly, but used the unknown helper ' + name, sexpr);
} else {
this.opcode('lookupOnContext', id.parts, id.falsy, id.isScoped);
path.falsy = true;
this.accept(path);
this.opcode('invokeHelper', params.length, path.original, AST.helpers.simpleId(path));
}
},
DATA: function(data) {
this.options.data = true;
this.opcode('lookupData', data.id.depth, data.id.parts);
PathExpression: function(path) {
this.addDepth(path.depth);
this.opcode('getContext', path.depth);
let name = path.parts[0],
scoped = AST.helpers.scopedId(path),
blockParamId = !path.depth && !scoped && this.blockParamIndex(name);
if (blockParamId) {
this.opcode('lookupBlockParam', blockParamId, path.parts);
} else if (!name) {
// Context reference, i.e. `{{foo .}}` or `{{foo ..}}`
this.opcode('pushContext');
} else if (path.data) {
this.options.data = true;
this.opcode('lookupData', path.depth, path.parts);
} else {
this.opcode('lookupOnContext', path.parts, path.falsy, scoped);
}
},
STRING: function(string) {
this.opcode('pushString', string.string);
StringLiteral: function(string) {
this.opcode('pushString', string.value);
},
NUMBER: function(number) {
this.opcode('pushLiteral', number.number);
NumberLiteral: function(number) {
this.opcode('pushLiteral', number.value);
},
BOOLEAN: function(bool) {
this.opcode('pushLiteral', bool.bool);
BooleanLiteral: function(bool) {
this.opcode('pushLiteral', bool.value);
},
comment: function() {},
UndefinedLiteral: function() {
this.opcode('pushLiteral', 'undefined');
},
NullLiteral: function() {
this.opcode('pushLiteral', 'null');
},
Hash: function(hash) {
let pairs = hash.pairs,
i = 0,
l = pairs.length;
this.opcode('pushHash');
for (; i < l; i++) {
this.pushParam(pairs[i].value);
}
while (i--) {
this.opcode('assignToHash', pairs[i].key);
}
this.opcode('popHash');
},
// HELPERS
opcode: function(name) {
this.opcodes.push({ opcode: name, args: slice.call(arguments, 1) });
this.opcodes.push({ opcode: name, args: slice.call(arguments, 1), loc: this.sourceNode[0].loc });
},
addDepth: function(depth) {
if(depth === 0) { return; }
if(!this.depths[depth]) {
this.depths[depth] = true;
this.depths.list.push(depth);
if (!depth) {
return;
}
this.useDepths = true;
},
classifySexpr: function(sexpr) {
var isHelper = sexpr.isHelper;
var isEligible = sexpr.eligibleHelper;
var options = this.options;
let isSimple = AST.helpers.simpleId(sexpr.path);
let isBlockParam = isSimple && !!this.blockParamIndex(sexpr.path.parts[0]);
// a mustache is an eligible helper if:
// * its id is simple (a single part, not `this` or `..`)
let isHelper = !isBlockParam && AST.helpers.helperExpression(sexpr);
// if a mustache is an eligible helper but not a definite
// helper, it is ambiguous, and will be resolved in a later
// pass or at runtime.
let isEligible = !isBlockParam && (isHelper || isSimple);
// if ambiguous, we can possibly resolve the ambiguity now
// An eligible helper is one that does not have a complex path, i.e. `this.foo`, `../foo` etc.
if (isEligible && !isHelper) {
var name = sexpr.id.parts[0];
let name = sexpr.path.parts[0],
options = this.options;
if (options.knownHelpers[name]) {
isHelper = true;
@@ -323,58 +340,96 @@ Compiler.prototype = {
}
}
if (isHelper) { return "helper"; }
else if (isEligible) { return "ambiguous"; }
else { return "simple"; }
if (isHelper) {
return 'helper';
} else if (isEligible) {
return 'ambiguous';
} else {
return 'simple';
}
},
pushParams: function(params) {
for(var i=0, l=params.length; i<l; i++) {
for (let i = 0, l = params.length; i < l; i++) {
this.pushParam(params[i]);
}
},
pushParam: function(val) {
let value = val.value != null ? val.value : val.original || '';
if (this.stringParams) {
if(val.depth) {
if (value.replace) {
value = value
.replace(/^(\.?\.\/)*/g, '')
.replace(/\//g, '.');
}
if (val.depth) {
this.addDepth(val.depth);
}
this.opcode('getContext', val.depth || 0);
this.opcode('pushStringParam', val.stringModeValue, val.type);
this.opcode('pushStringParam', value, val.type);
if (val.type === 'sexpr') {
// Subexpressions get evaluated and passed in
if (val.type === 'SubExpression') {
// SubExpressions get evaluated and passed in
// in string params mode.
this.sexpr(val);
this.accept(val);
}
} else {
if (this.trackIds) {
this.opcode('pushId', val.type, val.idName || val.stringModeValue);
let blockParamIndex;
if (val.parts && !AST.helpers.scopedId(val) && !val.depth) {
blockParamIndex = this.blockParamIndex(val.parts[0]);
}
if (blockParamIndex) {
let blockParamChild = val.parts.slice(1).join('.');
this.opcode('pushId', 'BlockParam', blockParamIndex, blockParamChild);
} else {
value = val.original || value;
if (value.replace) {
value = value
.replace(/^\.\//g, '')
.replace(/^\.$/g, '');
}
this.opcode('pushId', val.type, value);
}
}
this.accept(val);
}
},
setupFullMustacheParams: function(sexpr, program, inverse) {
var params = sexpr.params;
setupFullMustacheParams: function(sexpr, program, inverse, omitEmpty) {
let params = sexpr.params;
this.pushParams(params);
this.opcode('pushProgram', program);
this.opcode('pushProgram', inverse);
if (sexpr.hash) {
this.hash(sexpr.hash);
this.accept(sexpr.hash);
} else {
this.opcode('emptyHash');
this.opcode('emptyHash', omitEmpty);
}
return params;
},
blockParamIndex: function(name) {
for (let depth = 0, len = this.options.blockParams.length; depth < len; depth++) {
let blockParams = this.options.blockParams[depth],
param = blockParams && indexOf(blockParams, name);
if (blockParams && param >= 0) {
return [depth, param];
}
}
}
};
export function precompile(input, options, env) {
if (input == null || (typeof input !== 'string' && input.constructor !== env.AST.ProgramNode)) {
throw new Exception("You must pass a string or Handlebars AST to Handlebars.precompile. You passed " + input);
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);
}
options = options || {};
@@ -385,18 +440,16 @@ export function precompile(input, options, env) {
options.useDepths = true;
}
var ast = env.parse(input);
var environment = new env.Compiler().compile(ast, options);
let ast = env.parse(input, options),
environment = new env.Compiler().compile(ast, options);
return new env.JavaScriptCompiler().compile(environment, options);
}
export function compile(input, options, env) {
if (input == null || (typeof input !== 'string' && input.constructor !== env.AST.ProgramNode)) {
throw new Exception("You must pass a string or Handlebars AST to Handlebars.compile. You passed " + input);
export function compile(input, options = {}, env) {
if (input == null || (typeof input !== 'string' && input.type !== 'Program')) {
throw new Exception('You must pass a string or Handlebars AST to Handlebars.compile. You passed ' + input);
}
options = options || {};
if (!('data' in options)) {
options.data = true;
}
@@ -404,33 +457,33 @@ export function compile(input, options, env) {
options.useDepths = true;
}
var compiled;
let compiled;
function compileInput() {
var ast = env.parse(input);
var environment = new env.Compiler().compile(ast, options);
var templateSpec = new env.JavaScriptCompiler().compile(environment, options, undefined, true);
let ast = env.parse(input, options),
environment = new env.Compiler().compile(ast, options),
templateSpec = new env.JavaScriptCompiler().compile(environment, options, undefined, true);
return env.template(templateSpec);
}
// Template is only compiled on first use and cached after that point.
var ret = function(context, options) {
function ret(context, execOptions) {
if (!compiled) {
compiled = compileInput();
}
return compiled.call(this, context, options);
return compiled.call(this, context, execOptions);
}
ret._setup = function(setupOptions) {
if (!compiled) {
compiled = compileInput();
}
return compiled._setup(setupOptions);
};
ret._setup = function(options) {
ret._child = function(i, data, blockParams, depths) {
if (!compiled) {
compiled = compileInput();
}
return compiled._setup(options);
};
ret._child = function(i, data, depths) {
if (!compiled) {
compiled = compileInput();
}
return compiled._child(i, data, depths);
return compiled._child(i, data, blockParams, depths);
};
return ret;
}
@@ -441,7 +494,7 @@ function argEquals(a, b) {
}
if (isArray(a) && isArray(b) && a.length === b.length) {
for (var i = 0; i < a.length; i++) {
for (let i = 0; i < a.length; i++) {
if (!argEquals(a[i], b[i])) {
return false;
}
@@ -449,3 +502,12 @@ function argEquals(a, b) {
return true;
}
}
function transformLiteralToPath(sexpr) {
if (!sexpr.path.parts) {
let literal = sexpr.path;
// Casting to string here to make false and 0 literal values play nicely with the rest
// of the system.
sexpr.path = new AST.PathExpression(false, 0, [literal.original + ''], literal.original + '', literal.loc);
}
}
+101 -164
View File
@@ -1,186 +1,123 @@
import Exception from "../exception";
import Exception from '../exception';
export function SourceLocation(source, locInfo) {
this.source = source;
this.start = {
line: locInfo.first_line,
column: locInfo.first_column
};
this.end = {
line: locInfo.last_line,
column: locInfo.last_column
};
}
export function id(token) {
if (/^\[.*\]$/.test(token)) {
return token.substr(1, token.length - 2);
} else {
return token;
}
}
export function stripFlags(open, close) {
return {
left: open.charAt(2) === '~',
right: close.charAt(close.length-3) === '~'
open: open.charAt(2) === '~',
close: close.charAt(close.length - 3) === '~'
};
}
export function stripComment(comment) {
return comment.replace(/^\{\{~?\!-?-?/, '')
.replace(/-?-?~?\}\}$/, '');
}
export function prepareBlock(mustache, program, inverseAndProgram, close, inverted, locInfo) {
/*jshint -W040 */
if (mustache.sexpr.id.original !== close.path.original) {
throw new Exception(mustache.sexpr.id.original + ' doesn\'t match ' + close.path.original, mustache);
export function preparePath(data, parts, locInfo) {
locInfo = this.locInfo(locInfo);
let original = data ? '@' : '',
dig = [],
depth = 0,
depthString = '';
for (let i = 0, l = parts.length; i < l; i++) {
let part = parts[i].part,
// If we have [] syntax then we do not treat path references as operators,
// i.e. foo.[this] resolves to approximately context.foo['this']
isLiteral = parts[i].original !== part;
original += (parts[i].separator || '') + part;
if (!isLiteral && (part === '..' || part === '.' || part === 'this')) {
if (dig.length > 0) {
throw new Exception('Invalid path: ' + original, {loc: locInfo});
} else if (part === '..') {
depth++;
depthString += '../';
}
} else {
dig.push(part);
}
}
var inverse = inverseAndProgram && inverseAndProgram.program;
return new this.PathExpression(data, depth, dig, original, locInfo);
}
var strip = {
left: mustache.strip.left,
right: close.strip.right,
export function prepareMustache(path, params, hash, open, strip, locInfo) {
// Must use charAt to support IE pre-10
let escapeFlag = open.charAt(3) || open.charAt(2),
escaped = escapeFlag !== '{' && escapeFlag !== '&';
// Determine the standalone candiacy. Basically flag our content as being possibly standalone
// so our parent can determine if we actually are standalone
openStandalone: isNextWhitespace(program.statements),
closeStandalone: isPrevWhitespace((inverse || program).statements)
};
return new this.MustacheStatement(path, params, hash, escaped, strip, this.locInfo(locInfo));
}
if (mustache.strip.right) {
omitRight(program.statements, null, true);
export function prepareRawBlock(openRawBlock, content, close, locInfo) {
if (openRawBlock.path.original !== close) {
let errorNode = {loc: openRawBlock.path.loc};
throw new Exception(openRawBlock.path.original + " doesn't match " + close, errorNode);
}
if (inverse) {
var inverseStrip = inverseAndProgram.strip;
locInfo = this.locInfo(locInfo);
let program = new this.Program([content], null, {}, locInfo);
if (inverseStrip.left) {
omitLeft(program.statements, null, true);
}
if (inverseStrip.right) {
omitRight(inverse.statements, null, true);
}
if (close.strip.left) {
omitLeft(inverse.statements, null, true);
return new this.BlockStatement(
openRawBlock.path, openRawBlock.params, openRawBlock.hash,
program, undefined,
{}, {}, {},
locInfo);
}
export function prepareBlock(openBlock, program, inverseAndProgram, close, inverted, locInfo) {
// When we are chaining inverse calls, we will not have a close path
if (close && close.path && openBlock.path.original !== close.path.original) {
let errorNode = {loc: openBlock.path.loc};
throw new Exception(openBlock.path.original + ' doesn\'t match ' + close.path.original, errorNode);
}
program.blockParams = openBlock.blockParams;
let inverse,
inverseStrip;
if (inverseAndProgram) {
if (inverseAndProgram.chain) {
inverseAndProgram.program.body[0].closeStrip = close.strip;
}
// Find standalone else statments
if (isPrevWhitespace(program.statements)
&& isNextWhitespace(inverse.statements)) {
omitLeft(program.statements);
omitRight(inverse.statements);
}
} else {
if (close.strip.left) {
omitLeft(program.statements, null, true);
}
inverseStrip = inverseAndProgram.strip;
inverse = inverseAndProgram.program;
}
if (inverted) {
return new this.BlockNode(mustache, inverse, program, strip, locInfo);
} else {
return new this.BlockNode(mustache, program, inverse, strip, locInfo);
inverted = inverse;
inverse = program;
program = inverted;
}
}
export function prepareProgram(statements, isRoot) {
for (var i = 0, l = statements.length; i < l; i++) {
var current = statements[i],
strip = current.strip;
if (!strip) {
continue;
}
var _isPrevWhitespace = isPrevWhitespace(statements, i, isRoot, current.type === 'partial'),
_isNextWhitespace = isNextWhitespace(statements, i, isRoot),
openStandalone = strip.openStandalone && _isPrevWhitespace,
closeStandalone = strip.closeStandalone && _isNextWhitespace,
inlineStandalone = strip.inlineStandalone && _isPrevWhitespace && _isNextWhitespace;
if (strip.right) {
omitRight(statements, i, true);
}
if (strip.left) {
omitLeft(statements, i, true);
}
if (inlineStandalone) {
omitRight(statements, i);
if (omitLeft(statements, i)) {
// If we are on a standalone node, save the indent info for partials
if (current.type === 'partial') {
current.indent = (/([ \t]+$)/).exec(statements[i-1].original) ? RegExp.$1 : '';
}
}
}
if (openStandalone) {
omitRight((current.program || current.inverse).statements);
// Strip out the previous content node if it's whitespace only
omitLeft(statements, i);
}
if (closeStandalone) {
// Always strip the next node
omitRight(statements, i);
omitLeft((current.inverse || current.program).statements);
}
}
return statements;
}
function isPrevWhitespace(statements, i, isRoot) {
if (i === undefined) {
i = statements.length;
}
// Nodes that end with newlines are considered whitespace (but are special
// cased for strip operations)
var prev = statements[i-1],
sibling = statements[i-2];
if (!prev) {
return isRoot;
}
if (prev.type === 'content') {
return (sibling || !isRoot ? (/\r?\n\s*?$/) : (/(^|\r?\n)\s*?$/)).test(prev.original);
}
}
function isNextWhitespace(statements, i, isRoot) {
if (i === undefined) {
i = -1;
}
var next = statements[i+1],
sibling = statements[i+2];
if (!next) {
return isRoot;
}
if (next.type === 'content') {
return (sibling || !isRoot ? (/^\s*?\r?\n/) : (/^\s*?(\r?\n|$)/)).test(next.original);
}
}
// Marks the node to the right of the position as omitted.
// I.e. {{foo}}' ' will mark the ' ' node as omitted.
//
// If i is undefined, then the first child will be marked as such.
//
// If mulitple is truthy then all whitespace will be stripped out until non-whitespace
// content is met.
function omitRight(statements, i, multiple) {
var current = statements[i == null ? 0 : i + 1];
if (!current || current.type !== 'content' || (!multiple && current.rightStripped)) {
return;
}
var original = current.string;
current.string = current.string.replace(multiple ? (/^\s+/) : (/^[ \t]*\r?\n?/), '');
current.rightStripped = current.string !== original;
}
// Marks the node to the left of the position as omitted.
// I.e. ' '{{foo}} will mark the ' ' node as omitted.
//
// If i is undefined then the last child will be marked as such.
//
// If mulitple is truthy then all whitespace will be stripped out until non-whitespace
// content is met.
function omitLeft(statements, i, multiple) {
var current = statements[i == null ? statements.length - 1 : i - 1];
if (!current || current.type !== 'content' || (!multiple && current.leftStripped)) {
return;
}
// We omit the last node if it's whitespace only and not preceeded by a non-content node.
var original = current.string;
current.string = current.string.replace(multiple ? (/\s+$/) : (/[ \t]+$/), '');
current.leftStripped = current.string !== original;
return current.leftStripped;
return new this.BlockStatement(
openBlock.path, openBlock.params, openBlock.hash,
program, inverse,
openBlock.strip, inverseStrip, close && close.strip,
this.locInfo(locInfo));
}
File diff suppressed because it is too large Load Diff
+88 -78
View File
@@ -1,4 +1,5 @@
import Visitor from "./visitor";
/*eslint-disable new-cap */
import Visitor from './visitor';
export function print(ast) {
return new PrintVisitor().accept(ast);
@@ -11,23 +12,32 @@ export function PrintVisitor() {
PrintVisitor.prototype = new Visitor();
PrintVisitor.prototype.pad = function(string) {
var out = "";
let out = '';
for(var i=0,l=this.padding; i<l; i++) {
out = out + " ";
for (let i = 0, l = this.padding; i < l; i++) {
out = out + ' ';
}
out = out + string + "\n";
out = out + string + '\n';
return out;
};
PrintVisitor.prototype.program = function(program) {
var out = "",
statements = program.statements,
PrintVisitor.prototype.Program = function(program) {
let out = '',
body = program.body,
i, l;
for(i=0, l=statements.length; i<l; i++) {
out = out + this.accept(statements[i]);
if (program.blockParams) {
let blockParams = 'BLOCK PARAMS: [';
for (i = 0, l = program.blockParams.length; i < l; i++) {
blockParams += ' ' + program.blockParams[i];
}
blockParams += ' ]';
out += this.pad(blockParams);
}
for (i = 0, l = body.length; i < l; i++) {
out = out + this.accept(body[i]);
}
this.padding--;
@@ -35,21 +45,25 @@ PrintVisitor.prototype.program = function(program) {
return out;
};
PrintVisitor.prototype.block = function(block) {
var out = "";
PrintVisitor.prototype.MustacheStatement = function(mustache) {
return this.pad('{{ ' + this.SubExpression(mustache) + ' }}');
};
out = out + this.pad("BLOCK:");
PrintVisitor.prototype.BlockStatement = function(block) {
let out = '';
out = out + this.pad('BLOCK:');
this.padding++;
out = out + this.accept(block.mustache);
out = out + this.pad(this.SubExpression(block));
if (block.program) {
out = out + this.pad("PROGRAM:");
out = out + this.pad('PROGRAM:');
this.padding++;
out = out + this.accept(block.program);
this.padding--;
}
if (block.inverse) {
if (block.program) { this.padding++; }
out = out + this.pad("{{^}}");
out = out + this.pad('{{^}}');
this.padding++;
out = out + this.accept(block.inverse);
this.padding--;
@@ -60,82 +74,78 @@ PrintVisitor.prototype.block = function(block) {
return out;
};
PrintVisitor.prototype.sexpr = function(sexpr) {
var params = sexpr.params, paramStrings = [], hash;
PrintVisitor.prototype.PartialStatement = function(partial) {
let content = 'PARTIAL:' + partial.name.original;
if (partial.params[0]) {
content += ' ' + this.accept(partial.params[0]);
}
if (partial.hash) {
content += ' ' + this.accept(partial.hash);
}
return this.pad('{{> ' + content + ' }}');
};
for(var i=0, l=params.length; i<l; i++) {
PrintVisitor.prototype.ContentStatement = function(content) {
return this.pad("CONTENT[ '" + content.value + "' ]");
};
PrintVisitor.prototype.CommentStatement = function(comment) {
return this.pad("{{! '" + comment.value + "' }}");
};
PrintVisitor.prototype.SubExpression = function(sexpr) {
let params = sexpr.params,
paramStrings = [],
hash;
for (let i = 0, l = params.length; i < l; i++) {
paramStrings.push(this.accept(params[i]));
}
params = "[" + paramStrings.join(", ") + "]";
params = '[' + paramStrings.join(', ') + ']';
hash = sexpr.hash ? " " + this.accept(sexpr.hash) : "";
hash = sexpr.hash ? ' ' + this.accept(sexpr.hash) : '';
return this.accept(sexpr.id) + " " + params + hash;
return this.accept(sexpr.path) + ' ' + params + hash;
};
PrintVisitor.prototype.mustache = function(mustache) {
return this.pad("{{ " + this.accept(mustache.sexpr) + " }}");
PrintVisitor.prototype.PathExpression = function(id) {
let path = id.parts.join('/');
return (id.data ? '@' : '') + 'PATH:' + path;
};
PrintVisitor.prototype.partial = function(partial) {
var content = this.accept(partial.partialName);
if(partial.context) {
content += " " + this.accept(partial.context);
}
if (partial.hash) {
content += " " + this.accept(partial.hash);
}
return this.pad("{{> " + content + " }}");
PrintVisitor.prototype.StringLiteral = function(string) {
return '"' + string.value + '"';
};
PrintVisitor.prototype.hash = function(hash) {
var pairs = hash.pairs;
var joinedPairs = [], left, right;
PrintVisitor.prototype.NumberLiteral = function(number) {
return 'NUMBER{' + number.value + '}';
};
for(var i=0, l=pairs.length; i<l; i++) {
left = pairs[i][0];
right = this.accept(pairs[i][1]);
joinedPairs.push( left + "=" + right );
PrintVisitor.prototype.BooleanLiteral = function(bool) {
return 'BOOLEAN{' + bool.value + '}';
};
PrintVisitor.prototype.UndefinedLiteral = function() {
return 'UNDEFINED';
};
PrintVisitor.prototype.NullLiteral = function() {
return 'NULL';
};
PrintVisitor.prototype.Hash = function(hash) {
let pairs = hash.pairs,
joinedPairs = [];
for (let i = 0, l = pairs.length; i < l; i++) {
joinedPairs.push(this.accept(pairs[i]));
}
return "HASH{" + joinedPairs.join(", ") + "}";
return 'HASH{' + joinedPairs.join(', ') + '}';
};
PrintVisitor.prototype.STRING = function(string) {
return '"' + string.string + '"';
PrintVisitor.prototype.HashPair = function(pair) {
return pair.key + '=' + this.accept(pair.value);
};
PrintVisitor.prototype.NUMBER = function(number) {
return "NUMBER{" + number.number + "}";
};
PrintVisitor.prototype.BOOLEAN = function(bool) {
return "BOOLEAN{" + bool.bool + "}";
};
PrintVisitor.prototype.ID = function(id) {
var path = id.parts.join("/");
if(id.parts.length > 1) {
return "PATH:" + path;
} else {
return "ID:" + path;
}
};
PrintVisitor.prototype.PARTIAL_NAME = function(partialName) {
return "PARTIAL:" + partialName.name;
};
PrintVisitor.prototype.DATA = function(data) {
return "@" + this.accept(data.id);
};
PrintVisitor.prototype.content = function(content) {
return this.pad("CONTENT[ '" + content.string + "' ]");
};
PrintVisitor.prototype.comment = function(comment) {
return this.pad("{{! '" + comment.comment + "' }}");
};
/*eslint-enable new-cap */
+110 -2
View File
@@ -1,10 +1,118 @@
function Visitor() {}
import Exception from '../exception';
import AST from './ast';
function Visitor() {
this.parents = [];
}
Visitor.prototype = {
constructor: Visitor,
mutating: false,
// Visits a given value. If mutating, will replace the value if necessary.
acceptKey: function(node, name) {
let value = this.accept(node[name]);
if (this.mutating) {
// Hacky sanity check:
if (value && (!value.type || !AST[value.type])) {
throw new Exception('Unexpected node type "' + value.type + '" found when accepting ' + name + ' on ' + node.type);
}
node[name] = value;
}
},
// Performs an accept operation with added sanity check to ensure
// required keys are not removed.
acceptRequired: function(node, name) {
this.acceptKey(node, name);
if (!node[name]) {
throw new Exception(node.type + ' requires ' + name);
}
},
// Traverses a given array. If mutating, empty respnses will be removed
// for child elements.
acceptArray: function(array) {
for (let i = 0, l = array.length; i < l; i++) {
this.acceptKey(array, i);
if (!array[i]) {
array.splice(i, 1);
i--;
l--;
}
}
},
accept: function(object) {
return this[object.type](object);
if (!object) {
return;
}
if (this.current) {
this.parents.unshift(this.current);
}
this.current = object;
let ret = this[object.type](object);
this.current = this.parents.shift();
if (!this.mutating || ret) {
return ret;
} else if (ret !== false) {
return object;
}
},
Program: function(program) {
this.acceptArray(program.body);
},
MustacheStatement: function(mustache) {
this.acceptRequired(mustache, 'path');
this.acceptArray(mustache.params);
this.acceptKey(mustache, 'hash');
},
BlockStatement: function(block) {
this.acceptRequired(block, 'path');
this.acceptArray(block.params);
this.acceptKey(block, 'hash');
this.acceptKey(block, 'program');
this.acceptKey(block, 'inverse');
},
PartialStatement: function(partial) {
this.acceptRequired(partial, 'name');
this.acceptArray(partial.params);
this.acceptKey(partial, 'hash');
},
ContentStatement: function(/* content */) {},
CommentStatement: function(/* comment */) {},
SubExpression: function(sexpr) {
this.acceptRequired(sexpr, 'path');
this.acceptArray(sexpr.params);
this.acceptKey(sexpr, 'hash');
},
PathExpression: function(/* path */) {},
StringLiteral: function(/* string */) {},
NumberLiteral: function(/* number */) {},
BooleanLiteral: function(/* bool */) {},
UndefinedLiteral: function(/* literal */) {},
NullLiteral: function(/* literal */) {},
Hash: function(hash) {
this.acceptArray(hash.pairs);
},
HashPair: function(pair) {
this.acceptRequired(pair, 'value');
}
};
@@ -0,0 +1,208 @@
import Visitor from './visitor';
function WhitespaceControl() {
}
WhitespaceControl.prototype = new Visitor();
WhitespaceControl.prototype.Program = function(program) {
let isRoot = !this.isRootSeen;
this.isRootSeen = true;
let body = program.body;
for (let i = 0, l = body.length; i < l; i++) {
let current = body[i],
strip = this.accept(current);
if (!strip) {
continue;
}
let _isPrevWhitespace = isPrevWhitespace(body, i, isRoot),
_isNextWhitespace = isNextWhitespace(body, i, isRoot),
openStandalone = strip.openStandalone && _isPrevWhitespace,
closeStandalone = strip.closeStandalone && _isNextWhitespace,
inlineStandalone = strip.inlineStandalone && _isPrevWhitespace && _isNextWhitespace;
if (strip.close) {
omitRight(body, i, true);
}
if (strip.open) {
omitLeft(body, i, true);
}
if (inlineStandalone) {
omitRight(body, i);
if (omitLeft(body, i)) {
// If we are on a standalone node, save the indent info for partials
if (current.type === 'PartialStatement') {
// Pull out the whitespace from the final line
current.indent = (/([ \t]+$)/).exec(body[i - 1].original)[1];
}
}
}
if (openStandalone) {
omitRight((current.program || current.inverse).body);
// Strip out the previous content node if it's whitespace only
omitLeft(body, i);
}
if (closeStandalone) {
// Always strip the next node
omitRight(body, i);
omitLeft((current.inverse || current.program).body);
}
}
return program;
};
WhitespaceControl.prototype.BlockStatement = function(block) {
this.accept(block.program);
this.accept(block.inverse);
// Find the inverse program that is involed with whitespace stripping.
let program = block.program || block.inverse,
inverse = block.program && block.inverse,
firstInverse = inverse,
lastInverse = inverse;
if (inverse && inverse.chained) {
firstInverse = inverse.body[0].program;
// Walk the inverse chain to find the last inverse that is actually in the chain.
while (lastInverse.chained) {
lastInverse = lastInverse.body[lastInverse.body.length - 1].program;
}
}
let strip = {
open: block.openStrip.open,
close: block.closeStrip.close,
// Determine the standalone candiacy. Basically flag our content as being possibly standalone
// so our parent can determine if we actually are standalone
openStandalone: isNextWhitespace(program.body),
closeStandalone: isPrevWhitespace((firstInverse || program).body)
};
if (block.openStrip.close) {
omitRight(program.body, null, true);
}
if (inverse) {
let inverseStrip = block.inverseStrip;
if (inverseStrip.open) {
omitLeft(program.body, null, true);
}
if (inverseStrip.close) {
omitRight(firstInverse.body, null, true);
}
if (block.closeStrip.open) {
omitLeft(lastInverse.body, null, true);
}
// Find standalone else statments
if (isPrevWhitespace(program.body)
&& isNextWhitespace(firstInverse.body)) {
omitLeft(program.body);
omitRight(firstInverse.body);
}
} else if (block.closeStrip.open) {
omitLeft(program.body, null, true);
}
return strip;
};
WhitespaceControl.prototype.MustacheStatement = function(mustache) {
return mustache.strip;
};
WhitespaceControl.prototype.PartialStatement =
WhitespaceControl.prototype.CommentStatement = function(node) {
/* istanbul ignore next */
let strip = node.strip || {};
return {
inlineStandalone: true,
open: strip.open,
close: strip.close
};
};
function isPrevWhitespace(body, i, isRoot) {
if (i === undefined) {
i = body.length;
}
// Nodes that end with newlines are considered whitespace (but are special
// cased for strip operations)
let prev = body[i - 1],
sibling = body[i - 2];
if (!prev) {
return isRoot;
}
if (prev.type === 'ContentStatement') {
return (sibling || !isRoot ? (/\r?\n\s*?$/) : (/(^|\r?\n)\s*?$/)).test(prev.original);
}
}
function isNextWhitespace(body, i, isRoot) {
if (i === undefined) {
i = -1;
}
let next = body[i + 1],
sibling = body[i + 2];
if (!next) {
return isRoot;
}
if (next.type === 'ContentStatement') {
return (sibling || !isRoot ? (/^\s*?\r?\n/) : (/^\s*?(\r?\n|$)/)).test(next.original);
}
}
// Marks the node to the right of the position as omitted.
// I.e. {{foo}}' ' will mark the ' ' node as omitted.
//
// If i is undefined, then the first child will be marked as such.
//
// If mulitple is truthy then all whitespace will be stripped out until non-whitespace
// content is met.
function omitRight(body, i, multiple) {
let current = body[i == null ? 0 : i + 1];
if (!current || current.type !== 'ContentStatement' || (!multiple && current.rightStripped)) {
return;
}
let original = current.value;
current.value = current.value.replace(multiple ? (/^\s+/) : (/^[ \t]*\r?\n?/), '');
current.rightStripped = current.value !== original;
}
// Marks the node to the left of the position as omitted.
// I.e. ' '{{foo}} will mark the ' ' node as omitted.
//
// If i is undefined then the last child will be marked as such.
//
// If mulitple is truthy then all whitespace will be stripped out until non-whitespace
// content is met.
function omitLeft(body, i, multiple) {
let current = body[i == null ? body.length - 1 : i - 1];
if (!current || current.type !== 'ContentStatement' || (!multiple && current.leftStripped)) {
return;
}
// We omit the last node if it's whitespace only and not preceeded by a non-content node.
let original = current.value;
current.value = current.value.replace(multiple ? (/\s+$/) : (/[ \t]+$/), '');
current.leftStripped = current.value !== original;
return current.leftStripped;
}
export default WhitespaceControl;
+16 -9
View File
@@ -1,24 +1,31 @@
var errorProps = ['description', 'fileName', 'lineNumber', 'message', 'name', 'number', 'stack'];
const errorProps = ['description', 'fileName', 'lineNumber', 'message', 'name', 'number', 'stack'];
function Exception(message, node) {
var line;
if (node && node.firstLine) {
line = node.firstLine;
let loc = node && node.loc,
line,
column;
if (loc) {
line = loc.start.line;
column = loc.start.column;
message += ' - ' + line + ':' + node.firstColumn;
message += ' - ' + line + ':' + column;
}
var tmp = Error.prototype.constructor.call(this, message);
let tmp = Error.prototype.constructor.call(this, message);
// Unfortunately errors are not enumerable in Chrome (at least), so `for prop in tmp` doesn't work.
for (var idx = 0; idx < errorProps.length; idx++) {
for (let idx = 0; idx < errorProps.length; idx++) {
this[errorProps[idx]] = tmp[errorProps[idx]];
}
if (line) {
if (Error.captureStackTrace) {
Error.captureStackTrace(this, Exception);
}
if (loc) {
this.lineNumber = line;
this.column = node.firstColumn;
this.column = column;
}
}
+12
View File
@@ -0,0 +1,12 @@
/*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;
}
};
}
+84 -62
View File
@@ -1,31 +1,29 @@
module Utils from "./utils";
import Exception from "./exception";
import { COMPILER_REVISION, REVISION_CHANGES, createFrame } from "./base";
import * as Utils from './utils';
import Exception from './exception';
import { COMPILER_REVISION, REVISION_CHANGES, createFrame } from './base';
export function checkRevision(compilerInfo) {
var compilerRevision = compilerInfo && compilerInfo[0] || 1,
currentRevision = COMPILER_REVISION;
const compilerRevision = compilerInfo && compilerInfo[0] || 1,
currentRevision = COMPILER_REVISION;
if (compilerRevision !== currentRevision) {
if (compilerRevision < currentRevision) {
var runtimeVersions = REVISION_CHANGES[currentRevision],
compilerVersions = REVISION_CHANGES[compilerRevision];
throw new Exception("Template was precompiled with an older version of Handlebars than the current runtime. "+
"Please update your precompiler to a newer version ("+runtimeVersions+") or downgrade your runtime to an older version ("+compilerVersions+").");
const runtimeVersions = REVISION_CHANGES[currentRevision],
compilerVersions = REVISION_CHANGES[compilerRevision];
throw new Exception('Template was precompiled with an older version of Handlebars than the current runtime. ' +
'Please update your precompiler to a newer version (' + runtimeVersions + ') or downgrade your runtime to an older version (' + compilerVersions + ').');
} else {
// Use the embedded version info since the runtime doesn't know about this revision yet
throw new Exception("Template was precompiled with a newer version of Handlebars than the current runtime. "+
"Please update your runtime to a newer version ("+compilerInfo[1]+").");
throw new Exception('Template was precompiled with a newer version of Handlebars than the current runtime. ' +
'Please update your runtime to a newer version (' + compilerInfo[1] + ').');
}
}
}
// TODO: Remove this line and break up compilePartial
export function template(templateSpec, env) {
/* istanbul ignore next */
if (!env) {
throw new Exception("No environment passed to template");
throw new Exception('No environment passed to template');
}
if (!templateSpec || !templateSpec.main) {
throw new Exception('Unknown template object: ' + typeof templateSpec);
@@ -35,41 +33,47 @@ export function template(templateSpec, env) {
// for external users to override these as psuedo-supported APIs.
env.VM.checkRevision(templateSpec.compiler);
var invokePartialWrapper = function(partial, indent, name, context, hash, helpers, partials, data, depths) {
if (hash) {
context = Utils.extend({}, context, hash);
function invokePartialWrapper(partial, context, options) {
if (options.hash) {
context = Utils.extend({}, context, options.hash);
}
var result = env.VM.invokePartial.call(this, partial, name, context, helpers, partials, data, depths);
partial = env.VM.resolvePartial.call(this, partial, context, options);
let result = env.VM.invokePartial.call(this, partial, context, options);
if (result == null && env.compile) {
var options = { helpers: helpers, partials: partials, data: data, depths: depths };
partials[name] = env.compile(partial, { data: data !== undefined, compat: templateSpec.compat }, env);
result = partials[name](context, options);
options.partials[options.name] = env.compile(partial, templateSpec.compilerOptions, env);
result = options.partials[options.name](context, options);
}
if (result != null) {
if (indent) {
var lines = result.split('\n');
for (var i = 0, l = lines.length; i < l; i++) {
if (options.indent) {
let lines = result.split('\n');
for (let i = 0, l = lines.length; i < l; i++) {
if (!lines[i] && i + 1 === l) {
break;
}
lines[i] = indent + lines[i];
lines[i] = options.indent + lines[i];
}
result = lines.join('\n');
}
return result;
} else {
throw new Exception("The partial " + name + " could not be compiled when running in runtime-only mode");
throw new Exception('The partial ' + options.name + ' could not be compiled when running in runtime-only mode');
}
};
}
// Just add water
var container = {
let container = {
strict: function(obj, name) {
if (!(name in obj)) {
throw new Exception('"' + name + '" not defined in ' + obj);
}
return obj[name];
},
lookup: function(depths, name) {
var len = depths.length;
for (var i = 0; i < len; i++) {
const len = depths.length;
for (let i = 0; i < len; i++) {
if (depths[i] && depths[i][name] != null) {
return depths[i][name];
}
@@ -87,52 +91,52 @@ export function template(templateSpec, env) {
},
programs: [],
program: function(i, data, depths) {
var programWrapper = this.programs[i],
program: function(i, data, declaredBlockParams, blockParams, depths) {
let programWrapper = this.programs[i],
fn = this.fn(i);
if (data || depths) {
programWrapper = program(this, i, fn, data, depths);
if (data || depths || blockParams || declaredBlockParams) {
programWrapper = wrapProgram(this, i, fn, data, declaredBlockParams, blockParams, depths);
} else if (!programWrapper) {
programWrapper = this.programs[i] = program(this, i, fn);
programWrapper = this.programs[i] = wrapProgram(this, i, fn);
}
return programWrapper;
},
data: function(data, depth) {
while (data && depth--) {
data = data._parent;
data: function(value, depth) {
while (value && depth--) {
value = value._parent;
}
return data;
return value;
},
merge: function(param, common) {
var ret = param || common;
let obj = param || common;
if (param && common && (param !== common)) {
ret = Utils.extend({}, common, param);
obj = Utils.extend({}, common, param);
}
return ret;
return obj;
},
noop: env.VM.noop,
compilerInfo: templateSpec.compiler
};
var ret = function(context, options) {
options = options || {};
var data = options.data;
function ret(context, options = {}) {
let data = options.data;
ret._setup(options);
if (!options.partial && templateSpec.useData) {
data = initData(context, data);
}
var depths;
let depths,
blockParams = templateSpec.useBlockParams ? [] : undefined;
if (templateSpec.useDepths) {
depths = options.depths ? [context].concat(options.depths) : [context];
}
return templateSpec.main.call(container, context, container.helpers, container.partials, data, depths);
};
return templateSpec.main.call(container, context, container.helpers, container.partials, data, blockParams, depths);
}
ret.isTop = true;
ret._setup = function(options) {
@@ -148,38 +152,56 @@ export function template(templateSpec, env) {
}
};
ret._child = function(i, data, depths) {
ret._child = function(i, data, blockParams, depths) {
if (templateSpec.useBlockParams && !blockParams) {
throw new Exception('must pass block params');
}
if (templateSpec.useDepths && !depths) {
throw new Exception('must pass parent depths');
}
return program(container, i, templateSpec[i], data, depths);
return wrapProgram(container, i, templateSpec[i], data, 0, blockParams, depths);
};
return ret;
}
export function program(container, i, fn, data, depths) {
var prog = function(context, options) {
options = options || {};
return fn.call(container, context, container.helpers, container.partials, options.data || data, depths && [context].concat(depths));
};
export function wrapProgram(container, i, fn, data, declaredBlockParams, blockParams, depths) {
function prog(context, options = {}) {
return fn.call(container,
context,
container.helpers, container.partials,
options.data || data,
blockParams && [options.blockParams].concat(blockParams),
depths && [context].concat(depths));
}
prog.program = i;
prog.depth = depths ? depths.length : 0;
prog.blockParams = declaredBlockParams || 0;
return prog;
}
export function invokePartial(partial, name, context, helpers, partials, data, depths) {
var options = { partial: true, helpers: helpers, partials: partials, data: data, depths: depths };
export function resolvePartial(partial, context, options) {
if (!partial) {
partial = options.partials[options.name];
} else if (!partial.call && !options.name) {
// This is a dynamic partial that returned a string
options.name = partial;
partial = options.partials[partial];
}
return partial;
}
if(partial === undefined) {
throw new Exception("The partial " + name + " could not be found");
} else if(partial instanceof Function) {
export function invokePartial(partial, context, options) {
options.partial = true;
if (partial === undefined) {
throw new Exception('The partial ' + options.name + ' could not be found');
} else if (partial instanceof Function) {
return partial(context, options);
}
}
export function noop() { return ""; }
export function noop() { return ''; }
function initData(context, data) {
if (!data || !('root' in data)) {
+2 -2
View File
@@ -3,8 +3,8 @@ function SafeString(string) {
this.string = string;
}
SafeString.prototype.toString = function() {
return "" + this.string;
SafeString.prototype.toString = SafeString.prototype.toHTML = function() {
return '' + this.string;
};
export default SafeString;
+46 -29
View File
@@ -1,25 +1,23 @@
/*jshint -W004 */
import SafeString from "./safe-string";
var escape = {
"&": "&amp;",
"<": "&lt;",
">": "&gt;",
'"': "&quot;",
"'": "&#x27;",
"`": "&#x60;"
const escape = {
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
"'": '&#x27;',
'`': '&#x60;',
'=': '&#x3D;'
};
var badChars = /[&<>"'`]/g;
var possible = /[&<>"'`]/;
const badChars = /[&<>"'`=]/g,
possible = /[&<>"'`=]/;
function escapeChar(chr) {
return escape[chr];
}
export function extend(obj /* , ...source */) {
for (var i = 1; i < arguments.length; i++) {
for (var key in arguments[i]) {
for (let i = 1; i < arguments.length; i++) {
for (let key in arguments[i]) {
if (Object.prototype.hasOwnProperty.call(arguments[i], key)) {
obj[key] = arguments[i][key];
}
@@ -29,10 +27,11 @@ export function extend(obj /* , ...source */) {
return obj;
}
export var toString = Object.prototype.toString;
export let toString = Object.prototype.toString;
// Sourced from lodash
// https://github.com/bestiejs/lodash/blob/master/LICENSE.txt
/*eslint-disable func-style, no-var */
var isFunction = function(value) {
return typeof value === 'function';
};
@@ -44,29 +43,42 @@ if (isFunction(/x/)) {
};
}
export var isFunction;
/*eslint-enable func-style, no-var */
/* istanbul ignore next */
export var isArray = Array.isArray || function(value) {
export const isArray = Array.isArray || function(value) {
return (value && typeof value === 'object') ? toString.call(value) === '[object Array]' : false;
};
// Older IE versions do not directly support indexOf so we must implement our own, sadly.
export function indexOf(array, value) {
for (let i = 0, len = array.length; i < len; i++) {
if (array[i] === value) {
return i;
}
}
return -1;
}
export function escapeExpression(string) {
// don't escape SafeStrings, since they're already safe
if (string instanceof SafeString) {
return string.toString();
} else if (string == null) {
return "";
} else if (!string) {
return string + '';
if (typeof string !== 'string') {
// don't escape SafeStrings, since they're already safe
if (string && string.toHTML) {
return string.toHTML();
} else if (string == null) {
return '';
} 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
// 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; }
if (!possible.test(string)) { return string; }
return string.replace(badChars, escapeChar);
}
@@ -80,6 +92,11 @@ export function isEmpty(value) {
}
}
export function blockParams(params, ids) {
params.path = ids;
return params;
}
export function appendContextPath(contextPath, id) {
return (contextPath ? contextPath + '.' : '') + id;
}
+9 -10
View File
@@ -1,11 +1,10 @@
// USAGE:
// var handlebars = require('handlebars');
/* eslint-disable no-var */
// var local = handlebars.create();
var handlebars = require('../dist/cjs/handlebars')["default"];
handlebars.Visitor = require('../dist/cjs/handlebars/compiler/visitor')["default"];
var handlebars = require('../dist/cjs/handlebars')['default'];
var printer = require('../dist/cjs/handlebars/compiler/printer');
handlebars.PrintVisitor = printer.PrintVisitor;
@@ -14,13 +13,13 @@ handlebars.print = printer.print;
module.exports = handlebars;
// Publish a Node.js require() handler for .handlebars and .hbs files
function extension(module, filename) {
var fs = require('fs');
var templateString = fs.readFileSync(filename, 'utf8');
module.exports = handlebars.compile(templateString);
}
/* istanbul ignore else */
if (typeof require !== 'undefined' && require.extensions) {
var extension = function(module, filename) {
var fs = require("fs");
var templateString = fs.readFileSync(filename, "utf8");
module.exports = handlebars.compile(templateString);
};
require.extensions[".handlebars"] = extension;
require.extensions[".hbs"] = extension;
require.extensions['.handlebars'] = extension;
require.extensions['.hbs'] = extension;
}
+76 -42
View File
@@ -1,8 +1,9 @@
var fs = require('fs'),
Handlebars = require('./index'),
basename = require('path').basename,
uglify = require('uglify-js');
/*eslint-disable no-console */
import fs from 'fs';
import * as Handlebars from './handlebars';
import {basename} from 'path';
import {SourceMapConsumer, SourceNode} from 'source-map';
import uglify from 'uglify-js';
module.exports.cli = function(opts) {
if (opts.version) {
@@ -10,7 +11,6 @@ module.exports.cli = function(opts) {
return;
}
var template = [0];
if (!opts.templates.length) {
throw new Handlebars.Exception('Must define at least one template or directory.');
}
@@ -19,70 +19,76 @@ module.exports.cli = function(opts) {
try {
fs.statSync(template);
} catch (err) {
throw new Handlebars.Exception('Unable to open template file "' + template + '"');
throw new Handlebars.Exception(`Unable to open template file "${template}"`);
}
});
if (opts.simple && opts.min) {
throw new Handlebars.Exception('Unable to minimze simple output');
throw new Handlebars.Exception('Unable to minimize simple output');
}
if (opts.simple && (opts.templates.length !== 1 || fs.statSync(opts.templates[0]).isDirectory())) {
throw new Handlebars.Exception('Unable to output multiple templates in simple mode');
}
// Convert the known list into a hash
var known = {};
let known = {};
if (opts.known && !Array.isArray(opts.known)) {
opts.known = [opts.known];
}
if (opts.known) {
for (var i = 0, len = opts.known.length; i < len; i++) {
for (let i = 0, len = opts.known.length; i < len; i++) {
known[opts.known[i]] = true;
}
}
// Build file extension pattern
var extension = opts.extension.replace(/[\\^$*+?.():=!|{}\-\[\]]/g, function(arg) { return '\\' + arg; });
let extension = opts.extension.replace(/[\\^$*+?.():=!|{}\-\[\]]/g, function(arg) { return '\\' + arg; });
extension = new RegExp('\\.' + extension + '$');
var output = [];
let output = new SourceNode();
if (!opts.simple) {
if (opts.amd) {
output.push('define([\'' + opts.handlebarPath + 'handlebars.runtime\'], function(Handlebars) {\n Handlebars = Handlebars["default"];');
output.add('define([\'' + opts.handlebarPath + 'handlebars.runtime\'], function(Handlebars) {\n Handlebars = Handlebars["default"];');
} else if (opts.commonjs) {
output.push('var Handlebars = require("' + opts.commonjs + '");');
output.add('var Handlebars = require("' + opts.commonjs + '");');
} else {
output.push('(function() {\n');
output.add('(function() {\n');
}
output.push(' var template = Handlebars.template, templates = ');
output.push(opts.namespace);
output.push(' = ');
output.push(opts.namespace);
output.push(' || {};\n');
output.add(' var template = Handlebars.template, templates = ');
if (opts.namespace) {
output.add(opts.namespace);
output.add(' = ');
output.add(opts.namespace);
output.add(' || ');
}
output.add('{};\n');
}
function processTemplate(template, root) {
var path = template,
let path = template,
stat = fs.statSync(path);
if (stat.isDirectory()) {
fs.readdirSync(template).map(function(file) {
var path = template + '/' + file;
let childPath = template + '/' + file;
if (extension.test(path) || fs.statSync(path).isDirectory()) {
processTemplate(path, root || template);
if (extension.test(childPath) || fs.statSync(childPath).isDirectory()) {
processTemplate(childPath, root || template);
}
});
} else {
var data = fs.readFileSync(path, 'utf8');
let data = fs.readFileSync(path, 'utf8');
if (opts.bom && data.indexOf('\uFEFF') === 0) {
data = data.substring(1);
}
var options = {
let options = {
knownHelpers: known,
knownHelpersOnly: opts.o
};
if (opts.map) {
options.srcName = path;
}
if (opts.data) {
options.data = true;
}
@@ -91,22 +97,30 @@ module.exports.cli = function(opts) {
if (!root) {
template = basename(template);
} else if (template.indexOf(root) === 0) {
template = template.substring(root.length+1);
template = template.substring(root.length + 1);
}
template = template.replace(extension, '');
let precompiled = Handlebars.precompile(data, 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.push(Handlebars.precompile(data, options) + '\n');
output.add([precompiled, '\n']);
} else if (opts.partial) {
if(opts.amd && (opts.templates.length == 1 && !fs.statSync(opts.templates[0]).isDirectory())) {
output.push('return ');
if (opts.amd && (opts.templates.length == 1 && !fs.statSync(opts.templates[0]).isDirectory())) {
output.add('return ');
}
output.push('Handlebars.partials[\'' + template + '\'] = template(' + Handlebars.precompile(data, options) + ');\n');
output.add(['Handlebars.partials[\'', template, '\'] = template(', precompiled, ');\n']);
} else {
if(opts.amd && (opts.templates.length == 1 && !fs.statSync(opts.templates[0]).isDirectory())) {
output.push('return ');
if (opts.amd && (opts.templates.length == 1 && !fs.statSync(opts.templates[0]).isDirectory())) {
output.add('return ');
}
output.push('templates[\'' + template + '\'] = template(' + Handlebars.precompile(data, options) + ');\n');
output.add(['templates[\'', template, '\'] = template(', precompiled, ');\n']);
}
}
}
@@ -118,24 +132,44 @@ module.exports.cli = function(opts) {
// Output the content
if (!opts.simple) {
if (opts.amd) {
if(opts.templates.length > 1 || (opts.templates.length == 1 && fs.statSync(opts.templates[0]).isDirectory())) {
if(opts.partial){
output.push('return Handlebars.partials;\n');
if (opts.templates.length > 1 || (opts.templates.length == 1 && fs.statSync(opts.templates[0]).isDirectory())) {
if (opts.partial) {
output.add('return Handlebars.partials;\n');
} else {
output.push('return templates;\n');
output.add('return templates;\n');
}
}
output.push('});');
output.add('});');
} else if (!opts.commonjs) {
output.push('})();');
output.add('})();');
}
}
output = output.join('');
if (opts.map) {
output.add('\n//# sourceMappingURL=' + opts.map + '\n');
}
output = output.toStringWithSourceMap();
output.map = output.map + '';
if (opts.min) {
output = uglify.minify(output, {fromString: true}).code;
output = uglify.minify(output.code, {
fromString: true,
outSourceMap: opts.map,
inSourceMap: JSON.parse(output.map)
});
if (opts.map) {
output.code += '\n//# sourceMappingURL=' + opts.map + '\n';
}
}
if (opts.map) {
fs.writeFileSync(opts.map, output.map, 'utf8');
}
output = output.code;
if (opts.output) {
fs.writeFileSync(opts.output, output, 'utf8');
} else {
+32 -17
View File
@@ -1,7 +1,7 @@
{
"name": "handlebars",
"barename": "handlebars",
"version": "2.0.0-beta.1",
"version": "3.0.5",
"description": "Handlebars provides the power necessary to let you build semantic templates effectively with no frustration",
"homepage": "http://www.handlebarsjs.com/",
"keywords": [
@@ -21,36 +21,42 @@
"node": ">=0.4.7"
},
"dependencies": {
"optimist": "~0.3"
"optimist": "^0.6.1",
"source-map": "^0.1.40"
},
"optionalDependencies": {
"uglify-js": "~2.3"
"uglify-js": "^2.6"
},
"devDependencies": {
"async": "~0.2.9",
"async": "^0.9.0",
"aws-sdk": "~1.5.0",
"babel-loader": "^5.0.0",
"babel-runtime": "^5.1.10",
"benchmark": "~1.0",
"dustjs-linkedin": "~2.0.2",
"dustjs-linkedin": "^2.0.2",
"eco": "~1.1.0-rc-3",
"es6-module-packager": "1.x",
"grunt": "~0.4.1",
"grunt-babel": "^5.0.0",
"grunt-cli": "~0.1.10",
"grunt-contrib-clean": "~0.4.1",
"grunt-contrib-concat": "~0.3.0",
"grunt-contrib-connect": "~0.5.0",
"grunt-contrib-copy": "~0.4.1",
"grunt-contrib-jshint": "0.x",
"grunt-contrib-requirejs": "~0.4.1",
"grunt-contrib-uglify": "~0.2.2",
"grunt-contrib-watch": "~0.5.3",
"grunt-contrib-clean": "0.x",
"grunt-contrib-concat": "0.x",
"grunt-contrib-connect": "0.x",
"grunt-contrib-copy": "0.x",
"grunt-contrib-requirejs": "0.x",
"grunt-contrib-uglify": "0.x",
"grunt-contrib-watch": "0.x",
"grunt-eslint": "^11.0.0",
"grunt-saucelabs": "8.x",
"grunt-webpack": "^1.0.8",
"istanbul": "^0.3.0",
"jison": "~0.3.0",
"keen.io": "0.0.3",
"mocha": "~1.20.0",
"mustache": "~0.7.2",
"semver": "~2.1.0",
"underscore": "~1.5.1"
"mustache": "0.x",
"semver": "^4.0.0",
"underscore": "^1.5.1",
"webpack": "^1.15.0",
"webpack-dev-server": "^1.16.5"
},
"main": "lib/index.js",
"bin": {
@@ -58,5 +64,14 @@
},
"scripts": {
"test": "grunt"
},
"jspm": {
"main": "handlebars",
"directories": {
"lib": "dist/amd"
},
"buildConfig": {
"minify": true
}
}
}
+101 -1
View File
@@ -2,7 +2,107 @@
## Development
[Commits](https://github.com/wycats/handlebars.js/compare/v2.0.0-beta.1...master)
[Commits](https://github.com/wycats/handlebars.js/compare/v3.0.5...master)
## v3.0.5 - December 15th, 2018
- chore: use node 10.x in travis-build - 4ed0a62
Compatibility notes:
- no breaking changes
[Commits](https://github.com/wycats/handlebars.js/compare/v3.0.4...v3.0.5)
## v3.0.4 - December 15th, 2018
- Further relax uglify dependency - 4cd5305
- Update uglify-js to avoid vulnerability - d97c2e6
- Escape = in HTML content - 1c863e3
Compatibility notes:
- No breaking changes
[Commits](https://github.com/wycats/handlebars.js/compare/v3.0.3...v3.0.4)
## v3.0.3 - April 28th, 2015
- [#1004](https://github.com/wycats/handlebars.js/issues/1004) - Latest version breaks with RequireJS (global is undefined) ([@boskee](https://api.github.com/users/boskee))
[Commits](https://github.com/wycats/handlebars.js/compare/v3.0.2...v3.0.3)
## v3.0.2 - April 20th, 2015
- [#998](https://github.com/wycats/handlebars.js/pull/998) - Add full support for es6 ([@kpdecker](https://api.github.com/users/kpdecker))
- [#994](https://github.com/wycats/handlebars.js/issues/994) - Access Handlebars.Visitor in browser ([@tamlyn](https://api.github.com/users/tamlyn))
- [#990](https://github.com/wycats/handlebars.js/issues/990) - Allow passing null/undefined literals subexpressions ([@blimmer](https://api.github.com/users/blimmer))
- [#989](https://github.com/wycats/handlebars.js/issues/989) - Source-map error with requirejs ([@SteppeEagle](https://api.github.com/users/SteppeEagle))
- [#967](https://github.com/wycats/handlebars.js/issues/967) - can't access "this" property ([@75lb](https://api.github.com/users/75lb))
- Use captureStackTrace for error handler - a009a97
- Ignore branches tested without coverage monitoring - 37a664b
[Commits](https://github.com/wycats/handlebars.js/compare/v3.0.1...v3.0.2)
## v3.0.1 - March 24th, 2015
- [#984](https://github.com/wycats/handlebars.js/pull/984) - Adding documentation for passing arguments into partials ([@johneke](https://api.github.com/users/johneke))
- [#973](https://github.com/wycats/handlebars.js/issues/973) - version 3 is slower than version 2 ([@elover](https://api.github.com/users/elover))
- [#966](https://github.com/wycats/handlebars.js/issues/966) - "handlebars --version" does not work with v3.0.0 ([@abloomston](https://api.github.com/users/abloomston))
- [#964](https://github.com/wycats/handlebars.js/pull/964) - default is a reserved word ([@grassick](https://api.github.com/users/grassick))
- [#962](https://github.com/wycats/handlebars.js/pull/962) - Add dashbars' link on README. ([@pismute](https://api.github.com/users/pismute))
[Commits](https://github.com/wycats/handlebars.js/compare/v3.0.0...v3.0.1)
## v3.0.0 - February 10th, 2015
- [#941](https://github.com/wycats/handlebars.js/pull/941) - Add support for dynamic partial names ([@kpdecker](https://api.github.com/users/kpdecker))
- [#940](https://github.com/wycats/handlebars.js/pull/940) - Add missing reserved words so compiler knows to use array syntax: ([@mattflaschen](https://api.github.com/users/mattflaschen))
- [#938](https://github.com/wycats/handlebars.js/pull/938) - Fix example using #with helper ([@diwo](https://api.github.com/users/diwo))
- [#930](https://github.com/wycats/handlebars.js/pull/930) - Add parent tracking and mutation to AST visitors ([@kpdecker](https://api.github.com/users/kpdecker))
- [#926](https://github.com/wycats/handlebars.js/issues/926) - Depthed lookups fail when program duplicator runs ([@kpdecker](https://api.github.com/users/kpdecker))
- [#918](https://github.com/wycats/handlebars.js/pull/918) - Add instructions for 'spec/mustache' to CONTRIBUTING.md, fix a few typos ([@oneeman](https://api.github.com/users/oneeman))
- [#915](https://github.com/wycats/handlebars.js/pull/915) - Ast update ([@kpdecker](https://api.github.com/users/kpdecker))
- [#910](https://github.com/wycats/handlebars.js/issues/910) - Different behavior of {{@last}} when {{#each}} in {{#each}} ([@zordius](https://api.github.com/users/zordius))
- [#907](https://github.com/wycats/handlebars.js/issues/907) - Implement named helper variable references ([@kpdecker](https://api.github.com/users/kpdecker))
- [#906](https://github.com/wycats/handlebars.js/pull/906) - Add parser support for block params ([@mmun](https://api.github.com/users/mmun))
- [#903](https://github.com/wycats/handlebars.js/issues/903) - Only provide aliases for multiple use calls ([@kpdecker](https://api.github.com/users/kpdecker))
- [#902](https://github.com/wycats/handlebars.js/pull/902) - Generate Source Maps ([@kpdecker](https://api.github.com/users/kpdecker))
- [#901](https://github.com/wycats/handlebars.js/issues/901) - Still escapes with noEscape enabled on isolated Handlebars environment ([@zedknight](https://api.github.com/users/zedknight))
- [#896](https://github.com/wycats/handlebars.js/pull/896) - Simplify BlockNode by removing intermediate MustacheNode ([@mmun](https://api.github.com/users/mmun))
- [#892](https://github.com/wycats/handlebars.js/pull/892) - Implement parser for else chaining of helpers ([@kpdecker](https://api.github.com/users/kpdecker))
- [#889](https://github.com/wycats/handlebars.js/issues/889) - Consider extensible parser API ([@kpdecker](https://api.github.com/users/kpdecker))
- [#887](https://github.com/wycats/handlebars.js/issues/887) - Handlebars.noConflict() option? ([@bradvogel](https://api.github.com/users/bradvogel))
- [#886](https://github.com/wycats/handlebars.js/issues/886) - Add SafeString to context (or use duck-typing) ([@dominicbarnes](https://api.github.com/users/dominicbarnes))
- [#870](https://github.com/wycats/handlebars.js/pull/870) - Registering undefined partial throws exception. ([@max-b](https://api.github.com/users/max-b))
- [#866](https://github.com/wycats/handlebars.js/issues/866) - comments don't respect whitespace control ([@75lb](https://api.github.com/users/75lb))
- [#863](https://github.com/wycats/handlebars.js/pull/863) - + jsDelivr CDN info ([@tomByrer](https://api.github.com/users/tomByrer))
- [#858](https://github.com/wycats/handlebars.js/issues/858) - Disable new default auto-indent at included partials ([@majodev](https://api.github.com/users/majodev))
- [#856](https://github.com/wycats/handlebars.js/pull/856) - jspm compatibility ([@MajorBreakfast](https://api.github.com/users/MajorBreakfast))
- [#805](https://github.com/wycats/handlebars.js/issues/805) - Request: "strict" lookups ([@nzakas](https://api.github.com/users/nzakas))
- Export the default object for handlebars/runtime - 5594416
- Lookup partials when undefined - 617dd57
Compatibility notes:
- Runtime breaking changes. Must match 3.x runtime and precompiler.
- The AST has been upgraded to a public API.
- There are a number of changes to this, but the format is now documented in docs/compiler-api.md
- The Visitor API has been expanded to support mutation and provide a base implementation
- The `JavaScriptCompiler` APIs have been formalized and documented. As part of the sourcemap handling these should be updated to return arrays for concatenation.
- `JavaScriptCompiler.namespace` has been removed as it was unused.
- `SafeString` is now duck typed on `toHTML`
New Features:
- noConflict
- Source Maps
- Block Params
- Strict Mode
- @last and other each changes
- Chained else blocks
- @data methods can now have helper parameters passed to them
- Dynamic partials
[Commits](https://github.com/wycats/handlebars.js/compare/v2.0.0...v3.0.0)
## v2.0.0 - September 1st, 2014
- Update jsfiddle to 2.0.0-beta.1 - 0670f65
- Add contrib note regarding handlebarsjs.com docs - 4d17e3c
- Play nice with gemspec version numbers - 64d5481
[Commits](https://github.com/wycats/handlebars.js/compare/v2.0.0-beta.1...v2.0.0)
## v2.0.0-beta.1 - August 26th, 2014
- [#787](https://github.com/wycats/handlebars.js/pull/787) - Remove whitespace surrounding standalone statements ([@kpdecker](https://api.github.com/users/kpdecker))
+1 -1
View File
@@ -1,3 +1,3 @@
// Create a simple path alias to allow browserify to resolve
// the runtime on a supported path.
module.exports = require('./dist/cjs/handlebars.runtime');
module.exports = require('./dist/cjs/handlebars.runtime')['default'];
+36
View File
@@ -0,0 +1,36 @@
{
"globals": {
"CompilerContext": true,
"Handlebars": true,
"handlebarsEnv": true,
"shouldCompileTo": true,
"shouldCompileToWithPartials": true,
"shouldThrow": true,
"compileWithPartials": true,
"console": true,
"require": true,
"suite": true,
"equal": true,
"equals": true,
"test": true,
"testBoth": true,
"raises": true,
"deepEqual": true,
"start": true,
"stop": true,
"ok": true,
"strictEqual": true,
"define": true
},
"env": {
"mocha": true
},
"rules": {
// Disabling for tests, for now.
"no-path-concat": 0,
"no-var": 0
}
}
+16
View File
@@ -28,11 +28,16 @@
<script src="/spec/env/common.js"></script>
<script>
var requireFailure;
requirejs.config({
paths: {
'handlebars.runtime': '/dist/handlebars.runtime.amd'
}
});
requirejs.onError = function (err) {
requireFailure = err;
};
</script>
<script>
onload = function(){
@@ -60,6 +65,17 @@
runner.on('fail', logFailure);
// Inject any require initilizer failures into the first test so they are properly
// reported.
if (requireFailure) {
runner.on('hook end', function(hook){
if (requireFailure) {
runner.uncaught(requireFailure);
requireFailure = undefined;
}
});
}
function logFailure(test, err){
var flattenTitles = function(test){
+19 -4
View File
@@ -28,12 +28,17 @@
<script src="/spec/env/common.js"></script>
<script>
var requireFailure;
requirejs.config({
paths: {
handlebars: '/dist/handlebars.amd',
tests: '/tmp/tests'
}
});
requirejs.onError = function (err) {
requireFailure = err;
};
var CompilerContext = {
compile: function(template, options) {
@@ -79,16 +84,26 @@
runner.on('fail', logFailure);
function logFailure(test, err){
// Inject any require initilizer failures into the first test so they are properly
// reported.
if (requireFailure) {
runner.on('hook end', function(hook){
if (requireFailure) {
runner.uncaught(requireFailure);
requireFailure = undefined;
}
});
}
var flattenTitles = function(test){
function logFailure(test, err){
function flattenTitles(test){
var titles = [];
while (test.parent.title){
titles.push(test.parent.title);
test = test.parent;
}
return titles.reverse();
};
}
failedTests.push({
name: test.title,
@@ -97,7 +112,7 @@
stack: err.stack,
titles: flattenTitles(test)
});
};
}
});
});
};
+1
View File
@@ -0,0 +1 @@
a
+129 -213
View File
@@ -1,371 +1,287 @@
/*global Handlebars, handlebarsEnv, shouldThrow */
describe('ast', function() {
if (!Handlebars.AST) {
return;
}
var LOCATION_INFO = {
last_line: 0,
first_line: 0,
first_column: 0,
last_column: 0
start: {
line: 1,
column: 1
},
end: {
line: 1,
column: 1
}
};
function testLocationInfoStorage(node){
var properties = [ 'firstLine', 'lastLine', 'firstColumn', 'lastColumn' ],
property,
propertiesLen = properties.length,
i;
for (i = 0; i < propertiesLen; i++){
property = properties[0];
equals(node[property], 0);
}
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('MustacheNode', function() {
function testEscape(open, expected) {
var mustache = new handlebarsEnv.AST.MustacheNode([{}], {}, open, false);
equals(mustache.escaped, expected);
}
describe('MustacheStatement', function() {
it('should store args', function() {
var id = {isSimple: true},
hash = {},
mustache = new handlebarsEnv.AST.MustacheNode([id, 'param1'], hash, '', false, LOCATION_INFO);
equals(mustache.type, 'mustache');
equals(mustache.hash, hash);
var mustache = new handlebarsEnv.AST.MustacheStatement({}, null, null, true, {}, LOCATION_INFO);
equals(mustache.type, 'MustacheStatement');
equals(mustache.escaped, true);
equals(mustache.id, id);
equals(mustache.params.length, 1);
equals(mustache.params[0], 'param1');
equals(!!mustache.isHelper, true);
testLocationInfoStorage(mustache);
});
it('should accept token for escape', function() {
testEscape('{{', true);
testEscape('{{~', true);
testEscape('{{#', true);
testEscape('{{~#', true);
testEscape('{{/', true);
testEscape('{{~/', true);
testEscape('{{^', true);
testEscape('{{~^', true);
testEscape('{', true);
testEscape('{', true);
testEscape('{{&', false);
testEscape('{{~&', false);
testEscape('{{{', false);
testEscape('{{~{', false);
});
it('should accept boolean for escape', function() {
testEscape(true, true);
testEscape({}, true);
testEscape(false, false);
testEscape(undefined, false);
});
});
describe('BlockNode', function() {
describe('BlockStatement', function() {
it('should throw on mustache mismatch', function() {
shouldThrow(function() {
handlebarsEnv.parse("\n {{#foo}}{{/bar}}");
}, Handlebars.Exception, "foo doesn't match bar - 2:2");
handlebarsEnv.parse('\n {{#foo}}{{/bar}}');
}, Handlebars.Exception, "foo doesn't match bar - 2:5");
});
it('stores location info', function(){
var sexprNode = new handlebarsEnv.AST.SexprNode([{ original: 'foo'}], null);
var mustacheNode = new handlebarsEnv.AST.MustacheNode(sexprNode, null, '{{', {});
var block = new handlebarsEnv.AST.BlockNode(mustacheNode,
{statements: [], strip: {}}, {statements: [], strip: {}},
{
strip: {},
path: {original: 'foo'}
},
LOCATION_INFO);
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('IdNode', function() {
it('should throw on invalid path', function() {
shouldThrow(function() {
new handlebarsEnv.AST.IdNode([
{part: 'foo'},
{part: '..'},
{part: 'bar'}
], {first_line: 1, first_column: 1});
}, Handlebars.Exception, "Invalid path: foo.. - 1:1");
shouldThrow(function() {
new handlebarsEnv.AST.IdNode([
{part: 'foo'},
{part: '.'},
{part: 'bar'}
], {first_line: 1, first_column: 1});
}, Handlebars.Exception, "Invalid path: foo. - 1:1");
shouldThrow(function() {
new handlebarsEnv.AST.IdNode([
{part: 'foo'},
{part: 'this'},
{part: 'bar'}
], {first_line: 1, first_column: 1});
}, Handlebars.Exception, "Invalid path: foothis - 1:1");
});
it('stores location info', function(){
var idNode = new handlebarsEnv.AST.IdNode([], LOCATION_INFO);
describe('PathExpression', function() {
it('stores location info', function() {
var idNode = new handlebarsEnv.AST.PathExpression(false, 0, [], 'foo', LOCATION_INFO);
testLocationInfoStorage(idNode);
});
});
describe("HashNode", function(){
it('stores location info', function(){
var hash = new handlebarsEnv.AST.HashNode([], LOCATION_INFO);
describe('Hash', function() {
it('stores location info', function() {
var hash = new handlebarsEnv.AST.Hash([], LOCATION_INFO);
testLocationInfoStorage(hash);
});
});
describe("ContentNode", function(){
it('stores location info', function(){
var content = new handlebarsEnv.AST.ContentNode("HI", LOCATION_INFO);
describe('ContentStatement', function() {
it('stores location info', function() {
var content = new handlebarsEnv.AST.ContentStatement('HI', LOCATION_INFO);
testLocationInfoStorage(content);
});
});
describe("CommentNode", function(){
it('stores location info', function(){
var comment = new handlebarsEnv.AST.CommentNode("HI", LOCATION_INFO);
describe('CommentStatement', function() {
it('stores location info', function() {
var comment = new handlebarsEnv.AST.CommentStatement('HI', {}, LOCATION_INFO);
testLocationInfoStorage(comment);
});
});
describe("NumberNode", function(){
it('stores location info', function(){
var integer = new handlebarsEnv.AST.NumberNode("6", LOCATION_INFO);
describe('NumberLiteral', function() {
it('stores location info', function() {
var integer = new handlebarsEnv.AST.NumberLiteral('6', LOCATION_INFO);
testLocationInfoStorage(integer);
});
});
describe("StringNode", function(){
it('stores location info', function(){
var string = new handlebarsEnv.AST.StringNode("6", LOCATION_INFO);
describe('StringLiteral', function() {
it('stores location info', function() {
var string = new handlebarsEnv.AST.StringLiteral('6', LOCATION_INFO);
testLocationInfoStorage(string);
});
});
describe("BooleanNode", function(){
it('stores location info', function(){
var bool = new handlebarsEnv.AST.BooleanNode("true", LOCATION_INFO);
describe('BooleanLiteral', function() {
it('stores location info', function() {
var bool = new handlebarsEnv.AST.BooleanLiteral('true', LOCATION_INFO);
testLocationInfoStorage(bool);
});
});
describe("DataNode", function(){
it('stores location info', function(){
var data = new handlebarsEnv.AST.DataNode("YES", LOCATION_INFO);
testLocationInfoStorage(data);
});
});
describe("PartialNameNode", function(){
it('stores location info', function(){
var pnn = new handlebarsEnv.AST.PartialNameNode({original: "YES"}, LOCATION_INFO);
testLocationInfoStorage(pnn);
});
});
describe("PartialNode", function(){
it('stores location info', function(){
var pn = new handlebarsEnv.AST.PartialNode("so_partial", {}, {}, {}, LOCATION_INFO);
describe('PartialStatement', function() {
it('stores location info', function() {
var pn = new handlebarsEnv.AST.PartialStatement('so_partial', [], {}, {}, LOCATION_INFO);
testLocationInfoStorage(pn);
});
});
describe('ProgramNode', function(){
it('storing location info', function(){
var pn = new handlebarsEnv.AST.ProgramNode([], {}, LOCATION_INFO);
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, statements;
describe('Line Numbers', function() {
var ast, body;
function testColumns(node, firstLine, lastLine, firstColumn, lastColumn){
equals(node.firstLine, firstLine);
equals(node.lastLine, lastLine);
equals(node.firstColumn, firstColumn);
equals(node.lastColumn, lastColumn);
function testColumns(node, firstLine, lastLine, firstColumn, lastColumn) {
equals(node.loc.start.line, firstLine);
equals(node.loc.start.column, firstColumn);
equals(node.loc.end.line, lastLine);
equals(node.loc.end.column, lastColumn);
}
ast = Handlebars.parse("line 1 {{line1Token}}\n line 2 {{line2token}}\n line 3 {{#blockHelperOnLine3}}\nline 4{{line4token}}\n" +
"line5{{else}}\n{{line6Token}}\n{{/blockHelperOnLine3}}");
statements = ast.statements;
ast = Handlebars.parse('line 1 {{line1Token}}\n line 2 {{line2token}}\n line 3 {{#blockHelperOnLine3}}\nline 4{{line4token}}\n' +
'line5{{else}}\n{{line6Token}}\n{{/blockHelperOnLine3}}');
body = ast.body;
it('gets ContentNode line numbers', function(){
var contentNode = statements[0];
it('gets ContentNode line numbers', function() {
var contentNode = body[0];
testColumns(contentNode, 1, 1, 0, 7);
});
it('gets MustacheNode line numbers', function(){
var mustacheNode = statements[1];
it('gets MustacheStatement line numbers', function() {
var mustacheNode = body[1];
testColumns(mustacheNode, 1, 1, 7, 21);
});
it('gets line numbers correct when newlines appear', function(){
testColumns(statements[2], 1, 2, 21, 8);
it('gets line numbers correct when newlines appear', function() {
testColumns(body[2], 1, 2, 21, 8);
});
it('gets MustacheNode line numbers correct across newlines', function(){
var secondMustacheNode = statements[3];
testColumns(secondMustacheNode, 2, 2, 8, 22);
it('gets MustacheStatement line numbers correct across newlines', function() {
var secondMustacheStatement = body[3];
testColumns(secondMustacheStatement, 2, 2, 8, 22);
});
it('gets the block helper information correct', function(){
var blockHelperNode = statements[5];
it('gets the block helper information correct', function() {
var blockHelperNode = body[5];
testColumns(blockHelperNode, 3, 7, 8, 23);
});
it('correctly records the line numbers the program of a block helper', function(){
var blockHelperNode = statements[5],
it('correctly records the line numbers the program of a block helper', function() {
var blockHelperNode = body[5],
program = blockHelperNode.program;
testColumns(program, 3, 5, 8, 5);
});
it('correctly records the line numbers of an inverse of a block helper', function(){
var blockHelperNode = statements[5],
it('correctly records the line numbers of an inverse of a block helper', function() {
var blockHelperNode = body[5],
inverse = blockHelperNode.inverse;
testColumns(inverse, 5, 7, 5, 0);
});
});
describe('standalone flags', function(){
describe('standalone flags', function() {
describe('mustache', function() {
it('does not mark mustaches as standalone', function() {
var ast = Handlebars.parse(' {{comment}} ');
equals(!!ast.statements[0].string, true);
equals(!!ast.statements[2].string, true);
equals(!!ast.body[0].value, true);
equals(!!ast.body[2].value, true);
});
});
describe('blocks', function() {
it('marks block mustaches as standalone', function() {
var ast = Handlebars.parse(' {{# comment}} \nfoo\n {{else}} \n bar \n {{/comment}} '),
block = ast.statements[1];
block = ast.body[1];
equals(ast.statements[0].string, '');
equals(ast.body[0].value, '');
equals(block.program.statements[0].string, 'foo\n');
equals(block.inverse.statements[0].string, ' bar \n');
equals(block.program.body[0].value, 'foo\n');
equals(block.inverse.body[0].value, ' bar \n');
equals(ast.statements[2].string, '');
equals(ast.body[2].value, '');
});
it('marks initial block mustaches as standalone', function() {
var ast = Handlebars.parse('{{# comment}} \nfoo\n {{/comment}}'),
block = ast.statements[0];
block = ast.body[0];
equals(block.program.statements[0].string, 'foo\n');
equals(block.program.body[0].value, 'foo\n');
});
it('marks mustaches with children as standalone', function() {
var ast = Handlebars.parse('{{# comment}} \n{{foo}}\n {{/comment}}'),
block = ast.statements[0];
block = ast.body[0];
equals(block.program.statements[0].string, '');
equals(block.program.statements[1].id.original, 'foo');
equals(block.program.statements[2].string, '\n');
equals(block.program.body[0].value, '');
equals(block.program.body[1].path.original, 'foo');
equals(block.program.body[2].value, '\n');
});
it('marks nested block mustaches as standalone', function() {
var ast = Handlebars.parse('{{#foo}} \n{{# comment}} \nfoo\n {{else}} \n bar \n {{/comment}} \n{{/foo}}'),
statements = ast.statements[0].program.statements,
block = statements[1];
body = ast.body[0].program.body,
block = body[1];
equals(statements[0].string, '');
equals(body[0].value, '');
equals(block.program.statements[0].string, 'foo\n');
equals(block.inverse.statements[0].string, ' bar \n');
equals(block.program.body[0].value, 'foo\n');
equals(block.inverse.body[0].value, ' bar \n');
equals(statements[0].string, '');
equals(body[0].value, '');
});
it('does not mark nested block mustaches as standalone', function() {
var ast = Handlebars.parse('{{#foo}} {{# comment}} \nfoo\n {{else}} \n bar \n {{/comment}} {{/foo}}'),
statements = ast.statements[0].program.statements,
block = statements[1];
body = ast.body[0].program.body,
block = body[1];
equals(statements[0].omit, undefined);
equals(body[0].omit, undefined);
equals(block.program.statements[0].string, ' \nfoo\n');
equals(block.inverse.statements[0].string, ' bar \n ');
equals(block.program.body[0].value, ' \nfoo\n');
equals(block.inverse.body[0].value, ' bar \n ');
equals(statements[0].omit, undefined);
equals(body[0].omit, undefined);
});
it('does not mark nested initial block mustaches as standalone', function() {
var ast = Handlebars.parse('{{#foo}}{{# comment}} \nfoo\n {{else}} \n bar \n {{/comment}}{{/foo}}'),
statements = ast.statements[0].program.statements,
block = statements[0];
body = ast.body[0].program.body,
block = body[0];
equals(block.program.statements[0].string, ' \nfoo\n');
equals(block.inverse.statements[0].string, ' bar \n ');
equals(block.program.body[0].value, ' \nfoo\n');
equals(block.inverse.body[0].value, ' bar \n ');
equals(statements[0].omit, undefined);
equals(body[0].omit, undefined);
});
it('marks column 0 block mustaches as standalone', function() {
var ast = Handlebars.parse('test\n{{# comment}} \nfoo\n {{else}} \n bar \n {{/comment}} '),
block = ast.statements[1];
block = ast.body[1];
equals(ast.statements[0].omit, undefined);
equals(ast.body[0].omit, undefined);
equals(block.program.statements[0].string, 'foo\n');
equals(block.inverse.statements[0].string, ' bar \n');
equals(block.program.body[0].value, 'foo\n');
equals(block.inverse.body[0].value, ' bar \n');
equals(ast.statements[2].string, '');
equals(ast.body[2].value, '');
});
});
describe('partials', function() {
it('marks partial as standalone', function() {
var ast = Handlebars.parse('{{> partial }} ');
equals(ast.statements[1].string, '');
equals(ast.body[1].value, '');
});
it('marks indented partial as standalone', function() {
var ast = Handlebars.parse(' {{> partial }} ');
equals(ast.statements[0].string, '');
equals(ast.statements[1].indent, ' ');
equals(ast.statements[2].string, '');
equals(ast.body[0].value, '');
equals(ast.body[1].indent, ' ');
equals(ast.body[2].value, '');
});
it('marks those around content as not standalone', function() {
var ast = Handlebars.parse('a{{> partial }}');
equals(ast.statements[0].omit, undefined);
equals(ast.body[0].omit, undefined);
ast = Handlebars.parse('{{> partial }}a');
equals(ast.statements[1].omit, undefined);
equals(ast.body[1].omit, undefined);
});
});
describe('comments', function() {
it('marks comment as standalone', function() {
var ast = Handlebars.parse('{{! comment }} ');
equals(ast.statements[1].string, '');
equals(ast.body[1].value, '');
});
it('marks indented comment as standalone', function() {
var ast = Handlebars.parse(' {{! comment }} ');
equals(ast.statements[0].string, '');
equals(ast.statements[2].string, '');
equals(ast.body[0].value, '');
equals(ast.body[2].value, '');
});
it('marks those around content as not standalone', function() {
var ast = Handlebars.parse('a{{! comment }}');
equals(ast.statements[0].omit, undefined);
equals(ast.body[0].omit, undefined);
ast = Handlebars.parse('{{! comment }}a');
equals(ast.statements[1].omit, undefined);
equals(ast.body[1].omit, undefined);
});
});
});
+196 -124
View File
@@ -1,225 +1,297 @@
/*global CompilerContext, Handlebars, beforeEach, shouldCompileTo */
global.handlebarsEnv = null;
beforeEach(function() {
global.handlebarsEnv = Handlebars.create();
});
describe("basic context", function() {
it("most basic", function() {
shouldCompileTo("{{foo}}", { foo: "foo" }, "foo");
describe('basic context', function() {
it('most basic', function() {
shouldCompileTo('{{foo}}', { foo: 'foo' }, 'foo');
});
it("escaping", function() {
shouldCompileTo("\\{{foo}}", { foo: "food" }, "{{foo}}");
shouldCompileTo("content \\{{foo}}", { foo: "food" }, "content {{foo}}");
shouldCompileTo("\\\\{{foo}}", { foo: "food" }, "\\food");
shouldCompileTo("content \\\\{{foo}}", { foo: "food" }, "content \\food");
shouldCompileTo("\\\\ {{foo}}", { foo: "food" }, "\\\\ food");
it('escaping', function() {
shouldCompileTo('\\{{foo}}', { foo: 'food' }, '{{foo}}');
shouldCompileTo('content \\{{foo}}', { foo: 'food' }, 'content {{foo}}');
shouldCompileTo('\\\\{{foo}}', { foo: 'food' }, '\\food');
shouldCompileTo('content \\\\{{foo}}', { foo: 'food' }, 'content \\food');
shouldCompileTo('\\\\ {{foo}}', { foo: 'food' }, '\\\\ food');
});
it("compiling with a basic context", function() {
shouldCompileTo("Goodbye\n{{cruel}}\n{{world}}!", {cruel: "cruel", world: "world"}, "Goodbye\ncruel\nworld!",
"It works if all the required keys are provided");
it('compiling with a basic context', function() {
shouldCompileTo('Goodbye\n{{cruel}}\n{{world}}!', {cruel: 'cruel', world: 'world'}, 'Goodbye\ncruel\nworld!',
'It works if all the required keys are provided');
});
it("compiling with an undefined context", function() {
shouldCompileTo("Goodbye\n{{cruel}}\n{{world.bar}}!", undefined, "Goodbye\n\n!");
it('compiling with an undefined context', function() {
shouldCompileTo('Goodbye\n{{cruel}}\n{{world.bar}}!', undefined, 'Goodbye\n\n!');
shouldCompileTo("{{#unless foo}}Goodbye{{../test}}{{test2}}{{/unless}}", undefined, "Goodbye");
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");
it('comments', function() {
shouldCompileTo('{{! Goodbye}}Goodbye\n{{cruel}}\n{{world}}!',
{cruel: 'cruel', world: 'world'}, 'Goodbye\ncruel\nworld!',
'comments are ignored');
shouldCompileTo(' {{~! comment ~}} blah', {}, 'blah');
shouldCompileTo(' {{~!-- long-comment --~}} blah', {}, 'blah');
shouldCompileTo(' {{! comment ~}} blah', {}, ' blah');
shouldCompileTo(' {{!-- long-comment --~}} blah', {}, ' blah');
shouldCompileTo(' {{~! comment}} blah', {}, ' blah');
shouldCompileTo(' {{~!-- long-comment --}} blah', {}, ' blah');
});
it("boolean", function() {
var string = "{{#goodbye}}GOODBYE {{/goodbye}}cruel {{world}}!";
shouldCompileTo(string, {goodbye: true, world: "world"}, "GOODBYE cruel world!",
"booleans show the contents when true");
it('boolean', function() {
var string = '{{#goodbye}}GOODBYE {{/goodbye}}cruel {{world}}!';
shouldCompileTo(string, {goodbye: true, world: 'world'}, 'GOODBYE cruel world!',
'booleans show the contents when true');
shouldCompileTo(string, {goodbye: false, world: "world"}, "cruel world!",
"booleans do not show the contents when false");
shouldCompileTo(string, {goodbye: false, world: 'world'}, 'cruel world!',
'booleans do not show the contents when false');
});
it("zeros", function() {
shouldCompileTo("num1: {{num1}}, num2: {{num2}}", {num1: 42, num2: 0},
"num1: 42, num2: 0");
shouldCompileTo("num: {{.}}", 0, "num: 0");
shouldCompileTo("num: {{num1/num2}}", {num1: {num2: 0}}, "num: 0");
it('zeros', function() {
shouldCompileTo('num1: {{num1}}, num2: {{num2}}', {num1: 42, num2: 0},
'num1: 42, num2: 0');
shouldCompileTo('num: {{.}}', 0, 'num: 0');
shouldCompileTo('num: {{num1/num2}}', {num1: {num2: 0}}, 'num: 0');
});
it('false', function() {
/*eslint-disable no-new-wrappers */
shouldCompileTo('val1: {{val1}}, val2: {{val2}}', {val1: false, val2: new Boolean(false)}, 'val1: false, val2: false');
shouldCompileTo('val: {{.}}', false, 'val: false');
shouldCompileTo('val: {{val1/val2}}', {val1: {val2: false}}, 'val: false');
shouldCompileTo('val1: {{{val1}}}, val2: {{{val2}}}', {val1: false, val2: new Boolean(false)}, 'val1: false, val2: false');
shouldCompileTo('val: {{{val1/val2}}}', {val1: {val2: false}}, 'val: false');
/*eslint-enable */
});
it("newlines", function() {
it('should handle undefined and null', function() {
shouldCompileTo('{{awesome undefined null}}',
{
awesome: function(_undefined, _null, options) {
return (_undefined === undefined) + ' ' + (_null === null) + ' ' + (typeof options);
}
},
'true true object');
shouldCompileTo('{{undefined}}',
{
'undefined': function() {
return 'undefined!';
}
},
'undefined!');
shouldCompileTo('{{null}}',
{
'null': function() {
return 'null!';
}
},
'null!');
});
it('newlines', function() {
shouldCompileTo("Alan's\nTest", {}, "Alan's\nTest");
shouldCompileTo("Alan's\rTest", {}, "Alan's\rTest");
});
it("escaping text", function() {
it('escaping text', function() {
shouldCompileTo("Awesome's", {}, "Awesome's", "text is escaped so that it doesn't get caught on single quotes");
shouldCompileTo("Awesome\\", {}, "Awesome\\", "text is escaped so that the closing quote can't be ignored");
shouldCompileTo("Awesome\\\\ foo", {}, "Awesome\\\\ foo", "text is escaped so that it doesn't mess up backslashes");
shouldCompileTo("Awesome {{foo}}", {foo: '\\'}, "Awesome \\", "text is escaped so that it doesn't mess up backslashes");
shouldCompileTo(' " " ', {}, ' " " ', "double quotes never produce invalid javascript");
shouldCompileTo('Awesome\\', {}, 'Awesome\\', "text is escaped so that the closing quote can't be ignored");
shouldCompileTo('Awesome\\\\ foo', {}, 'Awesome\\\\ foo', "text is escaped so that it doesn't mess up backslashes");
shouldCompileTo('Awesome {{foo}}', {foo: '\\'}, 'Awesome \\', "text is escaped so that it doesn't mess up backslashes");
shouldCompileTo(" ' ' ", {}, " ' ' ", 'double quotes never produce invalid javascript');
});
it("escaping expressions", function() {
shouldCompileTo("{{{awesome}}}", {awesome: "&\"\\<>"}, '&\"\\<>',
it('escaping expressions', function() {
shouldCompileTo('{{{awesome}}}', {awesome: '&\'\\<>'}, '&\'\\<>',
"expressions with 3 handlebars aren't escaped");
shouldCompileTo("{{&awesome}}", {awesome: "&\"\\<>"}, '&\"\\<>',
shouldCompileTo('{{&awesome}}', {awesome: '&\'\\<>'}, '&\'\\<>',
"expressions with {{& handlebars aren't escaped");
shouldCompileTo("{{awesome}}", {awesome: "&\"'`\\<>"}, '&amp;&quot;&#x27;&#x60;\\&lt;&gt;',
"by default expressions should be escaped");
shouldCompileTo('{{awesome}}', {awesome: "&\"'`\\<>"}, '&amp;&quot;&#x27;&#x60;\\&lt;&gt;',
'by default expressions should be escaped');
shouldCompileTo("{{awesome}}", {awesome: "Escaped, <b> looks like: &lt;b&gt;"}, 'Escaped, &lt;b&gt; looks like: &amp;lt;b&amp;gt;',
"escaping should properly handle amperstands");
shouldCompileTo('{{awesome}}', {awesome: 'Escaped, <b> looks like: &lt;b&gt;'}, 'Escaped, &lt;b&gt; looks like: &amp;lt;b&amp;gt;',
'escaping should properly handle amperstands');
});
it("functions returning safestrings shouldn't be escaped", function() {
var hash = {awesome: function() { return new Handlebars.SafeString("&\"\\<>"); }};
shouldCompileTo("{{awesome}}", hash, '&\"\\<>',
var hash = {awesome: function() { return new Handlebars.SafeString('&\'\\<>'); }};
shouldCompileTo('{{awesome}}', hash, '&\'\\<>',
"functions returning safestrings aren't escaped");
});
it("functions", function() {
shouldCompileTo("{{awesome}}", {awesome: function() { return "Awesome"; }}, "Awesome",
"functions are called and render their output");
shouldCompileTo("{{awesome}}", {awesome: function() { return this.more; }, more: "More awesome"}, "More awesome",
"functions are bound to the context");
it('functions', function() {
shouldCompileTo('{{awesome}}', {awesome: function() { return 'Awesome'; }}, 'Awesome',
'functions are called and render their output');
shouldCompileTo('{{awesome}}', {awesome: function() { return this.more; }, more: 'More awesome'}, 'More awesome',
'functions are bound to the context');
});
it("functions with context argument", function() {
shouldCompileTo("{{awesome frank}}",
it('functions with context argument', function() {
shouldCompileTo('{{awesome frank}}',
{awesome: function(context) { return context; },
frank: "Frank"},
"Frank", "functions are called with context arguments");
frank: 'Frank'},
'Frank', 'functions are called with context arguments');
});
it("pathed functions with context argument", function() {
shouldCompileTo("{{bar.awesome frank}}",
it('pathed functions with context argument', function() {
shouldCompileTo('{{bar.awesome frank}}',
{bar: {awesome: function(context) { return context; }},
frank: "Frank"},
"Frank", "functions are called with context arguments");
frank: 'Frank'},
'Frank', 'functions are called with context arguments');
});
it("depthed functions with context argument", function() {
shouldCompileTo("{{#with frank}}{{../awesome .}}{{/with}}",
it('depthed functions with context argument', function() {
shouldCompileTo('{{#with frank}}{{../awesome .}}{{/with}}',
{awesome: function(context) { return context; },
frank: "Frank"},
"Frank", "functions are called with context arguments");
frank: 'Frank'},
'Frank', 'functions are called with context arguments');
});
it("block functions with context argument", function() {
shouldCompileTo("{{#awesome 1}}inner {{.}}{{/awesome}}",
it('block functions with context argument', function() {
shouldCompileTo('{{#awesome 1}}inner {{.}}{{/awesome}}',
{awesome: function(context, options) { return options.fn(context); }},
"inner 1", "block functions are called with context and options");
'inner 1', 'block functions are called with context and options');
});
it("depthed block functions with context argument", function() {
shouldCompileTo("{{#with value}}{{#../awesome 1}}inner {{.}}{{/../awesome}}{{/with}}",
it('depthed block functions with context argument', function() {
shouldCompileTo('{{#with value}}{{#../awesome 1}}inner {{.}}{{/../awesome}}{{/with}}',
{value: true, awesome: function(context, options) { return options.fn(context); }},
"inner 1", "block functions are called with context and options");
'inner 1', 'block functions are called with context and options');
});
it("block functions without context argument", function() {
shouldCompileTo("{{#awesome}}inner{{/awesome}}",
it('block functions without context argument', function() {
shouldCompileTo('{{#awesome}}inner{{/awesome}}',
{awesome: function(options) { return options.fn(this); }},
"inner", "block functions are called with options");
'inner', 'block functions are called with options');
});
it("pathed block functions without context argument", function() {
shouldCompileTo("{{#foo.awesome}}inner{{/foo.awesome}}",
{foo: {awesome: function(options) { return this; }}},
"inner", "block functions are called with options");
it('pathed block functions without context argument', function() {
shouldCompileTo('{{#foo.awesome}}inner{{/foo.awesome}}',
{foo: {awesome: function() { return this; }}},
'inner', 'block functions are called with options');
});
it("depthed block functions without context argument", function() {
shouldCompileTo("{{#with value}}{{#../awesome}}inner{{/../awesome}}{{/with}}",
{value: true, awesome: function(options) { return this; }},
"inner", "block functions are called with options");
it('depthed block functions without context argument', function() {
shouldCompileTo('{{#with value}}{{#../awesome}}inner{{/../awesome}}{{/with}}',
{value: true, awesome: function() { return this; }},
'inner', 'block functions are called with options');
});
it("paths with hyphens", function() {
shouldCompileTo("{{foo-bar}}", {"foo-bar": "baz"}, "baz", "Paths can contain hyphens (-)");
shouldCompileTo("{{foo.foo-bar}}", {foo: {"foo-bar": "baz"}}, "baz", "Paths can contain hyphens (-)");
shouldCompileTo("{{foo/foo-bar}}", {foo: {"foo-bar": "baz"}}, "baz", "Paths can contain hyphens (-)");
it('paths with hyphens', function() {
shouldCompileTo('{{foo-bar}}', {'foo-bar': 'baz'}, 'baz', 'Paths can contain hyphens (-)');
shouldCompileTo('{{foo.foo-bar}}', {foo: {'foo-bar': 'baz'}}, 'baz', 'Paths can contain hyphens (-)');
shouldCompileTo('{{foo/foo-bar}}', {foo: {'foo-bar': 'baz'}}, 'baz', 'Paths can contain hyphens (-)');
});
it("nested paths", function() {
shouldCompileTo("Goodbye {{alan/expression}} world!", {alan: {expression: "beautiful"}},
"Goodbye beautiful world!", "Nested paths access nested objects");
it('nested paths', function() {
shouldCompileTo('Goodbye {{alan/expression}} world!', {alan: {expression: 'beautiful'}},
'Goodbye beautiful world!', 'Nested paths access nested objects');
});
it("nested paths with empty string value", function() {
shouldCompileTo("Goodbye {{alan/expression}} world!", {alan: {expression: ""}},
"Goodbye world!", "Nested paths access nested objects with empty string");
it('nested paths with empty string value', function() {
shouldCompileTo('Goodbye {{alan/expression}} world!', {alan: {expression: ''}},
'Goodbye world!', 'Nested paths access nested objects with empty string');
});
it("literal paths", function() {
shouldCompileTo("Goodbye {{[@alan]/expression}} world!", {"@alan": {expression: "beautiful"}},
"Goodbye beautiful world!", "Literal paths can be used");
shouldCompileTo("Goodbye {{[foo bar]/expression}} world!", {"foo bar": {expression: "beautiful"}},
"Goodbye beautiful world!", "Literal paths can be used");
it('literal paths', function() {
shouldCompileTo('Goodbye {{[@alan]/expression}} world!', {'@alan': {expression: 'beautiful'}},
'Goodbye beautiful world!', 'Literal paths can be used');
shouldCompileTo('Goodbye {{[foo bar]/expression}} world!', {'foo bar': {expression: 'beautiful'}},
'Goodbye beautiful world!', 'Literal paths can be used');
});
it('literal references', function() {
shouldCompileTo("Goodbye {{[foo bar]}} world!", {"foo bar": "beautiful"},
"Goodbye beautiful world!", "Literal paths can be used");
shouldCompileTo('Goodbye {{[foo bar]}} world!', {'foo bar': 'beautiful'},
'Goodbye beautiful world!', 'Literal paths can be used');
});
it("that current context path ({{.}}) doesn't hit helpers", function() {
shouldCompileTo("test: {{.}}", [null, {helper: "awesome"}], "test: ");
shouldCompileTo('test: {{.}}', [null, {helper: 'awesome'}], 'test: ');
});
it("complex but empty paths", function() {
shouldCompileTo("{{person/name}}", {person: {name: null}}, "");
shouldCompileTo("{{person/name}}", {person: {}}, "");
it('complex but empty paths', function() {
shouldCompileTo('{{person/name}}', {person: {name: null}}, '');
shouldCompileTo('{{person/name}}', {person: {}}, '');
});
it("this keyword in paths", function() {
var string = "{{#goodbyes}}{{this}}{{/goodbyes}}";
var hash = {goodbyes: ["goodbye", "Goodbye", "GOODBYE"]};
shouldCompileTo(string, hash, "goodbyeGoodbyeGOODBYE",
"This keyword in paths evaluates to current context");
it('this keyword in paths', function() {
var string = '{{#goodbyes}}{{this}}{{/goodbyes}}';
var hash = {goodbyes: ['goodbye', 'Goodbye', 'GOODBYE']};
shouldCompileTo(string, hash, 'goodbyeGoodbyeGOODBYE',
'This keyword in paths evaluates to current context');
string = "{{#hellos}}{{this/text}}{{/hellos}}";
hash = {hellos: [{text: "hello"}, {text: "Hello"}, {text: "HELLO"}]};
shouldCompileTo(string, hash, "helloHelloHELLO", "This keyword evaluates in more complex paths");
string = '{{#hellos}}{{this/text}}{{/hellos}}';
hash = {hellos: [{text: 'hello'}, {text: 'Hello'}, {text: 'HELLO'}]};
shouldCompileTo(string, hash, 'helloHelloHELLO', 'This keyword evaluates in more complex paths');
});
it("this keyword nested inside path", function() {
var string = "{{#hellos}}{{text/this/foo}}{{/hellos}}";
it('this keyword nested inside path', function() {
shouldThrow(function() {
CompilerContext.compile(string);
}, Error);
CompilerContext.compile('{{#hellos}}{{text/this/foo}}{{/hellos}}');
}, Error, 'Invalid path: text/this - 1:13');
shouldCompileTo('{{[this]}}', {'this': 'bar'}, 'bar');
shouldCompileTo('{{text/[this]}}', {text: {'this': 'bar'}}, 'bar');
});
it("this keyword in helpers", function() {
it('this keyword in helpers', function() {
var helpers = {foo: function(value) {
return 'bar ' + value;
}};
var string = "{{#goodbyes}}{{foo this}}{{/goodbyes}}";
var hash = {goodbyes: ["goodbye", "Goodbye", "GOODBYE"]};
shouldCompileTo(string, [hash, helpers], "bar goodbyebar Goodbyebar GOODBYE",
"This keyword in paths evaluates to current context");
var string = '{{#goodbyes}}{{foo this}}{{/goodbyes}}';
var hash = {goodbyes: ['goodbye', 'Goodbye', 'GOODBYE']};
shouldCompileTo(string, [hash, helpers], 'bar goodbyebar Goodbyebar GOODBYE',
'This keyword in paths evaluates to current context');
string = "{{#hellos}}{{foo this/text}}{{/hellos}}";
hash = {hellos: [{text: "hello"}, {text: "Hello"}, {text: "HELLO"}]};
shouldCompileTo(string, [hash, helpers], "bar hellobar Hellobar HELLO", "This keyword evaluates in more complex paths");
string = '{{#hellos}}{{foo this/text}}{{/hellos}}';
hash = {hellos: [{text: 'hello'}, {text: 'Hello'}, {text: 'HELLO'}]};
shouldCompileTo(string, [hash, helpers], 'bar hellobar Hellobar HELLO', 'This keyword evaluates in more complex paths');
});
it("this keyword nested inside helpers param", function() {
var string = "{{#hellos}}{{foo text/this/foo}}{{/hellos}}";
it('this keyword nested inside helpers param', function() {
var string = '{{#hellos}}{{foo text/this/foo}}{{/hellos}}';
shouldThrow(function() {
CompilerContext.compile(string);
}, Error);
}, Error, 'Invalid path: text/this - 1:17');
shouldCompileTo(
'{{foo [this]}}',
{foo: function(value) { return value; }, 'this': 'bar'},
'bar');
shouldCompileTo(
'{{foo text/[this]}}',
{foo: function(value) { return value; }, text: {'this': 'bar'}},
'bar');
});
it('pass string literals', function() {
shouldCompileTo('{{"foo"}}', {}, '');
shouldCompileTo('{{"foo"}}', { foo: 'bar' }, 'bar');
shouldCompileTo('{{#"foo"}}{{.}}{{/"foo"}}', { foo: ['bar', 'baz'] }, 'barbaz');
});
it('pass number literals', function() {
shouldCompileTo('{{12}}', {}, '');
shouldCompileTo('{{12}}', { '12': 'bar' }, 'bar');
shouldCompileTo('{{12.34}}', {}, '');
shouldCompileTo('{{12.34}}', { '12.34': 'bar' }, 'bar');
shouldCompileTo('{{12.34 1}}', { '12.34': function(arg) { return 'bar' + arg; } }, 'bar1');
});
it('pass boolean literals', function() {
shouldCompileTo('{{true}}', {}, '');
shouldCompileTo('{{true}}', { '': 'foo' }, '');
shouldCompileTo('{{false}}', { 'false': 'foo' }, 'foo');
});
it('should handle literals in subexpression', function() {
var helpers = {
foo: function(arg) {
return arg;
}
};
shouldCompileTo('{{foo (false)}}', [{ 'false': function() { return 'bar'; } }, helpers], 'bar');
});
});
+83 -64
View File
@@ -1,98 +1,111 @@
/*global CompilerContext, shouldCompileTo, shouldThrow */
describe('blocks', function() {
it("array", function() {
var string = "{{#goodbyes}}{{text}}! {{/goodbyes}}cruel {{world}}!";
var hash = {goodbyes: [{text: "goodbye"}, {text: "Goodbye"}, {text: "GOODBYE"}], world: "world"};
shouldCompileTo(string, hash, "goodbye! Goodbye! GOODBYE! cruel world!",
"Arrays iterate over the contents when not empty");
it('array', function() {
var string = '{{#goodbyes}}{{text}}! {{/goodbyes}}cruel {{world}}!';
var hash = {goodbyes: [{text: 'goodbye'}, {text: 'Goodbye'}, {text: 'GOODBYE'}], world: 'world'};
shouldCompileTo(string, hash, 'goodbye! Goodbye! GOODBYE! cruel world!',
'Arrays iterate over the contents when not empty');
shouldCompileTo(string, {goodbyes: [], world: "world"}, "cruel world!",
"Arrays ignore the contents when empty");
shouldCompileTo(string, {goodbyes: [], world: 'world'}, 'cruel world!',
'Arrays ignore the contents when empty');
});
it('array without data', function() {
var string = '{{#goodbyes}}{{text}}{{/goodbyes}} {{#goodbyes}}{{text}}{{/goodbyes}}';
var hash = {goodbyes: [{text: 'goodbye'}, {text: 'Goodbye'}, {text: 'GOODBYE'}], world: 'world'};
shouldCompileTo(string, [hash,,,,false], 'goodbyeGoodbyeGOODBYE goodbyeGoodbyeGOODBYE');
var string = '{{#goodbyes}}{{text}}{{/goodbyes}} {{#goodbyes}}{{text}}{{/goodbyes}}';
var hash = {goodbyes: [{text: 'goodbye'}, {text: 'Goodbye'}, {text: 'GOODBYE'}], world: 'world'};
shouldCompileTo(string, [hash,,, false], 'goodbyeGoodbyeGOODBYE goodbyeGoodbyeGOODBYE');
});
it("array with @index", function() {
var string = "{{#goodbyes}}{{@index}}. {{text}}! {{/goodbyes}}cruel {{world}}!";
var hash = {goodbyes: [{text: "goodbye"}, {text: "Goodbye"}, {text: "GOODBYE"}], world: "world"};
it('array with @index', function() {
var string = '{{#goodbyes}}{{@index}}. {{text}}! {{/goodbyes}}cruel {{world}}!';
var hash = {goodbyes: [{text: 'goodbye'}, {text: 'Goodbye'}, {text: 'GOODBYE'}], world: 'world'};
var template = CompilerContext.compile(string);
var result = template(hash);
equal(result, "0. goodbye! 1. Goodbye! 2. GOODBYE! cruel world!", "The @index variable is used");
});
it("empty block", function() {
var string = "{{#goodbyes}}{{/goodbyes}}cruel {{world}}!";
var hash = {goodbyes: [{text: "goodbye"}, {text: "Goodbye"}, {text: "GOODBYE"}], world: "world"};
shouldCompileTo(string, hash, "cruel world!",
"Arrays iterate over the contents when not empty");
shouldCompileTo(string, {goodbyes: [], world: "world"}, "cruel world!",
"Arrays ignore the contents when empty");
equal(result, '0. goodbye! 1. Goodbye! 2. GOODBYE! cruel world!', 'The @index variable is used');
});
it("block with complex lookup", function() {
var string = "{{#goodbyes}}{{text}} cruel {{../name}}! {{/goodbyes}}";
var hash = {name: "Alan", goodbyes: [{text: "goodbye"}, {text: "Goodbye"}, {text: "GOODBYE"}]};
it('empty block', function() {
var string = '{{#goodbyes}}{{/goodbyes}}cruel {{world}}!';
var hash = {goodbyes: [{text: 'goodbye'}, {text: 'Goodbye'}, {text: 'GOODBYE'}], world: 'world'};
shouldCompileTo(string, hash, 'cruel world!',
'Arrays iterate over the contents when not empty');
shouldCompileTo(string, hash, "goodbye cruel Alan! Goodbye cruel Alan! GOODBYE cruel Alan! ",
"Templates can access variables in contexts up the stack with relative path syntax");
shouldCompileTo(string, {goodbyes: [], world: 'world'}, 'cruel world!',
'Arrays ignore the contents when empty');
});
it('block with complex lookup', function() {
var string = '{{#goodbyes}}{{text}} cruel {{../name}}! {{/goodbyes}}';
var hash = {name: 'Alan', goodbyes: [{text: 'goodbye'}, {text: 'Goodbye'}, {text: 'GOODBYE'}]};
shouldCompileTo(string, hash, 'goodbye cruel Alan! Goodbye cruel Alan! GOODBYE cruel Alan! ',
'Templates can access variables in contexts up the stack with relative path syntax');
});
it('multiple blocks with complex lookup', function() {
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');
});
it("block with complex lookup using nested context", function() {
var string = "{{#goodbyes}}{{text}} cruel {{foo/../name}}! {{/goodbyes}}";
it('block with complex lookup using nested context', function() {
var string = '{{#goodbyes}}{{text}} cruel {{foo/../name}}! {{/goodbyes}}';
shouldThrow(function() {
CompilerContext.compile(string);
}, Error);
});
it("block with deep nested complex lookup", function() {
var string = "{{#outer}}Goodbye {{#inner}}cruel {{../sibling}} {{../../omg}}{{/inner}}{{/outer}}";
var hash = {omg: "OMG!", outer: [{ sibling: 'sad', inner: [{ text: "goodbye" }] }] };
it('block with deep nested complex lookup', function() {
var string = '{{#outer}}Goodbye {{#inner}}cruel {{../sibling}} {{../../omg}}{{/inner}}{{/outer}}';
var hash = {omg: 'OMG!', outer: [{ sibling: 'sad', inner: [{ text: 'goodbye' }] }] };
shouldCompileTo(string, hash, "Goodbye cruel sad OMG!");
shouldCompileTo(string, hash, 'Goodbye cruel sad OMG!');
});
describe('inverted sections', function() {
it("inverted sections with unset value", function() {
var string = "{{#goodbyes}}{{this}}{{/goodbyes}}{{^goodbyes}}Right On!{{/goodbyes}}";
it('inverted sections with unset value', function() {
var string = '{{#goodbyes}}{{this}}{{/goodbyes}}{{^goodbyes}}Right On!{{/goodbyes}}';
var hash = {};
shouldCompileTo(string, hash, "Right On!", "Inverted section rendered when value isn't set.");
shouldCompileTo(string, hash, 'Right On!', "Inverted section rendered when value isn't set.");
});
it("inverted section with false value", function() {
var string = "{{#goodbyes}}{{this}}{{/goodbyes}}{{^goodbyes}}Right On!{{/goodbyes}}";
it('inverted section with false value', function() {
var string = '{{#goodbyes}}{{this}}{{/goodbyes}}{{^goodbyes}}Right On!{{/goodbyes}}';
var hash = {goodbyes: false};
shouldCompileTo(string, hash, "Right On!", "Inverted section rendered when value is false.");
shouldCompileTo(string, hash, 'Right On!', 'Inverted section rendered when value is false.');
});
it("inverted section with empty set", function() {
var string = "{{#goodbyes}}{{this}}{{/goodbyes}}{{^goodbyes}}Right On!{{/goodbyes}}";
it('inverted section with empty set', function() {
var string = '{{#goodbyes}}{{this}}{{/goodbyes}}{{^goodbyes}}Right On!{{/goodbyes}}';
var hash = {goodbyes: []};
shouldCompileTo(string, hash, "Right On!", "Inverted section rendered when value is empty set.");
shouldCompileTo(string, hash, 'Right On!', 'Inverted section rendered when value is empty set.');
});
it("block inverted sections", function() {
shouldCompileTo("{{#people}}{{name}}{{^}}{{none}}{{/people}}", {none: "No people"},
"No people");
it('block inverted sections', function() {
shouldCompileTo('{{#people}}{{name}}{{^}}{{none}}{{/people}}', {none: 'No people'},
'No people');
});
it('chained inverted sections', function() {
shouldCompileTo('{{#people}}{{name}}{{else if none}}{{none}}{{/people}}', {none: 'No people'},
'No people');
shouldCompileTo('{{#people}}{{name}}{{else if nothere}}fail{{else unless nothere}}{{none}}{{/people}}', {none: 'No people'},
'No people');
shouldCompileTo('{{#people}}{{name}}{{else if none}}{{none}}{{else}}fail{{/people}}', {none: 'No people'},
'No people');
});
it('chained inverted sections with mismatch', function() {
shouldThrow(function() {
shouldCompileTo('{{#people}}{{name}}{{else if none}}{{none}}{{/if}}', {none: 'No people'},
'No people');
}, Error);
});
it("block inverted sections with empty arrays", function() {
shouldCompileTo("{{#people}}{{name}}{{^}}{{none}}{{/people}}", {none: "No people", people: []},
"No people");
it('block inverted sections with empty arrays', function() {
shouldCompileTo('{{#people}}{{name}}{{^}}{{none}}{{/people}}', {none: 'No people', people: []},
'No people');
});
});
@@ -105,29 +118,35 @@ describe('blocks', function() {
shouldCompileTo('{{#people}}\n{{name}}\n{{^}}\n{{none}}\n{{/people}}\n', {none: 'No people'},
'No people\n');
});
it('block standalone chained else sections', function() {
shouldCompileTo('{{#people}}\n{{name}}\n{{else if none}}\n{{none}}\n{{/people}}\n', {none: 'No people'},
'No people\n');
shouldCompileTo('{{#people}}\n{{name}}\n{{else if none}}\n{{none}}\n{{^}}\n{{/people}}\n', {none: 'No people'},
'No people\n');
});
it('should handle nesting', function() {
shouldCompileTo('{{#data}}\n{{#if true}}\n{{.}}\n{{/if}}\n{{/data}}\nOK.', {data: [1, 3, 5]}, '1\n3\n5\nOK.');
});
});
describe('compat mode', function() {
it("block with deep recursive lookup lookup", function() {
var string = "{{#outer}}Goodbye {{#inner}}cruel {{omg}}{{/inner}}{{/outer}}";
var hash = {omg: "OMG!", outer: [{ inner: [{ text: "goodbye" }] }] };
it('block with deep recursive lookup lookup', function() {
var string = '{{#outer}}Goodbye {{#inner}}cruel {{omg}}{{/inner}}{{/outer}}';
var hash = {omg: 'OMG!', outer: [{ inner: [{ text: 'goodbye' }] }] };
shouldCompileTo(string, [hash, undefined, undefined, true], "Goodbye cruel OMG!");
shouldCompileTo(string, [hash, undefined, undefined, true], 'Goodbye cruel OMG!');
});
it("block with deep recursive pathed lookup", function() {
var string = "{{#outer}}Goodbye {{#inner}}cruel {{omg.yes}}{{/inner}}{{/outer}}";
var hash = {omg: {yes: "OMG!"}, outer: [{ inner: [{ yes: 'no', text: "goodbye" }] }] };
it('block with deep recursive pathed lookup', function() {
var string = '{{#outer}}Goodbye {{#inner}}cruel {{omg.yes}}{{/inner}}{{/outer}}';
var hash = {omg: {yes: 'OMG!'}, outer: [{ inner: [{ yes: 'no', text: 'goodbye' }] }] };
shouldCompileTo(string, [hash, undefined, undefined, true], "Goodbye cruel OMG!");
shouldCompileTo(string, [hash, undefined, undefined, true], 'Goodbye cruel OMG!');
});
it("block with missed recursive lookup", function() {
var string = "{{#outer}}Goodbye {{#inner}}cruel {{omg.yes}}{{/inner}}{{/outer}}";
var hash = {omg: {no: "OMG!"}, outer: [{ inner: [{ yes: 'no', text: "goodbye" }] }] };
it('block with missed recursive lookup', function() {
var string = '{{#outer}}Goodbye {{#inner}}cruel {{omg.yes}}{{/inner}}{{/outer}}';
var hash = {omg: {no: 'OMG!'}, outer: [{ inner: [{ yes: 'no', text: 'goodbye' }] }] };
shouldCompileTo(string, [hash, undefined, undefined, true], "Goodbye cruel ");
shouldCompileTo(string, [hash, undefined, undefined, true], 'Goodbye cruel ');
});
});
});
+173 -143
View File
@@ -1,52 +1,51 @@
/*global CompilerContext, shouldCompileTo, shouldThrow, compileWithPartials, handlebarsEnv */
describe('builtin helpers', function() {
describe('#if', function() {
it("if", function() {
var string = "{{#if goodbye}}GOODBYE {{/if}}cruel {{world}}!";
shouldCompileTo(string, {goodbye: true, world: "world"}, "GOODBYE cruel world!",
"if with boolean argument shows the contents when true");
shouldCompileTo(string, {goodbye: "dummy", world: "world"}, "GOODBYE cruel world!",
"if with string argument shows the contents");
shouldCompileTo(string, {goodbye: false, world: "world"}, "cruel world!",
"if with boolean argument does not show the contents when false");
shouldCompileTo(string, {world: "world"}, "cruel world!",
"if with undefined does not show the contents");
shouldCompileTo(string, {goodbye: ['foo'], world: "world"}, "GOODBYE cruel world!",
"if with non-empty array shows the contents");
shouldCompileTo(string, {goodbye: [], world: "world"}, "cruel world!",
"if with empty array does not show the contents");
shouldCompileTo(string, {goodbye: 0, world: "world"}, "cruel world!",
"if with zero does not show the contents");
shouldCompileTo("{{#if goodbye includeZero=true}}GOODBYE {{/if}}cruel {{world}}!",
{goodbye: 0, world: "world"}, "GOODBYE cruel world!",
"if with zero does not show the contents");
it('if', function() {
var string = '{{#if goodbye}}GOODBYE {{/if}}cruel {{world}}!';
shouldCompileTo(string, {goodbye: true, world: 'world'}, 'GOODBYE cruel world!',
'if with boolean argument shows the contents when true');
shouldCompileTo(string, {goodbye: 'dummy', world: 'world'}, 'GOODBYE cruel world!',
'if with string argument shows the contents');
shouldCompileTo(string, {goodbye: false, world: 'world'}, 'cruel world!',
'if with boolean argument does not show the contents when false');
shouldCompileTo(string, {world: 'world'}, 'cruel world!',
'if with undefined does not show the contents');
shouldCompileTo(string, {goodbye: ['foo'], world: 'world'}, 'GOODBYE cruel world!',
'if with non-empty array shows the contents');
shouldCompileTo(string, {goodbye: [], world: 'world'}, 'cruel world!',
'if with empty array does not show the contents');
shouldCompileTo(string, {goodbye: 0, world: 'world'}, 'cruel world!',
'if with zero does not show the contents');
shouldCompileTo('{{#if goodbye includeZero=true}}GOODBYE {{/if}}cruel {{world}}!',
{goodbye: 0, world: 'world'}, 'GOODBYE cruel world!',
'if with zero does not show the contents');
});
it("if with function argument", function() {
var string = "{{#if goodbye}}GOODBYE {{/if}}cruel {{world}}!";
shouldCompileTo(string, {goodbye: function() {return true;}, world: "world"}, "GOODBYE cruel world!",
"if with function shows the contents when function returns true");
shouldCompileTo(string, {goodbye: function() {return this.world;}, world: "world"}, "GOODBYE cruel world!",
"if with function shows the contents when function returns string");
shouldCompileTo(string, {goodbye: function() {return false;}, world: "world"}, "cruel world!",
"if with function does not show the contents when returns false");
shouldCompileTo(string, {goodbye: function() {return this.foo;}, world: "world"}, "cruel world!",
"if with function does not show the contents when returns undefined");
it('if with function argument', function() {
var string = '{{#if goodbye}}GOODBYE {{/if}}cruel {{world}}!';
shouldCompileTo(string, {goodbye: function() {return true; }, world: 'world'}, 'GOODBYE cruel world!',
'if with function shows the contents when function returns true');
shouldCompileTo(string, {goodbye: function() {return this.world; }, world: 'world'}, 'GOODBYE cruel world!',
'if with function shows the contents when function returns string');
shouldCompileTo(string, {goodbye: function() {return false; }, world: 'world'}, 'cruel world!',
'if with function does not show the contents when returns false');
shouldCompileTo(string, {goodbye: function() {return this.foo; }, world: 'world'}, 'cruel world!',
'if with function does not show the contents when returns undefined');
});
});
describe('#with', function() {
it("with", function() {
var string = "{{#with person}}{{first}} {{last}}{{/with}}";
shouldCompileTo(string, {person: {first: "Alan", last: "Johnson"}}, "Alan Johnson");
it('with', function() {
var string = '{{#with person}}{{first}} {{last}}{{/with}}';
shouldCompileTo(string, {person: {first: 'Alan', last: 'Johnson'}}, 'Alan Johnson');
});
it("with with function argument", function() {
var string = "{{#with person}}{{first}} {{last}}{{/with}}";
shouldCompileTo(string, {person: function() { return {first: "Alan", last: "Johnson"};}}, "Alan Johnson");
it('with with function argument', function() {
var string = '{{#with person}}{{first}} {{last}}{{/with}}';
shouldCompileTo(string, {person: function() { return {first: 'Alan', last: 'Johnson'}; }}, 'Alan Johnson');
});
it("with with else", function() {
var string = "{{#with person}}Person is present{{else}}Person is not present{{/with}}";
shouldCompileTo(string, {}, "Person is not present");
it('with with else', function() {
var string = '{{#with person}}Person is present{{else}}Person is not present{{/with}}';
shouldCompileTo(string, {}, 'Person is not present');
});
});
@@ -57,143 +56,162 @@ describe('builtin helpers', function() {
});
});
it("each", function() {
var string = "{{#each goodbyes}}{{text}}! {{/each}}cruel {{world}}!";
var hash = {goodbyes: [{text: "goodbye"}, {text: "Goodbye"}, {text: "GOODBYE"}], world: "world"};
shouldCompileTo(string, hash, "goodbye! Goodbye! GOODBYE! cruel world!",
"each with array argument iterates over the contents when not empty");
shouldCompileTo(string, {goodbyes: [], world: "world"}, "cruel world!",
"each with array argument ignores the contents when empty");
it('each', function() {
var string = '{{#each goodbyes}}{{text}}! {{/each}}cruel {{world}}!';
var hash = {goodbyes: [{text: 'goodbye'}, {text: 'Goodbye'}, {text: 'GOODBYE'}], world: 'world'};
shouldCompileTo(string, hash, 'goodbye! Goodbye! GOODBYE! cruel world!',
'each with array argument iterates over the contents when not empty');
shouldCompileTo(string, {goodbyes: [], world: 'world'}, 'cruel world!',
'each with array argument ignores the contents when empty');
});
it('each without data', function() {
var string = '{{#each goodbyes}}{{text}}! {{/each}}cruel {{world}}!';
var hash = {goodbyes: [{text: 'goodbye'}, {text: 'Goodbye'}, {text: 'GOODBYE'}], world: 'world'};
shouldCompileTo(string, [hash,,,,false], 'goodbye! Goodbye! GOODBYE! cruel world!');
var string = '{{#each goodbyes}}{{text}}! {{/each}}cruel {{world}}!';
var hash = {goodbyes: [{text: 'goodbye'}, {text: 'Goodbye'}, {text: 'GOODBYE'}], world: 'world'};
shouldCompileTo(string, [hash,,,, false], 'goodbye! Goodbye! GOODBYE! cruel world!');
hash = {goodbyes: 'cruel', world: 'world'};
shouldCompileTo('{{#each .}}{{.}}{{/each}}', [hash,,,,false], 'cruelworld');
shouldCompileTo('{{#each .}}{{.}}{{/each}}', [hash,,,, false], 'cruelworld');
});
it('each without context', function() {
var string = '{{#each goodbyes}}{{text}}! {{/each}}cruel {{world}}!';
shouldCompileTo(string, [,,,,], 'cruel !');
var string = '{{#each goodbyes}}{{text}}! {{/each}}cruel {{world}}!';
shouldCompileTo(string, [,,,, ], 'cruel !');
});
it("each with an object and @key", function() {
var string = "{{#each goodbyes}}{{@key}}. {{text}}! {{/each}}cruel {{world}}!";
it('each with an object and @key', function() {
var string = '{{#each goodbyes}}{{@key}}. {{text}}! {{/each}}cruel {{world}}!';
function Clazz() {
this['<b>#1</b>'] = {text: 'goodbye'};
this[2] = {text: 'GOODBYE'};
}
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,
// so we need to check both possible orders
// @see http://stackoverflow.com/questions/280713/elements-order-in-a-for-in-loop
var actual = compileWithPartials(string, hash);
var expected1 = "&lt;b&gt;#1&lt;/b&gt;. goodbye! 2. GOODBYE! cruel world!";
var expected2 = "2. GOODBYE! &lt;b&gt;#1&lt;/b&gt;. goodbye! cruel world!";
var expected1 = '&lt;b&gt;#1&lt;/b&gt;. goodbye! 2. GOODBYE! cruel world!';
var expected2 = '2. GOODBYE! &lt;b&gt;#1&lt;/b&gt;. goodbye! cruel world!';
equals(actual === expected1 || actual === expected2, true, "each with object argument iterates over the contents when not empty");
shouldCompileTo(string, {goodbyes: [], world: "world"}, "cruel world!",
"each with object argument ignores the contents when empty");
equals(actual === expected1 || actual === expected2, true, 'each with object argument iterates over the contents when not empty');
shouldCompileTo(string, {goodbyes: {}, world: 'world'}, 'cruel world!');
});
it("each with @index", function() {
var string = "{{#each goodbyes}}{{@index}}. {{text}}! {{/each}}cruel {{world}}!";
var hash = {goodbyes: [{text: "goodbye"}, {text: "Goodbye"}, {text: "GOODBYE"}], world: "world"};
it('each with @index', function() {
var string = '{{#each goodbyes}}{{@index}}. {{text}}! {{/each}}cruel {{world}}!';
var hash = {goodbyes: [{text: 'goodbye'}, {text: 'Goodbye'}, {text: 'GOODBYE'}], world: 'world'};
var template = CompilerContext.compile(string);
var result = template(hash);
equal(result, "0. goodbye! 1. Goodbye! 2. GOODBYE! cruel world!", "The @index variable is used");
equal(result, '0. goodbye! 1. Goodbye! 2. GOODBYE! cruel world!', 'The @index variable is used');
});
it("each with nested @index", function() {
var string = "{{#each goodbyes}}{{@index}}. {{text}}! {{#each ../goodbyes}}{{@index}} {{/each}}After {{@index}} {{/each}}{{@index}}cruel {{world}}!";
var hash = {goodbyes: [{text: "goodbye"}, {text: "Goodbye"}, {text: "GOODBYE"}], world: "world"};
it('each with nested @index', function() {
var string = '{{#each goodbyes}}{{@index}}. {{text}}! {{#each ../goodbyes}}{{@index}} {{/each}}After {{@index}} {{/each}}{{@index}}cruel {{world}}!';
var hash = {goodbyes: [{text: 'goodbye'}, {text: 'Goodbye'}, {text: 'GOODBYE'}], world: 'world'};
var template = CompilerContext.compile(string);
var result = template(hash);
equal(result, "0. goodbye! 0 1 2 After 0 1. Goodbye! 0 1 2 After 1 2. GOODBYE! 0 1 2 After 2 cruel world!", "The @index variable is used");
equal(result, '0. goodbye! 0 1 2 After 0 1. Goodbye! 0 1 2 After 1 2. GOODBYE! 0 1 2 After 2 cruel world!', 'The @index variable is used');
});
it("each object with @index", function() {
var string = "{{#each goodbyes}}{{@index}}. {{text}}! {{/each}}cruel {{world}}!";
var hash = {goodbyes: {'a': {text: "goodbye"}, b: {text: "Goodbye"}, c: {text: "GOODBYE"}}, world: "world"};
it('each 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 hash = {goodbyes: [{text: 'goodbye'}, {text: 'Goodbye'}], world: 'world'};
var template = CompilerContext.compile(string);
var result = template(hash);
equal(result, "0. goodbye! 1. Goodbye! 2. GOODBYE! cruel world!", "The @index variable is used");
equal(result, '0. goodbye! 0 0 0 1 After 0 1. Goodbye! 1 0 1 1 After 1 cruel world!');
});
it("each with @first", function() {
var string = "{{#each goodbyes}}{{#if @first}}{{text}}! {{/if}}{{/each}}cruel {{world}}!";
var hash = {goodbyes: [{text: "goodbye"}, {text: "Goodbye"}, {text: "GOODBYE"}], world: "world"};
it('each object with @index', function() {
var string = '{{#each goodbyes}}{{@index}}. {{text}}! {{/each}}cruel {{world}}!';
var hash = {goodbyes: {'a': {text: 'goodbye'}, b: {text: 'Goodbye'}, c: {text: 'GOODBYE'}}, world: 'world'};
var template = CompilerContext.compile(string);
var result = template(hash);
equal(result, "goodbye! cruel world!", "The @first variable is used");
equal(result, '0. goodbye! 1. Goodbye! 2. GOODBYE! cruel world!', 'The @index variable is used');
});
it("each with nested @first", function() {
var string = "{{#each goodbyes}}({{#if @first}}{{text}}! {{/if}}{{#each ../goodbyes}}{{#if @first}}{{text}}!{{/if}}{{/each}}{{#if @first}} {{text}}!{{/if}}) {{/each}}cruel {{world}}!";
var hash = {goodbyes: [{text: "goodbye"}, {text: "Goodbye"}, {text: "GOODBYE"}], world: "world"};
it('each with @first', function() {
var string = '{{#each goodbyes}}{{#if @first}}{{text}}! {{/if}}{{/each}}cruel {{world}}!';
var hash = {goodbyes: [{text: 'goodbye'}, {text: 'Goodbye'}, {text: 'GOODBYE'}], world: 'world'};
var template = CompilerContext.compile(string);
var result = template(hash);
equal(result, "(goodbye! goodbye! goodbye!) (goodbye!) (goodbye!) cruel world!", "The @first variable is used");
equal(result, 'goodbye! cruel world!', 'The @first variable is used');
});
it("each object with @first", function() {
var string = "{{#each goodbyes}}{{#if @first}}{{text}}! {{/if}}{{/each}}cruel {{world}}!";
var hash = {goodbyes: {'foo': {text: "goodbye"}, bar: {text: "Goodbye"}}, world: "world"};
it('each with nested @first', function() {
var string = '{{#each goodbyes}}({{#if @first}}{{text}}! {{/if}}{{#each ../goodbyes}}{{#if @first}}{{text}}!{{/if}}{{/each}}{{#if @first}} {{text}}!{{/if}}) {{/each}}cruel {{world}}!';
var hash = {goodbyes: [{text: 'goodbye'}, {text: 'Goodbye'}, {text: 'GOODBYE'}], world: 'world'};
var template = CompilerContext.compile(string);
var result = template(hash);
equal(result, "goodbye! cruel world!", "The @first variable is used");
equal(result, '(goodbye! goodbye! goodbye!) (goodbye!) (goodbye!) cruel world!', 'The @first variable is used');
});
it("each with @last", function() {
var string = "{{#each goodbyes}}{{#if @last}}{{text}}! {{/if}}{{/each}}cruel {{world}}!";
var hash = {goodbyes: [{text: "goodbye"}, {text: "Goodbye"}, {text: "GOODBYE"}], world: "world"};
it('each object with @first', function() {
var string = '{{#each goodbyes}}{{#if @first}}{{text}}! {{/if}}{{/each}}cruel {{world}}!';
var hash = {goodbyes: {'foo': {text: 'goodbye'}, bar: {text: 'Goodbye'}}, world: 'world'};
var template = CompilerContext.compile(string);
var result = template(hash);
equal(result, "GOODBYE! cruel world!", "The @last variable is used");
equal(result, 'goodbye! cruel world!', 'The @first variable is used');
});
it("each with nested @last", function() {
var string = "{{#each goodbyes}}({{#if @last}}{{text}}! {{/if}}{{#each ../goodbyes}}{{#if @last}}{{text}}!{{/if}}{{/each}}{{#if @last}} {{text}}!{{/if}}) {{/each}}cruel {{world}}!";
var hash = {goodbyes: [{text: "goodbye"}, {text: "Goodbye"}, {text: "GOODBYE"}], world: "world"};
it('each with @last', function() {
var string = '{{#each goodbyes}}{{#if @last}}{{text}}! {{/if}}{{/each}}cruel {{world}}!';
var hash = {goodbyes: [{text: 'goodbye'}, {text: 'Goodbye'}, {text: 'GOODBYE'}], world: 'world'};
var template = CompilerContext.compile(string);
var result = template(hash);
equal(result, "(GOODBYE!) (GOODBYE!) (GOODBYE! GOODBYE! GOODBYE!) cruel world!", "The @last variable is used");
equal(result, 'GOODBYE! cruel world!', 'The @last variable is used');
});
it("each with function argument", function() {
var string = "{{#each goodbyes}}{{text}}! {{/each}}cruel {{world}}!";
var hash = {goodbyes: function () { return [{text: "goodbye"}, {text: "Goodbye"}, {text: "GOODBYE"}];}, world: "world"};
shouldCompileTo(string, hash, "goodbye! Goodbye! GOODBYE! cruel world!",
"each with array function argument iterates over the contents when not empty");
shouldCompileTo(string, {goodbyes: [], world: "world"}, "cruel world!",
"each with array function argument ignores the contents when empty");
it('each object with @last', function() {
var string = '{{#each goodbyes}}{{#if @last}}{{text}}! {{/if}}{{/each}}cruel {{world}}!';
var hash = {goodbyes: {'foo': {text: 'goodbye'}, bar: {text: 'Goodbye'}}, world: 'world'};
var template = CompilerContext.compile(string);
var result = template(hash);
equal(result, 'Goodbye! cruel world!', 'The @last variable is used');
});
it("data passed to helpers", function() {
var string = "{{#each letters}}{{this}}{{detectDataInsideEach}}{{/each}}";
it('each with nested @last', function() {
var string = '{{#each goodbyes}}({{#if @last}}{{text}}! {{/if}}{{#each ../goodbyes}}{{#if @last}}{{text}}!{{/if}}{{/each}}{{#if @last}} {{text}}!{{/if}}) {{/each}}cruel {{world}}!';
var hash = {goodbyes: [{text: 'goodbye'}, {text: 'Goodbye'}, {text: 'GOODBYE'}], world: 'world'};
var template = CompilerContext.compile(string);
var result = template(hash);
equal(result, '(GOODBYE!) (GOODBYE!) (GOODBYE! GOODBYE! GOODBYE!) cruel world!', 'The @last variable is used');
});
it('each with function argument', function() {
var string = '{{#each goodbyes}}{{text}}! {{/each}}cruel {{world}}!';
var hash = {goodbyes: function() { return [{text: 'goodbye'}, {text: 'Goodbye'}, {text: 'GOODBYE'}]; }, world: 'world'};
shouldCompileTo(string, hash, 'goodbye! Goodbye! GOODBYE! cruel world!',
'each with array function argument iterates over the contents when not empty');
shouldCompileTo(string, {goodbyes: [], world: 'world'}, 'cruel world!',
'each with array function argument ignores the contents when empty');
});
it('data passed to helpers', function() {
var string = '{{#each letters}}{{this}}{{detectDataInsideEach}}{{/each}}';
var hash = {letters: ['a', 'b', 'c']};
var template = CompilerContext.compile(string);
@@ -205,96 +223,108 @@ describe('builtin helpers', function() {
equal(result, 'a!b!c!', 'should output data');
});
it("each on implicit context", function() {
it('each on implicit context', function() {
shouldThrow(function() {
var template = CompilerContext.compile("{{#each}}{{text}}! {{/each}}cruel world!");
var template = CompilerContext.compile('{{#each}}{{text}}! {{/each}}cruel world!');
template({});
}, handlebarsEnv.Exception, 'Must pass iterator to #each');
});
});
describe("#log", function() {
describe('#log', function() {
/*eslint-disable no-console */
if (typeof console === 'undefined') {
return;
}
var info,
error;
var $log,
$info,
$error;
beforeEach(function() {
info = console.info;
error = console.error;
$log = console.log;
$info = console.info;
$error = console.error;
});
afterEach(function() {
console.info = info;
console.error = error;
console.log = $log;
console.info = $info;
console.error = $error;
});
it('should call logger at default level', function() {
var string = "{{log blah}}";
var hash = { blah: "whee" };
var string = '{{log blah}}';
var hash = { blah: 'whee' };
var levelArg, logArg;
handlebarsEnv.log = function(level, arg){
handlebarsEnv.log = function(level, arg) {
levelArg = level;
logArg = arg;
};
shouldCompileTo(string, hash, "", "log should not display");
equals(1, levelArg, "should call log with 1");
equals("whee", logArg, "should call log with 'whee'");
shouldCompileTo(string, hash, '', 'log should not display');
equals(1, levelArg, 'should call log with 1');
equals('whee', logArg, "should call log with 'whee'");
});
it('should call logger at data level', function() {
var string = "{{log blah}}";
var hash = { blah: "whee" };
var string = '{{log blah}}';
var hash = { blah: 'whee' };
var levelArg, logArg;
handlebarsEnv.log = function(level, arg){
handlebarsEnv.log = function(level, arg) {
levelArg = level;
logArg = arg;
};
shouldCompileTo(string, [hash,,,,{level: '03'}], "");
shouldCompileTo(string, [hash,,,, {level: '03'}], '');
equals(3, levelArg);
equals("whee", logArg);
equals('whee', logArg);
});
it('should not output to console', function() {
var string = "{{log blah}}";
var hash = { blah: "whee" };
console.info = function() {
throw new Error();
};
shouldCompileTo(string, hash, "", "log should not display");
});
it('should log at data level', function() {
var string = "{{log blah}}";
var hash = { blah: "whee" };
it('should output to info', function() {
var string = '{{log blah}}';
var hash = { blah: 'whee' };
var called;
console.error = function(log) {
equals("whee", log);
console.info = function(info) {
equals('whee', info);
called = true;
};
console.log = function(log) {
equals('whee', log);
called = true;
};
shouldCompileTo(string, [hash,,,,{level: '03'}], "");
shouldCompileTo(string, hash, '');
equals(true, called);
});
it('should log at data level', function() {
var string = '{{log blah}}';
var hash = { blah: 'whee' };
var called;
console.error = function(log) {
equals('whee', log);
called = true;
};
shouldCompileTo(string, [hash,,,, {level: '03'}], '');
equals(true, called);
});
it('should handle missing logger', function() {
var string = "{{log blah}}";
var hash = { blah: "whee" };
var string = '{{log blah}}';
var hash = { blah: 'whee' };
console.error = undefined;
shouldCompileTo(string, [hash,,,,{level: '03'}], "");
shouldCompileTo(string, [hash,,,, {level: '03'}], '');
});
/*eslint-enable no-console */
});
describe('#lookup', function() {
it('should lookup arbitrary content', function() {
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 result = template(hash);
@@ -303,7 +333,7 @@ describe('builtin helpers', function() {
});
it('should not fail on undefined value', function() {
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 result = template(hash);
+4 -6
View File
@@ -1,5 +1,3 @@
/*global Handlebars, shouldThrow */
describe('compiler', function() {
if (!Handlebars.compile) {
return;
@@ -41,10 +39,10 @@ describe('compiler', function() {
});
it('can utilize AST instance', function() {
equal(Handlebars.compile(new Handlebars.AST.ProgramNode([ new Handlebars.AST.ContentNode("Hello")], {}))(), 'Hello');
equal(Handlebars.compile(new Handlebars.AST.Program([ new Handlebars.AST.ContentStatement('Hello')], null, {}))(), 'Hello');
});
it("can pass through an empty string", function() {
it('can pass through an empty string', function() {
equal(Handlebars.compile('')(), '');
});
});
@@ -60,10 +58,10 @@ describe('compiler', function() {
});
it('can utilize AST instance', function() {
equal(/return "Hello"/.test(Handlebars.precompile(new Handlebars.AST.ProgramNode([ new Handlebars.AST.ContentNode("Hello")]), {})), true);
equal(/return "Hello"/.test(Handlebars.precompile(new Handlebars.AST.Program([ new Handlebars.AST.ContentStatement('Hello')]), null, {})), true);
});
it("can pass through an empty string", function() {
it('can pass through an empty string', function() {
equal(/return ""/.test(Handlebars.precompile('')), true);
});
});
+94 -80
View File
@@ -1,25 +1,24 @@
/*global CompilerContext, Handlebars, handlebarsEnv, shouldThrow */
describe('data', function() {
it("passing in data to a compiled function that expects data - works with helpers", function() {
var template = CompilerContext.compile("{{hello}}", {data: true});
it('passing in data to a compiled function that expects data - works with helpers', function() {
var template = CompilerContext.compile('{{hello}}', {data: true});
var helpers = {
hello: function(options) {
return options.data.adjective + " " + this.noun;
return options.data.adjective + ' ' + this.noun;
}
};
var result = template({noun: "cat"}, {helpers: helpers, data: {adjective: "happy"}});
equals("happy cat", result, "Data output by helper");
var result = template({noun: 'cat'}, {helpers: helpers, data: {adjective: 'happy'}});
equals('happy cat', result, 'Data output by helper');
});
it("data can be looked up via @foo", function() {
var template = CompilerContext.compile("{{@hello}}");
var result = template({}, { data: { hello: "hello" } });
equals("hello", result, "@foo retrieves template data");
it('data can be looked up via @foo', function() {
var template = CompilerContext.compile('{{@hello}}');
var result = template({}, { data: { hello: 'hello' } });
equals('hello', result, '@foo retrieves template data');
});
it("deep @foo triggers automatic top-level data", function() {
it('deep @foo triggers automatic top-level data', function() {
var template = CompilerContext.compile('{{#let world="world"}}{{#if foo}}{{#if foo}}Hello {{@world}}{{/if}}{{/if}}{{/let}}');
var helpers = Handlebars.createFrame(handlebarsEnv.helpers);
@@ -28,196 +27,211 @@ describe('data', function() {
var frame = Handlebars.createFrame(options.data);
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 });
};
var result = template({ foo: true }, { helpers: helpers });
equals("Hello world", result, "Automatic data was triggered");
equals('Hello world', result, 'Automatic data was triggered');
});
it("parameter data can be looked up via @foo", function() {
var template = CompilerContext.compile("{{hello @world}}");
it('parameter data can be looked up via @foo', function() {
var template = CompilerContext.compile('{{hello @world}}');
var helpers = {
hello: function(noun) {
return "Hello " + noun;
return 'Hello ' + noun;
}
};
var result = template({}, { helpers: helpers, data: { world: "world" } });
equals("Hello world", result, "@foo as a parameter retrieves template data");
var result = template({}, { helpers: helpers, data: { world: 'world' } });
equals('Hello world', result, '@foo as a parameter retrieves template data');
});
it("hash values can be looked up via @foo", function() {
var template = CompilerContext.compile("{{hello noun=@world}}");
it('hash values can be looked up via @foo', function() {
var template = CompilerContext.compile('{{hello noun=@world}}');
var helpers = {
hello: function(options) {
return "Hello " + options.hash.noun;
return 'Hello ' + options.hash.noun;
}
};
var result = template({}, { helpers: helpers, data: { world: "world" } });
equals("Hello world", result, "@foo as a parameter retrieves template data");
var result = template({}, { helpers: helpers, data: { world: 'world' } });
equals('Hello world', result, '@foo as a parameter retrieves template data');
});
it("nested parameter data can be looked up via @foo.bar", function() {
var template = CompilerContext.compile("{{hello @world.bar}}");
it('nested parameter data can be looked up via @foo.bar', function() {
var template = CompilerContext.compile('{{hello @world.bar}}');
var helpers = {
hello: function(noun) {
return "Hello " + noun;
return 'Hello ' + noun;
}
};
var result = template({}, { helpers: helpers, data: { world: {bar: "world" } } });
equals("Hello world", result, "@foo as a parameter retrieves template data");
var result = template({}, { helpers: helpers, data: { world: {bar: 'world' } } });
equals('Hello world', result, '@foo as a parameter retrieves template data');
});
it("nested parameter data does not fail with @world.bar", function() {
var template = CompilerContext.compile("{{hello @world.bar}}");
it('nested parameter data does not fail with @world.bar', function() {
var template = CompilerContext.compile('{{hello @world.bar}}');
var helpers = {
hello: function(noun) {
return "Hello " + noun;
return 'Hello ' + noun;
}
};
var result = template({}, { helpers: helpers, data: { foo: {bar: "world" } } });
equals("Hello undefined", result, "@foo as a parameter retrieves template data");
var result = template({}, { helpers: helpers, data: { foo: {bar: 'world' } } });
equals('Hello undefined', result, '@foo as a parameter retrieves template data');
});
it("parameter data throws when using complex scope references", function() {
var string = "{{#goodbyes}}{{text}} cruel {{@foo/../name}}! {{/goodbyes}}";
it('parameter data throws when using complex scope references', function() {
var string = '{{#goodbyes}}{{text}} cruel {{@foo/../name}}! {{/goodbyes}}';
shouldThrow(function() {
CompilerContext.compile(string);
}, Error);
});
it("data is inherited downstream", function() {
var template = CompilerContext.compile("{{#let foo=1 bar=2}}{{#let foo=bar.baz}}{{@bar}}{{@foo}}{{/let}}{{@foo}}{{/let}}", { data: true });
it('data can be functions', function() {
var template = CompilerContext.compile('{{@hello}}');
var result = template({}, { data: { hello: function() { return 'hello'; } } });
equals('hello', result);
});
it('data can be functions with params', function() {
var template = CompilerContext.compile('{{@hello "hello"}}');
var result = template({}, { data: { hello: function(arg) { return arg; } } });
equals('hello', result);
});
it('data is inherited downstream', function() {
var template = CompilerContext.compile('{{#let foo=1 bar=2}}{{#let foo=bar.baz}}{{@bar}}{{@foo}}{{/let}}{{@foo}}{{/let}}', { data: true });
var helpers = {
let: function(options) {
var frame = Handlebars.createFrame(options.data);
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});
}
};
var result = template({ bar: { baz: "hello world" } }, { helpers: helpers, data: {} });
equals("2hello world1", result, "data variables are inherited downstream");
var result = template({ bar: { baz: 'hello world' } }, { helpers: helpers, data: {} });
equals('2hello world1', result, 'data variables are inherited downstream');
});
it("passing in data to a compiled function that expects data - works with helpers in partials", function() {
var template = CompilerContext.compile("{{>my_partial}}", {data: true});
it('passing in data to a compiled function that expects data - works with helpers in partials', function() {
var template = CompilerContext.compile('{{>myPartial}}', {data: true});
var partials = {
my_partial: CompilerContext.compile("{{hello}}", {data: true})
myPartial: CompilerContext.compile('{{hello}}', {data: true})
};
var helpers = {
hello: function(options) {
return options.data.adjective + " " + this.noun;
return options.data.adjective + ' ' + this.noun;
}
};
var result = template({noun: "cat"}, {helpers: helpers, partials: partials, data: {adjective: "happy"}});
equals("happy cat", result, "Data output by helper inside partial");
var result = template({noun: 'cat'}, {helpers: helpers, partials: partials, data: {adjective: 'happy'}});
equals('happy cat', result, 'Data output by helper inside partial');
});
it("passing in data to a compiled function that expects data - works with helpers and parameters", function() {
var template = CompilerContext.compile("{{hello world}}", {data: true});
it('passing in data to a compiled function that expects data - works with helpers and parameters', function() {
var template = CompilerContext.compile('{{hello world}}', {data: true});
var helpers = {
hello: function(noun, options) {
return options.data.adjective + " " + noun + (this.exclaim ? "!" : "");
return options.data.adjective + ' ' + noun + (this.exclaim ? '!' : '');
}
};
var result = template({exclaim: true, world: "world"}, {helpers: helpers, data: {adjective: "happy"}});
equals("happy world!", result, "Data output by helper");
var result = template({exclaim: true, world: 'world'}, {helpers: helpers, data: {adjective: 'happy'}});
equals('happy world!', result, 'Data output by helper');
});
it("passing in data to a compiled function that expects data - works with block helpers", function() {
var template = CompilerContext.compile("{{#hello}}{{world}}{{/hello}}", {data: true});
it('passing in data to a compiled function that expects data - works with block helpers', function() {
var template = CompilerContext.compile('{{#hello}}{{world}}{{/hello}}', {data: true});
var helpers = {
hello: function(options) {
return options.fn(this);
},
world: function(options) {
return options.data.adjective + " world" + (this.exclaim ? "!" : "");
return options.data.adjective + ' world' + (this.exclaim ? '!' : '');
}
};
var result = template({exclaim: true}, {helpers: helpers, data: {adjective: "happy"}});
equals("happy world!", result, "Data output by helper");
var result = template({exclaim: true}, {helpers: helpers, data: {adjective: 'happy'}});
equals('happy world!', result, 'Data output by helper');
});
it("passing in data to a compiled function that expects data - works with block helpers that use ..", function() {
var template = CompilerContext.compile("{{#hello}}{{world ../zomg}}{{/hello}}", {data: true});
it('passing in data to a compiled function that expects data - works with block helpers that use ..', function() {
var template = CompilerContext.compile('{{#hello}}{{world ../zomg}}{{/hello}}', {data: true});
var helpers = {
hello: function(options) {
return options.fn({exclaim: "?"});
return options.fn({exclaim: '?'});
},
world: function(thing, options) {
return options.data.adjective + " " + thing + (this.exclaim || "");
return options.data.adjective + ' ' + thing + (this.exclaim || '');
}
};
var result = template({exclaim: true, zomg: "world"}, {helpers: helpers, data: {adjective: "happy"}});
equals("happy world?", result, "Data output by helper");
var result = template({exclaim: true, zomg: 'world'}, {helpers: helpers, data: {adjective: 'happy'}});
equals('happy world?', result, 'Data output by helper');
});
it("passing in data to a compiled function that expects data - data is passed to with block helpers where children use ..", function() {
var template = CompilerContext.compile("{{#hello}}{{world ../zomg}}{{/hello}}", {data: true});
it('passing in data to a compiled function that expects data - data is passed to with block helpers where children use ..', function() {
var template = CompilerContext.compile('{{#hello}}{{world ../zomg}}{{/hello}}', {data: true});
var helpers = {
hello: function(options) {
return options.data.accessData + " " + options.fn({exclaim: "?"});
return options.data.accessData + ' ' + options.fn({exclaim: '?'});
},
world: function(thing, options) {
return options.data.adjective + " " + thing + (this.exclaim || "");
return options.data.adjective + ' ' + thing + (this.exclaim || '');
}
};
var result = template({exclaim: true, zomg: "world"}, {helpers: helpers, data: {adjective: "happy", accessData: "#win"}});
equals("#win happy world?", result, "Data output by helper");
var result = template({exclaim: true, zomg: 'world'}, {helpers: helpers, data: {adjective: 'happy', accessData: '#win'}});
equals('#win happy world?', result, 'Data output by helper');
});
it("you can override inherited data when invoking a helper", function() {
var template = CompilerContext.compile("{{#hello}}{{world zomg}}{{/hello}}", {data: true});
it('you can override inherited data when invoking a helper', function() {
var template = CompilerContext.compile('{{#hello}}{{world zomg}}{{/hello}}', {data: true});
var helpers = {
hello: function(options) {
return options.fn({exclaim: "?", zomg: "world"}, { data: {adjective: "sad"} });
return options.fn({exclaim: '?', zomg: 'world'}, { data: {adjective: 'sad'} });
},
world: function(thing, options) {
return options.data.adjective + " " + thing + (this.exclaim || "");
return options.data.adjective + ' ' + thing + (this.exclaim || '');
}
};
var result = template({exclaim: true, zomg: "planet"}, {helpers: helpers, data: {adjective: "happy"}});
equals("sad world?", result, "Overriden data output by helper");
var result = template({exclaim: true, zomg: 'planet'}, {helpers: helpers, data: {adjective: 'happy'}});
equals('sad world?', result, 'Overriden data output by helper');
});
it("you can override inherited data when invoking a helper with depth", function() {
var template = CompilerContext.compile("{{#hello}}{{world ../zomg}}{{/hello}}", {data: true});
it('you can override inherited data when invoking a helper with depth', function() {
var template = CompilerContext.compile('{{#hello}}{{world ../zomg}}{{/hello}}', {data: true});
var helpers = {
hello: function(options) {
return options.fn({exclaim: "?"}, { data: {adjective: "sad"} });
return options.fn({exclaim: '?'}, { data: {adjective: 'sad'} });
},
world: function(thing, options) {
return options.data.adjective + " " + thing + (this.exclaim || "");
return options.data.adjective + ' ' + thing + (this.exclaim || '');
}
};
var result = template({exclaim: true, zomg: "world"}, {helpers: helpers, data: {adjective: "happy"}});
equals("sad world?", result, "Overriden data output by helper");
var result = template({exclaim: true, zomg: 'world'}, {helpers: helpers, data: {adjective: 'happy'}});
equals('sad world?', result, 'Overriden data output by helper');
});
describe('@root', function() {
+6 -4
View File
@@ -1,14 +1,14 @@
/*global handlebarsEnv */
require('./common');
var _ = require('underscore'),
fs = require('fs'),
var fs = require('fs'),
vm = require('vm');
global.Handlebars = undefined;
global.Handlebars = 'no-conflict';
vm.runInThisContext(fs.readFileSync(__dirname + '/../../dist/handlebars.js'), 'dist/handlebars.js');
global.CompilerContext = {
browser: true,
compile: function(template, options) {
var templateSpec = handlebarsEnv.precompile(template, options);
return handlebarsEnv.template(safeEval(templateSpec));
@@ -19,10 +19,12 @@ global.CompilerContext = {
};
function safeEval(templateSpec) {
/*eslint-disable no-eval, no-console */
try {
return eval('(' + templateSpec + ')');
} catch (err) {
console.error(templateSpec);
throw err;
}
/*eslint-enable no-eval, no-console */
}
+4 -5
View File
@@ -1,4 +1,3 @@
/*global CompilerContext, compileWithPartials, shouldCompileToWithPartials */
global.shouldCompileTo = function(string, hashOrArray, expected, message) {
shouldCompileToWithPartials(string, hashOrArray, false, expected, message);
};
@@ -14,11 +13,11 @@ global.compileWithPartials = function(string, hashOrArray, partials) {
var template,
ary,
options;
if(Object.prototype.toString.call(hashOrArray) === "[object Array]") {
if (Object.prototype.toString.call(hashOrArray) === '[object Array]') {
ary = [];
ary.push(hashOrArray[0]);
ary.push({ helpers: hashOrArray[1], partials: hashOrArray[2] });
options = {compat: hashOrArray[3]};
options = typeof hashOrArray[3] === 'object' ? hashOrArray[3] : {compat: hashOrArray[3]};
if (hashOrArray[4] != null) {
options.data = !!hashOrArray[4];
ary[1].data = hashOrArray[4];
@@ -34,7 +33,7 @@ global.compileWithPartials = function(string, hashOrArray, partials) {
global.equals = global.equal = function(a, b, msg) {
if (a !== b) {
throw new Error("'" + a + "' should === '" + b + "'" + (msg ? ": " + msg : ''));
throw new Error("'" + a + "' should === '" + b + "'" + (msg ? ': ' + msg : ''));
}
};
@@ -45,7 +44,7 @@ global.shouldThrow = function(callback, type, msg) {
failed = true;
} catch (err) {
if (type && !(err instanceof type)) {
throw new Error('Type failure');
throw new Error('Type failure: ' + err);
}
if (msg && !(msg.test ? msg.test(err.message) : msg === err.message)) {
equal(msg, err.message);
+2 -1
View File
@@ -1,4 +1,3 @@
/*global handlebarsEnv */
require('./common');
global.Handlebars = require('../../lib');
@@ -14,10 +13,12 @@ global.CompilerContext = {
};
function safeEval(templateSpec) {
/*eslint-disable no-eval, no-console */
try {
return eval('(' + templateSpec + ')');
} catch (err) {
console.error(templateSpec);
throw err;
}
/*eslint-enable no-eval, no-console */
}
+4 -1
View File
@@ -1,3 +1,4 @@
/*eslint-disable no-console */
var fs = require('fs'),
Mocha = require('mocha'),
path = require('path');
@@ -6,7 +7,7 @@ var errors = 0,
testDir = path.dirname(__dirname),
grep = process.argv[2];
var files = [ testDir + "/basic.js" ];
var files = [ testDir + '/basic.js' ];
var files = fs.readdirSync(testDir)
.filter(function(name) { return (/.*\.js$/).test(name); })
@@ -15,7 +16,9 @@ var files = fs.readdirSync(testDir)
run('./node', function() {
run('./browser', function() {
run('./runtime', function() {
/*eslint-disable no-process-exit */
process.exit(errors);
/*eslint-enable no-process-exit */
});
});
});
+11 -9
View File
@@ -1,22 +1,22 @@
/*global handlebarsEnv */
require('./common');
var _ = require('underscore'),
fs = require('fs'),
var fs = require('fs'),
vm = require('vm');
global.Handlebars = undefined;
global.Handlebars = 'no-conflict';
vm.runInThisContext(fs.readFileSync(__dirname + '/../../dist/handlebars.runtime.js'), 'dist/handlebars.runtime.js');
var parse = require('../../dist/cjs/handlebars/compiler/base').parse;
var compiler = require('../../dist/cjs/handlebars/compiler/compiler');
var JavaScriptCompiler = require('../../dist/cjs/handlebars/compiler/javascript-compiler')['default'];
var JavaScriptCompiler = require('../../dist/cjs/handlebars/compiler/javascript-compiler');
global.CompilerContext = {
browser: true,
compile: function(template, options) {
// Hack the compiler on to the environment for these specific tests
handlebarsEnv.precompile = function(template, options) {
return compiler.precompile(template, options, handlebarsEnv);
handlebarsEnv.precompile = function(precompileTemplate, precompileOptions) {
return compiler.precompile(precompileTemplate, precompileOptions, handlebarsEnv);
};
handlebarsEnv.parse = parse;
handlebarsEnv.Compiler = compiler.Compiler;
@@ -27,8 +27,8 @@ global.CompilerContext = {
},
compileWithPartial: function(template, options) {
// Hack the compiler on to the environment for these specific tests
handlebarsEnv.compile = function(template, options) {
return compiler.compile(template, options, handlebarsEnv);
handlebarsEnv.compile = function(compileTemplate, compileOptions) {
return compiler.compile(compileTemplate, compileOptions, handlebarsEnv);
};
handlebarsEnv.parse = parse;
handlebarsEnv.Compiler = compiler.Compiler;
@@ -39,10 +39,12 @@ global.CompilerContext = {
};
function safeEval(templateSpec) {
/*eslint-disable no-eval, no-console */
try {
return eval('(' + templateSpec + ')');
} catch (err) {
console.error(templateSpec);
throw err;
}
/*eslint-enable no-eval, no-console */
}
+1 -1
View File
@@ -1,6 +1,6 @@
define(['handlebars.runtime'], function(Handlebars) {
Handlebars = Handlebars["default"]; var template = Handlebars.template, templates = Handlebars.templates = Handlebars.templates || {};
return templates['empty'] = template({"compiler":[6,">= 2.0.0-beta.1"],"main":function(depth0,helpers,partials,data) {
return "";
return "";
},"useData":true});
});
+328 -267
View File
@@ -1,161 +1,164 @@
/*global CompilerContext, Handlebars, shouldCompileTo, shouldCompileToWithPartials, shouldThrow, handlebarsEnv */
describe('helpers', function() {
it("helper with complex lookup$", function() {
var string = "{{#goodbyes}}{{{link ../prefix}}}{{/goodbyes}}";
var hash = {prefix: "/root", goodbyes: [{text: "Goodbye", url: "goodbye"}]};
it('helper with complex lookup$', function() {
var string = '{{#goodbyes}}{{{link ../prefix}}}{{/goodbyes}}';
var hash = {prefix: '/root', goodbyes: [{text: 'Goodbye', url: 'goodbye'}]};
var helpers = {link: function(prefix) {
return "<a href='" + prefix + "/" + this.url + "'>" + this.text + "</a>";
return '<a href="' + prefix + '/' + this.url + '">' + this.text + '</a>';
}};
shouldCompileTo(string, [hash, helpers], "<a href='/root/goodbye'>Goodbye</a>");
shouldCompileTo(string, [hash, helpers], '<a href="/root/goodbye">Goodbye</a>');
});
it("helper for raw block gets raw content", function() {
var string = "{{{{raw}}}} {{test}} {{{{/raw}}}}";
var hash = { test: "hello" };
it('helper for raw block gets raw content', function() {
var string = '{{{{raw}}}} {{test}} {{{{/raw}}}}';
var hash = { test: 'hello' };
var helpers = { raw: function(options) {
return options.fn();
} };
shouldCompileTo(string, [hash, helpers], " {{test}} ",
"raw block helper gets raw content");
shouldCompileTo(string, [hash, helpers], ' {{test}} ',
'raw block helper gets raw content');
});
it("helper for raw block gets parameters", function() {
var string = "{{{{raw 1 2 3}}}} {{test}} {{{{/raw}}}}";
var hash = { test: "hello" };
it('helper for raw block gets parameters', function() {
var string = '{{{{raw 1 2 3}}}} {{test}} {{{{/raw}}}}';
var hash = { test: 'hello' };
var helpers = { raw: function(a, b, c, options) {
return options.fn() + a + b + c;
} };
shouldCompileTo(string, [hash, helpers], " {{test}} 123",
"raw block helper gets raw content");
shouldCompileTo(string, [hash, helpers], ' {{test}} 123',
'raw block helper gets raw content');
});
it("helper block with complex lookup expression", function() {
var string = "{{#goodbyes}}{{../name}}{{/goodbyes}}";
var hash = {name: "Alan"};
it('helper block with complex lookup expression', function() {
var string = '{{#goodbyes}}{{../name}}{{/goodbyes}}';
var hash = {name: 'Alan'};
var helpers = {goodbyes: function(options) {
var out = "";
var byes = ["Goodbye", "goodbye", "GOODBYE"];
for (var i = 0,j = byes.length; i < j; i++) {
out += byes[i] + " " + options.fn(this) + "! ";
var out = '';
var byes = ['Goodbye', 'goodbye', 'GOODBYE'];
for (var i = 0, j = byes.length; i < j; i++) {
out += byes[i] + ' ' + options.fn(this) + '! ';
}
return out;
}};
shouldCompileTo(string, [hash, helpers], "Goodbye Alan! goodbye Alan! GOODBYE Alan! ");
shouldCompileTo(string, [hash, helpers], 'Goodbye Alan! goodbye Alan! GOODBYE Alan! ');
});
it("helper with complex lookup and nested template", function() {
var string = "{{#goodbyes}}{{#link ../prefix}}{{text}}{{/link}}{{/goodbyes}}";
var hash = {prefix: '/root', goodbyes: [{text: "Goodbye", url: "goodbye"}]};
var helpers = {link: function (prefix, options) {
return "<a href='" + prefix + "/" + this.url + "'>" + options.fn(this) + "</a>";
it('helper with complex lookup and nested template', function() {
var string = '{{#goodbyes}}{{#link ../prefix}}{{text}}{{/link}}{{/goodbyes}}';
var hash = {prefix: '/root', goodbyes: [{text: 'Goodbye', url: 'goodbye'}]};
var helpers = {link: function(prefix, options) {
return '<a href="' + prefix + '/' + this.url + '">' + options.fn(this) + '</a>';
}};
shouldCompileToWithPartials(string, [hash, helpers], false, "<a href='/root/goodbye'>Goodbye</a>");
shouldCompileToWithPartials(string, [hash, helpers], false, '<a href="/root/goodbye">Goodbye</a>');
});
it("helper with complex lookup and nested template in VM+Compiler", function() {
var string = "{{#goodbyes}}{{#link ../prefix}}{{text}}{{/link}}{{/goodbyes}}";
var hash = {prefix: '/root', goodbyes: [{text: "Goodbye", url: "goodbye"}]};
var helpers = {link: function (prefix, options) {
return "<a href='" + prefix + "/" + this.url + "'>" + options.fn(this) + "</a>";
it('helper with complex lookup and nested template in VM+Compiler', function() {
var string = '{{#goodbyes}}{{#link ../prefix}}{{text}}{{/link}}{{/goodbyes}}';
var hash = {prefix: '/root', goodbyes: [{text: 'Goodbye', url: 'goodbye'}]};
var helpers = {link: function(prefix, options) {
return '<a href="' + prefix + '/' + this.url + '">' + options.fn(this) + '</a>';
}};
shouldCompileToWithPartials(string, [hash, helpers], true, "<a href='/root/goodbye'>Goodbye</a>");
shouldCompileToWithPartials(string, [hash, helpers], true, '<a href="/root/goodbye">Goodbye</a>');
});
it('helper returning undefined value', function() {
shouldCompileTo(' {{nothere}}', [{}, {nothere: function() {}}], ' ');
shouldCompileTo(' {{#nothere}}{{/nothere}}', [{}, {nothere: function() {}}], ' ');
});
it("block helper", function() {
var string = "{{#goodbyes}}{{text}}! {{/goodbyes}}cruel {{world}}!";
it('block helper', function() {
var string = '{{#goodbyes}}{{text}}! {{/goodbyes}}cruel {{world}}!';
var template = CompilerContext.compile(string);
var result = template({world: "world"}, { helpers: {goodbyes: function(options) { return options.fn({text: "GOODBYE"}); }}});
equal(result, "GOODBYE! cruel world!", "Block helper executed");
var result = template({world: 'world'}, { helpers: {goodbyes: function(options) { return options.fn({text: 'GOODBYE'}); }}});
equal(result, 'GOODBYE! cruel world!', 'Block helper executed');
});
it("block helper staying in the same context", function() {
var string = "{{#form}}<p>{{name}}</p>{{/form}}";
it('block helper staying in the same context', function() {
var string = '{{#form}}<p>{{name}}</p>{{/form}}';
var template = CompilerContext.compile(string);
var result = template({name: "Yehuda"}, {helpers: {form: function(options) { return "<form>" + options.fn(this) + "</form>"; } }});
equal(result, "<form><p>Yehuda</p></form>", "Block helper executed with current context");
var result = template({name: 'Yehuda'}, {helpers: {form: function(options) { return '<form>' + options.fn(this) + '</form>'; } }});
equal(result, '<form><p>Yehuda</p></form>', 'Block helper executed with current context');
});
it("block helper should have context in this", function() {
var source = "<ul>{{#people}}<li>{{#link}}{{name}}{{/link}}</li>{{/people}}</ul>";
var link = function(options) {
it('block helper should have context in this', function() {
var source = '<ul>{{#people}}<li>{{#link}}{{name}}{{/link}}</li>{{/people}}</ul>';
function link(options) {
return '<a href="/people/' + this.id + '">' + options.fn(this) + '</a>';
};
var data = { "people": [
{ "name": "Alan", "id": 1 },
{ "name": "Yehuda", "id": 2 }
}
var data = { 'people': [
{ 'name': 'Alan', 'id': 1 },
{ 'name': 'Yehuda', 'id': 2 }
]};
shouldCompileTo(source, [data, {link: link}], "<ul><li><a href=\"/people/1\">Alan</a></li><li><a href=\"/people/2\">Yehuda</a></li></ul>");
shouldCompileTo(source, [data, {link: link}], '<ul><li><a href=\"/people/1\">Alan</a></li><li><a href=\"/people/2\">Yehuda</a></li></ul>');
});
it("block helper for undefined value", function() {
shouldCompileTo("{{#empty}}shouldn't render{{/empty}}", {}, "");
it('block helper for undefined value', function() {
shouldCompileTo("{{#empty}}shouldn't render{{/empty}}", {}, '');
});
it("block helper passing a new context", function() {
var string = "{{#form yehuda}}<p>{{name}}</p>{{/form}}";
it('block helper passing a new context', function() {
var string = '{{#form yehuda}}<p>{{name}}</p>{{/form}}';
var template = CompilerContext.compile(string);
var result = template({yehuda: {name: "Yehuda"}}, { helpers: {form: function(context, options) { return "<form>" + options.fn(context) + "</form>"; }}});
equal(result, "<form><p>Yehuda</p></form>", "Context variable resolved");
var result = template({yehuda: {name: 'Yehuda'}}, { helpers: {form: function(context, options) { return '<form>' + options.fn(context) + '</form>'; }}});
equal(result, '<form><p>Yehuda</p></form>', 'Context variable resolved');
});
it("block helper passing a complex path context", function() {
var string = "{{#form yehuda/cat}}<p>{{name}}</p>{{/form}}";
it('block helper passing a complex path context', function() {
var string = '{{#form yehuda/cat}}<p>{{name}}</p>{{/form}}';
var template = CompilerContext.compile(string);
var result = template({yehuda: {name: "Yehuda", cat: {name: "Harold"}}}, { helpers: {form: function(context, options) { return "<form>" + options.fn(context) + "</form>"; }}});
equal(result, "<form><p>Harold</p></form>", "Complex path variable resolved");
var result = template({yehuda: {name: 'Yehuda', cat: {name: 'Harold'}}}, { helpers: {form: function(context, options) { return '<form>' + options.fn(context) + '</form>'; }}});
equal(result, '<form><p>Harold</p></form>', 'Complex path variable resolved');
});
it("nested block helpers", function() {
var string = "{{#form yehuda}}<p>{{name}}</p>{{#link}}Hello{{/link}}{{/form}}";
it('nested block helpers', function() {
var string = '{{#form yehuda}}<p>{{name}}</p>{{#link}}Hello{{/link}}{{/form}}';
var template = CompilerContext.compile(string);
var result = template({
yehuda: {name: "Yehuda" }
yehuda: {name: 'Yehuda' }
}, {
helpers: {
link: function(options) { return "<a href='" + this.name + "'>" + options.fn(this) + "</a>"; },
form: function(context, options) { return "<form>" + options.fn(context) + "</form>"; }
link: function(options) { return '<a href="' + this.name + '">' + options.fn(this) + '</a>'; },
form: function(context, options) { return '<form>' + options.fn(context) + '</form>'; }
}
});
equal(result, "<form><p>Yehuda</p><a href='Yehuda'>Hello</a></form>", "Both blocks executed");
equal(result, '<form><p>Yehuda</p><a href="Yehuda">Hello</a></form>', 'Both blocks executed');
});
it("block helper inverted sections", function() {
var string = "{{#list people}}{{name}}{{^}}<em>Nobody's here</em>{{/list}}";
var list = function(context, options) {
it('block helper inverted sections', function() {
var string = '{{#list people}}{{name}}{{^}}<em>Nobody\'s here</em>{{/list}}';
function list(context, options) {
if (context.length > 0) {
var out = "<ul>";
for(var i = 0,j=context.length; i < j; i++) {
out += "<li>";
var out = '<ul>';
for (var i = 0, j = context.length; i < j; i++) {
out += '<li>';
out += options.fn(context[i]);
out += "</li>";
out += '</li>';
}
out += "</ul>";
out += '</ul>';
return out;
} else {
return "<p>" + options.inverse(this) + "</p>";
return '<p>' + options.inverse(this) + '</p>';
}
};
}
var hash = {people: [{name: "Alan"}, {name: "Yehuda"}]};
var hash = {people: [{name: 'Alan'}, {name: 'Yehuda'}]};
var empty = {people: []};
var rootMessage = {
people: [],
message: "Nobody's here"
message: 'Nobody\'s here'
};
var messageString = "{{#list people}}Hello{{^}}{{message}}{{/list}}";
var messageString = '{{#list people}}Hello{{^}}{{message}}{{/list}}';
// the meaning here may be kind of hard to catch, but list.not is always called,
// so we should see the output of both
shouldCompileTo(string, [hash, { list: list }], "<ul><li>Alan</li><li>Yehuda</li></ul>", "an inverse wrapper is passed in as a new context");
shouldCompileTo(string, [empty, { list: list }], "<p><em>Nobody's here</em></p>", "an inverse wrapper can be optionally called");
shouldCompileTo(messageString, [rootMessage, { list: list }], "<p>Nobody&#x27;s here</p>", "the context of an inverse is the parent of the block");
shouldCompileTo(string, [hash, { list: list }], '<ul><li>Alan</li><li>Yehuda</li></ul>', 'an inverse wrapper is passed in as a new context');
shouldCompileTo(string, [empty, { list: list }], '<p><em>Nobody\'s here</em></p>', 'an inverse wrapper can be optionally called');
shouldCompileTo(messageString, [rootMessage, { list: list }], '<p>Nobody&#x27;s here</p>', 'the context of an inverse is the parent of the block');
});
it('pathed lambas with parameters', function() {
@@ -174,43 +177,47 @@ describe('helpers', function() {
shouldCompileTo('{{hash/helper 1}}', [hash, helpers], 'winning');
});
describe("helpers hash", function() {
it("providing a helpers hash", function() {
shouldCompileTo("Goodbye {{cruel}} {{world}}!", [{cruel: "cruel"}, {world: function() { return "world"; }}], "Goodbye cruel world!",
"helpers hash is available");
describe('helpers hash', function() {
it('providing a helpers hash', function() {
shouldCompileTo('Goodbye {{cruel}} {{world}}!', [{cruel: 'cruel'}, {world: function() { return 'world'; }}], 'Goodbye cruel world!',
'helpers hash is available');
shouldCompileTo("Goodbye {{#iter}}{{cruel}} {{world}}{{/iter}}!", [{iter: [{cruel: "cruel"}]}, {world: function() { return "world"; }}],
"Goodbye cruel world!", "helpers hash is available inside other blocks");
shouldCompileTo('Goodbye {{#iter}}{{cruel}} {{world}}{{/iter}}!', [{iter: [{cruel: 'cruel'}]}, {world: function() { return 'world'; }}],
'Goodbye cruel world!', 'helpers hash is available inside other blocks');
});
it("in cases of conflict, helpers win", function() {
shouldCompileTo("{{{lookup}}}", [{lookup: 'Explicit'}, {lookup: function() { return 'helpers'; }}], "helpers",
"helpers hash has precedence escaped expansion");
shouldCompileTo("{{lookup}}", [{lookup: 'Explicit'}, {lookup: function() { return 'helpers'; }}], "helpers",
"helpers hash has precedence simple expansion");
it('in cases of conflict, helpers win', function() {
shouldCompileTo('{{{lookup}}}', [{lookup: 'Explicit'}, {lookup: function() { return 'helpers'; }}], 'helpers',
'helpers hash has precedence escaped expansion');
shouldCompileTo('{{lookup}}', [{lookup: 'Explicit'}, {lookup: function() { return 'helpers'; }}], 'helpers',
'helpers hash has precedence simple expansion');
});
it("the helpers hash is available is nested contexts", function() {
shouldCompileTo("{{#outer}}{{#inner}}{{helper}}{{/inner}}{{/outer}}",
[{'outer': {'inner': {'unused':[]}}}, {'helper': function() { return 'helper'; }}], "helper",
"helpers hash is available in nested contexts.");
it('the helpers hash is available is nested contexts', function() {
shouldCompileTo(
'{{#outer}}{{#inner}}{{helper}}{{/inner}}{{/outer}}',
[
{'outer': {'inner': {'unused': []}}},
{'helper': function() { return 'helper'; }}
],
'helper',
'helpers hash is available in nested contexts.');
});
it("the helper hash should augment the global hash", function() {
it('the helper hash should augment the global hash', function() {
handlebarsEnv.registerHelper('test_helper', function() { return 'found it!'; });
shouldCompileTo(
"{{test_helper}} {{#if cruel}}Goodbye {{cruel}} {{world}}!{{/if}}", [
{cruel: "cruel"},
{world: function() { return "world!"; }}
'{{test_helper}} {{#if cruel}}Goodbye {{cruel}} {{world}}!{{/if}}', [
{cruel: 'cruel'},
{world: function() { return 'world!'; }}
],
"found it! Goodbye cruel world!!");
'found it! Goodbye cruel world!!');
});
});
describe('registration', function() {
it('unregisters', function() {
var helpers = handlebarsEnv.helpers;
handlebarsEnv.helpers = {};
handlebarsEnv.registerHelper('foo', function() {
@@ -226,134 +233,128 @@ describe('helpers', function() {
handlebarsEnv.registerHelper({
'if': helpers['if'],
world: function() { return "world!"; },
test_helper: function() { return 'found it!'; }
world: function() { return 'world!'; },
testHelper: function() { return 'found it!'; }
});
shouldCompileTo(
"{{test_helper}} {{#if cruel}}Goodbye {{cruel}} {{world}}!{{/if}}",
[{cruel: "cruel"}],
"found it! Goodbye cruel world!!");
'{{testHelper}} {{#if cruel}}Goodbye {{cruel}} {{world}}!{{/if}}',
[{cruel: 'cruel'}],
'found it! Goodbye cruel world!!');
});
it('fails with multiple and args', function() {
shouldThrow(function() {
handlebarsEnv.registerHelper({
world: function() { return "world!"; },
test_helper: function() { return 'found it!'; }
world: function() { return 'world!'; },
testHelper: function() { return 'found it!'; }
}, {});
}, Error, 'Arg not supported with multiple helpers');
});
});
it("decimal number literals work", function() {
var string = 'Message: {{hello -1.2 1.2}}';
var hash = {};
var helpers = {hello: function(times, times2) {
if(typeof times !== 'number') { times = "NaN"; }
if(typeof times2 !== 'number') { times2 = "NaN"; }
return "Hello " + times + " " + times2 + " times";
it('decimal number literals work', function() {
var string = 'Message: {{hello -1.2 1.2}}';
var helpers = {hello: function(times, times2) {
if (typeof times !== 'number') { times = 'NaN'; }
if (typeof times2 !== 'number') { times2 = 'NaN'; }
return 'Hello ' + times + ' ' + times2 + ' times';
}};
shouldCompileTo(string, [hash, helpers], "Message: Hello -1.2 1.2 times", "template with a negative integer literal");
shouldCompileTo(string, [{}, helpers], 'Message: Hello -1.2 1.2 times', 'template with a negative integer literal');
});
it("negative number literals work", function() {
var string = 'Message: {{hello -12}}';
var hash = {};
var helpers = {hello: function(times) {
if(typeof times !== 'number') { times = "NaN"; }
return "Hello " + times + " times";
it('negative number literals work', function() {
var string = 'Message: {{hello -12}}';
var helpers = {hello: function(times) {
if (typeof times !== 'number') { times = 'NaN'; }
return 'Hello ' + times + ' times';
}};
shouldCompileTo(string, [hash, helpers], "Message: Hello -12 times", "template with a negative integer literal");
shouldCompileTo(string, [{}, helpers], 'Message: Hello -12 times', 'template with a negative integer literal');
});
describe("String literal parameters", function() {
it("simple literals work", function() {
var string = 'Message: {{hello "world" 12 true false}}';
var hash = {};
var helpers = {hello: function(param, times, bool1, bool2) {
if(typeof times !== 'number') { times = "NaN"; }
if(typeof bool1 !== 'boolean') { bool1 = "NaB"; }
if(typeof bool2 !== 'boolean') { bool2 = "NaB"; }
return "Hello " + param + " " + times + " times: " + bool1 + " " + bool2;
describe('String literal parameters', function() {
it('simple literals work', function() {
var string = 'Message: {{hello "world" 12 true false}}';
var helpers = {hello: function(param, times, bool1, bool2) {
if (typeof times !== 'number') { times = 'NaN'; }
if (typeof bool1 !== 'boolean') { bool1 = 'NaB'; }
if (typeof bool2 !== 'boolean') { bool2 = 'NaB'; }
return 'Hello ' + param + ' ' + times + ' times: ' + bool1 + ' ' + bool2;
}};
shouldCompileTo(string, [hash, helpers], "Message: Hello world 12 times: true false", "template with a simple String literal");
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() {
var string = 'Message: {{hello wo"rld"}}';
it('using a quote in the middle of a parameter raises an error', function() {
var string = 'Message: {{hello wo"rld"}}';
shouldThrow(function() {
CompilerContext.compile(string);
}, Error);
});
it("escaping a String is possible", function(){
var string = 'Message: {{{hello "\\"world\\""}}}';
var hash = {};
var helpers = {hello: function(param) { return "Hello " + param; }};
shouldCompileTo(string, [hash, helpers], "Message: Hello \"world\"", "template with an escaped String literal");
it('escaping a String is possible', function() {
var string = 'Message: {{{hello "\\"world\\""}}}';
var helpers = {hello: function(param) { return 'Hello ' + param; }};
shouldCompileTo(string, [{}, helpers], 'Message: Hello "world"', 'template with an escaped String literal');
});
it("it works with ' marks", function() {
var string = 'Message: {{{hello "Alan\'s world"}}}';
var hash = {};
var helpers = {hello: function(param) { return "Hello " + param; }};
shouldCompileTo(string, [hash, helpers], "Message: Hello Alan's world", "template with a ' mark");
var string = 'Message: {{{hello "Alan\'s world"}}}';
var helpers = {hello: function(param) { return 'Hello ' + param; }};
shouldCompileTo(string, [{}, helpers], "Message: Hello Alan's world", "template with a ' mark");
});
});
it("negative number literals work", function() {
var string = 'Message: {{hello -12}}';
var hash = {};
var helpers = {hello: function(times) {
if(typeof times !== 'number') { times = "NaN"; }
return "Hello " + times + " times";
it('negative number literals work', function() {
var string = 'Message: {{hello -12}}';
var helpers = {hello: function(times) {
if (typeof times !== 'number') { times = 'NaN'; }
return 'Hello ' + times + ' times';
}};
shouldCompileTo(string, [hash, helpers], "Message: Hello -12 times", "template with a negative integer literal");
shouldCompileTo(string, [{}, helpers], 'Message: Hello -12 times', 'template with a negative integer literal');
});
describe("multiple parameters", function() {
it("simple multi-params work", function() {
var string = 'Message: {{goodbye cruel world}}';
var hash = {cruel: "cruel", world: "world"};
var helpers = {goodbye: function(cruel, world) { return "Goodbye " + cruel + " " + world; }};
shouldCompileTo(string, [hash, helpers], "Message: Goodbye cruel world", "regular helpers with multiple params");
describe('multiple parameters', function() {
it('simple multi-params work', function() {
var string = 'Message: {{goodbye cruel world}}';
var hash = {cruel: 'cruel', world: 'world'};
var helpers = {goodbye: function(cruel, world) { return 'Goodbye ' + cruel + ' ' + world; }};
shouldCompileTo(string, [hash, helpers], 'Message: Goodbye cruel world', 'regular helpers with multiple params');
});
it("block multi-params work", function() {
var string = 'Message: {{#goodbye cruel world}}{{greeting}} {{adj}} {{noun}}{{/goodbye}}';
var hash = {cruel: "cruel", world: "world"};
it('block multi-params work', function() {
var string = 'Message: {{#goodbye cruel world}}{{greeting}} {{adj}} {{noun}}{{/goodbye}}';
var hash = {cruel: 'cruel', world: 'world'};
var helpers = {goodbye: function(cruel, world, options) {
return options.fn({greeting: "Goodbye", adj: cruel, noun: world});
return options.fn({greeting: 'Goodbye', adj: cruel, noun: world});
}};
shouldCompileTo(string, [hash, helpers], "Message: Goodbye cruel world", "block helpers with multiple params");
shouldCompileTo(string, [hash, helpers], 'Message: Goodbye cruel world', 'block helpers with multiple params');
});
});
describe('hash', function() {
it("helpers can take an optional hash", function() {
it('helpers can take an optional hash', function() {
var template = CompilerContext.compile('{{goodbye cruel="CRUEL" world="WORLD" times=12}}');
var helpers = {
goodbye: function(options) {
return "GOODBYE " + options.hash.cruel + " " + options.hash.world + " " + options.hash.times + " TIMES";
return 'GOODBYE ' + options.hash.cruel + ' ' + options.hash.world + ' ' + options.hash.times + ' TIMES';
}
};
var context = {};
var result = template(context, {helpers: helpers});
equals(result, "GOODBYE CRUEL WORLD 12 TIMES", "Helper output hash");
equals(result, 'GOODBYE CRUEL WORLD 12 TIMES', 'Helper output hash');
});
it("helpers can take an optional hash with booleans", function() {
it('helpers can take an optional hash with booleans', function() {
var helpers = {
goodbye: function(options) {
if (options.hash.print === true) {
return "GOODBYE " + options.hash.cruel + " " + options.hash.world;
return 'GOODBYE ' + options.hash.cruel + ' ' + options.hash.world;
} else if (options.hash.print === false) {
return "NOT PRINTING";
return 'NOT PRINTING';
} else {
return "THIS SHOULD NOT HAPPEN";
return 'THIS SHOULD NOT HAPPEN';
}
}
};
@@ -362,161 +363,161 @@ describe('helpers', function() {
var template = CompilerContext.compile('{{goodbye cruel="CRUEL" world="WORLD" print=true}}');
var result = template(context, {helpers: helpers});
equals(result, "GOODBYE CRUEL WORLD", "Helper output hash");
equals(result, 'GOODBYE CRUEL WORLD', 'Helper output hash');
template = CompilerContext.compile('{{goodbye cruel="CRUEL" world="WORLD" print=false}}');
result = template(context, {helpers: helpers});
equals(result, "NOT PRINTING", "Boolean helper parameter honored");
equals(result, 'NOT PRINTING', 'Boolean helper parameter honored');
});
it("block helpers can take an optional hash", function() {
it('block helpers can take an optional hash', function() {
var template = CompilerContext.compile('{{#goodbye cruel="CRUEL" times=12}}world{{/goodbye}}');
var helpers = {
goodbye: function(options) {
return "GOODBYE " + options.hash.cruel + " " + options.fn(this) + " " + options.hash.times + " TIMES";
return 'GOODBYE ' + options.hash.cruel + ' ' + options.fn(this) + ' ' + options.hash.times + ' TIMES';
}
};
var result = template({}, {helpers: helpers});
equals(result, "GOODBYE CRUEL world 12 TIMES", "Hash parameters output");
equals(result, 'GOODBYE CRUEL world 12 TIMES', 'Hash parameters output');
});
it("block helpers can take an optional hash with single quoted stings", function() {
var template = CompilerContext.compile("{{#goodbye cruel='CRUEL' times=12}}world{{/goodbye}}");
it('block helpers can take an optional hash with single quoted stings', function() {
var template = CompilerContext.compile('{{#goodbye cruel="CRUEL" times=12}}world{{/goodbye}}');
var helpers = {
goodbye: function(options) {
return "GOODBYE " + options.hash.cruel + " " + options.fn(this) + " " + options.hash.times + " TIMES";
return 'GOODBYE ' + options.hash.cruel + ' ' + options.fn(this) + ' ' + options.hash.times + ' TIMES';
}
};
var result = template({}, {helpers: helpers});
equals(result, "GOODBYE CRUEL world 12 TIMES", "Hash parameters output");
equals(result, 'GOODBYE CRUEL world 12 TIMES', 'Hash parameters output');
});
it("block helpers can take an optional hash with booleans", function() {
it('block helpers can take an optional hash with booleans', function() {
var helpers = {
goodbye: function(options) {
if (options.hash.print === true) {
return "GOODBYE " + options.hash.cruel + " " + options.fn(this);
return 'GOODBYE ' + options.hash.cruel + ' ' + options.fn(this);
} else if (options.hash.print === false) {
return "NOT PRINTING";
return 'NOT PRINTING';
} else {
return "THIS SHOULD NOT HAPPEN";
return 'THIS SHOULD NOT HAPPEN';
}
}
};
var template = CompilerContext.compile('{{#goodbye cruel="CRUEL" print=true}}world{{/goodbye}}');
var result = template({}, {helpers: helpers});
equals(result, "GOODBYE CRUEL world", "Boolean hash parameter honored");
equals(result, 'GOODBYE CRUEL world', 'Boolean hash parameter honored');
template = CompilerContext.compile('{{#goodbye cruel="CRUEL" print=false}}world{{/goodbye}}');
result = template({}, {helpers: helpers});
equals(result, "NOT PRINTING", "Boolean hash parameter honored");
equals(result, 'NOT PRINTING', 'Boolean hash parameter honored');
});
});
describe("helperMissing", function() {
it("if a context is not found, helperMissing is used", function() {
describe('helperMissing', function() {
it('if a context is not found, helperMissing is used', function() {
shouldThrow(function() {
var template = CompilerContext.compile("{{hello}} {{link_to world}}");
var template = CompilerContext.compile('{{hello}} {{link_to world}}');
template({});
}, undefined, /Missing helper: 'link_to'/);
}, undefined, /Missing helper: "link_to"/);
});
it("if a context is not found, custom helperMissing is used", function() {
var string = "{{hello}} {{link_to world}}";
var context = { hello: "Hello", world: "world" };
it('if a context is not found, custom helperMissing is used', function() {
var string = '{{hello}} {{link_to world}}';
var context = { hello: 'Hello', world: 'world' };
var helpers = {
helperMissing: function(mesg, options) {
if(options.name === "link_to") {
return new Handlebars.SafeString("<a>" + mesg + "</a>");
if (options.name === 'link_to') {
return new Handlebars.SafeString('<a>' + mesg + '</a>');
}
}
};
shouldCompileTo(string, [context, helpers], "Hello <a>world</a>");
shouldCompileTo(string, [context, helpers], 'Hello <a>world</a>');
});
it("if a value is not found, custom helperMissing is used", function() {
var string = "{{hello}} {{link_to}}";
var context = { hello: "Hello", world: "world" };
it('if a value is not found, custom helperMissing is used', function() {
var string = '{{hello}} {{link_to}}';
var context = { hello: 'Hello', world: 'world' };
var helpers = {
helperMissing: function(options) {
if(options.name === "link_to") {
return new Handlebars.SafeString("<a>winning</a>");
if (options.name === 'link_to') {
return new Handlebars.SafeString('<a>winning</a>');
}
}
};
shouldCompileTo(string, [context, helpers], "Hello <a>winning</a>");
shouldCompileTo(string, [context, helpers], 'Hello <a>winning</a>');
});
});
describe("knownHelpers", function() {
it("Known helper should render helper", function() {
var template = CompilerContext.compile("{{hello}}", {knownHelpers: {"hello" : true}});
describe('knownHelpers', function() {
it('Known helper should render helper', function() {
var template = CompilerContext.compile('{{hello}}', {knownHelpers: {hello: true}});
var result = template({}, {helpers: {hello: function() { return "foo"; }}});
equal(result, "foo", "'foo' should === '" + result);
var result = template({}, {helpers: {hello: function() { return 'foo'; }}});
equal(result, 'foo', "'foo' should === '" + result);
});
it("Unknown helper in knownHelpers only mode should be passed as undefined", function() {
var template = CompilerContext.compile("{{typeof hello}}", {knownHelpers: {'typeof': true}, knownHelpersOnly: true});
it('Unknown helper in knownHelpers only mode should be passed as undefined', function() {
var template = CompilerContext.compile('{{typeof hello}}', {knownHelpers: {'typeof': true}, knownHelpersOnly: true});
var result = template({}, {helpers: {'typeof': function(arg) { return typeof arg; }, hello: function() { return "foo"; }}});
equal(result, "undefined", "'undefined' should === '" + result);
var result = template({}, {helpers: {'typeof': function(arg) { return typeof arg; }, hello: function() { return 'foo'; }}});
equal(result, 'undefined', "'undefined' should === '" + result);
});
it("Builtin helpers available in knownHelpers only mode", function() {
var template = CompilerContext.compile("{{#unless foo}}bar{{/unless}}", {knownHelpersOnly: true});
it('Builtin helpers available in knownHelpers only mode', function() {
var template = CompilerContext.compile('{{#unless foo}}bar{{/unless}}', {knownHelpersOnly: true});
var result = template({});
equal(result, "bar", "'bar' should === '" + result);
equal(result, 'bar', "'bar' should === '" + result);
});
it("Field lookup works in knownHelpers only mode", function() {
var template = CompilerContext.compile("{{foo}}", {knownHelpersOnly: true});
it('Field lookup works in knownHelpers only mode', function() {
var template = CompilerContext.compile('{{foo}}', {knownHelpersOnly: true});
var result = template({foo: 'bar'});
equal(result, "bar", "'bar' should === '" + result);
equal(result, 'bar', "'bar' should === '" + result);
});
it("Conditional blocks work in knownHelpers only mode", function() {
var template = CompilerContext.compile("{{#foo}}bar{{/foo}}", {knownHelpersOnly: true});
it('Conditional blocks work in knownHelpers only mode', function() {
var template = CompilerContext.compile('{{#foo}}bar{{/foo}}', {knownHelpersOnly: true});
var result = template({foo: 'baz'});
equal(result, "bar", "'bar' should === '" + result);
equal(result, 'bar', "'bar' should === '" + result);
});
it("Invert blocks work in knownHelpers only mode", function() {
var template = CompilerContext.compile("{{^foo}}bar{{/foo}}", {knownHelpersOnly: true});
it('Invert blocks work in knownHelpers only mode', function() {
var template = CompilerContext.compile('{{^foo}}bar{{/foo}}', {knownHelpersOnly: true});
var result = template({foo: false});
equal(result, "bar", "'bar' should === '" + result);
equal(result, 'bar', "'bar' should === '" + result);
});
it("Functions are bound to the context in knownHelpers only mode", function() {
var template = CompilerContext.compile("{{foo}}", {knownHelpersOnly: true});
it('Functions are bound to the context in knownHelpers only mode', function() {
var template = CompilerContext.compile('{{foo}}', {knownHelpersOnly: true});
var result = template({foo: function() { return this.bar; }, bar: 'bar'});
equal(result, "bar", "'bar' should === '" + result);
equal(result, 'bar', "'bar' should === '" + result);
});
it("Unknown helper call in knownHelpers only mode should throw", function() {
it('Unknown helper call in knownHelpers only mode should throw', function() {
shouldThrow(function() {
CompilerContext.compile("{{typeof hello}}", {knownHelpersOnly: true});
CompilerContext.compile('{{typeof hello}}', {knownHelpersOnly: true});
}, Error);
});
});
describe("blockHelperMissing", function() {
it("lambdas are resolved by blockHelperMissing, not handlebars proper", function() {
var string = "{{#truthy}}yep{{/truthy}}";
describe('blockHelperMissing', function() {
it('lambdas are resolved by blockHelperMissing, not handlebars proper', function() {
var string = '{{#truthy}}yep{{/truthy}}';
var data = { truthy: function() { return true; } };
shouldCompileTo(string, data, "yep");
shouldCompileTo(string, data, 'yep');
});
it("lambdas resolved by blockHelperMissing are bound to the context", function() {
var string = "{{#truthy}}yep{{/truthy}}";
it('lambdas resolved by blockHelperMissing are bound to the context', function() {
var string = '{{#truthy}}yep{{/truthy}}';
var boundData = { truthy: function() { return this.truthiness(); }, truthiness: function() { return false; } };
shouldCompileTo(string, boundData, "");
shouldCompileTo(string, boundData, '');
});
});
@@ -524,13 +525,13 @@ describe('helpers', function() {
var context = {};
var helpers = {
blockHelperMissing: function() {
return 'missing: ' + arguments[arguments.length-1].name;
return 'missing: ' + arguments[arguments.length - 1].name;
},
helperMissing: function() {
return 'helper missing: ' + arguments[arguments.length-1].name;
return 'helper missing: ' + arguments[arguments.length - 1].name;
},
helper: function() {
return 'ran: ' + arguments[arguments.length-1].name;
return 'ran: ' + arguments[arguments.length - 1].name;
}
};
@@ -550,7 +551,7 @@ describe('helpers', function() {
shouldCompileTo('{{#helper 1}}{{/helper}}', [context, helpers], 'ran: helper');
});
it('should include in known helper calls', function() {
var template = CompilerContext.compile("{{helper}}", {knownHelpers: {'helper': true}, knownHelpersOnly: true});
var template = CompilerContext.compile('{{helper}}', {knownHelpers: {'helper': true}, knownHelpersOnly: true});
equal(template({}, {helpers: helpers}), 'ran: helper');
});
@@ -565,8 +566,8 @@ describe('helpers', function() {
});
describe('name conflicts', function() {
it("helpers take precedence over same-named context properties", function() {
var template = CompilerContext.compile("{{goodbye}} {{cruel world}}");
it('helpers take precedence over same-named context properties', function() {
var template = CompilerContext.compile('{{goodbye}} {{cruel world}}');
var helpers = {
goodbye: function() {
@@ -574,21 +575,21 @@ describe('helpers', function() {
},
cruel: function(world) {
return "cruel " + world.toUpperCase();
return 'cruel ' + world.toUpperCase();
}
};
var context = {
goodbye: "goodbye",
world: "world"
goodbye: 'goodbye',
world: 'world'
};
var result = template(context, {helpers: helpers});
equals(result, "GOODBYE cruel WORLD", "Helper executed");
equals(result, 'GOODBYE cruel WORLD', 'Helper executed');
});
it("helpers take precedence over same-named context properties$", function() {
var template = CompilerContext.compile("{{#goodbye}} {{cruel world}}{{/goodbye}}");
it('helpers take precedence over same-named context properties$', function() {
var template = CompilerContext.compile('{{#goodbye}} {{cruel world}}{{/goodbye}}');
var helpers = {
goodbye: function(options) {
@@ -596,21 +597,21 @@ describe('helpers', function() {
},
cruel: function(world) {
return "cruel " + world.toUpperCase();
return 'cruel ' + world.toUpperCase();
}
};
var context = {
goodbye: "goodbye",
world: "world"
goodbye: 'goodbye',
world: 'world'
};
var result = template(context, {helpers: helpers});
equals(result, "GOODBYE cruel WORLD", "Helper executed");
equals(result, 'GOODBYE cruel WORLD', 'Helper executed');
});
it("Scoped names take precedence over helpers", function() {
var template = CompilerContext.compile("{{this.goodbye}} {{cruel world}} {{cruel this.goodbye}}");
it('Scoped names take precedence over helpers', function() {
var template = CompilerContext.compile('{{this.goodbye}} {{cruel world}} {{cruel this.goodbye}}');
var helpers = {
goodbye: function() {
@@ -618,21 +619,21 @@ describe('helpers', function() {
},
cruel: function(world) {
return "cruel " + world.toUpperCase();
return 'cruel ' + world.toUpperCase();
}
};
var context = {
goodbye: "goodbye",
world: "world"
goodbye: 'goodbye',
world: 'world'
};
var result = template(context, {helpers: helpers});
equals(result, "goodbye cruel WORLD cruel GOODBYE", "Helper not executed");
equals(result, 'goodbye cruel WORLD cruel GOODBYE', 'Helper not executed');
});
it("Scoped names take precedence over block helpers", function() {
var template = CompilerContext.compile("{{#goodbye}} {{cruel world}}{{/goodbye}} {{this.goodbye}}");
it('Scoped names take precedence over block helpers', function() {
var template = CompilerContext.compile('{{#goodbye}} {{cruel world}}{{/goodbye}} {{this.goodbye}}');
var helpers = {
goodbye: function(options) {
@@ -640,17 +641,77 @@ describe('helpers', function() {
},
cruel: function(world) {
return "cruel " + world.toUpperCase();
return 'cruel ' + world.toUpperCase();
}
};
var context = {
goodbye: "goodbye",
world: "world"
goodbye: 'goodbye',
world: 'world'
};
var result = template(context, {helpers: helpers});
equals(result, "GOODBYE cruel WORLD goodbye", "Helper executed");
equals(result, 'GOODBYE cruel WORLD goodbye', 'Helper executed');
});
});
describe('block params', function() {
it('should take presedence over context values', function() {
var hash = {value: 'foo'};
var helpers = {
goodbyes: function(options) {
equals(options.fn.blockParams, 1);
return options.fn({value: 'bar'}, {blockParams: [1, 2]});
}
};
shouldCompileTo('{{#goodbyes as |value|}}{{value}}{{/goodbyes}}{{value}}', [hash, helpers], '1foo');
});
it('should take presedence over helper values', function() {
var hash = {};
var helpers = {
value: function() {
return 'foo';
},
goodbyes: function(options) {
equals(options.fn.blockParams, 1);
return options.fn({}, {blockParams: [1, 2]});
}
};
shouldCompileTo('{{#goodbyes as |value|}}{{value}}{{/goodbyes}}{{value}}', [hash, helpers], '1foo');
});
it('should not take presedence over pathed values', function() {
var hash = {value: 'bar'};
var helpers = {
value: function() {
return 'foo';
},
goodbyes: function(options) {
equals(options.fn.blockParams, 1);
return options.fn(this, {blockParams: [1, 2]});
}
};
shouldCompileTo('{{#goodbyes as |value|}}{{./value}}{{/goodbyes}}{{value}}', [hash, helpers], 'barfoo');
});
it('should take presednece over parent block params', function() {
var hash = {value: 'foo'},
value = 1;
var helpers = {
goodbyes: function(options) {
return options.fn({value: 'bar'}, {blockParams: options.fn.blockParams === 1 ? [value++, value++] : undefined});
}
};
shouldCompileTo('{{#goodbyes as |value|}}{{#goodbyes}}{{value}}{{#goodbyes as |value|}}{{value}}{{/goodbyes}}{{/goodbyes}}{{/goodbyes}}{{value}}', [hash, helpers], '13foo');
});
it('should allow block params on chained helpers', function() {
var hash = {value: 'foo'};
var helpers = {
goodbyes: function(options) {
equals(options.fn.blockParams, 1);
return options.fn({value: 'bar'}, {blockParams: [1, 2]});
}
};
shouldCompileTo('{{#if bar}}{{else goodbyes as |value|}}{{value}}{{/if}}{{value}}', [hash, helpers], '1foo');
});
});
});
+13 -6
View File
@@ -1,4 +1,3 @@
/*global Handlebars, beforeEach, handlebarsEnv, shouldCompileTo */
describe('javascript-compiler api', function() {
if (!Handlebars.JavaScriptCompiler) {
return;
@@ -17,7 +16,15 @@ describe('javascript-compiler api', function() {
handlebarsEnv.JavaScriptCompiler.prototype.nameLookup = function(parent, name) {
return parent + '.bar_' + name;
};
shouldCompileTo("{{foo}}", { bar_foo: "food" }, "food");
/*eslint-disable camelcase */
shouldCompileTo('{{foo}}', { bar_foo: 'food' }, 'food');
/*eslint-enable camelcase */
});
// Tests nameLookup dot vs. bracket behavior. Bracket is required in certain cases
// to avoid errors in older browsers.
it('should handle reserved words', function() {
shouldCompileTo('{{foo}} {{~null~}}', { foo: 'food' }, 'food');
});
});
describe('#compilerInfo', function() {
@@ -39,7 +46,7 @@ describe('javascript-compiler api', function() {
throw new Error('It didn\'t work');
}
};
shouldCompileTo("{{foo}} ", { foo: "food" }, "food ");
shouldCompileTo('{{foo}} ', { foo: 'food' }, 'food ');
});
});
describe('buffer', function() {
@@ -59,13 +66,13 @@ describe('javascript-compiler api', function() {
handlebarsEnv.JavaScriptCompiler.prototype.initializeBuffer = function() {
return this.quotedString('foo_');
};
shouldCompileTo("{{foo}} ", { foo: "food" }, "foo_food ");
shouldCompileTo('{{foo}} ', { foo: 'food' }, 'foo_food ');
});
it('should allow append buffer override', function() {
handlebarsEnv.JavaScriptCompiler.prototype.appendToBuffer = function(string) {
return $superAppend.call(this, string + ' + "_foo"');
return $superAppend.call(this, [string, ' + "_foo"']);
};
shouldCompileTo("{{foo}}", { foo: "food" }, "food_foo");
shouldCompileTo('{{foo}}', { foo: 'food' }, 'food_foo');
});
});
});
+101 -60
View File
@@ -1,193 +1,234 @@
/*global Handlebars, shouldThrow */
describe('parser', function() {
if (!Handlebars.print) {
return;
}
function ast_for(template) {
function astFor(template) {
var ast = Handlebars.parse(template);
return Handlebars.print(ast);
}
it('parses simple mustaches', function() {
equals(ast_for('{{foo}}'), "{{ ID:foo [] }}\n");
equals(ast_for('{{foo?}}'), "{{ ID:foo? [] }}\n");
equals(ast_for('{{foo_}}'), "{{ ID:foo_ [] }}\n");
equals(ast_for('{{foo-}}'), "{{ ID:foo- [] }}\n");
equals(ast_for('{{foo:}}'), "{{ ID:foo: [] }}\n");
equals(astFor('{{123}}'), '{{ NUMBER{123} [] }}\n');
equals(astFor('{{"foo"}}'), '{{ "foo" [] }}\n');
equals(astFor('{{false}}'), '{{ BOOLEAN{false} [] }}\n');
equals(astFor('{{true}}'), '{{ BOOLEAN{true} [] }}\n');
equals(astFor('{{foo}}'), '{{ PATH:foo [] }}\n');
equals(astFor('{{foo?}}'), '{{ PATH:foo? [] }}\n');
equals(astFor('{{foo_}}'), '{{ PATH:foo_ [] }}\n');
equals(astFor('{{foo-}}'), '{{ PATH:foo- [] }}\n');
equals(astFor('{{foo:}}'), '{{ PATH:foo: [] }}\n');
});
it('parses simple mustaches with data', function() {
equals(ast_for("{{@foo}}"), "{{ @ID:foo [] }}\n");
equals(astFor('{{@foo}}'), '{{ @PATH:foo [] }}\n');
});
it('parses simple mustaches with data paths', function() {
equals(ast_for("{{@../foo}}"), "{{ @ID:foo [] }}\n");
equals(astFor('{{@../foo}}'), '{{ @PATH:foo [] }}\n');
});
it('parses mustaches with paths', function() {
equals(ast_for("{{foo/bar}}"), "{{ PATH:foo/bar [] }}\n");
equals(astFor('{{foo/bar}}'), '{{ PATH:foo/bar [] }}\n');
});
it('parses mustaches with this/foo', function() {
equals(ast_for("{{this/foo}}"), "{{ ID:foo [] }}\n");
equals(astFor('{{this/foo}}'), '{{ PATH:foo [] }}\n');
});
it('parses mustaches with - in a path', function() {
equals(ast_for("{{foo-bar}}"), "{{ ID:foo-bar [] }}\n");
equals(astFor('{{foo-bar}}'), '{{ PATH:foo-bar [] }}\n');
});
it('parses mustaches with parameters', function() {
equals(ast_for("{{foo bar}}"), "{{ ID:foo [ID:bar] }}\n");
equals(astFor('{{foo bar}}'), '{{ PATH:foo [PATH:bar] }}\n');
});
it('parses mustaches with string parameters', function() {
equals(ast_for("{{foo bar \"baz\" }}"), '{{ ID:foo [ID:bar, "baz"] }}\n');
equals(astFor('{{foo bar \"baz\" }}'), '{{ PATH:foo [PATH:bar, "baz"] }}\n');
});
it('parses mustaches with NUMBER parameters', function() {
equals(ast_for("{{foo 1}}"), "{{ ID:foo [NUMBER{1}] }}\n");
equals(astFor('{{foo 1}}'), '{{ PATH:foo [NUMBER{1}] }}\n');
});
it('parses mustaches with BOOLEAN parameters', function() {
equals(ast_for("{{foo true}}"), "{{ ID:foo [BOOLEAN{true}] }}\n");
equals(ast_for("{{foo false}}"), "{{ ID:foo [BOOLEAN{false}] }}\n");
equals(astFor('{{foo true}}'), '{{ PATH:foo [BOOLEAN{true}] }}\n');
equals(astFor('{{foo false}}'), '{{ PATH:foo [BOOLEAN{false}] }}\n');
});
it('parses mustaches with undefined and null paths', function() {
equals(astFor('{{undefined}}'), '{{ UNDEFINED [] }}\n');
equals(astFor('{{null}}'), '{{ NULL [] }}\n');
});
it('parses mustaches with undefined and null parameters', function() {
equals(astFor('{{foo undefined null}}'), '{{ PATH:foo [UNDEFINED, NULL] }}\n');
});
it('parses mutaches with DATA parameters', function() {
equals(ast_for("{{foo @bar}}"), "{{ ID:foo [@ID:bar] }}\n");
equals(astFor('{{foo @bar}}'), '{{ PATH:foo [@PATH:bar] }}\n');
});
it('parses mustaches with hash arguments', function() {
equals(ast_for("{{foo bar=baz}}"), "{{ ID:foo [] HASH{bar=ID:baz} }}\n");
equals(ast_for("{{foo bar=1}}"), "{{ ID:foo [] HASH{bar=NUMBER{1}} }}\n");
equals(ast_for("{{foo bar=true}}"), "{{ ID:foo [] HASH{bar=BOOLEAN{true}} }}\n");
equals(ast_for("{{foo bar=false}}"), "{{ ID:foo [] HASH{bar=BOOLEAN{false}} }}\n");
equals(ast_for("{{foo bar=@baz}}"), "{{ ID:foo [] HASH{bar=@ID:baz} }}\n");
equals(astFor('{{foo bar=baz}}'), '{{ PATH:foo [] HASH{bar=PATH:baz} }}\n');
equals(astFor('{{foo bar=1}}'), '{{ PATH:foo [] HASH{bar=NUMBER{1}} }}\n');
equals(astFor('{{foo bar=true}}'), '{{ PATH:foo [] HASH{bar=BOOLEAN{true}} }}\n');
equals(astFor('{{foo bar=false}}'), '{{ PATH:foo [] HASH{bar=BOOLEAN{false}} }}\n');
equals(astFor('{{foo bar=@baz}}'), '{{ PATH:foo [] HASH{bar=@PATH:baz} }}\n');
equals(ast_for("{{foo bar=baz bat=bam}}"), "{{ ID:foo [] HASH{bar=ID:baz, bat=ID:bam} }}\n");
equals(ast_for("{{foo bar=baz bat=\"bam\"}}"), '{{ ID:foo [] HASH{bar=ID:baz, bat="bam"} }}\n');
equals(astFor('{{foo bar=baz bat=bam}}'), '{{ PATH:foo [] HASH{bar=PATH:baz, bat=PATH:bam} }}\n');
equals(astFor('{{foo bar=baz bat="bam"}}'), '{{ PATH:foo [] HASH{bar=PATH:baz, bat="bam"} }}\n');
equals(ast_for("{{foo bat='bam'}}"), '{{ ID: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\"}}"), '{{ ID:foo [ID:omg] HASH{bar=ID:baz, bat="bam"} }}\n');
equals(ast_for("{{foo omg bar=baz bat=\"bam\" baz=1}}"), '{{ ID:foo [ID:omg] HASH{bar=ID:baz, bat="bam", baz=NUMBER{1}} }}\n');
equals(ast_for("{{foo omg bar=baz bat=\"bam\" baz=true}}"), '{{ ID:foo [ID:omg] HASH{bar=ID:baz, bat="bam", baz=BOOLEAN{true}} }}\n');
equals(ast_for("{{foo omg bar=baz bat=\"bam\" baz=false}}"), '{{ ID:foo [ID:omg] HASH{bar=ID:baz, bat="bam", baz=BOOLEAN{false}} }}\n');
equals(astFor('{{foo omg bar=baz bat=\"bam\"}}'), '{{ PATH:foo [PATH:omg] HASH{bar=PATH:baz, bat="bam"} }}\n');
equals(astFor('{{foo omg bar=baz bat=\"bam\" baz=1}}'), '{{ PATH:foo [PATH:omg] HASH{bar=PATH:baz, bat="bam", baz=NUMBER{1}} }}\n');
equals(astFor('{{foo omg bar=baz bat=\"bam\" baz=true}}'), '{{ PATH:foo [PATH:omg] HASH{bar=PATH:baz, bat="bam", baz=BOOLEAN{true}} }}\n');
equals(astFor('{{foo omg bar=baz bat=\"bam\" baz=false}}'), '{{ PATH:foo [PATH:omg] HASH{bar=PATH:baz, bat="bam", baz=BOOLEAN{false}} }}\n');
});
it('parses contents followed by a mustache', function() {
equals(ast_for("foo bar {{baz}}"), "CONTENT[ \'foo bar \' ]\n{{ ID:baz [] }}\n");
equals(astFor('foo bar {{baz}}'), 'CONTENT[ \'foo bar \' ]\n{{ PATH:baz [] }}\n');
});
it('parses a partial', function() {
equals(ast_for("{{> foo }}"), "{{> PARTIAL:foo }}\n");
equals(astFor('{{> foo }}'), '{{> PARTIAL:foo }}\n');
equals(astFor('{{> "foo" }}'), '{{> PARTIAL:foo }}\n');
equals(astFor('{{> 1 }}'), '{{> PARTIAL:1 }}\n');
});
it('parses a partial with context', function() {
equals(ast_for("{{> foo bar}}"), "{{> PARTIAL:foo ID:bar }}\n");
equals(astFor('{{> foo bar}}'), '{{> PARTIAL:foo PATH:bar }}\n');
});
it('parses a partial with hash', function() {
equals(ast_for("{{> foo bar=bat}}"), "{{> PARTIAL:foo HASH{bar=ID:bat} }}\n");
equals(astFor('{{> foo bar=bat}}'), '{{> PARTIAL:foo HASH{bar=PATH:bat} }}\n');
});
it('parses a partial with context and hash', function() {
equals(ast_for("{{> foo bar bat=baz}}"), "{{> PARTIAL:foo ID:bar HASH{bat=ID:baz} }}\n");
equals(astFor('{{> foo bar bat=baz}}'), '{{> PARTIAL:foo PATH:bar HASH{bat=PATH:baz} }}\n');
});
it('parses a partial with a complex name', function() {
equals(ast_for("{{> shared/partial?.bar}}"), "{{> PARTIAL:shared/partial?.bar }}\n");
equals(astFor('{{> shared/partial?.bar}}'), '{{> PARTIAL:shared/partial?.bar }}\n');
});
it('parses a comment', function() {
equals(ast_for("{{! this is a comment }}"), "{{! ' this is a comment ' }}\n");
equals(astFor('{{! this is a comment }}'), "{{! ' this is a comment ' }}\n");
});
it('parses a multi-line comment', function() {
equals(ast_for("{{!\nthis is a multi-line comment\n}}"), "{{! \'\nthis is a multi-line comment\n\' }}\n");
equals(astFor('{{!\nthis is a multi-line comment\n}}'), "{{! \'\nthis is a multi-line comment\n\' }}\n");
});
it('parses an inverse section', function() {
equals(ast_for("{{#foo}} bar {{^}} baz {{/foo}}"), "BLOCK:\n {{ ID:foo [] }}\n PROGRAM:\n CONTENT[ ' bar ' ]\n {{^}}\n CONTENT[ ' baz ' ]\n");
equals(astFor('{{#foo}} bar {{^}} baz {{/foo}}'), "BLOCK:\n PATH:foo []\n PROGRAM:\n CONTENT[ ' bar ' ]\n {{^}}\n CONTENT[ ' baz ' ]\n");
});
it('parses an inverse (else-style) section', function() {
equals(ast_for("{{#foo}} bar {{else}} baz {{/foo}}"), "BLOCK:\n {{ ID:foo [] }}\n PROGRAM:\n CONTENT[ ' bar ' ]\n {{^}}\n CONTENT[ ' baz ' ]\n");
equals(astFor('{{#foo}} bar {{else}} baz {{/foo}}'), "BLOCK:\n PATH:foo []\n PROGRAM:\n CONTENT[ ' bar ' ]\n {{^}}\n CONTENT[ ' baz ' ]\n");
});
it('parses multiple inverse sections', function() {
equals(astFor('{{#foo}} bar {{else if bar}}{{else}} baz {{/foo}}'), "BLOCK:\n PATH:foo []\n PROGRAM:\n CONTENT[ ' bar ' ]\n {{^}}\n BLOCK:\n PATH:if [PATH:bar]\n PROGRAM:\n {{^}}\n CONTENT[ ' baz ' ]\n");
});
it('parses empty blocks', function() {
equals(ast_for("{{#foo}}{{/foo}}"), "BLOCK:\n {{ ID:foo [] }}\n PROGRAM:\n");
equals(astFor('{{#foo}}{{/foo}}'), 'BLOCK:\n PATH:foo []\n PROGRAM:\n');
});
it('parses empty blocks with empty inverse section', function() {
equals(ast_for("{{#foo}}{{^}}{{/foo}}"), "BLOCK:\n {{ ID:foo [] }}\n PROGRAM:\n {{^}}\n");
equals(astFor('{{#foo}}{{^}}{{/foo}}'), 'BLOCK:\n PATH:foo []\n PROGRAM:\n {{^}}\n');
});
it('parses empty blocks with empty inverse (else-style) section', function() {
equals(ast_for("{{#foo}}{{else}}{{/foo}}"), "BLOCK:\n {{ ID:foo [] }}\n PROGRAM:\n {{^}}\n");
equals(astFor('{{#foo}}{{else}}{{/foo}}'), 'BLOCK:\n PATH:foo []\n PROGRAM:\n {{^}}\n');
});
it('parses non-empty blocks with empty inverse section', function() {
equals(ast_for("{{#foo}} bar {{^}}{{/foo}}"), "BLOCK:\n {{ ID:foo [] }}\n PROGRAM:\n CONTENT[ ' bar ' ]\n {{^}}\n");
equals(astFor('{{#foo}} bar {{^}}{{/foo}}'), "BLOCK:\n PATH:foo []\n PROGRAM:\n CONTENT[ ' bar ' ]\n {{^}}\n");
});
it('parses non-empty blocks with empty inverse (else-style) section', function() {
equals(ast_for("{{#foo}} bar {{else}}{{/foo}}"), "BLOCK:\n {{ ID:foo [] }}\n PROGRAM:\n CONTENT[ ' bar ' ]\n {{^}}\n");
equals(astFor('{{#foo}} bar {{else}}{{/foo}}'), "BLOCK:\n PATH:foo []\n PROGRAM:\n CONTENT[ ' bar ' ]\n {{^}}\n");
});
it('parses empty blocks with non-empty inverse section', function() {
equals(ast_for("{{#foo}}{{^}} bar {{/foo}}"), "BLOCK:\n {{ ID:foo [] }}\n PROGRAM:\n {{^}}\n CONTENT[ ' bar ' ]\n");
equals(astFor('{{#foo}}{{^}} bar {{/foo}}'), "BLOCK:\n PATH:foo []\n PROGRAM:\n {{^}}\n CONTENT[ ' bar ' ]\n");
});
it('parses empty blocks with non-empty inverse (else-style) section', function() {
equals(ast_for("{{#foo}}{{else}} bar {{/foo}}"), "BLOCK:\n {{ ID:foo [] }}\n PROGRAM:\n {{^}}\n CONTENT[ ' bar ' ]\n");
equals(astFor('{{#foo}}{{else}} bar {{/foo}}'), "BLOCK:\n PATH:foo []\n PROGRAM:\n {{^}}\n CONTENT[ ' bar ' ]\n");
});
it('parses a standalone inverse section', function() {
equals(ast_for("{{^foo}}bar{{/foo}}"), "BLOCK:\n {{ ID:foo [] }}\n {{^}}\n CONTENT[ 'bar' ]\n");
equals(astFor('{{^foo}}bar{{/foo}}'), "BLOCK:\n PATH:foo []\n {{^}}\n CONTENT[ 'bar' ]\n");
});
it('parses a standalone inverse section', function() {
equals(ast_for("{{else foo}}bar{{/foo}}"), "BLOCK:\n {{ ID:foo [] }}\n {{^}}\n CONTENT[ 'bar' ]\n");
it('throws on old inverse section', function() {
shouldThrow(function() {
astFor('{{else foo}}bar{{/foo}}');
}, Error);
});
it("raises if there's a Parse error", function() {
it('parses block with block params', function() {
equals(astFor('{{#foo as |bar baz|}}content{{/foo}}'), "BLOCK:\n PATH:foo []\n PROGRAM:\n BLOCK PARAMS: [ bar baz ]\n CONTENT[ 'content' ]\n");
});
it('parses inverse block with block params', function() {
equals(astFor('{{^foo as |bar baz|}}content{{/foo}}'), "BLOCK:\n PATH:foo []\n {{^}}\n BLOCK PARAMS: [ bar baz ]\n CONTENT[ 'content' ]\n");
});
it('parses chained inverse block with block params', function() {
equals(astFor('{{#foo}}{{else foo as |bar baz|}}content{{/foo}}'), "BLOCK:\n PATH:foo []\n PROGRAM:\n {{^}}\n BLOCK:\n PATH:foo []\n PROGRAM:\n BLOCK PARAMS: [ bar baz ]\n CONTENT[ 'content' ]\n");
});
it('raises if there\'s a Parse error', function() {
shouldThrow(function() {
ast_for("foo{{^}}bar");
astFor('foo{{^}}bar');
}, Error, /Parse error on line 1/);
shouldThrow(function() {
ast_for("{{foo}");
astFor('{{foo}');
}, Error, /Parse error on line 1/);
shouldThrow(function() {
ast_for("{{foo &}}");
astFor('{{foo &}}');
}, Error, /Parse error on line 1/);
shouldThrow(function() {
ast_for("{{#goodbyes}}{{/hellos}}");
astFor('{{#goodbyes}}{{/hellos}}');
}, Error, /goodbyes doesn't match hellos/);
shouldThrow(function() {
ast_for("{{{{goodbyes}}}} {{{{/hellos}}}}");
astFor('{{{{goodbyes}}}} {{{{/hellos}}}}');
}, Error, /goodbyes doesn't match hellos/);
});
it('should handle invalid paths', function() {
shouldThrow(function() {
astFor('{{foo/../bar}}');
}, Error, /Invalid path: foo\/\.\. - 1:2/);
shouldThrow(function() {
astFor('{{foo/./bar}}');
}, Error, /Invalid path: foo\/\. - 1:2/);
shouldThrow(function() {
astFor('{{foo/this/bar}}');
}, Error, /Invalid path: foo\/this - 1:2/);
});
it('knows how to report the correct line number in errors', function() {
shouldThrow(function() {
ast_for("hello\nmy\n{{foo}");
astFor('hello\nmy\n{{foo}');
}, Error, /Parse error on line 3/);
shouldThrow(function() {
ast_for("hello\n\nmy\n\n{{foo}");
astFor('hello\n\nmy\n\n{{foo}');
}, Error, /Parse error on line 5/);
});
it('knows how to report the correct line number in errors when the first character is a newline', function() {
shouldThrow(function() {
ast_for("\n\nhello\n\nmy\n\n{{foo}");
astFor('\n\nhello\n\nmy\n\n{{foo}');
}, Error, /Parse error on line 7/);
});
describe('externally compiled AST', function() {
it('can pass through an already-compiled AST', function() {
equals(ast_for(new Handlebars.AST.ProgramNode([ new Handlebars.AST.ContentNode("Hello")])), "CONTENT[ \'Hello\' ]\n");
equals(astFor(new Handlebars.AST.Program([new Handlebars.AST.ContentStatement('Hello')], null)), 'CONTENT[ \'Hello\' ]\n');
});
});
});
+160 -105
View File
@@ -1,148 +1,186 @@
/*global CompilerContext, Handlebars, handlebarsEnv, shouldCompileTo, shouldCompileToWithPartials, shouldThrow */
describe('partials', function() {
it('basic partials', function() {
var string = 'Dudes: {{#dudes}}{{> dude}}{{/dudes}}';
var partial = '{{name}} ({{url}}) ';
var hash = {dudes: [{name: 'Yehuda', url: 'http://yehuda'}, {name: 'Alan', url: 'http://alan'}]};
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("partials with context", function() {
var string = "Dudes: {{>dude dudes}}";
var partial = "{{#this}}{{name}} ({{url}}) {{/this}}";
var hash = {dudes: [{name: "Yehuda", url: "http://yehuda"}, {name: "Alan", url: "http://alan"}]};
shouldCompileToWithPartials(string, [hash, {}, {dude: partial}], true, "Dudes: Yehuda (http://yehuda) Alan (http://alan) ",
"Partials can be passed a context");
it('dynamic partials', function() {
var string = 'Dudes: {{#dudes}}{{> (partial)}}{{/dudes}}';
var partial = '{{name}} ({{url}}) ';
var hash = {dudes: [{name: 'Yehuda', url: 'http://yehuda'}, {name: 'Alan', url: 'http://alan'}]};
var helpers = {
partial: function() {
return 'dude';
}
};
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) ');
});
it('failing dynamic partials', function() {
var string = 'Dudes: {{#dudes}}{{> (partial)}}{{/dudes}}';
var partial = '{{name}} ({{url}}) ';
var hash = {dudes: [{name: 'Yehuda', url: 'http://yehuda'}, {name: 'Alan', url: 'http://alan'}]};
var helpers = {
partial: function() {
return 'missing';
}
};
shouldThrow(function() {
shouldCompileToWithPartials(string, [hash, helpers, {dude: partial}], true, 'Dudes: Yehuda (http://yehuda) Alan (http://alan) ');
}, Handlebars.Exception, 'The partial missing could not be found');
});
it("partials with undefined context", function() {
var string = "Dudes: {{>dude dudes}}";
var partial = "{{foo}} Empty";
it('partials with context', function() {
var string = 'Dudes: {{>dude dudes}}';
var partial = '{{#this}}{{name}} ({{url}}) {{/this}}';
var hash = {dudes: [{name: 'Yehuda', url: 'http://yehuda'}, {name: 'Alan', url: 'http://alan'}]};
shouldCompileToWithPartials(string, [hash, {}, {dude: partial}], true, 'Dudes: Yehuda (http://yehuda) Alan (http://alan) ',
'Partials can be passed a context');
});
it('partials with undefined context', function() {
var string = 'Dudes: {{>dude dudes}}';
var partial = '{{foo}} Empty';
var hash = {};
shouldCompileToWithPartials(string, [hash, {}, {dude: partial}], true, "Dudes: Empty");
shouldCompileToWithPartials(string, [hash, {}, {dude: partial}], true, 'Dudes: Empty');
});
it("partials with parameters", function() {
var string = "Dudes: {{#dudes}}{{> dude others=..}}{{/dudes}}";
var partial = "{{others.foo}}{{name}} ({{url}}) ";
var hash = {foo: 'bar', dudes: [{name: "Yehuda", url: "http://yehuda"}, {name: "Alan", url: "http://alan"}]};
shouldCompileToWithPartials(string, [hash, {}, {dude: partial}], true, "Dudes: barYehuda (http://yehuda) barAlan (http://alan) ",
"Basic partials output based on current context.");
});
it("partial in a partial", function() {
var string = "Dudes: {{#dudes}}{{>dude}}{{/dudes}}";
var dude = "{{name}} {{> url}} ";
var url = "<a href='{{url}}'>{{url}}</a>";
var hash = {dudes: [{name: "Yehuda", url: "http://yehuda"}, {name: "Alan", url: "http://alan"}]};
shouldCompileToWithPartials(string, [hash, {}, {dude: dude, url: url}], true, "Dudes: Yehuda <a href='http://yehuda'>http://yehuda</a> Alan <a href='http://alan'>http://alan</a> ", "Partials are rendered inside of other partials");
});
it("rendering undefined partial throws an exception", function() {
it('partials with duplicate parameters', function() {
shouldThrow(function() {
var template = CompilerContext.compile("{{> whatever}}");
template();
}, Handlebars.Exception, 'The partial whatever could not be found');
CompilerContext.compile('Dudes: {{>dude dudes foo bar=baz}}');
}, Error, 'Unsupported number of partial arguments: 2 - 1:7');
});
it("rendering template partial in vm mode throws an exception", function() {
it('partials with parameters', function() {
var string = 'Dudes: {{#dudes}}{{> dude others=..}}{{/dudes}}';
var partial = '{{others.foo}}{{name}} ({{url}}) ';
var hash = {foo: 'bar', dudes: [{name: 'Yehuda', url: 'http://yehuda'}, {name: 'Alan', url: 'http://alan'}]};
shouldCompileToWithPartials(string, [hash, {}, {dude: partial}], true, 'Dudes: barYehuda (http://yehuda) barAlan (http://alan) ',
'Basic partials output based on current context.');
});
it('partial in a partial', function() {
var string = 'Dudes: {{#dudes}}{{>dude}}{{/dudes}}';
var dude = '{{name}} {{> url}} ';
var url = '<a href="{{url}}">{{url}}</a>';
var hash = {dudes: [{name: 'Yehuda', url: 'http://yehuda'}, {name: 'Alan', url: 'http://alan'}]};
shouldCompileToWithPartials(string, [hash, {}, {dude: dude, url: url}], true, 'Dudes: Yehuda <a href="http://yehuda">http://yehuda</a> Alan <a href="http://alan">http://alan</a> ', 'Partials are rendered inside of other partials');
});
it('rendering undefined partial throws an exception', function() {
shouldThrow(function() {
var template = CompilerContext.compile("{{> whatever}}");
var template = CompilerContext.compile('{{> whatever}}');
template();
}, Handlebars.Exception, 'The partial whatever could not be found');
});
it("rendering function partial in vm mode", function() {
var string = "Dudes: {{#dudes}}{{> dude}}{{/dudes}}";
var partial = function(context) {
it('registering undefined partial throws an exception', function() {
shouldThrow(function() {
var undef;
handlebarsEnv.registerPartial('undefined_test', undef);
}, Handlebars.Exception, 'Attempting to register a partial as undefined');
});
it('rendering template partial in vm mode throws an exception', function() {
shouldThrow(function() {
var template = CompilerContext.compile('{{> whatever}}');
template();
}, Handlebars.Exception, 'The partial whatever could not be found');
});
it('rendering function partial in vm mode', function() {
var string = 'Dudes: {{#dudes}}{{> dude}}{{/dudes}}';
function partial(context) {
return context.name + ' (' + context.url + ') ';
};
var hash = {dudes: [{name: "Yehuda", url: "http://yehuda"}, {name: "Alan", url: "http://alan"}]};
shouldCompileTo(string, [hash, {}, {dude: partial}], "Dudes: Yehuda (http://yehuda) Alan (http://alan) ",
"Function partials output based in VM.");
}
var hash = {dudes: [{name: 'Yehuda', url: 'http://yehuda'}, {name: 'Alan', url: 'http://alan'}]};
shouldCompileTo(string, [hash, {}, {dude: partial}], 'Dudes: Yehuda (http://yehuda) Alan (http://alan) ',
'Function partials output based in VM.');
});
it("GH-14: a partial preceding a selector", function() {
var string = "Dudes: {{>dude}} {{another_dude}}";
var dude = "{{name}}";
var hash = {name:"Jeepers", another_dude:"Creepers"};
shouldCompileToWithPartials(string, [hash, {}, {dude:dude}], true, "Dudes: Jeepers Creepers", "Regular selectors can follow a partial");
it('GH-14: a partial preceding a selector', function() {
var string = 'Dudes: {{>dude}} {{anotherDude}}';
var dude = '{{name}}';
var hash = {name: 'Jeepers', anotherDude: 'Creepers'};
shouldCompileToWithPartials(string, [hash, {}, {dude: dude}], true, 'Dudes: Jeepers Creepers', 'Regular selectors can follow a partial');
});
it("Partials with slash paths", function() {
var string = "Dudes: {{> shared/dude}}";
var dude = "{{name}}";
var hash = {name:"Jeepers", another_dude:"Creepers"};
shouldCompileToWithPartials(string, [hash, {}, {'shared/dude':dude}], true, "Dudes: Jeepers", "Partials can use literal paths");
it('Partials with slash paths', function() {
var string = 'Dudes: {{> shared/dude}}';
var dude = '{{name}}';
var hash = {name: 'Jeepers', anotherDude: 'Creepers'};
shouldCompileToWithPartials(string, [hash, {}, {'shared/dude': dude}], true, 'Dudes: Jeepers', 'Partials can use literal paths');
});
it("Partials with slash and point paths", function() {
var string = "Dudes: {{> shared/dude.thing}}";
var dude = "{{name}}";
var hash = {name:"Jeepers", another_dude:"Creepers"};
shouldCompileToWithPartials(string, [hash, {}, {'shared/dude.thing':dude}], true, "Dudes: Jeepers", "Partials can use literal with points in paths");
it('Partials with slash and point paths', function() {
var string = 'Dudes: {{> shared/dude.thing}}';
var dude = '{{name}}';
var hash = {name: 'Jeepers', anotherDude: 'Creepers'};
shouldCompileToWithPartials(string, [hash, {}, {'shared/dude.thing': dude}], true, 'Dudes: Jeepers', 'Partials can use literal with points in paths');
});
it("Global Partials", function() {
handlebarsEnv.registerPartial('global_test', '{{another_dude}}');
it('Global Partials', function() {
handlebarsEnv.registerPartial('globalTest', '{{anotherDude}}');
var string = "Dudes: {{> shared/dude}} {{> global_test}}";
var dude = "{{name}}";
var hash = {name:"Jeepers", another_dude:"Creepers"};
shouldCompileToWithPartials(string, [hash, {}, {'shared/dude':dude}], true, "Dudes: Jeepers Creepers", "Partials can use globals or passed");
var string = 'Dudes: {{> shared/dude}} {{> globalTest}}';
var dude = '{{name}}';
var hash = {name: 'Jeepers', anotherDude: 'Creepers'};
shouldCompileToWithPartials(string, [hash, {}, {'shared/dude': dude}], true, 'Dudes: Jeepers Creepers', 'Partials can use globals or passed');
handlebarsEnv.unregisterPartial('global_test');
equals(handlebarsEnv.partials.global_test, undefined);
handlebarsEnv.unregisterPartial('globalTest');
equals(handlebarsEnv.partials.globalTest, undefined);
});
it("Multiple partial registration", function() {
it('Multiple partial registration', function() {
handlebarsEnv.registerPartial({
'shared/dude': '{{name}}',
global_test: '{{another_dude}}'
globalTest: '{{anotherDude}}'
});
var string = "Dudes: {{> shared/dude}} {{> global_test}}";
var hash = {name:"Jeepers", another_dude:"Creepers"};
shouldCompileToWithPartials(string, [hash], true, "Dudes: Jeepers Creepers", "Partials can use globals or passed");
var string = 'Dudes: {{> shared/dude}} {{> globalTest}}';
var hash = {name: 'Jeepers', anotherDude: 'Creepers'};
shouldCompileToWithPartials(string, [hash], true, 'Dudes: Jeepers Creepers', 'Partials can use globals or passed');
});
it("Partials with integer path", function() {
var string = "Dudes: {{> 404}}";
var dude = "{{name}}";
var hash = {name:"Jeepers", another_dude:"Creepers"};
shouldCompileToWithPartials(string, [hash, {}, {404:dude}], true, "Dudes: Jeepers", "Partials can use literal paths");
it('Partials with integer path', function() {
var string = 'Dudes: {{> 404}}';
var dude = '{{name}}';
var hash = {name: 'Jeepers', anotherDude: 'Creepers'};
shouldCompileToWithPartials(string, [hash, {}, {404: dude}], true, 'Dudes: Jeepers', 'Partials can use literal paths');
});
it("Partials with complex path", function() {
var string = "Dudes: {{> 404/asdf?.bar}}";
var dude = "{{name}}";
var hash = {name:"Jeepers", another_dude:"Creepers"};
shouldCompileToWithPartials(string, [hash, {}, {'404/asdf?.bar':dude}], true, "Dudes: Jeepers", "Partials can use literal paths");
it('Partials with complex path', function() {
var string = 'Dudes: {{> 404/asdf?.bar}}';
var dude = '{{name}}';
var hash = {name: 'Jeepers', anotherDude: 'Creepers'};
shouldCompileToWithPartials(string, [hash, {}, {'404/asdf?.bar': dude}], true, 'Dudes: Jeepers', 'Partials can use literal paths');
});
it("Partials with escaped", function() {
var string = "Dudes: {{> [+404/asdf?.bar]}}";
var dude = "{{name}}";
var hash = {name:"Jeepers", another_dude:"Creepers"};
shouldCompileToWithPartials(string, [hash, {}, {'+404/asdf?.bar':dude}], true, "Dudes: Jeepers", "Partials can use literal paths");
it('Partials with escaped', function() {
var string = 'Dudes: {{> [+404/asdf?.bar]}}';
var dude = '{{name}}';
var hash = {name: 'Jeepers', anotherDude: 'Creepers'};
shouldCompileToWithPartials(string, [hash, {}, {'+404/asdf?.bar': dude}], true, 'Dudes: Jeepers', 'Partials can use literal paths');
});
it("Partials with string", function() {
var string = "Dudes: {{> \"+404/asdf?.bar\"}}";
var dude = "{{name}}";
var hash = {name:"Jeepers", another_dude:"Creepers"};
shouldCompileToWithPartials(string, [hash, {}, {'+404/asdf?.bar':dude}], true, "Dudes: Jeepers", "Partials can use literal paths");
it('Partials with string', function() {
var string = 'Dudes: {{> \'+404/asdf?.bar\'}}';
var dude = '{{name}}';
var hash = {name: 'Jeepers', anotherDude: 'Creepers'};
shouldCompileToWithPartials(string, [hash, {}, {'+404/asdf?.bar': dude}], true, 'Dudes: Jeepers', 'Partials can use literal paths');
});
it('should handle empty partial', function() {
var string = "Dudes: {{#dudes}}{{> dude}}{{/dudes}}";
var partial = "";
var hash = {dudes: [{name: "Yehuda", url: "http://yehuda"}, {name: "Alan", url: "http://alan"}]};
shouldCompileToWithPartials(string, [hash, {}, {dude: partial}], true, "Dudes: ");
var string = 'Dudes: {{#dudes}}{{> dude}}{{/dudes}}';
var partial = '';
var hash = {dudes: [{name: 'Yehuda', url: 'http://yehuda'}, {name: 'Alan', url: 'http://alan'}]};
shouldCompileToWithPartials(string, [hash, {}, {dude: partial}], true, 'Dudes: ');
});
it("throw on missing partial", function() {
it('throw on missing partial', function() {
var compile = handlebarsEnv.compile;
handlebarsEnv.compile = undefined;
shouldThrow(function() {
@@ -151,21 +189,38 @@ describe('partials', function() {
handlebarsEnv.compile = compile;
});
it('should pass compiler flags', function() {
if (Handlebars.compile) {
var env = Handlebars.create();
env.registerPartial('partial', '{{foo}}');
var template = env.compile('{{foo}} {{> partial}}', {noEscape: true});
equal(template({foo: '<'}), '< <');
}
});
describe('standalone partials', function() {
it("indented partials", function() {
var string = "Dudes:\n{{#dudes}}\n {{>dude}}\n{{/dudes}}";
var dude = "{{name}}\n";
var hash = {dudes: [{name: "Yehuda", url: "http://yehuda"}, {name: "Alan", url: "http://alan"}]};
it('indented partials', function() {
var string = 'Dudes:\n{{#dudes}}\n {{>dude}}\n{{/dudes}}';
var dude = '{{name}}\n';
var hash = {dudes: [{name: 'Yehuda', url: 'http://yehuda'}, {name: 'Alan', url: 'http://alan'}]};
shouldCompileToWithPartials(string, [hash, {}, {dude: dude}], true,
"Dudes:\n Yehuda\n Alan\n");
'Dudes:\n Yehuda\n Alan\n');
});
it("nested indented partials", function() {
var string = "Dudes:\n{{#dudes}}\n {{>dude}}\n{{/dudes}}";
var dude = "{{name}}\n {{> url}}";
var url = "{{url}}!\n";
var hash = {dudes: [{name: "Yehuda", url: "http://yehuda"}, {name: "Alan", url: "http://alan"}]};
it('nested indented partials', function() {
var string = 'Dudes:\n{{#dudes}}\n {{>dude}}\n{{/dudes}}';
var dude = '{{name}}\n {{> url}}';
var url = '{{url}}!\n';
var hash = {dudes: [{name: 'Yehuda', url: 'http://yehuda'}, {name: 'Alan', url: 'http://alan'}]};
shouldCompileToWithPartials(string, [hash, {}, {dude: dude, url: url}], true,
"Dudes:\n Yehuda\n http://yehuda!\n Alan\n http://alan!\n");
'Dudes:\n Yehuda\n http://yehuda!\n Alan\n http://alan!\n');
});
it('prevent nested indented partials', function() {
var string = 'Dudes:\n{{#dudes}}\n {{>dude}}\n{{/dudes}}';
var dude = '{{name}}\n {{> url}}';
var url = '{{url}}!\n';
var hash = {dudes: [{name: 'Yehuda', url: 'http://yehuda'}, {name: 'Alan', url: 'http://alan'}]};
shouldCompileToWithPartials(string, [hash, {}, {dude: dude, url: url}, {preventIndent: true}], true,
'Dudes:\n Yehuda\n http://yehuda!\n Alan\n http://alan!\n');
});
});
+49 -7
View File
@@ -1,6 +1,4 @@
/*global shouldThrow */
/*eslint-disable no-console */
describe('precompiler', function() {
// NOP Under non-node environments
if (typeof process === 'undefined') {
@@ -8,28 +6,39 @@ describe('precompiler', function() {
}
var Handlebars = require('../lib'),
Precompiler = require('../lib/precompiler'),
Precompiler = require('../dist/cjs/precompiler'),
fs = require('fs'),
uglify = require('uglify-js');
var log,
logFunction,
precompile,
minify;
minify,
file,
content,
writeFileSync;
beforeEach(function() {
precompile = Handlebars.precompile;
minify = uglify.minify;
writeFileSync = fs.writeFileSync;
logFunction = console.log;
log = '';
console.log = function() {
log += Array.prototype.join.call(arguments, '');
};
fs.writeFileSync = function(_file, _content) {
file = _file;
content = _content;
};
});
afterEach(function() {
Handlebars.precompile = precompile;
uglify.minify = minify;
fs.writeFileSync = writeFileSync;
console.log = logFunction;
});
@@ -50,7 +59,7 @@ describe('precompiler', function() {
it('should throw when combining simple and minimized', function() {
shouldThrow(function() {
Precompiler.cli({templates: [__dirname], simple: true, min: true});
}, Handlebars.Exception, 'Unable to minimze simple output');
}, Handlebars.Exception, 'Unable to minimize simple output');
});
it('should throw when combining simple and multiple templates', function() {
shouldThrow(function() {
@@ -87,7 +96,8 @@ describe('precompiler', function() {
});
it('should output multiple amd', function() {
Handlebars.precompile = function() { return 'amd'; };
Precompiler.cli({templates: [__dirname + '/artifacts'], amd: true, extension: 'handlebars'});
Precompiler.cli({templates: [__dirname + '/artifacts'], amd: true, extension: 'handlebars', namespace: 'foo'});
equal(/templates = foo = foo \|\|/.test(log), true);
equal(/return templates/.test(log), true);
equal(/template\(amd\)/.test(log), true);
});
@@ -121,10 +131,42 @@ describe('precompiler', function() {
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');
});
it('should output to file system', function() {
Handlebars.precompile = function() { return 'simple'; };
Precompiler.cli({templates: [__dirname + '/artifacts/empty.handlebars'], simple: true, extension: 'handlebars', output: 'file!'});
equal(file, 'file!');
equal(content, 'simple\n');
equal(log, '');
});
it('should handle BOM', function() {
Handlebars.precompile = function(template) { return template === 'a' ? 'simple' : 'fail'; };
Precompiler.cli({templates: [__dirname + '/artifacts/bom.handlebars'], simple: true, extension: 'handlebars', bom: true});
equal(log, 'simple\n');
});
it('should output minimized templates', function() {
Handlebars.precompile = function() { return 'amd'; };
uglify.minify = function() { return {code: 'min'}; };
Precompiler.cli({templates: [__dirname + '/artifacts/empty.handlebars'], min: true, extension: 'handlebars'});
equal(log, 'min');
});
it('should output map', function() {
Precompiler.cli({templates: [__dirname + '/artifacts/empty.handlebars'], map: 'foo.js.map', extension: 'handlebars'});
equal(file, 'foo.js.map');
equal(/sourceMappingURL=/.test(log), true);
});
it('should output map', function() {
Precompiler.cli({templates: [__dirname + '/artifacts/empty.handlebars'], min: true, map: 'foo.js.map', extension: 'handlebars'});
equal(file, 'foo.js.map');
equal(/sourceMappingURL=/.test(log), true);
});
});
+60 -33
View File
@@ -1,26 +1,34 @@
/*global CompilerContext, Handlebars, shouldCompileTo, shouldThrow */
describe('Regressions', function() {
it("GH-94: Cannot read property of undefined", function() {
var data = {"books":[{"title":"The origin of species","author":{"name":"Charles Darwin"}},{"title":"Lazarillo de Tormes"}]};
var string = "{{#books}}{{title}}{{author.name}}{{/books}}";
shouldCompileTo(string, data, "The origin of speciesCharles DarwinLazarillo de Tormes",
"Renders without an undefined property error");
it('GH-94: Cannot read property of undefined', function() {
var data = {
'books': [{
'title': 'The origin of species',
'author': {
'name': 'Charles Darwin'
}
}, {
'title': 'Lazarillo de Tormes'
}]
};
var string = '{{#books}}{{title}}{{author.name}}{{/books}}';
shouldCompileTo(string, data, 'The origin of speciesCharles DarwinLazarillo de Tormes',
'Renders without an undefined property error');
});
it("GH-150: Inverted sections print when they shouldn't", function() {
var string = "{{^set}}not set{{/set}} :: {{#set}}set{{/set}}";
var string = '{{^set}}not set{{/set}} :: {{#set}}set{{/set}}';
shouldCompileTo(string, {}, "not set :: ", "inverted sections run when property isn't present in context");
shouldCompileTo(string, {set: undefined}, "not set :: ", "inverted sections run when property is undefined");
shouldCompileTo(string, {set: false}, "not set :: ", "inverted sections run when property is false");
shouldCompileTo(string, {set: true}, " :: set", "inverted sections don't run when property is true");
shouldCompileTo(string, {}, 'not set :: ', "inverted sections run when property isn't present in context");
shouldCompileTo(string, {set: undefined}, 'not set :: ', 'inverted sections run when property is undefined');
shouldCompileTo(string, {set: false}, 'not set :: ', 'inverted sections run when property is false');
shouldCompileTo(string, {set: true}, ' :: set', "inverted sections don't run when property is true");
});
it("GH-158: Using array index twice, breaks the template", function() {
var string = "{{arr.[0]}}, {{arr.[1]}}";
var data = { "arr": [1,2] };
it('GH-158: Using array index twice, breaks the template', function() {
var string = '{{arr.[0]}}, {{arr.[1]}}';
var data = { 'arr': [1, 2] };
shouldCompileTo(string, data, "1, 2", "it works as expected");
shouldCompileTo(string, data, '1, 2', 'it works as expected');
});
it("bug reported by @fat where lambdas weren't being properly resolved", function() {
@@ -39,12 +47,12 @@ describe('Regressions', function() {
+ '{{/hasThings}}';
var data = {
thing: function() {
return "blah";
return 'blah';
},
things: [
{className: "one", word: "@fat"},
{className: "two", word: "@dhg"},
{className: "three", word:"@sayrer"}
{className: 'one', word: '@fat'},
{className: 'two', word: '@dhg'},
{className: 'three', word: '@sayrer'}
],
hasThings: function() {
return true;
@@ -63,14 +71,14 @@ describe('Regressions', function() {
it('GH-408: Multiple loops fail', function() {
var context = [
{ name: "John Doe", location: { city: "Chicago" } },
{ name: "Jane Doe", location: { city: "New York"} }
{ name: 'John Doe', location: { city: 'Chicago' } },
{ name: 'Jane Doe', location: { city: 'New York'} }
];
var template = CompilerContext.compile('{{#.}}{{name}}{{/.}}{{#.}}{{name}}{{/.}}{{#.}}{{name}}{{/.}}');
var result = template(context);
equals(result, "John DoeJane DoeJohn DoeJane DoeJohn DoeJane Doe", 'It should output multiple times');
equals(result, 'John DoeJane DoeJohn DoeJane DoeJohn DoeJane Doe', 'It should output multiple times');
});
it('GS-428: Nested if else rendering', function() {
@@ -95,11 +103,13 @@ describe('Regressions', function() {
});
it('GH-534: Object prototype aliases', function() {
/*eslint-disable no-extend-native */
Object.prototype[0xD834] = true;
shouldCompileTo('{{foo}}', { foo: 'bar' }, 'bar');
delete Object.prototype[0xD834];
/*eslint-enable no-extend-native */
});
it('GH-437: Matching escaping', function() {
@@ -111,23 +121,23 @@ describe('Regressions', function() {
}, Error);
});
it("GH-676: Using array in escaping mustache fails", function() {
var string = "{{arr}}";
var data = { "arr": [1,2] };
it('GH-676: Using array in escaping mustache fails', function() {
var string = '{{arr}}';
var data = { 'arr': [1, 2] };
shouldCompileTo(string, data, data.arr.toString(), "it works as expected");
shouldCompileTo(string, data, data.arr.toString(), 'it works as expected');
});
it("Mustache man page", function() {
var string = "Hello {{name}}. You have just won ${{value}}!{{#in_ca}} Well, ${{taxed_value}}, after taxes.{{/in_ca}}";
it('Mustache man page', function() {
var string = 'Hello {{name}}. You have just won ${{value}}!{{#in_ca}} Well, ${{taxed_value}}, after taxes.{{/in_ca}}';
var data = {
"name": "Chris",
"value": 10000,
"taxed_value": 10000 - (10000 * 0.4),
"in_ca": true
'name': 'Chris',
'value': 10000,
'taxed_value': 10000 - (10000 * 0.4),
'in_ca': true
};
shouldCompileTo(string, data, "Hello Chris. You have just won $10000! Well, $6000, after taxes.", "the hello world mustache example works");
shouldCompileTo(string, data, 'Hello Chris. You have just won $10000! Well, $6000, after taxes.', 'the hello world mustache example works');
});
it('GH-731: zero context rendering', function() {
@@ -145,4 +155,21 @@ describe('Regressions', function() {
shouldCompileTo('{{str bar.baz}}', [{}, helpers], 'undefined');
});
it('GH-926: Depths and de-dupe', function() {
var context = {
name: 'foo',
data: [
1
],
notData: [
1
]
};
var template = CompilerContext.compile('{{#if dater}}{{#each data}}{{../name}}{{/each}}{{else}}{{#each notData}}{{../name}}{{/each}}{{/if}}');
var result = template(context);
equals(result, 'foo');
});
});
+7 -7
View File
@@ -1,21 +1,21 @@
if (typeof(require) !== 'undefined' && require.extensions[".handlebars"]) {
if (typeof require !== 'undefined' && require.extensions['.handlebars']) {
describe('Require', function() {
it('Load .handlebars files with require()', function() {
var template = require("./artifacts/example_1");
equal(template, require("./artifacts/example_1.handlebars"));
var template = require('./artifacts/example_1');
equal(template, require('./artifacts/example_1.handlebars'));
var expected = 'foo\n';
var result = template({foo: "foo"});
var result = template({foo: 'foo'});
equal(result, expected);
});
it('Load .hbs files with require()', function() {
var template = require("./artifacts/example_2");
equal(template, require("./artifacts/example_2.hbs"));
var template = require('./artifacts/example_2');
equal(template, require('./artifacts/example_2.hbs'));
var expected = 'Hello, World!\n';
var result = template({name: "World"});
var result = template({name: 'World'});
equal(result, expected);
});
+29 -3
View File
@@ -1,5 +1,3 @@
/*globals Handlebars, shouldThrow */
describe('runtime', function() {
describe('#template', function() {
it('should throw on invalid templates', function() {
@@ -48,6 +46,16 @@ describe('runtime', function() {
template._child(1);
}, Error, 'must pass parent depths');
});
it('should throw for block param methods without params', function() {
shouldThrow(function() {
var template = Handlebars.compile('{{#foo as |foo|}}{{foo}}{{/foo}}');
// Calling twice to hit the non-compiled case.
template._setup({});
template._setup({});
template._child(1);
}, Error, 'must pass block params');
});
it('should expose child template', function() {
var template = Handlebars.compile('{{#foo}}bar{{/foo}}');
// Calling twice to hit the non-compiled case.
@@ -57,7 +65,25 @@ describe('runtime', function() {
it('should render depthed content', function() {
var template = Handlebars.compile('{{#foo}}{{../bar}}{{/foo}}');
// Calling twice to hit the non-compiled case.
equal(template._child(1, undefined, [{bar: 'baz'}])(), 'baz');
equal(template._child(1, undefined, [], [{bar: 'baz'}])(), 'baz');
});
});
describe('#noConflict', function() {
if (!CompilerContext.browser) {
return;
}
it('should reset on no conflict', function() {
var reset = Handlebars;
Handlebars.noConflict();
equal(Handlebars, 'no-conflict');
Handlebars = 'really, none';
reset.noConflict();
equal(Handlebars, 'really, none');
Handlebars = reset;
});
});
});
+50
View File
@@ -0,0 +1,50 @@
try {
if (typeof define !== 'function' || !define.amd) {
var SourceMap = require('source-map'),
SourceMapConsumer = SourceMap.SourceMapConsumer;
}
} catch (err) {
/* NOP for in browser */
}
describe('source-map', function() {
if (!Handlebars.precompile || !SourceMap) {
return;
}
it('should safely include source map info', function() {
var template = Handlebars.precompile('{{hello}}', {destName: 'dest.js', srcName: 'src.hbs'});
equal(!!template.code, true);
equal(!!template.map, !CompilerContext.browser);
});
it('should map source properly', function() {
var templateSource = ' b{{hello}} \n {{bar}}a {{#block arg hash=(subex 1 subval)}}{{/block}}',
template = Handlebars.precompile(templateSource, {destName: 'dest.js', srcName: 'src.hbs'});
if (template.map) {
var consumer = new SourceMapConsumer(template.map),
lines = template.code.split('\n'),
srcLines = templateSource.split('\n'),
generated = grepLine('" b"', lines),
source = grepLine(' b', srcLines);
var mapped = consumer.originalPositionFor(generated);
equal(mapped.line, source.line);
equal(mapped.column, source.column);
}
});
});
function grepLine(token, lines) {
for (var i = 0; i < lines.length; i++) {
var column = lines[i].indexOf(token);
if (column >= 0) {
return {
line: i + 1,
column: column
};
}
}
}
+6 -5
View File
@@ -5,12 +5,11 @@ describe('spec', function() {
}
var _ = require('underscore'),
Handlebars = require('../lib'),
fs = require('fs');
var specDir =__dirname + '/mustache/specs/';
var specDir = __dirname + '/mustache/specs/';
var specs = _.filter(fs.readdirSync(specDir), function(name) {
return /.*\.json$/.test(name);
return (/.*\.json$/).test(name);
});
_.each(specs, function(name) {
@@ -26,8 +25,8 @@ describe('spec', function() {
// We nest the entire response from partials, not just the literals
|| (name === 'partials.json' && test.name === 'Standalone Indentation')
|| /\{\{\=/.test(test.template)
|| _.any(test.partials, function(partial) { return /\{\{\=/.test(partial); })) {
|| (/\{\{\=/).test(test.template)
|| _.any(test.partials, function(partial) { return (/\{\{\=/).test(partial); })) {
it.skip(name + ' - ' + test.name);
return;
}
@@ -35,7 +34,9 @@ describe('spec', function() {
var data = _.clone(test.data);
if (data.lambda) {
// Blergh
/*eslint-disable no-eval */
data.lambda = eval('(' + data.lambda.js + ')');
/*eslint-enable no-eval */
}
it(name + ' - ' + test.name, function() {
if (test.partials) {
+123
View File
@@ -0,0 +1,123 @@
var Exception = Handlebars.Exception;
describe('strict', function() {
describe('strict mode', function() {
it('should error on missing property lookup', function() {
shouldThrow(function() {
var template = CompilerContext.compile('{{hello}}', {strict: true});
template({});
}, Exception, /"hello" not defined in/);
});
it('should error on missing child', function() {
var template = CompilerContext.compile('{{hello.bar}}', {strict: true});
equals(template({hello: {bar: 'foo'}}), 'foo');
shouldThrow(function() {
template({hello: {}});
}, Exception, /"bar" not defined in/);
});
it('should handle explicit undefined', function() {
var template = CompilerContext.compile('{{hello.bar}}', {strict: true});
equals(template({hello: {bar: undefined}}), '');
});
it('should error on missing property lookup in known helpers mode', function() {
shouldThrow(function() {
var template = CompilerContext.compile('{{hello}}', {strict: true, knownHelpersOnly: true});
template({});
}, Exception, /"hello" not defined in/);
});
it('should error on missing context', function() {
shouldThrow(function() {
var template = CompilerContext.compile('{{hello}}', {strict: true});
template();
}, Error);
});
it('should error on missing data lookup', function() {
var template = CompilerContext.compile('{{@hello}}', {strict: true});
equals(template(undefined, {data: {hello: 'foo'}}), 'foo');
shouldThrow(function() {
template();
}, Error);
});
it('should not run helperMissing for helper calls', function() {
shouldThrow(function() {
var template = CompilerContext.compile('{{hello foo}}', {strict: true});
template({foo: true});
}, Exception, /"hello" not defined in/);
shouldThrow(function() {
var template = CompilerContext.compile('{{#hello foo}}{{/hello}}', {strict: true});
template({foo: true});
}, Exception, /"hello" not defined in/);
});
it('should throw on ambiguous blocks', function() {
shouldThrow(function() {
var template = CompilerContext.compile('{{#hello}}{{/hello}}', {strict: true});
template({});
}, Exception, /"hello" not defined in/);
shouldThrow(function() {
var template = CompilerContext.compile('{{^hello}}{{/hello}}', {strict: true});
template({});
}, Exception, /"hello" not defined in/);
shouldThrow(function() {
var template = CompilerContext.compile('{{#hello.bar}}{{/hello.bar}}', {strict: true});
template({hello: {}});
}, Exception, /"bar" not defined in/);
});
});
describe('assume objects', function() {
it('should ignore missing property', function() {
var template = CompilerContext.compile('{{hello}}', {assumeObjects: true});
equal(template({}), '');
});
it('should ignore missing child', function() {
var template = CompilerContext.compile('{{hello.bar}}', {assumeObjects: true});
equal(template({hello: {}}), '');
});
it('should error on missing object', function() {
shouldThrow(function() {
var template = CompilerContext.compile('{{hello.bar}}', {assumeObjects: true});
template({});
}, Error);
});
it('should error on missing context', function() {
shouldThrow(function() {
var template = CompilerContext.compile('{{hello}}', {assumeObjects: true});
template();
}, Error);
});
it('should error on missing data lookup', function() {
shouldThrow(function() {
var template = CompilerContext.compile('{{@hello.bar}}', {assumeObjects: true});
template();
}, Error);
});
it('should execute blockHelperMissing', function() {
var template = CompilerContext.compile('{{^hello}}foo{{/hello}}', {assumeObjects: true});
equals(template({}), 'foo');
});
});
});
+56 -56
View File
@@ -1,43 +1,43 @@
describe('string params mode', function() {
it("arguments to helpers can be retrieved from options hash in string form", function() {
it('arguments to helpers can be retrieved from options hash in string form', function() {
var template = CompilerContext.compile('{{wycats is.a slave.driver}}', {stringParams: true});
var helpers = {
wycats: function(passiveVoice, noun) {
return "HELP ME MY BOSS " + passiveVoice + ' ' + noun;
return 'HELP ME MY BOSS ' + passiveVoice + ' ' + noun;
}
};
var result = template({}, {helpers: helpers});
equals(result, "HELP ME MY BOSS is.a slave.driver", "String parameters output");
equals(result, 'HELP ME MY BOSS is.a slave.driver', 'String parameters output');
});
it("when using block form, arguments to helpers can be retrieved from options hash in string form", function() {
it('when using block form, arguments to helpers can be retrieved from options hash in string form', function() {
var template = CompilerContext.compile('{{#wycats is.a slave.driver}}help :({{/wycats}}', {stringParams: true});
var helpers = {
wycats: function(passiveVoice, noun, options) {
return "HELP ME MY BOSS " + passiveVoice + ' ' +
return 'HELP ME MY BOSS ' + passiveVoice + ' ' +
noun + ': ' + options.fn(this);
}
};
var result = template({}, {helpers: helpers});
equals(result, "HELP ME MY BOSS is.a slave.driver: help :(", "String parameters output");
equals(result, 'HELP ME MY BOSS is.a slave.driver: help :(', 'String parameters output');
});
it("when inside a block in String mode, .. passes the appropriate context in the options hash", function() {
it('when inside a block in String mode, .. passes the appropriate context in the options hash', function() {
var template = CompilerContext.compile('{{#with dale}}{{tomdale ../need dad.joke}}{{/with}}', {stringParams: true});
var helpers = {
tomdale: function(desire, noun, options) {
return "STOP ME FROM READING HACKER NEWS I " +
options.contexts[0][desire] + " " + noun;
return 'STOP ME FROM READING HACKER NEWS I ' +
options.contexts[0][desire] + ' ' + noun;
},
"with": function(context, options) {
'with': function(context, options) {
return options.fn(options.contexts[0][context]);
}
};
@@ -48,88 +48,88 @@ describe('string params mode', function() {
need: 'need-a'
}, {helpers: helpers});
equals(result, "STOP ME FROM READING HACKER NEWS I need-a dad.joke", "Proper context variable output");
equals(result, 'STOP ME FROM READING HACKER NEWS I need-a dad.joke', 'Proper context variable output');
});
it("information about the types is passed along", function() {
var template = CompilerContext.compile('{{tomdale "need" dad.joke true false}}', { stringParams: true });
it('information about the types is passed along', function() {
var template = CompilerContext.compile("{{tomdale 'need' dad.joke true false}}", { stringParams: true });
var helpers = {
tomdale: function(desire, noun, trueBool, falseBool, options) {
equal(options.types[0], 'STRING', "the string type is passed");
equal(options.types[1], 'ID', "the expression type is passed");
equal(options.types[2], 'BOOLEAN', "the expression type is passed");
equal(desire, "need", "the string form is passed for strings");
equal(noun, "dad.joke", "the string form is passed for expressions");
equal(trueBool, true, "raw booleans are passed through");
equal(falseBool, false, "raw booleans are passed through");
return "Helper called";
equal(options.types[0], 'StringLiteral', 'the string type is passed');
equal(options.types[1], 'PathExpression', 'the expression type is passed');
equal(options.types[2], 'BooleanLiteral', 'the expression type is passed');
equal(desire, 'need', 'the string form is passed for strings');
equal(noun, 'dad.joke', 'the string form is passed for expressions');
equal(trueBool, true, 'raw booleans are passed through');
equal(falseBool, false, 'raw booleans are passed through');
return 'Helper called';
}
};
var result = template({}, { helpers: helpers });
equal(result, "Helper called");
equal(result, 'Helper called');
});
it("hash parameters get type information", function() {
var template = CompilerContext.compile('{{tomdale he.says desire="need" noun=dad.joke bool=true}}', { stringParams: true });
it('hash parameters get type information', function() {
var template = CompilerContext.compile("{{tomdale he.says desire='need' noun=dad.joke bool=true}}", { stringParams: true });
var helpers = {
tomdale: function(exclamation, options) {
equal(exclamation, "he.says");
equal(options.types[0], "ID");
equal(exclamation, 'he.says');
equal(options.types[0], 'PathExpression');
equal(options.hashTypes.desire, "STRING");
equal(options.hashTypes.noun, "ID");
equal(options.hashTypes.bool, "BOOLEAN");
equal(options.hash.desire, "need");
equal(options.hash.noun, "dad.joke");
equal(options.hashTypes.desire, 'StringLiteral');
equal(options.hashTypes.noun, 'PathExpression');
equal(options.hashTypes.bool, 'BooleanLiteral');
equal(options.hash.desire, 'need');
equal(options.hash.noun, 'dad.joke');
equal(options.hash.bool, true);
return "Helper called";
return 'Helper called';
}
};
var result = template({}, { helpers: helpers });
equal(result, "Helper called");
equal(result, 'Helper called');
});
it("hash parameters get context information", function() {
var template = CompilerContext.compile('{{#with dale}}{{tomdale he.says desire="need" noun=../dad/joke bool=true}}{{/with}}', { stringParams: true });
it('hash parameters get context information', function() {
var template = CompilerContext.compile("{{#with dale}}{{tomdale he.says desire='need' noun=../dad/joke bool=true}}{{/with}}", { stringParams: true });
var context = {dale: {}};
var helpers = {
tomdale: function(exclamation, options) {
equal(exclamation, "he.says");
equal(options.types[0], "ID");
equal(exclamation, 'he.says');
equal(options.types[0], 'PathExpression');
equal(options.contexts.length, 1);
equal(options.hashContexts.noun, context);
equal(options.hash.desire, "need");
equal(options.hash.noun, "dad.joke");
equal(options.hash.desire, 'need');
equal(options.hash.noun, 'dad.joke');
equal(options.hash.bool, true);
return "Helper called";
return 'Helper called';
},
"with": function(context, options) {
return options.fn(options.contexts[0][context]);
'with': function(withContext, options) {
return options.fn(options.contexts[0][withContext]);
}
};
var result = template(context, { helpers: helpers });
equal(result, "Helper called");
equal(result, 'Helper called');
});
it("when inside a block in String mode, .. passes the appropriate context in the options hash to a block helper", function() {
it('when inside a block in String mode, .. passes the appropriate context in the options hash to a block helper', function() {
var template = CompilerContext.compile('{{#with dale}}{{#tomdale ../need dad.joke}}wot{{/tomdale}}{{/with}}', {stringParams: true});
var helpers = {
tomdale: function(desire, noun, options) {
return "STOP ME FROM READING HACKER NEWS I " +
options.contexts[0][desire] + " " + noun + " " +
return 'STOP ME FROM READING HACKER NEWS I ' +
options.contexts[0][desire] + ' ' + noun + ' ' +
options.fn(this);
},
"with": function(context, options) {
'with': function(context, options) {
return options.fn(options.contexts[0][context]);
}
};
@@ -140,23 +140,23 @@ describe('string params mode', function() {
need: 'need-a'
}, {helpers: helpers});
equals(result, "STOP ME FROM READING HACKER NEWS I need-a dad.joke wot", "Proper context variable output");
equals(result, 'STOP ME FROM READING HACKER NEWS I need-a dad.joke wot', 'Proper context variable output');
});
it("with nested block ambiguous", function() {
it('with nested block ambiguous', function() {
var template = CompilerContext.compile('{{#with content}}{{#view}}{{firstName}} {{lastName}}{{/view}}{{/with}}', {stringParams: true});
var helpers = {
'with': function(options) {
return "WITH";
var helpers = {
'with': function() {
return 'WITH';
},
view: function() {
return "VIEW";
return 'VIEW';
}
};
var result = template({}, {helpers: helpers});
equals(result, "WITH");
equals(result, 'WITH');
});
it('should handle DATA', function() {
@@ -164,8 +164,8 @@ describe('string params mode', function() {
var helpers = {
foo: function(bar, options) {
equal(bar, 'bar');
equal(options.types[0], 'DATA');
equal(bar, '@bar');
equal(options.types[0], 'PathExpression');
return 'Foo!';
}
};
+82 -84
View File
@@ -1,183 +1,182 @@
/*global CompilerContext, shouldCompileTo */
describe('subexpressions', function() {
it("arg-less helper", function() {
var string = "{{foo (bar)}}!";
var context = {};
var helpers = {
it('arg-less helper', function() {
var string = '{{foo (bar)}}!';
var context = {};
var helpers = {
foo: function(val) {
return val+val;
return val + val;
},
bar: function() {
return "LOL";
return 'LOL';
}
};
shouldCompileTo(string, [context, helpers], "LOLLOL!");
shouldCompileTo(string, [context, helpers], 'LOLLOL!');
});
it("helper w args", function() {
var string = '{{blog (equal a b)}}';
it('helper w args', function() {
var string = '{{blog (equal a b)}}';
var context = { bar: "LOL" };
var helpers = {
var context = { bar: 'LOL' };
var helpers = {
blog: function(val) {
return "val is " + val;
return 'val is ' + val;
},
equal: function(x, y) {
return x === y;
}
};
shouldCompileTo(string, [context, helpers], "val is true");
shouldCompileTo(string, [context, helpers], 'val is true');
});
it("mixed paths and helpers", function() {
var string = '{{blog baz.bat (equal a b) baz.bar}}';
it('mixed paths and helpers', function() {
var string = '{{blog baz.bat (equal a b) baz.bar}}';
var context = { bar: "LOL", baz: {bat: 'foo!', bar: 'bar!'} };
var helpers = {
var context = { bar: 'LOL', baz: {bat: 'foo!', bar: 'bar!'} };
var helpers = {
blog: function(val, that, theOther) {
return "val is " + val + ', ' + that + ' and ' + theOther;
return 'val is ' + val + ', ' + that + ' and ' + theOther;
},
equal: function(x, y) {
return x === y;
}
};
shouldCompileTo(string, [context, helpers], "val is foo!, true and bar!");
shouldCompileTo(string, [context, helpers], 'val is foo!, true and bar!');
});
it("supports much nesting", function() {
var string = '{{blog (equal (equal true true) true)}}';
it('supports much nesting', function() {
var string = '{{blog (equal (equal true true) true)}}';
var context = { bar: "LOL" };
var helpers = {
var context = { bar: 'LOL' };
var helpers = {
blog: function(val) {
return "val is " + val;
return 'val is ' + val;
},
equal: function(x, y) {
return x === y;
}
};
shouldCompileTo(string, [context, helpers], "val is true");
shouldCompileTo(string, [context, helpers], 'val is true');
});
it('GH-800 : Complex subexpressions', function() {
var context = {a: 'a', b:'b', c:{c:'c'}, d:'d', e: {e: 'e'}};
var helpers = {
var context = {a: 'a', b: 'b', c: {c: 'c'}, d: 'd', e: {e: 'e'}};
var helpers = {
dash: function(a, b) {
return a + "-" + b;
return a + '-' + b;
},
concat: function(a, b) {
return a + b;
}
};
shouldCompileTo('{{dash "abc" (concat a b)}}', [context, helpers], 'abc-ab');
shouldCompileTo("{{dash 'abc' (concat a b)}}", [context, helpers], 'abc-ab');
shouldCompileTo('{{dash d (concat a b)}}', [context, helpers], 'd-ab');
shouldCompileTo('{{dash c.c (concat a b)}}', [context, helpers], 'c-ab');
shouldCompileTo('{{dash (concat a b) c.c}}', [context, helpers], 'ab-c');
shouldCompileTo('{{dash (concat a e.e) c.c}}', [context, helpers], 'ae-c');
});
it("provides each nested helper invocation its own options hash", function() {
it('provides each nested helper invocation its own options hash', function() {
var string = '{{equal (equal true true) true}}';
var lastOptions = null;
var helpers = {
var helpers = {
equal: function(x, y, options) {
if (!options || options === lastOptions) {
throw new Error("options hash was reused");
throw new Error('options hash was reused');
}
lastOptions = options;
return x === y;
}
};
shouldCompileTo(string, [{}, helpers], "true");
shouldCompileTo(string, [{}, helpers], 'true');
});
it("with hashes", function() {
var string = '{{blog (equal (equal true true) true fun="yes")}}';
it('with hashes', function() {
var string = "{{blog (equal (equal true true) true fun='yes')}}";
var context = { bar: "LOL" };
var helpers = {
var context = { bar: 'LOL' };
var helpers = {
blog: function(val) {
return "val is " + val;
return 'val is ' + val;
},
equal: function(x, y) {
return x === y;
}
};
shouldCompileTo(string, [context, helpers], "val is true");
shouldCompileTo(string, [context, helpers], 'val is true');
});
it("as hashes", function() {
var string = '{{blog fun=(equal (blog fun=1) "val is 1")}}';
it('as hashes', function() {
var string = "{{blog fun=(equal (blog fun=1) 'val is 1')}}";
var helpers = {
var helpers = {
blog: function(options) {
return "val is " + options.hash.fun;
return 'val is ' + options.hash.fun;
},
equal: function(x, y) {
return x === y;
}
};
shouldCompileTo(string, [{}, helpers], "val is true");
shouldCompileTo(string, [{}, helpers], 'val is true');
});
it("multiple subexpressions in a hash", function() {
it('multiple subexpressions in a hash', function() {
var string = '{{input aria-label=(t "Name") placeholder=(t "Example User")}}';
var helpers = {
input: function(options) {
var hash = options.hash;
var ariaLabel = Handlebars.Utils.escapeExpression(hash['aria-label']);
var hash = options.hash;
var ariaLabel = Handlebars.Utils.escapeExpression(hash['aria-label']);
var placeholder = Handlebars.Utils.escapeExpression(hash.placeholder);
return new Handlebars.SafeString('<input aria-label="' + ariaLabel + '" placeholder="' + placeholder + '" />');
},
t: function(defaultString) {
return new Handlebars.SafeString(defaultString);
}
}
};
shouldCompileTo(string, [{}, helpers], '<input aria-label="Name" placeholder="Example User" />');
});
it("multiple subexpressions in a hash with context", function() {
it('multiple subexpressions in a hash with context', function() {
var string = '{{input aria-label=(t item.field) placeholder=(t item.placeholder)}}';
var context = {
item: {
field: "Name",
placeholder: "Example User"
}
field: 'Name',
placeholder: 'Example User'
}
};
var helpers = {
input: function(options) {
var hash = options.hash;
var ariaLabel = Handlebars.Utils.escapeExpression(hash['aria-label']);
var hash = options.hash;
var ariaLabel = Handlebars.Utils.escapeExpression(hash['aria-label']);
var placeholder = Handlebars.Utils.escapeExpression(hash.placeholder);
return new Handlebars.SafeString('<input aria-label="' + ariaLabel + '" placeholder="' + placeholder + '" />');
},
t: function(defaultString) {
return new Handlebars.SafeString(defaultString);
}
}
};
shouldCompileTo(string, [context, helpers], '<input aria-label="Name" placeholder="Example User" />');
});
});
it("in string params mode,", function() {
it('in string params mode,', function() {
var template = CompilerContext.compile('{{snog (blorg foo x=y) yeah a=b}}', {stringParams: true});
var helpers = {
snog: function(a, b, options) {
equals(a, 'foo');
equals(options.types.length, 2, "string params for outer helper processed correctly");
equals(options.types[0], 'sexpr', "string params for outer helper processed correctly");
equals(options.types[1], 'ID', "string params for outer helper processed correctly");
equals(options.types.length, 2, 'string params for outer helper processed correctly');
equals(options.types[0], 'SubExpression', 'string params for outer helper processed correctly');
equals(options.types[1], 'PathExpression', 'string params for outer helper processed correctly');
return a + b;
},
blorg: function(a, options) {
equals(options.types.length, 1, "string params for inner helper processed correctly");
equals(options.types[0], 'ID', "string params for inner helper processed correctly");
equals(options.types.length, 1, 'string params for inner helper processed correctly');
equals(options.types[0], 'PathExpression', 'string params for inner helper processed correctly');
return a;
}
};
@@ -187,54 +186,53 @@ describe('subexpressions', function() {
yeah: {}
}, {helpers: helpers});
equals(result, "fooyeah");
equals(result, 'fooyeah');
});
it("as hashes in string params mode", function() {
it('as hashes in string params mode', function() {
var template = CompilerContext.compile('{{blog fun=(bork)}}', {stringParams: true});
var helpers = {
var helpers = {
blog: function(options) {
equals(options.hashTypes.fun, 'sexpr');
return "val is " + options.hash.fun;
equals(options.hashTypes.fun, 'SubExpression');
return 'val is ' + options.hash.fun;
},
bork: function() {
return "BORK";
return 'BORK';
}
};
var result = template({}, {helpers: helpers});
equals(result, "val is BORK");
equals(result, 'val is BORK');
});
it("subexpression functions on the context", function() {
var string = "{{foo (bar)}}!";
var context = {
it('subexpression functions on the context', function() {
var string = '{{foo (bar)}}!';
var context = {
bar: function() {
return "LOL";
return 'LOL';
}
};
var helpers = {
var helpers = {
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() {
var string = "{{foo (bar)}}!";
var context = {
bar: "LOL"
var string = '{{foo (bar)}}!';
var context = {
bar: 'LOL'
};
var helpers = {
var helpers = {
foo: function(val) {
return val+val;
return val + val;
}
};
shouldThrow(function() {
shouldCompileTo(string, [context, helpers], "LOLLOL!");
shouldCompileTo(string, [context, helpers], 'LOLLOL!');
});
});
});
+149 -125
View File
@@ -21,7 +21,7 @@ describe('Tokenizer', function() {
var out = [],
token;
while (token = lexer.lex()) {
while ((token = lexer.lex())) {
var result = parser.terminals_[token] || token;
if (!result || result === 'EOF' || result === 'INVALID') {
break;
@@ -33,370 +33,394 @@ describe('Tokenizer', function() {
}
it('tokenizes a simple mustache as "OPEN ID CLOSE"', function() {
var result = tokenize("{{foo}}");
var result = tokenize('{{foo}}');
shouldMatchTokens(result, ['OPEN', 'ID', 'CLOSE']);
shouldBeToken(result[1], "ID", "foo");
shouldBeToken(result[1], 'ID', 'foo');
});
it('supports unescaping with &', function() {
var result = tokenize("{{&bar}}");
var result = tokenize('{{&bar}}');
shouldMatchTokens(result, ['OPEN', 'ID', 'CLOSE']);
shouldBeToken(result[0], "OPEN", "{{&");
shouldBeToken(result[1], "ID", "bar");
shouldBeToken(result[0], 'OPEN', '{{&');
shouldBeToken(result[1], 'ID', 'bar');
});
it('supports unescaping with {{{', function() {
var result = tokenize("{{{bar}}}");
var result = tokenize('{{{bar}}}');
shouldMatchTokens(result, ['OPEN_UNESCAPED', 'ID', 'CLOSE_UNESCAPED']);
shouldBeToken(result[1], "ID", "bar");
shouldBeToken(result[1], 'ID', 'bar');
});
it('supports escaping delimiters', function() {
var result = tokenize("{{foo}} \\{{bar}} {{baz}}");
var result = tokenize('{{foo}} \\{{bar}} {{baz}}');
shouldMatchTokens(result, ['OPEN', 'ID', 'CLOSE', 'CONTENT', 'CONTENT', 'OPEN', 'ID', 'CLOSE']);
shouldBeToken(result[3], "CONTENT", " ");
shouldBeToken(result[4], "CONTENT", "{{bar}} ");
shouldBeToken(result[3], 'CONTENT', ' ');
shouldBeToken(result[4], 'CONTENT', '{{bar}} ');
});
it('supports escaping multiple delimiters', function() {
var result = tokenize("{{foo}} \\{{bar}} \\{{baz}}");
var result = tokenize('{{foo}} \\{{bar}} \\{{baz}}');
shouldMatchTokens(result, ['OPEN', 'ID', 'CLOSE', 'CONTENT', 'CONTENT', 'CONTENT']);
shouldBeToken(result[3], "CONTENT", " ");
shouldBeToken(result[4], "CONTENT", "{{bar}} ");
shouldBeToken(result[5], "CONTENT", "{{baz}}");
shouldBeToken(result[3], 'CONTENT', ' ');
shouldBeToken(result[4], 'CONTENT', '{{bar}} ');
shouldBeToken(result[5], 'CONTENT', '{{baz}}');
});
it('supports escaping a triple stash', function() {
var result = tokenize("{{foo}} \\{{{bar}}} {{baz}}");
var result = tokenize('{{foo}} \\{{{bar}}} {{baz}}');
shouldMatchTokens(result, ['OPEN', 'ID', 'CLOSE', 'CONTENT', 'CONTENT', 'OPEN', 'ID', 'CLOSE']);
shouldBeToken(result[4], "CONTENT", "{{{bar}}} ");
shouldBeToken(result[4], 'CONTENT', '{{{bar}}} ');
});
it('supports escaping escape character', function() {
var result = tokenize("{{foo}} \\\\{{bar}} {{baz}}");
var result = tokenize('{{foo}} \\\\{{bar}} {{baz}}');
shouldMatchTokens(result, ['OPEN', 'ID', 'CLOSE', 'CONTENT', 'OPEN', 'ID', 'CLOSE', 'CONTENT', 'OPEN', 'ID', 'CLOSE']);
shouldBeToken(result[3], "CONTENT", " \\");
shouldBeToken(result[5], "ID", "bar");
shouldBeToken(result[3], 'CONTENT', ' \\');
shouldBeToken(result[5], 'ID', 'bar');
});
it('supports escaping multiple escape characters', function() {
var result = tokenize("{{foo}} \\\\{{bar}} \\\\{{baz}}");
var result = tokenize('{{foo}} \\\\{{bar}} \\\\{{baz}}');
shouldMatchTokens(result, ['OPEN', 'ID', 'CLOSE', 'CONTENT', 'OPEN', 'ID', 'CLOSE', 'CONTENT', 'OPEN', 'ID', 'CLOSE']);
shouldBeToken(result[3], "CONTENT", " \\");
shouldBeToken(result[5], "ID", "bar");
shouldBeToken(result[7], "CONTENT", " \\");
shouldBeToken(result[9], "ID", "baz");
shouldBeToken(result[3], 'CONTENT', ' \\');
shouldBeToken(result[5], 'ID', 'bar');
shouldBeToken(result[7], 'CONTENT', ' \\');
shouldBeToken(result[9], 'ID', 'baz');
});
it('supports escaped mustaches after escaped escape characters', function() {
var result = tokenize("{{foo}} \\\\{{bar}} \\{{baz}}");
var result = tokenize('{{foo}} \\\\{{bar}} \\{{baz}}');
shouldMatchTokens(result, ['OPEN', 'ID', 'CLOSE', 'CONTENT', 'OPEN', 'ID', 'CLOSE', 'CONTENT', 'CONTENT', 'CONTENT']);
shouldBeToken(result[3], "CONTENT", " \\");
shouldBeToken(result[4], "OPEN", "{{");
shouldBeToken(result[5], "ID", "bar");
shouldBeToken(result[7], "CONTENT", " ");
shouldBeToken(result[8], "CONTENT", "{{baz}}");
shouldBeToken(result[3], 'CONTENT', ' \\');
shouldBeToken(result[4], 'OPEN', '{{');
shouldBeToken(result[5], 'ID', 'bar');
shouldBeToken(result[7], 'CONTENT', ' ');
shouldBeToken(result[8], 'CONTENT', '{{baz}}');
});
it('supports escaped escape characters after escaped mustaches', function() {
var result = tokenize("{{foo}} \\{{bar}} \\\\{{baz}}");
var result = tokenize('{{foo}} \\{{bar}} \\\\{{baz}}');
shouldMatchTokens(result, ['OPEN', 'ID', 'CLOSE', 'CONTENT', 'CONTENT', 'CONTENT', 'OPEN', 'ID', 'CLOSE']);
shouldBeToken(result[4], "CONTENT", "{{bar}} ");
shouldBeToken(result[5], "CONTENT", "\\");
shouldBeToken(result[6], "OPEN", "{{");
shouldBeToken(result[7], "ID", "baz");
shouldBeToken(result[4], 'CONTENT', '{{bar}} ');
shouldBeToken(result[5], 'CONTENT', '\\');
shouldBeToken(result[6], 'OPEN', '{{');
shouldBeToken(result[7], 'ID', 'baz');
});
it('supports escaped escape character on a triple stash', function() {
var result = tokenize("{{foo}} \\\\{{{bar}}} {{baz}}");
var result = tokenize('{{foo}} \\\\{{{bar}}} {{baz}}');
shouldMatchTokens(result, ['OPEN', 'ID', 'CLOSE', 'CONTENT', 'OPEN_UNESCAPED', 'ID', 'CLOSE_UNESCAPED', 'CONTENT', 'OPEN', 'ID', 'CLOSE']);
shouldBeToken(result[3], "CONTENT", " \\");
shouldBeToken(result[5], "ID", "bar");
shouldBeToken(result[3], 'CONTENT', ' \\');
shouldBeToken(result[5], 'ID', 'bar');
});
it('tokenizes a simple path', function() {
var result = tokenize("{{foo/bar}}");
var result = tokenize('{{foo/bar}}');
shouldMatchTokens(result, ['OPEN', 'ID', 'SEP', 'ID', 'CLOSE']);
});
it('allows dot notation', function() {
var result = tokenize("{{foo.bar}}");
var result = tokenize('{{foo.bar}}');
shouldMatchTokens(result, ['OPEN', 'ID', 'SEP', 'ID', 'CLOSE']);
shouldMatchTokens(tokenize("{{foo.bar.baz}}"), ['OPEN', 'ID', 'SEP', 'ID', 'SEP', 'ID', 'CLOSE']);
shouldMatchTokens(tokenize('{{foo.bar.baz}}'), ['OPEN', 'ID', 'SEP', 'ID', 'SEP', 'ID', 'CLOSE']);
});
it('allows path literals with []', function() {
var result = tokenize("{{foo.[bar]}}");
var result = tokenize('{{foo.[bar]}}');
shouldMatchTokens(result, ['OPEN', 'ID', 'SEP', 'ID', 'CLOSE']);
});
it('allows multiple path literals on a line with []', function() {
var result = tokenize("{{foo.[bar]}}{{foo.[baz]}}");
var result = tokenize('{{foo.[bar]}}{{foo.[baz]}}');
shouldMatchTokens(result, ['OPEN', 'ID', 'SEP', 'ID', 'CLOSE', 'OPEN', 'ID', 'SEP', 'ID', 'CLOSE']);
});
it('tokenizes {{.}} as OPEN ID CLOSE', function() {
var result = tokenize("{{.}}");
var result = tokenize('{{.}}');
shouldMatchTokens(result, ['OPEN', 'ID', 'CLOSE']);
});
it('tokenizes a path as "OPEN (ID SEP)* ID CLOSE"', function() {
var result = tokenize("{{../foo/bar}}");
var result = tokenize('{{../foo/bar}}');
shouldMatchTokens(result, ['OPEN', 'ID', 'SEP', 'ID', 'SEP', 'ID', 'CLOSE']);
shouldBeToken(result[1], "ID", "..");
shouldBeToken(result[1], 'ID', '..');
});
it('tokenizes a path with .. as a parent path', function() {
var result = tokenize("{{../foo.bar}}");
var result = tokenize('{{../foo.bar}}');
shouldMatchTokens(result, ['OPEN', 'ID', 'SEP', 'ID', 'SEP', 'ID', 'CLOSE']);
shouldBeToken(result[1], "ID", "..");
shouldBeToken(result[1], 'ID', '..');
});
it('tokenizes a path with this/foo as OPEN ID SEP ID CLOSE', function() {
var result = tokenize("{{this/foo}}");
var result = tokenize('{{this/foo}}');
shouldMatchTokens(result, ['OPEN', 'ID', 'SEP', 'ID', 'CLOSE']);
shouldBeToken(result[1], "ID", "this");
shouldBeToken(result[3], "ID", "foo");
shouldBeToken(result[1], 'ID', 'this');
shouldBeToken(result[3], 'ID', 'foo');
});
it('tokenizes a simple mustache with spaces as "OPEN ID CLOSE"', function() {
var result = tokenize("{{ foo }}");
var result = tokenize('{{ foo }}');
shouldMatchTokens(result, ['OPEN', 'ID', 'CLOSE']);
shouldBeToken(result[1], "ID", "foo");
shouldBeToken(result[1], 'ID', 'foo');
});
it('tokenizes a simple mustache with line breaks as "OPEN ID ID CLOSE"', function() {
var result = tokenize("{{ foo \n bar }}");
var result = tokenize('{{ foo \n bar }}');
shouldMatchTokens(result, ['OPEN', 'ID', 'ID', 'CLOSE']);
shouldBeToken(result[1], "ID", "foo");
shouldBeToken(result[1], 'ID', 'foo');
});
it('tokenizes raw content as "CONTENT"', function() {
var result = tokenize("foo {{ bar }} baz");
var result = tokenize('foo {{ bar }} baz');
shouldMatchTokens(result, ['CONTENT', 'OPEN', 'ID', 'CLOSE', 'CONTENT']);
shouldBeToken(result[0], "CONTENT", "foo ");
shouldBeToken(result[4], "CONTENT", " baz");
shouldBeToken(result[0], 'CONTENT', 'foo ');
shouldBeToken(result[4], 'CONTENT', ' baz');
});
it('tokenizes a partial as "OPEN_PARTIAL ID CLOSE"', function() {
var result = tokenize("{{> foo}}");
var result = tokenize('{{> foo}}');
shouldMatchTokens(result, ['OPEN_PARTIAL', 'ID', 'CLOSE']);
});
it('tokenizes a partial with context as "OPEN_PARTIAL ID ID CLOSE"', function() {
var result = tokenize("{{> foo bar }}");
var result = tokenize('{{> foo bar }}');
shouldMatchTokens(result, ['OPEN_PARTIAL', 'ID', 'ID', 'CLOSE']);
});
it('tokenizes a partial without spaces as "OPEN_PARTIAL ID CLOSE"', function() {
var result = tokenize("{{>foo}}");
var result = tokenize('{{>foo}}');
shouldMatchTokens(result, ['OPEN_PARTIAL', 'ID', 'CLOSE']);
});
it('tokenizes a partial space at the }); as "OPEN_PARTIAL ID CLOSE"', function() {
var result = tokenize("{{>foo }}");
var result = tokenize('{{>foo }}');
shouldMatchTokens(result, ['OPEN_PARTIAL', 'ID', 'CLOSE']);
});
it('tokenizes a partial space at the }); as "OPEN_PARTIAL ID CLOSE"', function() {
var result = tokenize("{{>foo/bar.baz }}");
var result = tokenize('{{>foo/bar.baz }}');
shouldMatchTokens(result, ['OPEN_PARTIAL', 'ID', 'SEP', 'ID', 'SEP', 'ID', 'CLOSE']);
});
it('tokenizes a comment as "COMMENT"', function() {
var result = tokenize("foo {{! this is a comment }} bar {{ baz }}");
var result = tokenize('foo {{! this is a comment }} bar {{ baz }}');
shouldMatchTokens(result, ['CONTENT', 'COMMENT', 'CONTENT', 'OPEN', 'ID', 'CLOSE']);
shouldBeToken(result[1], "COMMENT", " this is a comment ");
shouldBeToken(result[1], 'COMMENT', '{{! this is a comment }}');
});
it('tokenizes a block comment as "COMMENT"', function() {
var result = tokenize("foo {{!-- this is a {{comment}} --}} bar {{ baz }}");
var result = tokenize('foo {{!-- this is a {{comment}} --}} bar {{ baz }}');
shouldMatchTokens(result, ['CONTENT', 'COMMENT', 'CONTENT', 'OPEN', 'ID', 'CLOSE']);
shouldBeToken(result[1], "COMMENT", " this is a {{comment}} ");
shouldBeToken(result[1], 'COMMENT', '{{!-- this is a {{comment}} --}}');
});
it('tokenizes a block comment with whitespace as "COMMENT"', function() {
var result = tokenize("foo {{!-- this is a\n{{comment}}\n--}} bar {{ baz }}");
var result = tokenize('foo {{!-- this is a\n{{comment}}\n--}} bar {{ baz }}');
shouldMatchTokens(result, ['CONTENT', 'COMMENT', 'CONTENT', 'OPEN', 'ID', 'CLOSE']);
shouldBeToken(result[1], "COMMENT", " this is a\n{{comment}}\n");
shouldBeToken(result[1], 'COMMENT', '{{!-- this is a\n{{comment}}\n--}}');
});
it('tokenizes open and closing blocks as OPEN_BLOCK, ID, CLOSE ..., OPEN_ENDBLOCK ID CLOSE', function() {
var result = tokenize("{{#foo}}content{{/foo}}");
var result = tokenize('{{#foo}}content{{/foo}}');
shouldMatchTokens(result, ['OPEN_BLOCK', 'ID', 'CLOSE', 'CONTENT', 'OPEN_ENDBLOCK', 'ID', 'CLOSE']);
});
it('tokenizes inverse sections as "INVERSE"', function() {
shouldMatchTokens(tokenize("{{^}}"), ['INVERSE']);
shouldMatchTokens(tokenize("{{else}}"), ['INVERSE']);
shouldMatchTokens(tokenize("{{ else }}"), ['INVERSE']);
shouldMatchTokens(tokenize('{{^}}'), ['INVERSE']);
shouldMatchTokens(tokenize('{{else}}'), ['INVERSE']);
shouldMatchTokens(tokenize('{{ else }}'), ['INVERSE']);
});
it('tokenizes inverse sections with ID as "OPEN_INVERSE ID CLOSE"', function() {
var result = tokenize("{{^foo}}");
var result = tokenize('{{^foo}}');
shouldMatchTokens(result, ['OPEN_INVERSE', 'ID', 'CLOSE']);
shouldBeToken(result[1], "ID", "foo");
shouldBeToken(result[1], 'ID', 'foo');
});
it('tokenizes inverse sections with ID and spaces as "OPEN_INVERSE ID CLOSE"', function() {
var result = tokenize("{{^ foo }}");
var result = tokenize('{{^ foo }}');
shouldMatchTokens(result, ['OPEN_INVERSE', 'ID', 'CLOSE']);
shouldBeToken(result[1], "ID", "foo");
shouldBeToken(result[1], 'ID', 'foo');
});
it('tokenizes mustaches with params as "OPEN ID ID ID CLOSE"', function() {
var result = tokenize("{{ foo bar baz }}");
var result = tokenize('{{ foo bar baz }}');
shouldMatchTokens(result, ['OPEN', 'ID', 'ID', 'ID', 'CLOSE']);
shouldBeToken(result[1], "ID", "foo");
shouldBeToken(result[2], "ID", "bar");
shouldBeToken(result[3], "ID", "baz");
shouldBeToken(result[1], 'ID', 'foo');
shouldBeToken(result[2], 'ID', 'bar');
shouldBeToken(result[3], 'ID', 'baz');
});
it('tokenizes mustaches with String params as "OPEN ID ID STRING CLOSE"', function() {
var result = tokenize("{{ foo bar \"baz\" }}");
var result = tokenize('{{ foo bar \'baz\' }}');
shouldMatchTokens(result, ['OPEN', 'ID', 'ID', 'STRING', 'CLOSE']);
shouldBeToken(result[3], "STRING", "baz");
shouldBeToken(result[3], 'STRING', 'baz');
});
it('tokenizes mustaches with String params using single quotes as "OPEN ID ID STRING CLOSE"', function() {
var result = tokenize("{{ foo bar \'baz\' }}");
shouldMatchTokens(result, ['OPEN', 'ID', 'ID', 'STRING', 'CLOSE']);
shouldBeToken(result[3], "STRING", "baz");
shouldBeToken(result[3], 'STRING', 'baz');
});
it('tokenizes String params with spaces inside as "STRING"', function() {
var result = tokenize("{{ foo bar \"baz bat\" }}");
var result = tokenize('{{ foo bar "baz bat" }}');
shouldMatchTokens(result, ['OPEN', 'ID', 'ID', 'STRING', 'CLOSE']);
shouldBeToken(result[3], "STRING", "baz bat");
shouldBeToken(result[3], 'STRING', 'baz bat');
});
it('tokenizes String params with escapes quotes as STRING', function() {
var result = tokenize('{{ foo "bar\\"baz" }}');
shouldMatchTokens(result, ['OPEN', 'ID', 'STRING', 'CLOSE']);
shouldBeToken(result[2], "STRING", 'bar"baz');
shouldBeToken(result[2], 'STRING', 'bar"baz');
});
it('tokenizes String params using single quotes with escapes quotes as STRING', function() {
var result = tokenize("{{ foo 'bar\\'baz' }}");
shouldMatchTokens(result, ['OPEN', 'ID', 'STRING', 'CLOSE']);
shouldBeToken(result[2], "STRING", "bar'baz");
shouldBeToken(result[2], 'STRING', "bar'baz");
});
it('tokenizes numbers', function() {
var result = tokenize('{{ foo 1 }}');
shouldMatchTokens(result, ['OPEN', 'ID', 'NUMBER', 'CLOSE']);
shouldBeToken(result[2], "NUMBER", "1");
shouldBeToken(result[2], 'NUMBER', '1');
result = tokenize('{{ foo 1.1 }}');
shouldMatchTokens(result, ['OPEN', 'ID', 'NUMBER', 'CLOSE']);
shouldBeToken(result[2], "NUMBER", "1.1");
shouldBeToken(result[2], 'NUMBER', '1.1');
result = tokenize('{{ foo -1 }}');
shouldMatchTokens(result, ['OPEN', 'ID', 'NUMBER', 'CLOSE']);
shouldBeToken(result[2], "NUMBER", "-1");
shouldBeToken(result[2], 'NUMBER', '-1');
result = tokenize('{{ foo -1.1 }}');
shouldMatchTokens(result, ['OPEN', 'ID', 'NUMBER', 'CLOSE']);
shouldBeToken(result[2], "NUMBER", "-1.1");
shouldBeToken(result[2], 'NUMBER', '-1.1');
});
it('tokenizes booleans', function() {
var result = tokenize('{{ foo true }}');
shouldMatchTokens(result, ['OPEN', 'ID', 'BOOLEAN', 'CLOSE']);
shouldBeToken(result[2], "BOOLEAN", "true");
shouldBeToken(result[2], 'BOOLEAN', 'true');
result = tokenize('{{ foo false }}');
shouldMatchTokens(result, ['OPEN', 'ID', 'BOOLEAN', 'CLOSE']);
shouldBeToken(result[2], "BOOLEAN", "false");
shouldBeToken(result[2], 'BOOLEAN', 'false');
});
it('tokenizes undefined and null', function() {
var result = tokenize('{{ foo undefined null }}');
shouldMatchTokens(result, ['OPEN', 'ID', 'UNDEFINED', 'NULL', 'CLOSE']);
shouldBeToken(result[2], 'UNDEFINED', 'undefined');
shouldBeToken(result[3], 'NULL', 'null');
});
it('tokenizes hash arguments', function() {
var result = tokenize("{{ foo bar=baz }}");
var result = tokenize('{{ foo bar=baz }}');
shouldMatchTokens(result, ['OPEN', 'ID', 'ID', 'EQUALS', 'ID', 'CLOSE']);
result = tokenize("{{ foo bar baz=bat }}");
result = tokenize('{{ foo bar baz=bat }}');
shouldMatchTokens(result, ['OPEN', 'ID', 'ID', 'ID', 'EQUALS', 'ID', 'CLOSE']);
result = tokenize("{{ foo bar baz=1 }}");
result = tokenize('{{ foo bar baz=1 }}');
shouldMatchTokens(result, ['OPEN', 'ID', 'ID', 'ID', 'EQUALS', 'NUMBER', 'CLOSE']);
result = tokenize("{{ foo bar baz=true }}");
result = tokenize('{{ foo bar baz=true }}');
shouldMatchTokens(result, ['OPEN', 'ID', 'ID', 'ID', 'EQUALS', 'BOOLEAN', 'CLOSE']);
result = tokenize("{{ foo bar baz=false }}");
result = tokenize('{{ foo bar baz=false }}');
shouldMatchTokens(result, ['OPEN', 'ID', 'ID', 'ID', 'EQUALS', 'BOOLEAN', 'CLOSE']);
result = tokenize("{{ foo bar\n baz=bat }}");
result = tokenize('{{ foo bar\n baz=bat }}');
shouldMatchTokens(result, ['OPEN', 'ID', 'ID', 'ID', 'EQUALS', 'ID', 'CLOSE']);
result = tokenize("{{ foo bar baz=\"bat\" }}");
result = tokenize('{{ foo bar baz=\"bat\" }}');
shouldMatchTokens(result, ['OPEN', 'ID', 'ID', 'ID', 'EQUALS', 'STRING', 'CLOSE']);
result = tokenize("{{ foo bar baz=\"bat\" bam=wot }}");
result = tokenize('{{ foo bar baz=\"bat\" bam=wot }}');
shouldMatchTokens(result, ['OPEN', 'ID', 'ID', 'ID', 'EQUALS', 'STRING', 'ID', 'EQUALS', 'ID', 'CLOSE']);
result = tokenize("{{foo omg bar=baz bat=\"bam\"}}");
result = tokenize('{{foo omg bar=baz bat=\"bam\"}}');
shouldMatchTokens(result, ['OPEN', 'ID', 'ID', 'ID', 'EQUALS', 'ID', 'ID', 'EQUALS', 'STRING', 'CLOSE']);
shouldBeToken(result[2], "ID", "omg");
shouldBeToken(result[2], 'ID', 'omg');
});
it('tokenizes special @ identifiers', function() {
var result = tokenize("{{ @foo }}");
var result = tokenize('{{ @foo }}');
shouldMatchTokens(result, ['OPEN', 'DATA', 'ID', 'CLOSE']);
shouldBeToken(result[2], "ID", "foo");
shouldBeToken(result[2], 'ID', 'foo');
result = tokenize("{{ foo @bar }}");
result = tokenize('{{ foo @bar }}');
shouldMatchTokens(result, ['OPEN', 'ID', 'DATA', 'ID', 'CLOSE']);
shouldBeToken(result[3], "ID", "bar");
shouldBeToken(result[3], 'ID', 'bar');
result = tokenize("{{ foo bar=@baz }}");
result = tokenize('{{ foo bar=@baz }}');
shouldMatchTokens(result, ['OPEN', 'ID', 'ID', 'EQUALS', 'DATA', 'ID', 'CLOSE']);
shouldBeToken(result[5], "ID", "baz");
shouldBeToken(result[5], 'ID', 'baz');
});
it('does not time out in a mustache with a single } followed by EOF', function() {
shouldMatchTokens(tokenize("{{foo}"), ['OPEN', 'ID']);
shouldMatchTokens(tokenize('{{foo}'), ['OPEN', 'ID']);
});
it('does not time out in a mustache when invalid ID characters are used', function() {
shouldMatchTokens(tokenize("{{foo & }}"), ['OPEN', 'ID']);
shouldMatchTokens(tokenize('{{foo & }}'), ['OPEN', 'ID']);
});
it('tokenizes subexpressions', function() {
var result = tokenize("{{foo (bar)}}");
var result = tokenize('{{foo (bar)}}');
shouldMatchTokens(result, ['OPEN', 'ID', 'OPEN_SEXPR', 'ID', 'CLOSE_SEXPR', 'CLOSE']);
shouldBeToken(result[1], "ID", "foo");
shouldBeToken(result[3], "ID", "bar");
shouldBeToken(result[1], 'ID', 'foo');
shouldBeToken(result[3], 'ID', 'bar');
result = tokenize("{{foo (a-x b-y)}}");
result = tokenize('{{foo (a-x b-y)}}');
shouldMatchTokens(result, ['OPEN', 'ID', 'OPEN_SEXPR', 'ID', 'ID', 'CLOSE_SEXPR', 'CLOSE']);
shouldBeToken(result[1], "ID", "foo");
shouldBeToken(result[3], "ID", "a-x");
shouldBeToken(result[4], "ID", "b-y");
shouldBeToken(result[1], 'ID', 'foo');
shouldBeToken(result[3], 'ID', 'a-x');
shouldBeToken(result[4], 'ID', 'b-y');
});
it('tokenizes nested subexpressions', function() {
var result = tokenize("{{foo (bar (lol rofl)) (baz)}}");
var result = tokenize('{{foo (bar (lol rofl)) (baz)}}');
shouldMatchTokens(result, ['OPEN', 'ID', 'OPEN_SEXPR', 'ID', 'OPEN_SEXPR', 'ID', 'ID', 'CLOSE_SEXPR', 'CLOSE_SEXPR', 'OPEN_SEXPR', 'ID', 'CLOSE_SEXPR', 'CLOSE']);
shouldBeToken(result[3], "ID", "bar");
shouldBeToken(result[5], "ID", "lol");
shouldBeToken(result[6], "ID", "rofl");
shouldBeToken(result[10], "ID", "baz");
shouldBeToken(result[3], 'ID', 'bar');
shouldBeToken(result[5], 'ID', 'lol');
shouldBeToken(result[6], 'ID', 'rofl');
shouldBeToken(result[10], 'ID', 'baz');
});
it('tokenizes nested subexpressions: literals', function() {
var result = tokenize("{{foo (bar (lol true) false) (baz 1) (blah 'b') (blorg \"c\")}}");
shouldMatchTokens(result, ['OPEN', 'ID', 'OPEN_SEXPR', 'ID', 'OPEN_SEXPR', 'ID', 'BOOLEAN', 'CLOSE_SEXPR', 'BOOLEAN', 'CLOSE_SEXPR', 'OPEN_SEXPR', 'ID', 'NUMBER', 'CLOSE_SEXPR', 'OPEN_SEXPR', 'ID', 'STRING', 'CLOSE_SEXPR', 'OPEN_SEXPR', 'ID', 'STRING', 'CLOSE_SEXPR', 'CLOSE']);
});
it('tokenizes block params', function() {
var result = tokenize('{{#foo as |bar|}}');
shouldMatchTokens(result, ['OPEN_BLOCK', 'ID', 'OPEN_BLOCK_PARAMS', 'ID', 'CLOSE_BLOCK_PARAMS', 'CLOSE']);
result = tokenize('{{#foo as |bar baz|}}');
shouldMatchTokens(result, ['OPEN_BLOCK', 'ID', 'OPEN_BLOCK_PARAMS', 'ID', 'ID', 'CLOSE_BLOCK_PARAMS', 'CLOSE']);
result = tokenize('{{#foo as | bar baz |}}');
shouldMatchTokens(result, ['OPEN_BLOCK', 'ID', 'OPEN_BLOCK_PARAMS', 'ID', 'ID', 'CLOSE_BLOCK_PARAMS', 'CLOSE']);
result = tokenize('{{#foo as as | bar baz |}}');
shouldMatchTokens(result, ['OPEN_BLOCK', 'ID', 'ID', 'OPEN_BLOCK_PARAMS', 'ID', 'ID', 'CLOSE_BLOCK_PARAMS', 'CLOSE']);
result = tokenize('{{else foo as |bar baz|}}');
shouldMatchTokens(result, ['OPEN_INVERSE_CHAIN', 'ID', 'OPEN_BLOCK_PARAMS', 'ID', 'ID', 'CLOSE_BLOCK_PARAMS', 'CLOSE']);
});
});
+36 -8
View File
@@ -1,4 +1,3 @@
/*global CompilerContext */
describe('track ids', function() {
var context;
beforeEach(function() {
@@ -27,7 +26,7 @@ describe('track ids', function() {
equal(options.ids[0], 'is.a');
equal(options.ids[1], 'slave.driver');
return "HELP ME MY BOSS " + options.ids[0] + ':' + passiveVoice + ' ' + options.ids[1] + ':' + noun;
return 'HELP ME MY BOSS ' + options.ids[0] + ':' + passiveVoice + ' ' + options.ids[1] + ':' + noun;
}
};
@@ -41,7 +40,7 @@ describe('track ids', function() {
equal(options.hashIds.bat, 'is.a');
equal(options.hashIds.baz, 'slave.driver');
return "HELP ME MY BOSS " + options.hashIds.bat + ':' + options.hash.bat + ' ' + options.hashIds.baz + ':' + options.hash.baz;
return 'HELP ME MY BOSS ' + options.hashIds.bat + ':' + options.hash.bat + ' ' + options.hashIds.baz + ':' + options.hash.baz;
}
};
@@ -55,7 +54,7 @@ describe('track ids', function() {
equal(options.ids[0], 'is.a');
equal(options.ids[1], '../slave.driver');
return "HELP ME MY BOSS " + options.ids[0] + ':' + passiveVoice + ' ' + options.ids[1] + ':' + noun;
return 'HELP ME MY BOSS ' + options.ids[0] + ':' + passiveVoice + ' ' + options.ids[1] + ':' + noun;
}
};
@@ -69,11 +68,11 @@ describe('track ids', function() {
equal(options.ids[0], '@is.a');
equal(options.ids[1], '@slave.driver');
return "HELP ME MY BOSS " + options.ids[0] + ':' + passiveVoice + ' ' + options.ids[1] + ':' + noun;
return 'HELP ME MY BOSS ' + options.ids[0] + ':' + passiveVoice + ' ' + options.ids[1] + ':' + noun;
}
};
equals(template({}, {helpers: helpers, data:context}), 'HELP ME MY BOSS @is.a:foo @slave.driver:bar');
equals(template({}, {helpers: helpers, data: context}), 'HELP ME MY BOSS @is.a:foo @slave.driver:bar');
});
it('should return null for constants', function() {
@@ -85,7 +84,7 @@ describe('track ids', function() {
equal(options.ids[1], null);
equal(options.hashIds.key, null);
return "HELP ME MY BOSS " + passiveVoice + ' ' + noun + ' ' + options.hash.key;
return 'HELP ME MY BOSS ' + passiveVoice + ' ' + noun + ' ' + options.hash.key;
}
};
@@ -99,15 +98,39 @@ describe('track ids', function() {
wycats: function(passiveVoice, options) {
equal(options.ids[0], true);
return "HELP ME MY BOSS " + passiveVoice;
return 'HELP ME MY BOSS ' + passiveVoice;
}
};
equals(template(context, {helpers: helpers}), 'HELP ME MY BOSS 1');
});
it('should use block param paths', function() {
var template = CompilerContext.compile('{{#doIt as |is|}}{{wycats is.a slave.driver is}}{{/doIt}}', {trackIds: true});
var helpers = {
doIt: function(options) {
var blockParams = [this.is];
blockParams.path = ['zomg'];
return options.fn(this, {blockParams: blockParams});
},
wycats: function(passiveVoice, noun, blah, options) {
equal(options.ids[0], 'zomg.a');
equal(options.ids[1], 'slave.driver');
equal(options.ids[2], 'zomg');
return 'HELP ME MY BOSS ' + options.ids[0] + ':' + passiveVoice + ' ' + options.ids[1] + ':' + noun;
}
};
equals(template(context, {helpers: helpers}), 'HELP ME MY BOSS zomg.a:foo slave.driver:bar');
});
describe('builtin helpers', function() {
var helpers = {
blockParams: function(name, options) {
return name + ':' + options.ids[0] + '\n';
},
wycats: function(name, options) {
return name + ':' + options.data.contextPath + '\n';
}
@@ -129,6 +152,11 @@ describe('track ids', function() {
equals(template({array: [{name: 'foo'}, {name: 'bar'}]}, {helpers: helpers}), 'foo:.array..0\nbar:.array..1\n');
});
it('should handle block params', function() {
var template = CompilerContext.compile('{{#each array as |value|}}{{blockParams value.name}}{{/each}}', {trackIds: true});
equals(template({array: [{name: 'foo'}, {name: 'bar'}]}, {helpers: helpers}), 'foo:array.0.name\nbar:array.1.name\n');
});
});
describe('#with', function() {
it('should track contextPath', function() {
+13 -8
View File
@@ -1,30 +1,35 @@
/*global Handlebars, shouldCompileTo */
describe('utils', function() {
describe('#SafeString', function() {
it("constructing a safestring from a string and checking its type", function() {
var safe = new Handlebars.SafeString("testing 1, 2, 3");
it('constructing a safestring from a string and checking its type', function() {
var safe = new Handlebars.SafeString('testing 1, 2, 3');
if (!(safe instanceof Handlebars.SafeString)) {
throw new Error('Must be instance of SafeString');
}
equals(safe == 'testing 1, 2, 3', true, 'SafeString is equivalent to its underlying string');
equals(safe.toString(), 'testing 1, 2, 3', 'SafeString is equivalent to its underlying string');
});
it("it should not escape SafeString properties", function() {
var name = new Handlebars.SafeString("<em>Sean O&#x27;Malley</em>");
it('it should not escape SafeString properties', function() {
var name = new Handlebars.SafeString('<em>Sean O&#x27;Malley</em>');
shouldCompileTo('{{name}}', [{ name: name }], "<em>Sean O&#x27;Malley</em>");
shouldCompileTo('{{name}}', [{name: name}], '<em>Sean O&#x27;Malley</em>');
});
});
describe('#escapeExpression', function() {
it('shouhld escape html', function() {
equals(Handlebars.Utils.escapeExpression('foo<&"\'>'), 'foo&lt;&amp;&quot;&#x27;&gt;');
equals(Handlebars.Utils.escapeExpression('foo='), 'foo&#x3D;');
});
it('should not escape SafeString', function() {
var string = new Handlebars.SafeString('foo<&"\'>');
equals(Handlebars.Utils.escapeExpression(string), 'foo<&"\'>');
var obj = {
toHTML: function() {
return 'foo<&"\'>';
}
};
equals(Handlebars.Utils.escapeExpression(obj), 'foo<&"\'>');
});
it('should handle falsy', function() {
equals(Handlebars.Utils.escapeExpression(''), '');
+141
View File
@@ -0,0 +1,141 @@
describe('Visitor', function() {
if (!Handlebars.Visitor || !Handlebars.print) {
return;
}
it('should provide coverage', function() {
// Simply run the thing and make sure it does not fail and that all of the
// stub methods are executed
var visitor = new Handlebars.Visitor();
visitor.accept(Handlebars.parse('{{foo}}{{#foo (bar 1 "1" true undefined null) foo=@data}}{{!comment}}{{> bar }} {{/foo}}'));
});
it('should traverse to stubs', function() {
var visitor = new Handlebars.Visitor();
visitor.StringLiteral = function(string) {
equal(string.value, '2');
};
visitor.NumberLiteral = function(number) {
equal(number.value, 1);
};
visitor.BooleanLiteral = function(bool) {
equal(bool.value, true);
equal(this.parents.length, 3);
equal(this.parents[0].type, 'SubExpression');
equal(this.parents[1].type, 'BlockStatement');
equal(this.parents[2].type, 'Program');
};
visitor.PathExpression = function(id) {
equal(/(foo\.)?bar$/.test(id.original), true);
};
visitor.ContentStatement = function(content) {
equal(content.value, ' ');
};
visitor.CommentStatement = function(comment) {
equal(comment.value, 'comment');
};
visitor.accept(Handlebars.parse('{{#foo.bar (foo.bar 1 "2" true) foo=@foo.bar}}{{!comment}}{{> bar }} {{/foo.bar}}'));
});
it('should return undefined');
describe('mutating', function() {
describe('fields', function() {
it('should replace value', function() {
var visitor = new Handlebars.Visitor();
visitor.mutating = true;
visitor.StringLiteral = function(string) {
return new Handlebars.AST.NumberLiteral(42, string.locInfo);
};
var ast = Handlebars.parse('{{foo foo="foo"}}');
visitor.accept(ast);
equals(Handlebars.print(ast), '{{ PATH:foo [] HASH{foo=NUMBER{42}} }}\n');
});
it('should treat undefined resonse as identity', function() {
var visitor = new Handlebars.Visitor();
visitor.mutating = true;
var ast = Handlebars.parse('{{foo foo=42}}');
visitor.accept(ast);
equals(Handlebars.print(ast), '{{ PATH:foo [] HASH{foo=NUMBER{42}} }}\n');
});
it('should remove false responses', function() {
var visitor = new Handlebars.Visitor();
visitor.mutating = true;
visitor.Hash = function() {
return false;
};
var ast = Handlebars.parse('{{foo foo=42}}');
visitor.accept(ast);
equals(Handlebars.print(ast), '{{ PATH:foo [] }}\n');
});
it('should throw when removing required values', function() {
shouldThrow(function() {
var visitor = new Handlebars.Visitor();
visitor.mutating = true;
visitor.PathExpression = function() {
return false;
};
var ast = Handlebars.parse('{{foo 42}}');
visitor.accept(ast);
}, Handlebars.Exception, 'MustacheStatement requires path');
});
it('should throw when returning non-node responses', function() {
shouldThrow(function() {
var visitor = new Handlebars.Visitor();
visitor.mutating = true;
visitor.PathExpression = function() {
return {};
};
var ast = Handlebars.parse('{{foo 42}}');
visitor.accept(ast);
}, Handlebars.Exception, 'Unexpected node type "undefined" found when accepting path on MustacheStatement');
});
});
describe('arrays', function() {
it('should replace value', function() {
var visitor = new Handlebars.Visitor();
visitor.mutating = true;
visitor.StringLiteral = function(string) {
return new Handlebars.AST.NumberLiteral(42, string.locInfo);
};
var ast = Handlebars.parse('{{foo "foo"}}');
visitor.accept(ast);
equals(Handlebars.print(ast), '{{ PATH:foo [NUMBER{42}] }}\n');
});
it('should treat undefined resonse as identity', function() {
var visitor = new Handlebars.Visitor();
visitor.mutating = true;
var ast = Handlebars.parse('{{foo 42}}');
visitor.accept(ast);
equals(Handlebars.print(ast), '{{ PATH:foo [NUMBER{42}] }}\n');
});
it('should remove false responses', function() {
var visitor = new Handlebars.Visitor();
visitor.mutating = true;
visitor.NumberLiteral = function() {
return false;
};
var ast = Handlebars.parse('{{foo 42}}');
visitor.accept(ast);
equals(Handlebars.print(ast), '{{ PATH:foo [] }}\n');
});
});
});
});
+3 -2
View File
@@ -1,5 +1,3 @@
/*global shouldCompileTo, shouldCompileToWithPartials */
describe('whitespace control', function() {
it('should strip whitespace around mustache calls', function() {
var hash = {foo: 'bar<'};
@@ -66,6 +64,9 @@ describe('whitespace control', function() {
shouldCompileToWithPartials('foo {{~> dude~}} ', [{}, {}, {dude: 'bar'}], true, 'foobar');
shouldCompileToWithPartials('foo {{> dude~}} ', [{}, {}, {dude: 'bar'}], true, 'foo bar');
shouldCompileToWithPartials('foo {{> dude}} ', [{}, {}, {dude: 'bar'}], true, 'foo bar ');
shouldCompileToWithPartials('foo\n {{~> dude}} ', [{}, {}, {dude: 'bar'}], true, 'foobar');
shouldCompileToWithPartials('foo\n {{> dude}} ', [{}, {}, {dude: 'bar'}], true, 'foo\n bar');
});
it('should only strip whitespace once', function() {
+20 -6
View File
@@ -12,7 +12,7 @@ function strip(start, end) {
LEFT_STRIP "~"
RIGHT_STRIP "~"
LOOKAHEAD [=~}\s\/.)]
LOOKAHEAD [=~}\s\/.)|]
LITERAL_LOOKAHEAD [~}\s)]
/*
@@ -56,7 +56,10 @@ ID [^\s!"#%-,\.\/;->@\[-\^`\{-~]+/{LOOKAHEAD}
}
<raw>[^\x00]*?/("{{{{/") { return 'CONTENT'; }
<com>[\s\S]*?"--}}" strip(0,4); this.popState(); return 'COMMENT';
<com>[\s\S]*?"--"{RIGHT_STRIP}?"}}" {
this.popState();
return 'COMMENT';
}
<mu>"(" return 'OPEN_SEXPR';
<mu>")" return 'CLOSE_SEXPR';
@@ -73,11 +76,18 @@ ID [^\s!"#%-,\.\/;->@\[-\^`\{-~]+/{LOOKAHEAD}
<mu>"{{"{LEFT_STRIP}?"^"\s*{RIGHT_STRIP}?"}}" this.popState(); return 'INVERSE';
<mu>"{{"{LEFT_STRIP}?\s*"else"\s*{RIGHT_STRIP}?"}}" this.popState(); return 'INVERSE';
<mu>"{{"{LEFT_STRIP}?"^" return 'OPEN_INVERSE';
<mu>"{{"{LEFT_STRIP}?\s*"else" return 'OPEN_INVERSE';
<mu>"{{"{LEFT_STRIP}?\s*"else" return 'OPEN_INVERSE_CHAIN';
<mu>"{{"{LEFT_STRIP}?"{" return 'OPEN_UNESCAPED';
<mu>"{{"{LEFT_STRIP}?"&" return 'OPEN';
<mu>"{{!--" this.popState(); this.begin('com');
<mu>"{{!"[\s\S]*?"}}" strip(3,5); this.popState(); return 'COMMENT';
<mu>"{{"{LEFT_STRIP}?"!--" {
this.unput(yytext);
this.popState();
this.begin('com');
}
<mu>"{{"{LEFT_STRIP}?"!"[\s\S]*?"}}" {
this.popState();
return 'COMMENT';
}
<mu>"{{"{LEFT_STRIP}? return 'OPEN';
<mu>"=" return 'EQUALS';
@@ -92,11 +102,15 @@ ID [^\s!"#%-,\.\/;->@\[-\^`\{-~]+/{LOOKAHEAD}
<mu>"@" return 'DATA';
<mu>"true"/{LITERAL_LOOKAHEAD} return 'BOOLEAN';
<mu>"false"/{LITERAL_LOOKAHEAD} return 'BOOLEAN';
<mu>"undefined"/{LITERAL_LOOKAHEAD} return 'UNDEFINED';
<mu>"null"/{LITERAL_LOOKAHEAD} return 'NULL';
<mu>\-?[0-9]+(?:\.[0-9]+)?/{LITERAL_LOOKAHEAD} return 'NUMBER';
<mu>"as"\s+"|" return 'OPEN_BLOCK_PARAMS';
<mu>"|" return 'CLOSE_BLOCK_PARAMS';
<mu>{ID} return 'ID';
<mu>'['[^\]]*']' yytext = strip(1,2); return 'ID';
<mu>'['[^\]]*']' return 'ID';
<mu>. return 'INVALID';
<INITIAL,mu><<EOF>> return 'EOF';
+60 -35
View File
@@ -5,11 +5,11 @@
%%
root
: program EOF { yy.prepareProgram($1.statements, true); return $1; }
: program EOF { return $1; }
;
program
: statement* -> new yy.ProgramNode(yy.prepareProgram($1), {}, @$)
: statement* -> new yy.Program($1, null, {}, yy.locInfo(@$))
;
statement
@@ -17,89 +17,114 @@ statement
| block -> $1
| rawBlock -> $1
| partial -> $1
| CONTENT -> new yy.ContentNode($1, @$)
| COMMENT -> new yy.CommentNode($1, @$)
| content -> $1
| COMMENT -> new yy.CommentStatement(yy.stripComment($1), yy.stripFlags($1, $1), yy.locInfo(@$))
;
content
: CONTENT -> new yy.ContentStatement($1, yy.locInfo(@$))
;
rawBlock
: openRawBlock CONTENT END_RAW_BLOCK -> new yy.RawBlockNode($1, $2, $3, @$)
: openRawBlock content END_RAW_BLOCK -> yy.prepareRawBlock($1, $2, $3, @$)
;
openRawBlock
: OPEN_RAW_BLOCK sexpr CLOSE_RAW_BLOCK -> new yy.MustacheNode($2, null, '', '', @$)
: OPEN_RAW_BLOCK helperName param* hash? CLOSE_RAW_BLOCK -> { path: $2, params: $3, hash: $4 }
;
block
: openBlock program inverseAndProgram? closeBlock -> yy.prepareBlock($1, $2, $3, $4, false, @$)
: openBlock program inverseChain? closeBlock -> yy.prepareBlock($1, $2, $3, $4, false, @$)
| openInverse program inverseAndProgram? closeBlock -> yy.prepareBlock($1, $2, $3, $4, true, @$)
;
openBlock
: OPEN_BLOCK sexpr CLOSE -> new yy.MustacheNode($2, null, $1, yy.stripFlags($1, $3), @$)
: OPEN_BLOCK helperName param* hash? blockParams? CLOSE -> { path: $2, params: $3, hash: $4, blockParams: $5, strip: yy.stripFlags($1, $6) }
;
openInverse
: OPEN_INVERSE sexpr CLOSE -> new yy.MustacheNode($2, null, $1, yy.stripFlags($1, $3), @$)
: OPEN_INVERSE helperName param* hash? blockParams? CLOSE -> { path: $2, params: $3, hash: $4, blockParams: $5, strip: yy.stripFlags($1, $6) }
;
openInverseChain
: OPEN_INVERSE_CHAIN helperName param* hash? blockParams? CLOSE -> { path: $2, params: $3, hash: $4, blockParams: $5, strip: yy.stripFlags($1, $6) }
;
inverseAndProgram
: INVERSE program -> { strip: yy.stripFlags($1, $1), program: $2 }
;
inverseChain
: openInverseChain program inverseChain? {
var inverse = yy.prepareBlock($1, $2, $3, $3, false, @$),
program = new yy.Program([inverse], null, {}, yy.locInfo(@$));
program.chained = true;
$$ = { strip: $1.strip, program: program, chain: true };
}
| inverseAndProgram -> $1
;
closeBlock
: OPEN_ENDBLOCK path CLOSE -> {path: $2, strip: yy.stripFlags($1, $3)}
: OPEN_ENDBLOCK helperName CLOSE -> {path: $2, strip: yy.stripFlags($1, $3)}
;
mustache
// Parsing out the '&' escape token at AST level saves ~500 bytes after min due to the removal of one parser node.
// This also allows for handler unification as all mustache node instances can utilize the same handler
: OPEN sexpr CLOSE -> new yy.MustacheNode($2, null, $1, yy.stripFlags($1, $3), @$)
| OPEN_UNESCAPED sexpr CLOSE_UNESCAPED -> new yy.MustacheNode($2, null, $1, yy.stripFlags($1, $3), @$)
: OPEN helperName param* hash? CLOSE -> yy.prepareMustache($2, $3, $4, $1, yy.stripFlags($1, $5), @$)
| OPEN_UNESCAPED helperName param* hash? CLOSE_UNESCAPED -> yy.prepareMustache($2, $3, $4, $1, yy.stripFlags($1, $5), @$)
;
partial
: OPEN_PARTIAL partialName param hash? CLOSE -> new yy.PartialNode($2, $3, $4, yy.stripFlags($1, $5), @$)
| OPEN_PARTIAL partialName hash? CLOSE -> new yy.PartialNode($2, undefined, $3, yy.stripFlags($1, $4), @$)
;
sexpr
: path param* hash? -> new yy.SexprNode([$1].concat($2), $3, @$)
| dataName -> new yy.SexprNode([$1], null, @$)
: OPEN_PARTIAL partialName param* hash? CLOSE -> new yy.PartialStatement($2, $3, $4, yy.stripFlags($1, $5), yy.locInfo(@$))
;
param
: path -> $1
| STRING -> new yy.StringNode($1, @$)
| NUMBER -> new yy.NumberNode($1, @$)
| BOOLEAN -> new yy.BooleanNode($1, @$)
| dataName -> $1
| OPEN_SEXPR sexpr CLOSE_SEXPR {$2.isHelper = true; $$ = $2;}
: helperName -> $1
| sexpr -> $1
;
sexpr
: OPEN_SEXPR helperName param* hash? CLOSE_SEXPR -> new yy.SubExpression($2, $3, $4, yy.locInfo(@$))
;
hash
: hashSegment+ -> new yy.HashNode($1, @$)
: hashSegment+ -> new yy.Hash($1, yy.locInfo(@$))
;
hashSegment
: ID EQUALS param -> [$1, $3]
: ID EQUALS param -> new yy.HashPair(yy.id($1), $3, yy.locInfo(@$))
;
blockParams
: OPEN_BLOCK_PARAMS ID+ CLOSE_BLOCK_PARAMS -> yy.id($2)
;
helperName
: path -> $1
| dataName -> $1
| STRING -> new yy.StringLiteral($1, yy.locInfo(@$))
| NUMBER -> new yy.NumberLiteral($1, yy.locInfo(@$))
| BOOLEAN -> new yy.BooleanLiteral($1, yy.locInfo(@$))
| UNDEFINED -> new yy.UndefinedLiteral(yy.locInfo(@$))
| NULL -> new yy.NullLiteral(yy.locInfo(@$))
;
partialName
: path -> new yy.PartialNameNode($1, @$)
| STRING -> new yy.PartialNameNode(new yy.StringNode($1, @$), @$)
| NUMBER -> new yy.PartialNameNode(new yy.NumberNode($1, @$))
: helperName -> $1
| sexpr -> $1
;
dataName
: DATA path -> new yy.DataNode($2, @$)
: DATA pathSegments -> yy.preparePath(true, $2, @$)
;
path
: pathSegments -> new yy.IdNode($1, @$)
: pathSegments -> yy.preparePath(false, $1, @$)
;
pathSegments
: pathSegments SEP ID { $1.push({part: $3, separator: $2}); $$ = $1; }
| ID -> [{part: $1}]
: pathSegments SEP ID { $1.push({part: yy.id($3), original: $3, separator: $2}); $$ = $1; }
| ID -> [{part: yy.id($1), original: $1}]
;
-1
View File
@@ -1,2 +1 @@
/* jshint ignore:start */
/* istanbul ignore next */
-1
View File
@@ -1,2 +1 @@
export default handlebars;
/* jshint ignore:end */
+1 -1
View File
@@ -18,7 +18,7 @@ module.exports = function(grunt) {
grunt.log.writeln('Updating to version ' + version);
async.each([
['lib/handlebars/base.js', /var VERSION = "(.*)";/, 'var VERSION = "' + version + '";'],
['lib/handlebars/base.js', /var VERSION = ['"](.*)['"];/, 'var VERSION = "' + version + '";'],
['components/bower.json', /"version":.*/, '"version": "' + version + '",'],
['components/handlebars.js.nuspec', /<version>.*<\/version>/, '<version>' + version + '</version>']
],