Compare commits

...

210 Commits

Author SHA1 Message Date
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
kpdecker cee52ea68f v2.0.0-beta.1 2014-08-26 18:32:15 -05:00
kpdecker d33b362d97 Update release notes 2014-08-26 18:27:11 -05:00
Kevin Decker c5acea75aa Update FAQ.md 2014-08-25 23:37:40 -05:00
kpdecker 4f01f650dc Render false literal as “false”
Fixes #827
2014-08-25 23:35:43 -05:00
kpdecker 3c869866c8 Update FAQ with comment on UMD vs. AMD build
Fixes #796
2014-08-25 22:55:05 -05:00
kpdecker ca1486b960 Prune unused code 2014-08-25 22:39:17 -05:00
kpdecker dfca676d63 Fix test path 2014-08-25 22:39:01 -05:00
kpdecker 09d3c74c7f Convert build to expose UMD from the default files 2014-08-25 22:06:15 -05:00
kpdecker c98147b31f Move uglify include past conditional 2014-08-25 22:05:38 -05:00
kpdecker b41459055a Expose default compatibility wrapper for umd build 2014-08-25 22:04:11 -05:00
kpdecker 84342b6215 Add template isTop flag 2014-08-25 21:15:26 -05:00
kpdecker 24d63b6966 Update Ember testing instructions 2014-08-25 21:14:56 -05:00
kpdecker 36f00db073 Fix compiler version tag 2014-08-25 21:13:31 -05:00
kpdecker 64f422b57f Rev runtime version 2014-08-25 13:49:20 -05:00
kpdecker dc9a2ed7eb Refactor content blocks to ignore lines
We can simplify our previous standalone determination logic by merging content blocks again.

Fixes #854
2014-08-25 13:27:28 -05:00
kpdecker 980c38cebf Expose escapeExpression on the root object 2014-08-25 00:34:19 -05:00
kpdecker 31f6fd7131 Merge mustache compatibility sections 2014-08-25 00:34:05 -05:00
kpdecker c9886fdc0f FAQ tweaks 2014-08-25 00:33:46 -05:00
kpdecker 501a640330 Allow passing depths to _child 2014-08-25 00:32:57 -05:00
kpdecker 477a26913a Optimize compiler opcode call 2014-08-24 18:16:11 -05:00
kpdecker 4ce474d3e4 Update issue filing guidelines 2014-08-24 15:56:27 -05:00
kpdecker 3e6bf8b0f6 Link to current performance tests in readme 2014-08-24 15:45:10 -05:00
kpdecker e5aeca648a Create CONTRIBUTING.md
Cleans up some of the process focused content in the README and also adds nice message to github issue tracker, etc.
2014-08-24 15:28:22 -05:00
kpdecker 2b3fdf7ea8 Additional test coverage cleanup
Also fixes the template._child implementation which broke with the depthed work.
2014-08-23 18:52:30 -05:00
kpdecker 0c7c37df6a Restore helperMissing for ambiguous statements
Fixes #318 (regression)
Partial fix for #783
2014-08-23 18:26:57 -05:00
kpdecker cde008b49f Cleanup from code coverage report 2014-08-23 17:44:45 -05:00
kpdecker 84d646bb5d Move strip processing into AST helper logic
We already have to track these behaviors for the standalone parsing and rather than having two whitespace pruning implementations this moves all of the behavior into one place.

Fixes #852
2014-08-23 17:44:20 -05:00
kpdecker ccd803ff15 Replace string value rather than add omit flag 2014-08-23 16:37:30 -05:00
kpdecker 0528b91644 Add original value tracking to ContentNode 2014-08-23 16:33:14 -05:00
kpdecker 35b196d069 Merge branch 'mmun-refactor-parser' 2014-08-23 16:29:50 -05:00
kpdecker 529e2b6796 Merge branch 'refactor-parser' of github.com:mmun/handlebars.js into mmun-refactor-parser
Conflicts:
	lib/handlebars/compiler/ast.js
	spec/ast.js
	src/handlebars.yy
2014-08-23 16:29:22 -05:00
kpdecker 3531e04117 Optimize replaceStack for inline methods
Only use case was with inline input so most of this code was unnecessary.
2014-08-23 07:43:09 -05:00
kpdecker c3fde1c01c Remove unnecessary stack variable from blockHelper 2014-08-23 07:42:24 -05:00
kpdecker 67b3f40eb2 Merge branch 'evensoul-master' 2014-08-23 07:36:16 -05:00
kpdecker 19ce981296 Do not flush subexpressions
They are no longer duplicated with the new helper calling pattern and this also introduced stack corruption issues due to improper value lookups.

Fixes #767
Fixes #768
2014-08-23 07:36:01 -05:00
Marko Zabcic f4b8c5260c added test multiple subexpressions in a hash with context 2014-08-23 06:56:22 -05:00
Kevin Decker cf343a1949 Merge pull request #849 from wycats/helper-undefined
Fix undefined handling for pathed lookups
2014-08-15 02:07:24 -05:00
kpdecker 3ce105ae89 Fix undefined handling for pathed lookups
Fixes #837
2014-08-15 01:45:50 -05:00
kpdecker ae44e071d8 Test case for #837 2014-08-15 01:45:50 -05:00
kpdecker 2f14fa57e5 Fix/simplify tests under sauce 2014-08-15 01:02:26 -05:00
kpdecker f2b5519e76 Strip dead code from javascript compiler 2014-08-15 00:40:07 -05:00
kpdecker cc5de5df3c Fix compat partial exec without data 2014-08-15 00:38:23 -05:00
kpdecker d05245b5f5 Make depthed lookup call non-literal
Prevents duplicate calls from being made.
2014-08-15 00:37:57 -05:00
kpdecker 084e8fe1b7 Provide better error on unexpected template
Fixes #806
2014-08-15 00:06:40 -05:00
kpdecker 9f07a34955 Additional test coverage 2014-08-15 00:05:37 -05:00
kpdecker 6f22ec1e01 Remove nested function eval in blockHelperMissing
This only worked when a function returned a function, which was then evaluated. This seems like unexpected behavior so this was dropped.
2014-08-14 23:10:17 -05:00
kpdecker c71ca34fd3 Add FAQ entry for .default export
Fixes #824
2014-08-14 22:00:59 -05:00
kpdecker 85b67d7f3d Use public url for spec submodule 2014-08-14 12:31:01 -05:00
kpdecker 9e3f824bf5 Fix compiler program de-duping 2014-08-14 12:27:39 -05:00
kpdecker b94656a31f Drop unused Compiler.disassemble method 2014-08-14 12:27:23 -05:00
kpdecker 3cea85b2b0 Drop unused DECLARE opcode 2014-08-14 12:27:02 -05:00
kpdecker 4d0e3c98eb Drop unused registerHelper inverse parameter 2014-08-14 12:25:58 -05:00
kpdecker cb22ee5681 Increase test coverage a touch 2014-08-14 12:25:35 -05:00
kpdecker 9ab31e1fc6 Add istanbul code coverage 2014-08-14 01:11:55 -05:00
kpdecker 1bd8d38c3c Update compatibility documentation
Provides an official statement regarding the issues discussed in #425
2014-08-14 00:48:19 -05:00
kpdecker 59607aa9ab Add mustache spec to tests 2014-08-14 00:48:19 -05:00
Kevin Decker 48263be204 Merge pull request #848 from wycats/recursive-partial
Implement partial recursive lookup
2014-08-14 00:48:01 -05:00
kpdecker 9f8110fe15 Implement partial recursive lookup 2014-08-14 00:29:25 -05:00
kpdecker 0edce6e1d1 Inherit compat flag for partials 2014-08-14 00:29:25 -05:00
Kevin Decker d95725d5fd Merge pull request #819 from wycats/compat-mode
Implement recursive field lookup
2014-08-13 21:14:32 -05:00
kpdecker c98613b711 Implement recursive field lookup in compat mode
Provides the mustache behavior of recursive lookup without the use of depthed paths.

Note that this does incur a fairly dramatic performance penalty for depthed queries.
2014-08-13 20:46:42 -05:00
kpdecker 625b00e1da Avoid depth construction overhead for non-depthed 2014-08-13 14:51:33 -05:00
kpdecker b695aaec7c Use depths array rather than passing array args
Approximately doubles the throughput performance of depthed templates and clears the way for doing recursive path lookups in pathed mode.
2014-08-13 14:11:53 -05:00
kpdecker f6dc5ad716 Create contextName helper for context lookup 2014-08-13 11:41:27 -05:00
Kevin Decker 867322adf2 Merge pull request #787 from wycats/standalone-mustaches
Remove whitespace surrounding standalone statements
2014-08-13 09:16:08 -05:00
Kevin Decker 3288f525f3 Merge pull request #846 from espadrine/patch-1
Readme: Use an SVG badge.
2014-08-12 20:49:08 -05:00
kpdecker f2a2914d4f Add additional tests for standalone partials 2014-08-12 15:38:50 -05:00
kpdecker e93d516672 Render indent for standalone partials 2014-08-12 14:41:58 -05:00
kpdecker 2a47d66ee9 Drop omitted content tags from generated output 2014-08-12 14:41:58 -05:00
kpdecker 50c657f138 Flag standalone nodes in the AST 2014-08-12 14:41:58 -05:00
kpdecker 6070179009 Track root status in ProgramNode constructor 2014-08-12 14:41:58 -05:00
kpdecker 4a4dcf1657 Parse context sections by line 2014-08-12 14:41:58 -05:00
kpdecker 6efcaaf8d1 Update to latest saucelabs lib 2014-08-12 14:32:19 -05:00
kpdecker f4af8e06b6 Allow compat flag in test specs 2014-08-11 20:10:12 -05:00
kpdecker 63c5eb7a82 Remove unused JSCompiler.register method 2014-08-11 20:10:12 -05:00
kpdecker 248ad1acae Fix type in README
Fixes #843
2014-08-11 20:08:29 -05:00
kpdecker 905f99b18c Minor test cleanup 2014-08-11 20:08:29 -05:00
Thaddee Tyl 11f85089cc Readme: Use an SVG badge.
Using the up-to-date badge design.
2014-08-08 17:23:38 +02:00
Kevin Decker 5f1bf78555 Merge pull request #833 from mmun/patch-1
Lock mocha dependency to ~1.20.0
2014-07-29 20:38:52 -05:00
Kevin Decker f374939cfd Merge pull request #823 from fractal-surfing/with-inverse
Support inverse sections on the with helper
2014-07-29 09:25:45 -05:00
Martin Muñoz eee2c4d4f2 Refactor blocks, programs and inverses 2014-07-29 02:45:50 -04:00
Martin Muñoz e11c1beb72 Lock mocha dependency to ~1.20.0 2014-07-29 02:22:35 -04:00
Dan Manges 2bdfd2a31d Support inverse sections on the with helper 2014-07-16 19:37:01 +00:00
kpdecker 271106d43f Do not lookup pathed helpers on the helper stack
Fixes #764
2014-07-12 12:50:54 -05:00
kpdecker 1fb7b51ee6 Fix rendering of paths that resolve to zero
Fixes #820
2014-07-12 11:43:45 -05:00
kpdecker c90cfe247c Use ID lookup for ambiguous helper evaluation
Final change for #731
2014-07-07 00:20:02 -05:00
kpdecker 107aa4a66a Add test case for GH-731 2014-07-07 00:01:17 -05:00
Kevin Decker 9a1eda2caf Merge pull request #731 from wycats/path-lookup
Strange behavior for {{#foo}} {{bar}} {{/foo}} when foo is 0
2014-07-06 23:56:33 -05:00
kpdecker b5a5c76ceb Rework lookup null protector logic
- Move the lookup null protection out of `nameLookup` and into that contexts that are aware of the needs for falsy vs. not displayed values.
- Optimize lookup for nested path operations

Fixes #731
2014-07-06 23:41:15 -05:00
kpdecker 4aad72d223 Move lambda resolution to runtime
This has a very positive impact on precompiled output size, particularly for known-helpers cases, and little or no impact on the throughput numbers.
2014-07-06 12:53:01 -05:00
kpdecker 704961b378 Add additional paths to path throughput test 2014-07-06 11:37:24 -05:00
kpdecker 054627adb0 Move precompiler test filter into describe
This allows us to play nicely with in-browser tests
2014-07-05 15:03:18 -05:00
kpdecker 058e7495f9 Add precompiler version FAQ entry.
Resolves #789
2014-07-05 14:45:13 -05:00
kpdecker 059532295d Move precompiler to safer location
Attempts to avoid some of the Node vs. ES6 confusion between the different styles within the application.

Also adds some simple tests in addition to the integration test.
2014-07-05 14:39:28 -05:00
Kevin Decker 0c084399e2 Merge pull request #795 from jwietelmann/master
Turn the precompile script into a wrapper around a module.
2014-07-05 12:30:24 -05:00
Kevin Decker efb17b80a6 Update FAQ styling 2014-07-05 12:22:28 -05:00
kpdecker 7890c7dc89 Add basic FAQ 2014-07-05 12:21:39 -05:00
kpdecker b79e31e5a5 Remove disassemble log statement
Fixes #772
2014-06-28 18:44:11 -07:00
kpdecker 7172d167dd Provide clear throw on {{#each}}
Fixes #773
2014-05-27 10:54:28 -04:00
kpdecker 3cdf14d294 Add test case for Issue #800
This isn’t failing in master but this is a useful test to prevent regressions.
2014-05-27 09:32:29 -04:00
kpdecker efad61b1b7 v2.0.0-alpha.4 2014-05-19 23:15:50 -05:00
kpdecker 2a20976e6a Update release notes 2014-05-19 23:15:18 -05:00
kpdecker 3638874ddc Expose setup wrappers for compiled templates 2014-05-19 23:08:34 -05:00
kpdecker f419838362 v2.0.0-alpha.3 2014-05-19 22:29:25 -05:00
kpdecker 8ee73b9d13 Update release notes 2014-05-19 22:29:08 -05:00
kpdecker 3dcc49927b Drop travis 0.8 build support
The date-now dependency has dropped support and that version is sufficiently old that we shouldn’t have any issues not testing against it anymore.
2014-05-19 22:02:57 -05:00
Kevin Decker e10eb537ab Merge pull request #797 from tomdale/full-helper-names
Pass full helper ID to helperMissing when options are provided
2014-05-19 21:59:20 -05:00
Kevin Decker 8f09ea5f40 Merge pull request #793 from mmun/ast-tweaks
Ensure isHelper is coerced to a boolean
2014-05-19 21:56:24 -05:00
Tom Dale 72c1262394 Pass full id to helperMissing 2014-05-13 13:24:25 +01:00
Joel Wietelmann a14c689fa6 Stupid line ending 2014-05-10 14:11:07 -05:00
Joel Wietelmann e143849f8a Knocking the indent back to what it was to make for a less scary-looking pull request 2014-05-10 14:10:05 -05:00
Joel Wietelmann 2e7a3bd702 Turning CLI precompiler into a wrapper around a module. 2014-05-10 14:07:15 -05:00
Martin Muñoz 356ea1a909 Ensure isHelper is coerced to a boolean 2014-05-05 01:51:29 -04:00
kpdecker 085e5e1937 Refactor template init logic 2014-04-13 07:21:08 -05:00
kpdecker a601b24a73 Update travis exec for S3 push 2014-03-29 13:27:12 -05:00
kpdecker 04c312649b v2.0.0-alpha.2 2014-03-06 01:26:26 -06:00
kpdecker fea46dbb8c Update release notes 2014-03-06 01:26:15 -06:00
kpdecker b61c75512a Remove log in test 2014-03-05 21:11:47 -06:00
kpdecker 1da136490b Add version option to compiler
Fixes #746
2014-03-05 18:32:23 -06:00
kpdecker d55f9966b9 Update escaping in link_to example 2014-03-05 18:27:48 -06:00
Kevin Decker d33ae150cd Merge pull request #740 from SimonCropp/patch-1
Update README.markdown
2014-03-05 18:24:59 -06:00
kpdecker 8347ee2591 Temporarily kill the AWS publish CI step 2014-03-05 18:17:46 -06:00
kpdecker 55782f08c5 Fix evaluation of paths and subexprs
Fixes #743
2014-03-05 17:49:39 -06:00
kpdecker 058c0fb6ba Make JSON a requirement for the compiler.
Include the polyfill for testing under IE8 and below and document this.
2014-03-05 17:15:23 -06:00
kpdecker 10168cb0a9 Remove keyword that chokes up IE tests 2014-03-05 15:07:19 -06:00
kpdecker 66538cc7fa Drop opera tests
The Sauce implementation doesn't seem to work properly.
2014-03-05 15:03:13 -06:00
Kevin Decker 7dd64e5e92 Merge pull request #749 from jenseng/option_subexpressions_fix
properly handle multiple subexpressions in the same hash, fixes #748
2014-03-05 14:38:26 -06:00
Kevin Decker 674d07fd03 Merge pull request #756 from jenseng/mapfix
fix bug in IE<=8 (no Array::map), closes #751
2014-03-05 14:36:57 -06:00
Jon Jensen 5999baf746 fix bug in IE<=8 (no Array::map), closes #751
looks like this broke in 2812fe27

also s/this.i/var i/, since the functions that needed it were removed in
a1edab6ef
2014-03-04 22:18:50 -07:00
Kevin Decker fd5dfc861c Merge pull request #747 from Jonahss/master
updated grunt-saucelabs, failing tests revealed
2014-02-24 19:20:08 -06:00
Jon Jensen 8493822b62 properly handle multiple subexpressions in the same hash, fixes #748
push all hash params before popping any so as to avoid the last stackN var
stomping previous ones
2014-02-20 17:37:25 -06:00
jonahss c86ac60876 updated grunt-saucelabs, failing tests revealed 2014-02-18 16:14:31 -08:00
Simon Cropp c68620caf9 Update README.markdown 2014-02-15 22:05:38 +11:00
kpdecker a5ff1f3d22 vv2.0.0-alpha.1 2014-02-10 02:11:07 -06:00
kpdecker b22a150a37 Update release notes 2014-02-10 02:01:20 -06:00
kpdecker 6da3c31494 Update compiler revision info 2014-02-10 01:50:32 -06:00
kpdecker c72a8d5e84 Add child accessor API 2014-02-09 22:48:38 -06:00
kpdecker d00d8b6776 Better docs on how to file issues 2014-02-09 22:47:40 -06:00
Kevin Decker db651e7ff4 Merge pull request #491 from wycats/data-depth
For nested helpers: get the @ variables of the outer helper from the inner one
2014-02-09 19:16:47 -06:00
kpdecker 16f135835e Add support for depthed resolution of data fields 2014-02-09 18:35:22 -06:00
Kevin Decker fcec69ae2c Merge pull request #569 from wycats/lookup-helper
Unable to lookup array values using @index
2014-02-09 17:50:28 -06:00
kpdecker 306feb497c Implement lookup helper 2014-02-09 17:22:18 -06:00
kpdecker a9f76e1475 jshint changes 2014-02-09 17:21:49 -06:00
Kevin Decker fe3b4cc98e Merge pull request #730 from wycats/jezell-master
Raw block helpers
2014-02-09 15:27:59 -06:00
kpdecker 14b7ef9066 Make raw blocks operate like blocks 2014-02-09 15:11:12 -06:00
Jesse Ezell 9b14dc40a5 raw block helpers 2014-02-09 14:41:32 -06:00
Jesse Ezell 954253305a Add {{{{ }}}} for raw blocks 2014-02-09 14:41:32 -06:00
kpdecker 7a7ad74ff1 Optimize buffer generate first and all edge cases 2014-02-09 14:24:18 -06:00
kpdecker d6e86af41a Update to latest packager version
Fixes #705
2014-02-09 13:09:38 -06:00
kpdecker 71ed8b5e93 Merge branch 'master' of github.com:wycats/handlebars.js 2014-02-09 12:20:25 -06:00
kpdecker 7a6f706dc4 Add initial copyright year 2014-02-09 12:19:55 -06:00
PatrickJS dd365eaede update copyright year 2014-02-09 12:19:27 -06:00
Kevin Decker bc8f6792e8 Merge pull request #729 from wycats/precompile-literal
Convert template spec to object literal
2014-02-08 20:01:52 -06:00
kpdecker 2812fe2775 Convert template spec to object literal
This allows for metadata to be associated with the template and a simplification of the template init logic.
2014-02-06 23:05:45 -08:00
kpdecker b0e7298523 Remove unnecessary conditional 2014-02-06 23:01:43 -08:00
kpdecker 6ca06d4115 Add depth benchmark cases 2014-02-06 22:50:12 -08:00
kpdecker 8f07bbabea Add quotes to exception 2014-01-20 00:55:53 -06:00
kpdecker 4679b1d931 Include content on binary output test failure 2014-01-18 09:29:47 -06:00
kpdecker cea57c05e2 Optimize initData for root defined case 2014-01-18 09:23:11 -06:00
kpdecker 311be67bde Add partial and helper unregister APIs
Fixes #669
2014-01-18 09:22:29 -06:00
Kevin Decker 88c52ded2e Merge pull request #712 from wycats/track-ids
Add trackIds compiler flag
2014-01-17 21:36:01 -08:00
kpdecker 5a0bcb932d Fix handler execution in nondata/nonid mode 2014-01-17 23:24:35 -06:00
kpdecker 49fcf10de2 Add contextPath tracking in builtin helpers 2014-01-17 23:15:18 -06:00
kpdecker ace2896ec8 Add trackIds compiler flag
Allows helpers that care about where a particular field came from derive this data while maintaining backward compatibility with existing helpers.
2014-01-17 23:15:17 -06:00
kpdecker 9df919083d Create track ids test stub 2014-01-17 23:15:17 -06:00
kpdecker 103e5f8409 Only lint 1 set of release content 2014-01-17 23:11:35 -06:00
kpdecker a14daca1c6 Simplify sauce test environments 2014-01-17 23:11:13 -06:00
kpdecker 33921beaa0 Fix missing parameters for pathed mustaches
Fixes #658
2014-01-17 23:09:59 -06:00
kpdecker d4cfe90959 Allow decimal number values
Fixes #472
2014-01-17 20:42:02 -06:00
kpdecker 051618c024 Always process explicitly passed files in CLI
Fixes #689
2014-01-17 20:16:10 -06:00
kpdecker fe4880feaa Allow implicit context iteration with each
Fixes #671
2014-01-17 20:13:00 -06:00
Kevin Decker 363cb4b0fb Merge pull request #182 from wycats/partial-hash
Context different for partial
2014-01-17 16:07:02 -08:00
kpdecker e290ec24f1 Test more concrete behavior in partial hash test 2014-01-17 17:51:52 -06:00
kpdecker 45ae86a248 Implement partial hash evaluation 2014-01-17 17:51:52 -06:00
kpdecker f90981adf6 Add partial hash parser support 2014-01-17 17:51:52 -06:00
Kevin Decker 93a3725480 Merge pull request #711 from wycats/git-tag
Attempt to resolve git tag using describe
2014-01-17 15:51:30 -08:00
kpdecker 1b8c0e3c76 Use git describe to lookup tag 2014-01-17 17:43:48 -06:00
kpdecker 1b0b44a5b7 Remove apt git install 2014-01-17 17:18:11 -06:00
Kevin Decker f9e0272513 Merge pull request #704 from blakeembrey/clean-compiler
JavaScript Compiler Cleanup
2014-01-17 12:49:52 -08:00
Blake Embrey 13633e7896 Improve usefulness of extend util, properly use namespace property, update setup options to use a hash helper. 2014-01-16 21:24:26 +10:00
Kevin Decker 5659db4877 Merge pull request #634 from wycats/name-option
It would be great to have the helper name passed to `blockHelperMissing`
2014-01-15 08:42:32 -08:00
Kevin Decker cb80f46843 Merge pull request #392 from wycats/root-data
Access to root context in partials and helpers
2014-01-15 08:42:11 -08:00
kpdecker f650b0d69e Expose the initial context via @root 2014-01-15 09:41:53 -06:00
Kevin Decker d841ad5962 Merge pull request #707 from wycats/git-fix
Attempt manual install of git in travis
2014-01-15 07:13:51 -08:00
kpdecker fe9e93fc4d Attempt manual install of git in travis 2014-01-15 08:57:21 -06:00
Yehuda Katz 1a91fbda40 Merge pull request #706 from scottgonzalez/readme-with
README: Use with helper instead of relying on blockHelperMissing
2014-01-14 12:39:20 -08:00
Scott González 91aa96da7d README: Use with helper instead of relying on blockHelperMissing 2014-01-14 15:35:27 -05:00
Kevin Decker db7c9f3565 Merge pull request #703 from nbibler/patch-1
Add MIT license identification to the Ruby gemspec
2014-01-09 23:04:00 -08:00
Nathaniel Bibler 76b47e3393 Add MIT license identification to the Ruby gemspec 2014-01-08 22:05:16 -05:00
kpdecker d385e2cab6 Cleanup now unused code in jscompiler 2014-01-06 16:07:56 -06:00
kpdecker 0f328f375e Include name option for all helper calls
All helper calls will have access to `options.name` which is the first id value of the mustache operation.

As part of this the helperMissing call has been simplified to remove the indexed name in order to optimize the call. This is a breaking change.

Fixes #634
2014-01-06 16:07:56 -06:00
kpdecker c73aceb3d3 Refactor out pushParams method
Simplifies hash and param push logic
2014-01-06 14:41:23 -06:00
kpdecker daeecf825c Track stringParams mode in local state vars 2014-01-06 02:55:13 -06:00
kpdecker 607748af3b Add better logging of test compile errors 2014-01-06 02:45:10 -06:00
kpdecker 7197289364 Update release instructions 2014-01-06 02:45:09 -06:00
kpdecker fc60f16475 Fix DATA value in stringParams mode
Fixes #699
2014-01-06 02:45:09 -06:00
Kevin Decker 08fab253ef Merge pull request #700 from blakeembrey/valid-js-variable
Remove redundant conditions
2014-01-05 21:15:54 -08:00
Blake Embrey 3d9685c6a2 Remove redundant conditions. 2014-01-05 12:14:24 -05:00
kpdecker 6d996ef270 Simplify ambiguous code
Remove if conditional in favor of boolean failover.
2014-01-04 09:19:58 -06:00
Kevin Decker 55c7cbbbfa Merge pull request #697 from jwilm/handlebars-in-wild-addition
Add koa-hbs to "handlebars in the wild" list
2014-01-03 11:33:47 -08:00
Kevin Decker e9f3a169c9 Merge pull request #698 from osiris43/master
Fix for issue #673
2014-01-03 10:53:18 -08:00
Brett Bim 70b27bcef6 Fix for issue #673 - Parser task did not execute correctly on windows environments because of forward/backward slash issues 2014-01-02 11:36:01 -06:00
Joe Wilm a1df862f06 Add koa-hbs to "handlebars in the wild" list 2014-01-01 20:21:27 -08:00
70 changed files with 3628 additions and 1032 deletions
+1
View File
@@ -4,6 +4,7 @@ vendor
lib/handlebars/compiler/parser.js
/dist/
/tmp/
/coverage/
node_modules
*.sublime-project
*.sublime-workspace
+3
View File
@@ -0,0 +1,3 @@
[submodule "spec/mustache"]
path = spec/mustache
url = git://github.com/mustache/spec.git
+2
View File
@@ -0,0 +1,2 @@
instrumentation:
excludes: ['**/spec/**']
+10 -17
View File
@@ -1,30 +1,23 @@
language: node_js
node_js:
- "0.8"
before_install:
- npm install -g grunt-cli
- npm install -g grunt-cli
script:
- grunt --stack travis
- grunt --stack travis
email:
on_failure: change
on_success: never
env:
global:
- S3_BUCKET_NAME=builds.handlebarsjs.com
- secure: PJaukuvkBBsSDOqbIcNSSMgb96VVEaIt/eq9GPjXPeFbSd3hXgwhwVE62LrqtJO8BaUfX+PzpiQjEl4D5/KBmvlFZ057Hqmy0zmPOT5mDZfJe8Ja5zyvTMb+KkCWN/tjAp8kawHojE04pn6jIpPdwXFnAYwPhaHbATFrmdt9fdg=
- secure: mBcGL2tnmiRujJdV/4fxrVd8E8wn6AW9IQKVcMv8tvOc7i5dOzZ39rpBKLuTMRXDtMV1LyLiuKYb1pHj1IyeadEahcLYFfGygF4LG7Yzp4NWHtRzQ7Q8LXaJV7dXDboYCFkn2a8/Rtx1YSVh/sCONf5UoRC+MUIqrj4UiHN9r3s=
- secure: ckyEe5dzjdFDjmZ6wIrhGm0CFBEnKq8c1dYptfgVV/Q5/nJFGzu8T0yTjouS/ERxzdT2H327/63VCxhFnLCRHrsh4rlW/rCy4XI3O/0TeMLgFPa4TXkO8359qZ4CB44TBb3NsJyQXNMYdJpPLTCVTMpuiqqkFFOr+6OeggR7ufA=
- secure: Nm4AgSfsgNB21kgKrF9Tl7qVZU8YYREhouQunFracTcZZh2NZ2XH5aHuSiXCj88B13Cr/jGbJKsZ4T3QS3wWYtz6lkyVOx3H3iI+TMtqhD9RM3a7A4O+4vVN8IioB2YjhEu0OKjwgX5gp+0uF+pLEi7Hpj6fupD3AbbL5uYcKg8=
matrix:
include:
- node_js: "0.10"
env:
- PUBLISH=true
- secure: pLTzghtVll9yGKJI0AaB0uI8GypfWxLTaIB0ZL8//yN3nAEIKMhf/RRilYTsn/rKj2NUa7vt2edYILi3lttOUlCBOwTc9amiRms1W8Lwr/3IdWPeBLvLuH1zNJRm2lBAwU4LBSqaOwhGaxOQr6KHTnWudhNhgOucxpZfvfI/dFw=
- secure: yERYCf7AwL11D9uMtacly/THGV8BlzsMmrt+iQVvGA3GaY6QMmfYqf6P6cCH98sH5etd1Y+1e6YrPeMjqI6lyRllT7FptoyOdHulazQe86VQN4sc0EpqMlH088kB7gGjTut9Z+X9ViooT5XEh9WA5jXEI9pXhQJNoIHkWPuwGuY=
- node_js: '0.10'
env:
- PUBLISH=true
- secure: pLTzghtVll9yGKJI0AaB0uI8GypfWxLTaIB0ZL8//yN3nAEIKMhf/RRilYTsn/rKj2NUa7vt2edYILi3lttOUlCBOwTc9amiRms1W8Lwr/3IdWPeBLvLuH1zNJRm2lBAwU4LBSqaOwhGaxOQr6KHTnWudhNhgOucxpZfvfI/dFw=
- secure: yERYCf7AwL11D9uMtacly/THGV8BlzsMmrt+iQVvGA3GaY6QMmfYqf6P6cCH98sH5etd1Y+1e6YrPeMjqI6lyRllT7FptoyOdHulazQe86VQN4sc0EpqMlH088kB7gGjTut9Z+X9ViooT5XEh9WA5jXEI9pXhQJNoIHkWPuwGuY=
cache:
directories:
- node_modules
- node_modules
+78
View File
@@ -0,0 +1,78 @@
# How to Contribute
## Reporting Issues
Please see our [FAQ](https://github.com/wycats/handlebars.js/blob/master/FAQ.md) for common issues that people run into.
Should you run into other issues with the project, please don't hesitate to let us know by filing an [issue][issue]! In general we are going to ask for an example of the problem failing, which can be as simple as a jsfiddle/jsbin/etc. We've put together a jsfiddle [template][jsfiddle] to ease this. (We will keep this link up to date as new releases occur, so feel free to check back here)
Pull requests containing only failing thats 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).
## Pull Requests
We also accept [pull requests][pull-request]!
Generally we like to see pull requests that
- Maintain the existing code style
- Are focused on a single change (i.e. avoid large refactoring or style adjustments in untouched code if not the primary goal of the pull request)
- Have [good commit messages](http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html)
- Have tests
- Don't significantly decrease the current code coverage (see coverage/lcov-report/index.html)
## Building
To build Handlebars.js you'll need a few things installed.
* Node.js
* [Grunt](http://gruntjs.com/getting-started)
Project dependencies may be installed via `npm install`.
To build Handlebars.js from scratch, you'll want to run `grunt`
in the root of the project. That will build Handlebars and output the
results to the dist/ folder. To re-run tests, run `grunt test` or `npm test`.
You can also run our set of benchmarks with `grunt bench`.
The `grunt dev` implements watching for tests and allows for in browser testing at `http://localhost:9999/spec/`.
If you notice any problems, please report them to the GitHub issue tracker at
[http://github.com/wycats/handlebars.js/issues](http://github.com/wycats/handlebars.js/issues).
## 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.
```sh
npm link
grunt build release
cp dist/*.js $emberRepoDir/bower_components/handlebars/
cd $emberRepoDir
npm link handlebars
npm test
```
## Releasing
Handlebars utilizes the [release yeoman generator][generator-release] to perform most release tasks.
A full release may be completed with the following:
```
yo release
npm publish
yo release:publish components handlebars.js dist/components/
cd dist/components/
gem build handlebars-source.gemspec
gem push handlebars-source-*.gem
```
After this point the handlebars site needs to be updated to point to the new version numbers. The jsfiddle link should be updated to point to the most recent distribution for all instances in our documentation.
[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/25/
+60
View File
@@ -0,0 +1,60 @@
# Frequently Asked Questions
1. How can I file a bug report:
See our guidelines on [reporting issues](https://github.com/wycats/handlebars.js/blob/master/CONTRIBUTING.md#reporting-issues).
1. Why isn't my Mustache template working?
Handlebars deviates from Mustache slightly on a few behaviors. These variations are documented in our [readme](https://github.com/wycats/handlebars.js#differences-between-handlebarsjs-and-mustache).
1. Why is it slower when compiling?
The Handlebars compiler must parse the template and construct a JavaScript program which can then be run. Under some environments such as older mobile devices this can have a performance impact which can be avoided by precompiling. Generally it's recommended that precompilation and the runtime library be used on all clients.
1. Why doesn't this work with Content Security Policy restrictions?
When not using the precompiler, Handlebars generates a dynamic function for each template which can cause issues with pages that have enabled Content Policy. It's recommended that templates are precompiled or the `unsafe-eval` policy is enabled for sites that must generate dynamic templates at runtime.
1. How can I include script tags in my template?
If loading the template via an inlined `<script type="text/x-handlebars">` tag then you may need to break up the script tag with an empty comment to avoid browser parser errors:
```html
<script type="text/x-handlebars">
foo
<scr{{!}}ipt src="bar"></scr{{!}}ipt>
</script>
```
It's generally recommended that templates are served through external, precompiled, files, which do not suffer from this issue.
1. Why are my precompiled scripts throwing exceptions?
When using the precompiler, it's important that a supporting version of the Handlebars runtime be loaded on the target page. In version 1.x there were rudimentary checks to compare the version but these did not always work. This is fixed under 2.x but the version checking does not work between these two versions. If you see unexpected errors such as `undefined is not a function` or similar, please verify that the same version is being used for both the precompiler and the client. This can be checked via:
```sh
handlebars --version
```
If using the integrated precompiler and
```javascript
console.log(Handlebars.VERSION);
```
On the client side.
We include the built client libraries in the npm package for those who want to be certain that they are using the same client libraries as the compiler.
Should these match, please file an issue with us, per our [issue filing guidelines](https://github.com/wycats/handlebars.js/blob/master/CONTRIBUTING.md#reporting-issues).
1. Why doesn't IE like the `default` name in the AMD module?
Some browsers such as particular versions of IE treat `default` as a reserved word in JavaScript source files. To safely use this you need to reference this via the `Handlebars['default']` lookup method. This is an unfortunate side effect of the shims necessary to backport the Handlebars ES6 code to all current browsers.
1. How do I load the runtime library when using AMD?
There are two options for loading under AMD environments. The first is to use the `handlebars.runtime.amd.js` file. This may require a [path mapping](https://github.com/wycats/handlebars.js/blob/master/spec/amd-runtime.html#L31) as well as access via the `default` field.
The other option is to load the `handlebars.runtime.js` UMD build, which might not require path configuration and exposes the library as both the module root and the `default` field for compatibility.
If not using ES6 transpilers or accessing submodules in the build the former option should be sufficent for most use cases.
+14 -10
View File
@@ -10,7 +10,7 @@ module.exports = function(grunt) {
jshintrc: '.jshintrc'
},
files: [
'dist/**/!(*.min|parser).js'
'dist/cjs/**/!(*.min|parser).js'
]
},
@@ -43,7 +43,7 @@ module.exports = function(grunt) {
packager: {
global: {
type: 'global',
type: 'umd',
export: 'Handlebars',
files: [{
cwd: 'lib/',
@@ -138,18 +138,22 @@ module.exports = function(grunt) {
browsers: [
{browserName: 'chrome'},
{browserName: 'firefox'},
{browserName: 'firefox', version: '3.6'},
{browserName: 'safari', version: 7, platform: 'OS X 10.9'},
{browserName: 'safari', version: 6, platform: 'OS X 10.8'},
{browserName: 'safari', version: 5},
{browserName: 'opera', version: 12},
{browserName: 'opera', version: 11},
{browserName: 'internet explorer', version: 11, platform: 'Windows 8.1'},
{browserName: 'internet explorer', version: 10, platform: 'Windows 8'},
{browserName: 'internet explorer', version: 9, platform: 'Windows 7'},
{browserName: 'internet explorer', version: 8, platform: 'XP'},
{browserName: 'internet explorer', version: 7, platform: 'XP'},
{browserName: 'internet explorer', version: 6, platform: 'XP'}
{browserName: 'internet explorer', version: 9, platform: 'Windows 7'}
]
}
},
sanity: {
options: {
build: process.env.TRAVIS_JOB_ID,
urls: ['http://localhost:9999/spec/umd.html?headless=true', 'http://localhost:9999/spec/amd-runtime.html?headless=true', 'http://localhost:9999/spec/umd-runtime.html?headless=true'],
detailedError: true,
concurrency: 2,
browsers: [
{browserName: 'chrome'}
]
}
}
+1 -1
View File
@@ -1,4 +1,4 @@
Copyright (C) 2011 by Yehuda Katz
Copyright (C) 2011-2014 by Yehuda Katz
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
+21 -74
View File
@@ -1,4 +1,4 @@
[![Travis Build Status](https://travis-ci.org/wycats/handlebars.js.png?branch=master)](https://travis-ci.org/wycats/handlebars.js)
[![Travis Build Status](https://img.shields.io/travis/wycats/handlebars.js/master.svg)](https://travis-ci.org/wycats/handlebars.js)
[![Selenium Test Status](https://saucelabs.com/buildstatus/handlebars)](https://saucelabs.com/u/handlebars)
Handlebars.js
@@ -65,11 +65,11 @@ embedded in them, as well as the text for a link:
```js
Handlebars.registerHelper('link_to', function() {
return "<a href='" + this.url + "'>" + this.body + "</a>";
return new Handlebars.SafeString("<a href='" + Handlebars.Utils.escapeExpression(this.url) + "'>" + Handlebars.Utils.escapeExpression(this.body) + "</a>");
});
var context = { posts: [{url: "/hello-world", body: "Hello World!"}] };
var source = "<ul>{{#posts}}<li>{{{link_to}}}</li>{{/posts}}</ul>"
var source = "<ul>{{#posts}}<li>{{link_to}}</li>{{/posts}}</ul>"
var template = Handlebars.compile(source);
template(context);
@@ -132,7 +132,7 @@ into the person object you could still display the company's name with
an expression like `{{../company.name}}`, so:
```
{{#person}}{{name}} - {{../company.name}}{{/person}}
{{#with person}}{{name}} - {{../company.name}}{{/person}}
```
would render:
@@ -252,6 +252,14 @@ You can also use real html comments if you want them to end up in the output.
```
### Compatibility
There are a few Mustache behaviors that Handlebars does not implement.
- Handlebars deviates from Mustache slightly in that it does not perform recursive lookup by default. The compile time `compat` flag must be set to enable this functionality. Users should note that there is a performance cost for enabling this flag. The exact cost varies by template, but it's recommended that performance sensitive operations should avoid this mode and instead opt for explicit path references.
- The optional Mustache-style lambdas are not supported. Instead Handlebars provides it's own lambda resolution that follows the behaviors of helpers.
- Alternative delimeters are not supported.
Precompiling Templates
----------------------
@@ -291,9 +299,8 @@ name sans the extension. These templates may be executed in the same
manner as templates.
If using the simple mode the precompiler will generate a single
javascript method. To execute this method it must be passed to the using
the `Handlebars.template` method and the resulting object may be as
normal.
javascript method. To execute this method it must be passed to
the `Handlebars.template` method and the resulting object may be used as normal.
### Optimizations
@@ -321,7 +328,7 @@ Handlebars has been designed to work in any ECMAScript 3 environment. This inclu
- IE 6+
Older versions and other runtimes are likely to work but have not been formally
tested.
tested. The compiler requires `JSON.stringify` to be implemented natively or via a polyfill. If using the precompiler this is not necessary.
[![Selenium Test Status](https://saucelabs.com/browser-matrix/handlebars.svg)](https://saucelabs.com/u/handlebars)
@@ -336,13 +343,7 @@ does have some big performance advantages. Justin Marney, a.k.a.
[gotascii](http://github.com/gotascii), confirmed that with an
[independent test](http://sorescode.com/2010/09/12/benchmarks.html). The
rewritten Handlebars (current version) is faster than the old version,
and we will have some benchmarks in the near future.
Building
--------
To build handlebars, just run `grunt build`, and the build will output to the `dist` directory.
with many [performance tests](https://travis-ci.org/wycats/handlebars.js/builds/33392182#L538) being 5 to 7 times faster than the Mustache equivalent.
Upgrading
@@ -352,8 +353,9 @@ See [release-notes.md](https://github.com/wycats/handlebars.js/blob/master/relea
Known Issues
------------
* Handlebars.js can be cryptic when there's an error while rendering.
* Using a variable, helper, or partial named `class` causes errors in IE browsers. (Instead, use `className`)
See [FAQ.md](https://github.com/wycats/handlebars.js/blob/master/FAQ.md) for known issues and common pitfalls.
Handlebars in the Wild
----------------------
@@ -371,6 +373,8 @@ Handlebars in the Wild
with 100+ handlebars helpers.
* [hbs](http://github.com/donpark/hbs): An Express.js view engine adapter for Handlebars.js,
from Don Park.
* [koa-hbs](https://github.com/jwilm/koa-hbs): [koa](https://github.com/koajs/koa) generator based
renderer for Handlebars.js.
* [jblotus](http://github.com/jblotus) created [http://tryhandlebarsjs.com](http://tryhandlebarsjs.com)
for anyone who would like to try out Handlebars.js in their browser.
* [jQuery plugin](http://71104.github.io/jquery-handlebars/): allows you to use
@@ -392,67 +396,10 @@ External Resources
Have a project using Handlebars? Send us a [pull request][pull-request]!
Helping Out
-----------
To build Handlebars.js you'll need a few things installed.
* Node.js
* [Grunt](http://gruntjs.com/getting-started)
Project dependencies may be installed via `npm install`.
To build Handlebars.js from scratch, you'll want to run `grunt`
in the root of the project. That will build Handlebars and output the
results to the dist/ folder. To re-run tests, run `grunt test` or `npm test`.
You can also run our set of benchmarks with `grunt bench`.
The `grunt dev` implements watching for tests and allows for in browser testing at `http://localhost:9999/spec/`.
If you notice any problems, please report them to the GitHub issue tracker at
[http://github.com/wycats/handlebars.js/issues](http://github.com/wycats/handlebars.js/issues).
Feel free to contact commondream or wycats through GitHub with any other
questions or feature requests. To submit changes fork the project and
send a pull request.
### 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.
```sh
grunt build release
export HANDLEBARS_PATH=`pwd`
cd $emberRepoDir
bundle exec rake clean
bundle exec rake test
```
### Releasing
Handlebars utilizes the [release yeoman generator][generator-release] to perform most release tasks.
A full release may be completed with the following:
```
yo release:notes patch
yo release:release patch
npm publish
yo release:publish cdnjs handlebars.js dist/cdnjs/
yo release:publish components handlebars.js dist/components/
cd dist/components/
gem build handlebars-source.gemspec
gem push handlebars-source-*.gem
```
After this point the handlebars site needs to be updated to point to the new version numbers.
License
-------
Handlebars.js is released under the MIT license.
[bower-repo]: https://github.com/components/handlebars.js
[builds-page]: http://builds.handlebarsjs.com.s3.amazonaws.com/bucket-listing.html?sort=lastmod&sortdir=desc
[generator-release]: https://github.com/walmartlabs/generator-release
[pull-request]: https://github.com/wycats/handlebars.js/pull/new/master
+6
View File
@@ -0,0 +1,6 @@
module.exports = {
context: { names: [{name: "Moe"}, {name: "Larry"}, {name: "Curly"}, {name: "Shemp"}], foo: 'bar' },
handlebars: "{{#each names}}{{../foo}}{{/each}}",
mustache: "{{#names}}{{foo}}{{/names}}",
eco: "<% for item in @names: %><%= @foo %><% end %>"
};
+6
View File
@@ -0,0 +1,6 @@
module.exports = {
context: { names: [{bat: 'foo', name: ["Moe"]}, {bat: 'foo', name: ["Larry"]}, {bat: 'foo', name: ["Curly"]}, {bat: 'foo', name: ["Shemp"]}], foo: 'bar' },
handlebars: "{{#each names}}{{#each name}}{{../bat}}{{../../foo}}{{/each}}{{/each}}",
mustache: "{{#names}}{{#name}}{{bat}}{{foo}}{{/name}}{{/names}}",
eco: "<% for item in @names: %><% for child in item.name: %><%= item.bat %><%= @foo %><% end %><% end %>"
};
+5 -5
View File
@@ -1,7 +1,7 @@
module.exports = {
context: { person: { name: "Larry", age: 45 } },
handlebars: "{{person.name}}{{person.age}}{{person.foo}}{{animal.age}}",
dust: "{person.name}{person.age}{person.foo}{animal.age}",
eco: "<%= @person.name %><%= @person.age %><%= @person.foo %><% if @animal: %><%= @animal.age %><% end %>",
mustache: "{{person.name}}{{person.age}}{{person.foo}}{{animal.age}}"
context: { person: { name: {bar: {baz: "Larry"}}, age: 45 } },
handlebars: "{{person.name.bar.baz}}{{person.age}}{{person.foo}}{{animal.age}}",
dust: "{person.name.bar.baz}{person.age}{person.foo}{animal.age}",
eco: "<%= @person.name.bar.baz %><%= @person.age %><%= @person.foo %><% if @animal: %><%= @animal.age %><% end %>",
mustache: "{{person.name.bar.baz}}{{person.age}}{{person.foo}}{{animal.age}}"
};
+8
View File
@@ -28,11 +28,13 @@ function makeSuite(bench, name, template, handlebarsOnly) {
partials = template.partials,
handlebarsOut,
compatOut,
dustOut,
ecoOut,
mustacheOut;
var handlebar = Handlebars.compile(template.handlebars, {data: false}),
compat = Handlebars.compile(template.handlebars, {data: false, compat: true}),
options = {helpers: template.helpers};
_.each(template.partials && template.partials.handlebars, function(partial, name) {
Handlebars.registerPartial(name, Handlebars.compile(partial, {data: false}));
@@ -43,6 +45,11 @@ function makeSuite(bench, name, template, handlebarsOnly) {
handlebar(context, options);
});
compatOut = compat(context, options);
bench("compat", function() {
compat(context, options);
});
if (handlebarsOnly) {
return;
}
@@ -107,6 +114,7 @@ function makeSuite(bench, name, template, handlebarsOnly) {
}
}
compare(compatOut, 'compat');
compare(dustOut, 'dust');
compare(ecoOut, 'eco');
compare(mustacheOut, 'mustache');
+12 -139
View File
@@ -22,7 +22,7 @@ var optimist = require('optimist')
'type': 'string',
'description': 'Path to handlebar.js (only valid for amd-style)',
'alias': 'handlebarPath',
'default': ''
'default': ''
},
'k': {
'type': 'string',
@@ -75,148 +75,21 @@ var optimist = require('optimist')
'type': 'boolean',
'description': 'Removes the BOM (Byte Order Mark) from the beginning of the templates.',
'alias': 'bom'
},
'v': {
'type': 'boolean',
'description': 'Prints the current compiler version',
'alias': 'version'
}
})
.check(function(argv) {
var template = [0];
if (!argv._.length) {
throw 'Must define at least one template or directory.';
}
argv._.forEach(function(template) {
try {
fs.statSync(template);
} catch (err) {
throw 'Unable to open template file "' + template + '"';
}
});
})
.check(function(argv) {
if (argv.simple && argv.min) {
throw 'Unable to minimze simple output';
}
if (argv.simple && (argv._.length !== 1 || fs.statSync(argv._[0]).isDirectory())) {
throw 'Unable to output multiple templates in simple mode';
if (argv.version) {
return;
}
});
var fs = require('fs'),
handlebars = require('../lib'),
basename = require('path').basename,
uglify = require('uglify-js');
var argv = optimist.argv,
template = argv._[0];
// Convert the known list into a hash
var known = {};
if (argv.known && !Array.isArray(argv.known)) {
argv.known = [argv.known];
}
if (argv.known) {
for (var i = 0, len = argv.known.length; i < len; i++) {
known[argv.known[i]] = true;
}
}
// Build file extension pattern
var extension = argv.extension.replace(/[\\^$*+?.():=!|{}\-\[\]]/g, function(arg) { return '\\' + arg; });
extension = new RegExp('\\.' + extension + '$');
var output = [];
if (!argv.simple) {
if (argv.amd) {
output.push('define([\'' + argv.handlebarPath + 'handlebars.runtime\'], function(Handlebars) {\n Handlebars = Handlebars["default"];');
} else if (argv.commonjs) {
output.push('var Handlebars = require("' + argv.commonjs + '");');
} else {
output.push('(function() {\n');
}
output.push(' var template = Handlebars.template, templates = ');
output.push(argv.namespace);
output.push(' = ');
output.push(argv.namespace);
output.push(' || {};\n');
}
function processTemplate(template, root) {
var path = template,
stat = fs.statSync(path);
if (stat.isDirectory()) {
fs.readdirSync(template).map(function(file) {
var path = template + '/' + file;
if (extension.test(path) || fs.statSync(path).isDirectory()) {
processTemplate(path, root || template);
}
});
} else if (extension.test(path)) {
var data = fs.readFileSync(path, 'utf8');
if (argv.bom && data.indexOf('\uFEFF') === 0) {
data = data.substring(1);
}
var options = {
knownHelpers: known,
knownHelpersOnly: argv.o
};
if (argv.data) {
options.data = true;
}
// Clean the template name
if (!root) {
template = basename(template);
} else if (template.indexOf(root) === 0) {
template = template.substring(root.length+1);
}
template = template.replace(extension, '');
if (argv.simple) {
output.push(handlebars.precompile(data, options) + '\n');
} else if (argv.partial) {
if(argv.amd && (argv._.length == 1 && !fs.statSync(argv._[0]).isDirectory())) {
output.push('return ');
}
output.push('Handlebars.partials[\'' + template + '\'] = template(' + handlebars.precompile(data, options) + ');\n');
} else {
if(argv.amd && (argv._.length == 1 && !fs.statSync(argv._[0]).isDirectory())) {
output.push('return ');
}
output.push('templates[\'' + template + '\'] = template(' + handlebars.precompile(data, options) + ');\n');
}
}
}
argv._.forEach(function(template) {
processTemplate(template, argv.root);
});
// Output the content
if (!argv.simple) {
if (argv.amd) {
if(argv._.length > 1 || (argv._.length == 1 && fs.statSync(argv._[0]).isDirectory())) {
if(argv.partial){
output.push('return Handlebars.partials;\n');
} else {
output.push('return templates;\n');
}
}
output.push('});');
} else if (!argv.commonjs) {
output.push('})();');
}
}
output = output.join('');
if (argv.min) {
output = uglify.minify(output, {fromString: true}).code;
}
if (argv.output) {
fs.writeFileSync(argv.output, output, 'utf8');
} else {
console.log(output);
}
var argv = optimist.argv;
argv.templates = argv._;
delete argv._;
return require('../lib/precompiler').cli(argv);
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "handlebars",
"version": "1.3.0",
"version": "2.0.0",
"main": "handlebars.js",
"dependencies": {}
}
+2 -1
View File
@@ -11,7 +11,8 @@ 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 = [
'handlebars.js',
+1 -1
View File
@@ -2,7 +2,7 @@
<package>
<metadata>
<id>handlebars.js</id>
<version>1.3.0</version>
<version>2.0.0</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>
+2
View File
@@ -30,4 +30,6 @@ var create = function() {
Handlebars = create();
Handlebars.create = create;
Handlebars['default'] = Handlebars;
export default Handlebars;
+3
View File
@@ -16,6 +16,7 @@ var create = function() {
hb.SafeString = SafeString;
hb.Exception = Exception;
hb.Utils = Utils;
hb.escapeExpression = Utils.escapeExpression;
hb.VM = runtime;
hb.template = function(spec) {
@@ -28,4 +29,6 @@ var create = function() {
var Handlebars = create();
Handlebars.create = create;
Handlebars['default'] = Handlebars;
export default Handlebars;
+77 -26
View File
@@ -1,14 +1,16 @@
module Utils from "./utils";
import Exception from "./exception";
export var VERSION = "1.3.0";
export var COMPILER_REVISION = 4;
export var VERSION = "2.0.0";
export var COMPILER_REVISION = 6;
export var REVISION_CHANGES = {
1: '<= 1.0.rc.2', // 1.0.rc.2 is actually rev2 but doesn't report it
2: '== 1.0.0-rc.3',
3: '== 1.0.0-rc.4',
4: '>= 1.0.0'
4: '== 1.x.x',
5: '== 2.0.0-alpha.x',
6: '>= 2.0.0-beta.1'
};
var isArray = Utils.isArray,
@@ -29,38 +31,44 @@ HandlebarsEnvironment.prototype = {
logger: logger,
log: log,
registerHelper: function(name, fn, inverse) {
registerHelper: function(name, fn) {
if (toString.call(name) === objectType) {
if (inverse || fn) { throw new Exception('Arg not supported with multiple helpers'); }
if (fn) { throw new Exception('Arg not supported with multiple helpers'); }
Utils.extend(this.helpers, name);
} else {
if (inverse) { fn.not = inverse; }
this.helpers[name] = fn;
}
},
unregisterHelper: function(name) {
delete this.helpers[name];
},
registerPartial: function(name, str) {
registerPartial: function(name, partial) {
if (toString.call(name) === objectType) {
Utils.extend(this.partials, name);
} else {
this.partials[name] = str;
this.partials[name] = partial;
}
},
unregisterPartial: function(name) {
delete this.partials[name];
}
};
function registerDefaultHelpers(instance) {
instance.registerHelper('helperMissing', function(arg) {
if(arguments.length === 2) {
instance.registerHelper('helperMissing', function(/* [args, ]options */) {
if(arguments.length === 1) {
// A missing field in a {{foo}} constuct.
return undefined;
} else {
throw new Exception("Missing helper: '" + arg + "'");
// Someone is actually trying to call something, blow up.
throw new Exception("Missing helper: '" + arguments[arguments.length-1].name + "'");
}
});
instance.registerHelper('blockHelperMissing', function(context, options) {
var inverse = options.inverse || function() {}, fn = options.fn;
if (isFunction(context)) { context = context.call(this); }
var inverse = options.inverse,
fn = options.fn;
if(context === true) {
return fn(this);
@@ -68,19 +76,38 @@ function registerDefaultHelpers(instance) {
return inverse(this);
} else if (isArray(context)) {
if(context.length > 0) {
if (options.ids) {
options.ids = [options.name];
}
return instance.helpers.each(context, options);
} else {
return inverse(this);
}
} else {
return fn(context);
if (options.data && options.ids) {
var data = createFrame(options.data);
data.contextPath = Utils.appendContextPath(options.data.contextPath, options.name);
options = {data: data};
}
return fn(context, options);
}
});
instance.registerHelper('each', function(context, options) {
if (!options) {
throw new Exception('Must pass iterator to #each');
}
var fn = options.fn, inverse = options.inverse;
var i = 0, ret = "", data;
var contextPath;
if (options.data && options.ids) {
contextPath = Utils.appendContextPath(options.data.contextPath, options.ids[0]) + '.';
}
if (isFunction(context)) { context = context.call(this); }
if (options.data) {
@@ -94,16 +121,24 @@ function registerDefaultHelpers(instance) {
data.index = i;
data.first = (i === 0);
data.last = (i === (context.length-1));
if (contextPath) {
data.contextPath = contextPath + i;
}
}
ret = ret + fn(context[i], { data: data });
}
} else {
for(var key in context) {
if(context.hasOwnProperty(key)) {
if(data) {
data.key = key;
if(data) {
data.key = key;
data.index = i;
data.first = (i === 0);
if (contextPath) {
data.contextPath = contextPath + key;
}
}
ret = ret + fn(context[key], {data: data});
i++;
@@ -139,12 +174,28 @@ function registerDefaultHelpers(instance) {
instance.registerHelper('with', function(context, options) {
if (isFunction(context)) { context = context.call(this); }
if (!Utils.isEmpty(context)) return options.fn(context);
var fn = options.fn;
if (!Utils.isEmpty(context)) {
if (options.data && options.ids) {
var data = createFrame(options.data);
data.contextPath = Utils.appendContextPath(options.data.contextPath, options.ids[0]);
options = {data:data};
}
return fn(context, options);
} else {
return options.inverse(this);
}
});
instance.registerHelper('log', function(context, options) {
instance.registerHelper('log', function(message, options) {
var level = options.data && options.data.level != null ? parseInt(options.data.level, 10) : 1;
instance.log(level, context);
instance.log(level, message);
});
instance.registerHelper('lookup', function(obj, field) {
return obj && obj[field];
});
}
@@ -159,20 +210,20 @@ export var logger = {
level: 3,
// can be overridden in the host environment
log: function(level, obj) {
log: function(level, message) {
if (logger.level <= level) {
var method = logger.methodMap[level];
if (typeof console !== 'undefined' && console[method]) {
console[method].call(console, obj);
console[method].call(console, message);
}
}
}
};
export function log(level, obj) { logger.log(level, obj); }
export var log = logger.log;
export var createFrame = function(object) {
var obj = {};
Utils.extend(obj, object);
return obj;
var frame = Utils.extend({}, object);
frame._parent = object;
return frame;
};
+44 -57
View File
@@ -1,6 +1,6 @@
import Exception from "../exception";
function LocationInfo(locInfo){
function LocationInfo(locInfo) {
locInfo = locInfo || {};
this.firstLine = locInfo.first_line;
this.firstColumn = locInfo.first_column;
@@ -9,38 +9,11 @@ function LocationInfo(locInfo){
}
var AST = {
ProgramNode: function(statements, inverseStrip, inverse, locInfo) {
var inverseLocationInfo, firstInverseNode;
if (arguments.length === 3) {
locInfo = inverse;
inverse = null;
} else if (arguments.length === 2) {
locInfo = inverseStrip;
inverseStrip = null;
}
ProgramNode: function(statements, strip, locInfo) {
LocationInfo.call(this, locInfo);
this.type = "program";
this.statements = statements;
this.strip = {};
if(inverse) {
firstInverseNode = inverse[0];
if (firstInverseNode) {
inverseLocationInfo = {
first_line: firstInverseNode.firstLine,
last_line: firstInverseNode.lastLine,
last_column: firstInverseNode.lastColumn,
first_column: firstInverseNode.firstColumn
};
this.inverse = new AST.ProgramNode(inverse, inverseStrip, inverseLocationInfo);
} else {
this.inverse = new AST.ProgramNode(inverse, inverseStrip);
}
this.strip.right = inverseStrip.left;
} else if (inverseStrip) {
this.strip.left = inverseStrip.right;
}
this.strip = strip;
},
MustacheNode: function(rawParams, hash, open, strip, locInfo) {
@@ -64,8 +37,6 @@ var AST = {
this.sexpr = new AST.SexprNode(rawParams, hash);
}
this.sexpr.isRoot = true;
// Support old AST API that stored this info in MustacheNode
this.id = this.sexpr.id;
this.params = this.sexpr.params;
@@ -83,57 +54,63 @@ var AST = {
var id = this.id = rawParams[0];
var params = this.params = rawParams.slice(1);
// a mustache is an eligible helper if:
// * its id is simple (a single part, not `this` or `..`)
var eligibleHelper = this.eligibleHelper = id.isSimple;
// a mustache is definitely a helper if:
// * it is an eligible helper, and
// * it has at least one parameter or hash segment
this.isHelper = eligibleHelper && (params.length || hash);
this.isHelper = !!(params.length || 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, strip, locInfo) {
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, close, locInfo) {
BlockNode: function(mustache, program, inverse, strip, locInfo) {
LocationInfo.call(this, locInfo);
if(mustache.sexpr.id.original !== close.path.original) {
throw new Exception(mustache.sexpr.id.original + " doesn't match " + close.path.original, this);
}
this.type = 'block';
this.mustache = mustache;
this.program = program;
this.inverse = inverse;
this.strip = {
left: mustache.strip.left,
right: close.strip.right
};
(program || inverse).strip.left = mustache.strip.right;
(inverse || program).strip.right = close.strip.left;
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.string = string;
this.original = this.string = string;
},
HashNode: function(pairs, locInfo) {
@@ -148,7 +125,8 @@ var AST = {
var original = "",
dig = [],
depth = 0;
depth = 0,
depthString = '';
for(var i=0,l=parts.length; i<l; i++) {
var part = parts[i].part;
@@ -159,6 +137,7 @@ var AST = {
throw new Exception("Invalid path: " + original, this);
} else if (part === "..") {
depth++;
depthString += '../';
} else {
this.isScoped = true;
}
@@ -171,6 +150,7 @@ var AST = {
this.parts = dig;
this.string = dig.join('.');
this.depth = depth;
this.idName = depthString + this.string;
// an ID is simple if it only has one part, and that part is not
// `..` or `this`.
@@ -189,6 +169,8 @@ var AST = {
LocationInfo.call(this, locInfo);
this.type = "DATA";
this.id = id;
this.stringModeValue = id.stringModeValue;
this.idName = '@' + id.stringModeValue;
},
StringNode: function(string, locInfo) {
@@ -199,12 +181,12 @@ var AST = {
this.stringModeValue = string;
},
IntegerNode: function(integer, locInfo) {
NumberNode: function(number, locInfo) {
LocationInfo.call(this, locInfo);
this.type = "INTEGER";
this.type = "NUMBER";
this.original =
this.integer = integer;
this.stringModeValue = Number(integer);
this.number = number;
this.stringModeValue = Number(number);
},
BooleanNode: function(bool, locInfo) {
@@ -218,9 +200,14 @@ var AST = {
LocationInfo.call(this, locInfo);
this.type = "comment";
this.comment = comment;
this.strip = {
inlineStandalone: true
};
}
};
// Must be exported as an object rather than the root of the module as the jison lexer
// most modify the object to operate properly.
export default AST;
+8 -2
View File
@@ -1,12 +1,18 @@
import parser from "./parser";
import AST from "./ast";
module Helpers from "./helpers";
import { extend } from "../utils";
export { parser };
var 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; }
if (input.constructor === AST.ProgramNode) { return input; }
parser.yy = yy;
parser.yy = AST;
return parser.parse(input);
}
+103 -118
View File
@@ -1,4 +1,7 @@
import Exception from "../exception";
import {isArray} from "../utils";
var slice = [].slice;
export function Compiler() {}
@@ -10,30 +13,6 @@ export function Compiler() {}
Compiler.prototype = {
compiler: Compiler,
disassemble: function() {
var opcodes = this.opcodes, opcode, out = [], params, param;
for (var i=0, l=opcodes.length; i<l; i++) {
opcode = opcodes[i];
if (opcode.opcode === 'DECLARE') {
out.push("DECLARE " + opcode.name + "=" + opcode.value);
} else {
params = [];
for (var j=0; j<opcode.args.length; j++) {
param = opcode.args[j];
if (typeof param === "string") {
param = "\"" + param.replace("\n", "\\n") + "\"";
}
params.push(param);
}
out.push(opcode.opcode + " " + params.join(" "));
}
}
return out.join("\n");
},
equals: function(other) {
var len = this.opcodes.length;
if (other.opcodes.length !== len) {
@@ -43,20 +22,14 @@ Compiler.prototype = {
for (var i = 0; i < len; i++) {
var opcode = this.opcodes[i],
otherOpcode = other.opcodes[i];
if (opcode.opcode !== otherOpcode.opcode || opcode.args.length !== otherOpcode.args.length) {
if (opcode.opcode !== otherOpcode.opcode || !argEquals(opcode.args, otherOpcode.args)) {
return false;
}
for (var j = 0; j < opcode.args.length; j++) {
if (opcode.args[j] !== otherOpcode.args[j]) {
return false;
}
}
}
// 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;
if (other.children.length !== len) {
return false;
}
for (i = 0; i < len; i++) {
if (!this.children[i].equals(other.children[i])) {
return false;
@@ -73,6 +46,8 @@ Compiler.prototype = {
this.children = [];
this.depths = {list: []};
this.options = options;
this.stringParams = options.stringParams;
this.trackIds = options.trackIds;
// These changes will propagate to the other compiler components
var knownHelpers = this.options.knownHelpers;
@@ -83,7 +58,8 @@ Compiler.prototype = {
'if': true,
'unless': true,
'with': true,
'log': true
'log': true,
'lookup': true
};
if (knownHelpers) {
for (var name in knownHelpers) {
@@ -95,19 +71,7 @@ Compiler.prototype = {
},
accept: function(node) {
var strip = node.strip || {},
ret;
if (strip.left) {
this.opcode('strip');
}
ret = this[node.type](node);
if (strip.right) {
this.opcode('strip');
}
return ret;
return this[node.type](node);
},
program: function(program) {
@@ -169,7 +133,7 @@ Compiler.prototype = {
this.opcode('pushProgram', program);
this.opcode('pushProgram', inverse);
this.opcode('emptyHash');
this.opcode('blockValue');
this.opcode('blockValue', sexpr.id.original);
} else {
this.ambiguousSexpr(sexpr, program, inverse);
@@ -185,31 +149,15 @@ Compiler.prototype = {
},
hash: function(hash) {
var pairs = hash.pairs, pair, val;
var pairs = hash.pairs, i, l;
this.opcode('pushHash');
for(var i=0, l=pairs.length; i<l; i++) {
pair = pairs[i];
val = pair[1];
if (this.options.stringParams) {
if(val.depth) {
this.addDepth(val.depth);
}
this.opcode('getContext', val.depth || 0);
this.opcode('pushStringParam', val.stringModeValue, val.type);
if (val.type === 'sexpr') {
// Subexpressions get evaluated and passed in
// in string params mode.
this.sexpr(val);
}
} else {
this.accept(val);
}
this.opcode('assignToHash', pair[0]);
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');
},
@@ -218,18 +166,27 @@ Compiler.prototype = {
var partialName = partial.partialName;
this.usePartial = true;
if(partial.context) {
this.ID(partial.context);
if (partial.hash) {
this.accept(partial.hash);
} else {
this.opcode('push', 'depth0');
this.opcode('push', 'undefined');
}
this.opcode('invokePartial', partialName.name);
if (partial.context) {
this.accept(partial.context);
} else {
this.opcode('getContext', 0);
this.opcode('pushContext');
}
this.opcode('invokePartial', partialName.name, partial.indent || '');
this.opcode('append');
},
content: function(content) {
this.opcode('appendContent', content.string);
if (content.string) {
this.opcode('appendContent', content.string);
}
},
mustache: function(mustache) {
@@ -252,6 +209,8 @@ Compiler.prototype = {
this.opcode('pushProgram', program);
this.opcode('pushProgram', inverse);
this.ID(id);
this.opcode('invokeAmbiguous', name, isBlock);
},
@@ -274,14 +233,18 @@ Compiler.prototype = {
helperSexpr: function(sexpr, program, inverse) {
var params = this.setupFullMustacheParams(sexpr, program, inverse),
name = sexpr.id.parts[0];
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 {
this.opcode('invokeHelper', params.length, name, sexpr.isRoot);
id.falsy = true;
this.ID(id);
this.opcode('invokeHelper', params.length, id.original, id.isSimple);
}
},
@@ -303,35 +266,24 @@ Compiler.prototype = {
var name = id.parts[0];
if (!name) {
// Context reference, i.e. `{{foo .}}` or `{{foo ..}}`
this.opcode('pushContext');
} else {
this.opcode('lookupOnContext', id.parts[0]);
}
for(var i=1, l=id.parts.length; i<l; i++) {
this.opcode('lookup', id.parts[i]);
this.opcode('lookupOnContext', id.parts, id.falsy, id.isScoped);
}
},
DATA: function(data) {
this.options.data = true;
if (data.id.isScoped || data.id.depth) {
throw new Exception('Scoped data references are not supported: ' + data.original, data);
}
this.opcode('lookupData');
var parts = data.id.parts;
for(var i=0, l=parts.length; i<l; i++) {
this.opcode('lookup', parts[i]);
}
this.opcode('lookupData', data.id.depth, data.id.parts);
},
STRING: function(string) {
this.opcode('pushString', string.string);
},
INTEGER: function(integer) {
this.opcode('pushLiteral', integer.integer);
NUMBER: function(number) {
this.opcode('pushLiteral', number.number);
},
BOOLEAN: function(bool) {
@@ -342,11 +294,7 @@ Compiler.prototype = {
// HELPERS
opcode: function(name) {
this.opcodes.push({ opcode: name, args: [].slice.call(arguments, 1) });
},
declare: function(name, value) {
this.opcodes.push({ opcode: 'DECLARE', name: name, value: value });
this.opcodes.push({ opcode: name, args: slice.call(arguments, 1) });
},
addDepth: function(depth) {
@@ -364,6 +312,7 @@ Compiler.prototype = {
var options = this.options;
// 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];
@@ -380,27 +329,29 @@ Compiler.prototype = {
},
pushParams: function(params) {
var i = params.length, param;
for(var i=0, l=params.length; i<l; i++) {
this.pushParam(params[i]);
}
},
while(i--) {
param = params[i];
if(this.options.stringParams) {
if(param.depth) {
this.addDepth(param.depth);
}
this.opcode('getContext', param.depth || 0);
this.opcode('pushStringParam', param.stringModeValue, param.type);
if (param.type === 'sexpr') {
// Subexpressions get evaluated and passed in
// in string params mode.
this.sexpr(param);
}
} else {
this[param.type](param);
pushParam: function(val) {
if (this.stringParams) {
if(val.depth) {
this.addDepth(val.depth);
}
this.opcode('getContext', val.depth || 0);
this.opcode('pushStringParam', val.stringModeValue, val.type);
if (val.type === 'sexpr') {
// Subexpressions get evaluated and passed in
// in string params mode.
this.sexpr(val);
}
} else {
if (this.trackIds) {
this.opcode('pushId', val.type, val.idName || val.stringModeValue);
}
this.accept(val);
}
},
@@ -430,6 +381,9 @@ export function precompile(input, options, env) {
if (!('data' in options)) {
options.data = true;
}
if (options.compat) {
options.useDepths = true;
}
var ast = env.parse(input);
var environment = new env.Compiler().compile(ast, options);
@@ -446,6 +400,9 @@ export function compile(input, options, env) {
if (!('data' in options)) {
options.data = true;
}
if (options.compat) {
options.useDepths = true;
}
var compiled;
@@ -457,10 +414,38 @@ export function compile(input, options, env) {
}
// Template is only compiled on first use and cached after that point.
return function(context, options) {
var ret = function(context, options) {
if (!compiled) {
compiled = compileInput();
}
return compiled.call(this, context, options);
};
ret._setup = function(options) {
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 ret;
}
function argEquals(a, b) {
if (a === b) {
return true;
}
if (isArray(a) && isArray(b) && a.length === b.length) {
for (var i = 0; i < a.length; i++) {
if (!argEquals(a[i], b[i])) {
return false;
}
}
return true;
}
}
+186
View File
@@ -0,0 +1,186 @@
import Exception from "../exception";
export function stripFlags(open, close) {
return {
left: open.charAt(2) === '~',
right: close.charAt(close.length-3) === '~'
};
}
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);
}
var inverse = inverseAndProgram && inverseAndProgram.program;
var strip = {
left: mustache.strip.left,
right: close.strip.right,
// 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)
};
if (mustache.strip.right) {
omitRight(program.statements, null, true);
}
if (inverse) {
var inverseStrip = inverseAndProgram.strip;
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);
}
// 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);
}
}
if (inverted) {
return new this.BlockNode(mustache, inverse, program, strip, locInfo);
} else {
return new this.BlockNode(mustache, program, inverse, strip, locInfo);
}
}
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;
}
+331 -308
View File
@@ -1,4 +1,4 @@
import { COMPILER_REVISION, REVISION_CHANGES, log } from "../base";
import { COMPILER_REVISION, REVISION_CHANGES } from "../base";
import Exception from "../exception";
function Literal(value) {
@@ -11,32 +11,22 @@ JavaScriptCompiler.prototype = {
// PUBLIC API: You can override these methods in a subclass to provide
// alternative compiled forms for name lookup and buffering semantics
nameLookup: function(parent, name /* , type*/) {
var wrap,
ret;
if (parent.indexOf('depth') === 0) {
wrap = true;
}
if (/^[0-9]+$/.test(name)) {
ret = parent + "[" + name + "]";
} else if (JavaScriptCompiler.isValidJavaScriptVariableName(name)) {
ret = parent + "." + name;
}
else {
ret = parent + "['" + name + "']";
}
if (wrap) {
return '(' + parent + ' && ' + ret + ')';
if (JavaScriptCompiler.isValidJavaScriptVariableName(name)) {
return parent + "." + name;
} else {
return ret;
return parent + "['" + name + "']";
}
},
depthedLookup: function(name) {
this.aliases.lookup = 'this.lookup';
return 'lookup(depths, "' + name + '")';
},
compilerInfo: function() {
var revision = COMPILER_REVISION,
versions = REVISION_CHANGES[revision];
return "this.compilerInfo = ["+revision+",'"+versions+"'];\n";
return [revision, versions];
},
appendToBuffer: function(string) {
@@ -60,22 +50,23 @@ JavaScriptCompiler.prototype = {
compile: function(environment, options, context, asObject) {
this.environment = environment;
this.options = options || {};
log('debug', this.environment.disassemble() + "\n\n");
this.options = options;
this.stringParams = this.options.stringParams;
this.trackIds = this.options.trackIds;
this.precompile = !asObject;
this.name = this.environment.name;
this.isChild = !!context;
this.context = context || {
programs: [],
environments: [],
aliases: { }
environments: []
};
this.preamble();
this.stackSlot = 0;
this.stackVars = [];
this.aliases = {};
this.registers = { list: [] };
this.hashes = [];
this.compileStack = [];
@@ -83,118 +74,109 @@ JavaScriptCompiler.prototype = {
this.compileChildren(environment, options);
var opcodes = environment.opcodes, opcode;
this.useDepths = this.useDepths || environment.depths.list.length || this.options.compat;
this.i = 0;
var opcodes = environment.opcodes,
opcode,
i,
l;
for(var l=opcodes.length; this.i<l; this.i++) {
opcode = opcodes[this.i];
for (i = 0, l = opcodes.length; i < l; i++) {
opcode = opcodes[i];
if(opcode.opcode === 'DECLARE') {
this[opcode.name] = opcode.value;
} else {
this[opcode.opcode].apply(this, opcode.args);
}
// Reset the stripNext flag if it was not set by this operation.
if (opcode.opcode !== this.stripNext) {
this.stripNext = false;
}
this[opcode.opcode].apply(this, opcode.args);
}
// Flush any trailing content that might be pending.
this.pushSource('');
/* istanbul ignore next */
if (this.stackSlot || this.inlineStack.length || this.compileStack.length) {
throw new Exception('Compile completed with content left on stack');
}
return this.createFunctionContext(asObject);
var fn = this.createFunctionContext(asObject);
if (!this.isChild) {
var ret = {
compiler: this.compilerInfo(),
main: fn
};
var programs = this.context.programs;
for (i = 0, l = programs.length; i < l; i++) {
if (programs[i]) {
ret[i] = programs[i];
}
}
if (this.environment.usePartial) {
ret.usePartial = true;
}
if (this.options.data) {
ret.useData = true;
}
if (this.useDepths) {
ret.useDepths = true;
}
if (this.options.compat) {
ret.compat = true;
}
if (!asObject) {
ret.compiler = JSON.stringify(ret.compiler);
ret = this.objectLiteral(ret);
}
return ret;
} else {
return fn;
}
},
preamble: function() {
var out = [];
if (!this.isChild) {
var namespace = this.namespace;
var copies = "helpers = this.merge(helpers, " + namespace + ".helpers);";
if (this.environment.usePartial) { copies = copies + " partials = this.merge(partials, " + namespace + ".partials);"; }
if (this.options.data) { copies = copies + " data = data || {};"; }
out.push(copies);
} else {
out.push('');
}
if (!this.environment.isSimple) {
out.push(", buffer = " + this.initializeBuffer());
} else {
out.push("");
}
// track the last context pushed into place to allow skipping the
// getContext opcode when it would be a noop
this.lastContext = 0;
this.source = out;
this.source = [];
},
createFunctionContext: function(asObject) {
var locals = this.stackVars.concat(this.registers.list);
var varDeclarations = '';
var locals = this.stackVars.concat(this.registers.list);
if(locals.length > 0) {
this.source[1] = this.source[1] + ", " + locals.join(", ");
varDeclarations += ", " + locals.join(", ");
}
// Generate minimizer alias mappings
if (!this.isChild) {
for (var alias in this.context.aliases) {
if (this.context.aliases.hasOwnProperty(alias)) {
this.source[1] = this.source[1] + ', ' + alias + '=' + this.context.aliases[alias];
}
for (var alias in this.aliases) {
if (this.aliases.hasOwnProperty(alias)) {
varDeclarations += ', ' + alias + '=' + this.aliases[alias];
}
}
if (this.source[1]) {
this.source[1] = "var " + this.source[1].substring(2) + ";";
}
var params = ["depth0", "helpers", "partials", "data"];
// Merge children
if (!this.isChild) {
this.source[1] += '\n' + this.context.programs.join('\n') + '\n';
}
if (!this.environment.isSimple) {
this.pushSource("return buffer;");
}
var params = this.isChild ? ["depth0", "data"] : ["Handlebars", "depth0", "helpers", "partials", "data"];
for(var i=0, l=this.environment.depths.list.length; i<l; i++) {
params.push("depth" + this.environment.depths.list[i]);
if (this.useDepths) {
params.push('depths');
}
// Perform a second pass over the output to merge content when possible
var source = this.mergeSource();
if (!this.isChild) {
source = this.compilerInfo()+source;
}
var source = this.mergeSource(varDeclarations);
if (asObject) {
params.push(source);
return Function.apply(this, params);
} else {
var functionSource = 'function ' + (this.name || '') + '(' + params.join(',') + ') {\n ' + source + '}';
log('debug', functionSource + "\n\n");
return functionSource;
return 'function(' + params.join(',') + ') {\n ' + source + '}';
}
},
mergeSource: function() {
// WARN: We are not handling the case where buffer is still populated as the source should
// not have buffer append operations as their final action.
mergeSource: function(varDeclarations) {
var source = '',
buffer;
buffer,
appendOnly = !this.forceBuffer,
appendFirst;
for (var i = 0, len = this.source.length; i < len; i++) {
var line = this.source[i];
if (line.appendToBuffer) {
@@ -205,12 +187,39 @@ JavaScriptCompiler.prototype = {
}
} else {
if (buffer) {
source += 'buffer += ' + buffer + ';\n ';
if (!source) {
appendFirst = true;
source = buffer + ';\n ';
} else {
source += 'buffer += ' + buffer + ';\n ';
}
buffer = undefined;
}
source += line + '\n ';
if (!this.environment.isSimple) {
appendOnly = false;
}
}
}
if (appendOnly) {
if (buffer || !source) {
source += 'return ' + (buffer || '""') + ';\n';
}
} else {
varDeclarations += ", buffer = " + (appendFirst ? '' : this.initializeBuffer());
if (buffer) {
source += 'return buffer + ' + buffer + ';\n';
} else {
source += 'return buffer;\n';
}
}
if (varDeclarations) {
source = 'var ' + varDeclarations.substring(2) + (appendFirst ? '' : ';\n ') + source;
}
return source;
},
@@ -220,19 +229,19 @@ JavaScriptCompiler.prototype = {
// On stack, after: return value of blockHelperMissing
//
// The purpose of this opcode is to take a block of the form
// `{{#foo}}...{{/foo}}`, resolve the value of `foo`, and
// `{{#this.foo}}...{{/this.foo}}`, resolve the value of `foo`, and
// replace it on the stack with the result of properly
// invoking blockHelperMissing.
blockValue: function() {
this.context.aliases.blockHelperMissing = 'helpers.blockHelperMissing';
blockValue: function(name) {
this.aliases.blockHelperMissing = 'helpers.blockHelperMissing';
var params = ["depth0"];
this.setupParams(0, params);
var params = [this.contextName(0)];
this.setupParams(name, 0, params);
this.replaceStack(function(current) {
params.splice(1, 0, current);
return "blockHelperMissing.call(" + params.join(", ") + ")";
});
var blockName = this.popStack();
params.splice(1, 0, blockName);
this.push('blockHelperMissing.call(' + params.join(', ') + ')');
},
// [ambiguousBlockValue]
@@ -242,10 +251,13 @@ JavaScriptCompiler.prototype = {
// On stack, after, if no lastHelper: same as [blockValue]
// On stack, after, if lastHelper: value
ambiguousBlockValue: function() {
this.context.aliases.blockHelperMissing = 'helpers.blockHelperMissing';
this.aliases.blockHelperMissing = 'helpers.blockHelperMissing';
var params = ["depth0"];
this.setupParams(0, params);
// We're being a bit cheeky and reusing the options value from the prior exec
var params = [this.contextName(0)];
this.setupParams('', 0, params, true);
this.flushInline();
var current = this.topStack();
params.splice(1, 0, current);
@@ -263,27 +275,10 @@ JavaScriptCompiler.prototype = {
if (this.pendingContent) {
content = this.pendingContent + content;
}
if (this.stripNext) {
content = content.replace(/^\s+/, '');
}
this.pendingContent = content;
},
// [strip]
//
// On stack, before: ...
// On stack, after: ...
//
// Removes any trailing whitespace from the prior content node and flags
// the next operation for stripping if it is a content node.
strip: function() {
if (this.pendingContent) {
this.pendingContent = this.pendingContent.replace(/\s+$/, '');
}
this.stripNext = 'strip';
},
// [append]
//
// On stack, before: value, ...
@@ -298,7 +293,7 @@ JavaScriptCompiler.prototype = {
// when we examine local
this.flushInline();
var local = this.popStack();
this.pushSource("if(" + local + " || " + local + " === 0) { " + this.appendToBuffer(local) + " }");
this.pushSource('if (' + local + ' != null) { ' + this.appendToBuffer(local) + ' }');
if (this.environment.isSimple) {
this.pushSource("else { " + this.appendToBuffer("''") + " }");
}
@@ -311,7 +306,7 @@ JavaScriptCompiler.prototype = {
//
// Escape `value` and append it to the buffer
appendEscaped: function() {
this.context.aliases.escapeExpression = 'this.escapeExpression';
this.aliases.escapeExpression = 'this.escapeExpression';
this.pushSource(this.appendToBuffer("escapeExpression(" + this.popStack() + ")"));
},
@@ -324,20 +319,7 @@ JavaScriptCompiler.prototype = {
//
// Set the value of the `lastContext` compiler value to the depth
getContext: function(depth) {
if(this.lastContext !== depth) {
this.lastContext = depth;
}
},
// [lookupOnContext]
//
// On stack, before: ...
// On stack, after: currentContext[name], ...
//
// Looks up the value of `name` on the current context and pushes
// it onto the stack.
lookupOnContext: function(name) {
this.push(this.nameLookup('depth' + this.lastContext, name, 'context'));
this.lastContext = depth;
},
// [pushContext]
@@ -347,7 +329,64 @@ JavaScriptCompiler.prototype = {
//
// Pushes the value of the current context onto the stack.
pushContext: function() {
this.pushStackLiteral('depth' + this.lastContext);
this.pushStackLiteral(this.contextName(this.lastContext));
},
// [lookupOnContext]
//
// On stack, before: ...
// On stack, after: currentContext[name], ...
//
// Looks up the value of `name` on the current context and pushes
// it onto the stack.
lookupOnContext: function(parts, falsy, scoped) {
/*jshint -W083 */
var i = 0,
len = parts.length;
if (!scoped && this.options.compat && !this.lastContext) {
// The depthed query is expected to handle the undefined logic for the root level that
// is implemented below, so we evaluate that directly in compat mode
this.push(this.depthedLookup(parts[i++]));
} else {
this.pushContext();
}
for (; i < len; i++) {
this.replaceStack(function(current) {
var lookup = this.nameLookup(current, parts[i], 'context');
// We want to ensure that zero and false are handled properly if the context (falsy flag)
// needs to have the special handling for these values.
if (!falsy) {
return ' != null ? ' + lookup + ' : ' + current;
} else {
// Otherwise we can use generic falsy handling
return ' && ' + lookup;
}
});
}
},
// [lookupData]
//
// On stack, before: ...
// On stack, after: data, ...
//
// Push the data lookup operator
lookupData: function(depth, parts) {
/*jshint -W083 */
if (!depth) {
this.pushStackLiteral('data');
} else {
this.pushStackLiteral('this.data(data, ' + depth + ')');
}
var len = parts.length;
for (var i = 0; i < len; i++) {
this.replaceStack(function(current) {
return ' && ' + this.nameLookup(current, parts[i], 'data');
});
}
},
// [resolvePossibleLambda]
@@ -358,34 +397,9 @@ JavaScriptCompiler.prototype = {
// If the `value` is a lambda, replace it on the stack by
// the return value of the lambda
resolvePossibleLambda: function() {
this.context.aliases.functionType = '"function"';
this.aliases.lambda = 'this.lambda';
this.replaceStack(function(current) {
return "typeof " + current + " === functionType ? " + current + ".apply(depth0) : " + current;
});
},
// [lookup]
//
// On stack, before: value, ...
// On stack, after: value[name], ...
//
// Replace the value on the stack with the result of looking
// up `name` on `value`
lookup: function(name) {
this.replaceStack(function(current) {
return current + " == null || " + current + " === false ? " + current + " : " + this.nameLookup(current, name, 'context');
});
},
// [lookupData]
//
// On stack, before: ...
// On stack, after: data, ...
//
// Push the data lookup operator
lookupData: function() {
this.pushStackLiteral('data');
this.push('lambda(' + this.popStack() + ', ' + this.contextName(0) + ')');
},
// [pushStringParam]
@@ -397,8 +411,7 @@ JavaScriptCompiler.prototype = {
// provides the string value of a parameter along with its
// depth rather than resolving it immediately.
pushStringParam: function(string, type) {
this.pushStackLiteral('depth' + this.lastContext);
this.pushContext();
this.pushString(type);
// If it's a subexpression, the string result
@@ -415,7 +428,10 @@ JavaScriptCompiler.prototype = {
emptyHash: function() {
this.pushStackLiteral('{}');
if (this.options.stringParams) {
if (this.trackIds) {
this.push('{}'); // hashIds
}
if (this.stringParams) {
this.push('{}'); // hashContexts
this.push('{}'); // hashTypes
}
@@ -424,13 +440,16 @@ JavaScriptCompiler.prototype = {
if (this.hash) {
this.hashes.push(this.hash);
}
this.hash = {values: [], types: [], contexts: []};
this.hash = {values: [], types: [], contexts: [], ids: []};
},
popHash: function() {
var hash = this.hash;
this.hash = this.hashes.pop();
if (this.options.stringParams) {
if (this.trackIds) {
this.push('{' + hash.ids.join(',') + '}');
}
if (this.stringParams) {
this.push('{' + hash.contexts.join(',') + '}');
this.push('{' + hash.types.join(',') + '}');
}
@@ -496,31 +515,14 @@ JavaScriptCompiler.prototype = {
// and pushes the helper's return value onto the stack.
//
// If the helper is not found, `helperMissing` is called.
invokeHelper: function(paramSize, name, isRoot) {
this.context.aliases.helperMissing = 'helpers.helperMissing';
this.useRegister('helper');
invokeHelper: function(paramSize, name, isSimple) {
this.aliases.helperMissing = 'helpers.helperMissing';
var helper = this.lastHelper = this.setupHelper(paramSize, name, true);
var nonHelper = this.nameLookup('depth' + this.lastContext, name, 'context');
var nonHelper = this.popStack();
var helper = this.setupHelper(paramSize, name);
var lookup = 'helper = ' + helper.name + ' || ' + nonHelper;
if (helper.paramsInit) {
lookup += ',' + helper.paramsInit;
}
this.push(
'('
+ lookup
+ ',helper '
+ '? helper.call(' + helper.callParams + ') '
+ ': helperMissing.call(' + helper.helperMissingParams + '))');
// Always flush subexpressions. This is both to prevent the compounding size issue that
// occurs when the code has to be duplicated for inlining and also to prevent errors
// due to the incorrect options object being passed due to the shared register.
if (!isRoot) {
this.flushInline();
}
var lookup = (isSimple ? helper.name + ' || ' : '') + nonHelper + ' || helperMissing';
this.push('((' + lookup + ').call(' + helper.callParams + '))');
},
// [invokeKnownHelper]
@@ -548,22 +550,21 @@ JavaScriptCompiler.prototype = {
// and can be avoided by passing the `knownHelpers` and
// `knownHelpersOnly` flags at compile-time.
invokeAmbiguous: function(name, helperCall) {
this.context.aliases.functionType = '"function"';
this.aliases.functionType = '"function"';
this.aliases.helperMissing = 'helpers.helperMissing';
this.useRegister('helper');
var nonHelper = this.popStack();
this.emptyHash();
var helper = this.setupHelper(0, name, helperCall);
var helperName = this.lastHelper = this.nameLookup('helpers', name, 'helper');
var nonHelper = this.nameLookup('depth' + this.lastContext, name, 'context');
var nextStack = this.nextStack();
if (helper.paramsInit) {
this.pushSource(helper.paramsInit);
}
this.pushSource('if (helper = ' + helperName + ') { ' + nextStack + ' = helper.call(' + helper.callParams + '); }');
this.pushSource('else { helper = ' + nonHelper + '; ' + nextStack + ' = typeof helper === functionType ? helper.call(' + helper.callParams + ') : helper; }');
this.push(
'((helper = (helper = ' + helperName + ' || ' + nonHelper + ') != null ? helper : helperMissing'
+ (helper.paramsInit ? '),(' + helper.paramsInit : '') + '),'
+ '(typeof helper === functionType ? helper.call(' + helper.callParams + ') : helper))');
},
// [invokePartial]
@@ -573,30 +574,37 @@ JavaScriptCompiler.prototype = {
//
// This operation pops off a context, invokes a partial with that context,
// and pushes the result of the invocation back.
invokePartial: function(name) {
var params = [this.nameLookup('partials', name, 'partial'), "'" + name + "'", this.popStack(), "helpers", "partials"];
invokePartial: function(name, indent) {
var params = [this.nameLookup('partials', name, 'partial'), "'" + indent + "'", "'" + name + "'", this.popStack(), this.popStack(), "helpers", "partials"];
if (this.options.data) {
params.push("data");
} else if (this.options.compat) {
params.push('undefined');
}
if (this.options.compat) {
params.push('depths');
}
this.context.aliases.self = "this";
this.push("self.invokePartial(" + params.join(", ") + ")");
this.push("this.invokePartial(" + params.join(", ") + ")");
},
// [assignToHash]
//
// On stack, before: value, hash, ...
// On stack, after: hash, ...
// On stack, before: value, ..., hash, ...
// On stack, after: ..., hash, ...
//
// Pops a value and hash off the stack, assigns `hash[key] = value`
// and pushes the hash back onto the stack.
// Pops a value off the stack and assigns it to the current hash
assignToHash: function(key) {
var value = this.popStack(),
context,
type;
type,
id;
if (this.options.stringParams) {
if (this.trackIds) {
id = this.popStack();
}
if (this.stringParams) {
type = this.popStack();
context = this.popStack();
}
@@ -608,9 +616,22 @@ JavaScriptCompiler.prototype = {
if (type) {
hash.types.push("'" + key + "': " + type);
}
if (id) {
hash.ids.push("'" + key + "': " + id);
}
hash.values.push("'" + key + "': (" + value + ")");
},
pushId: function(type, name) {
if (type === 'ID' || type === 'DATA') {
this.pushString(name);
} else if (type === 'sexpr') {
this.pushStackLiteral('true');
} else {
this.pushStackLiteral('null');
}
},
// HELPERS
compiler: JavaScriptCompiler,
@@ -629,8 +650,10 @@ JavaScriptCompiler.prototype = {
index = this.context.programs.length;
child.index = index;
child.name = 'program' + index;
this.context.programs[index] = compiler.compile(child, options, this.context);
this.context.programs[index] = compiler.compile(child, options, this.context, !this.precompile);
this.context.environments[index] = child;
this.useDepths = this.useDepths || compiler.useDepths;
} else {
child.index = index;
child.name = 'program' + index;
@@ -647,30 +670,18 @@ JavaScriptCompiler.prototype = {
},
programExpression: function(guid) {
this.context.aliases.self = "this";
if(guid == null) {
return "self.noop";
}
var child = this.environment.children[guid],
depths = child.depths.list, depth;
depths = child.depths.list,
useDepths = this.useDepths,
depth;
var programParams = [child.index, child.name, "data"];
var programParams = [child.index, 'data'];
for(var i=0, l = depths.length; i<l; i++) {
depth = depths[i];
if(depth === 1) { programParams.push("depth0"); }
else { programParams.push("depth" + (depth - 1)); }
if (useDepths) {
programParams.push('depths');
}
return (depths.length === 0 ? "self.program(" : "self.programWithDepth(") + programParams.join(", ") + ")";
},
register: function(name, val) {
this.useRegister(name);
this.pushSource(name + " = " + val + ";");
return 'this.program(' + programParams.join(', ') + ')';
},
useRegister: function(name) {
@@ -699,9 +710,7 @@ JavaScriptCompiler.prototype = {
this.flushInline();
var stack = this.incrStack();
if (item) {
this.pushSource(stack + " = " + item + ";");
}
this.pushSource(stack + " = " + item + ";");
this.compileStack.push(stack);
return stack;
},
@@ -713,50 +722,36 @@ JavaScriptCompiler.prototype = {
createdStack,
usedLiteral;
// If we are currently inline then we want to merge the inline statement into the
// replacement statement via ','
if (inline) {
var top = this.popStack(true);
/* istanbul ignore next */
if (!this.isInline()) {
throw new Exception('replaceStack on non-inline');
}
if (top instanceof Literal) {
// Literals do not need to be inlined
stack = top.value;
usedLiteral = true;
} else {
// Get or create the current stack name for use by the inline
createdStack = !this.stackSlot;
var name = !createdStack ? this.topStackName() : this.incrStack();
// We want to merge the inline statement into the replacement statement via ','
var top = this.popStack(true);
prefix = '(' + this.push(name) + ' = ' + top + '),';
stack = this.topStack();
}
if (top instanceof Literal) {
// Literals do not need to be inlined
prefix = stack = top.value;
usedLiteral = true;
} else {
// Get or create the current stack name for use by the inline
createdStack = !this.stackSlot;
var name = !createdStack ? this.topStackName() : this.incrStack();
prefix = '(' + this.push(name) + ' = ' + top + ')';
stack = this.topStack();
}
var item = callback.call(this, stack);
if (inline) {
if (!usedLiteral) {
this.popStack();
}
if (createdStack) {
this.stackSlot--;
}
this.push('(' + prefix + item + ')');
} else {
// Prevent modification of the context depth variable. Through replaceStack
if (!/^stack/.test(stack)) {
stack = this.nextStack();
}
this.pushSource(stack + " = (" + prefix + item + ");");
if (!usedLiteral) {
this.popStack();
}
return stack;
},
nextStack: function() {
return this.pushStack();
if (createdStack) {
this.stackSlot--;
}
this.push('(' + prefix + item + ')');
},
incrStack: function() {
@@ -793,6 +788,7 @@ JavaScriptCompiler.prototype = {
return item.value;
} else {
if (!inline) {
/* istanbul ignore next */
if (!this.stackSlot) {
throw new Exception('Invalid stack pop');
}
@@ -802,17 +798,25 @@ JavaScriptCompiler.prototype = {
}
},
topStack: function(wrapped) {
topStack: function() {
var stack = (this.isInline() ? this.inlineStack : this.compileStack),
item = stack[stack.length - 1];
if (!wrapped && (item instanceof Literal)) {
if (item instanceof Literal) {
return item.value;
} else {
return item;
}
},
contextName: function(context) {
if (this.useDepths && context) {
return 'depths[' + context + ']';
} else {
return 'depth' + context;
}
},
quotedString: function(str) {
return '"' + str
.replace(/\\/g, '\\\\')
@@ -823,28 +827,43 @@ JavaScriptCompiler.prototype = {
.replace(/\u2029/g, '\\u2029') + '"';
},
setupHelper: function(paramSize, name, missingParams) {
objectLiteral: function(obj) {
var pairs = [];
for (var key in obj) {
if (obj.hasOwnProperty(key)) {
pairs.push(this.quotedString(key) + ':' + obj[key]);
}
}
return '{' + pairs.join(',') + '}';
},
setupHelper: function(paramSize, name, blockHelper) {
var params = [],
paramsInit = this.setupParams(paramSize, params, missingParams);
paramsInit = this.setupParams(name, paramSize, params, blockHelper);
var foundHelper = this.nameLookup('helpers', name, 'helper');
return {
params: params,
paramsInit: paramsInit,
name: foundHelper,
callParams: ["depth0"].concat(params).join(", "),
helperMissingParams: missingParams && ["depth0", this.quotedString(name)].concat(params).join(", ")
callParams: [this.contextName(0)].concat(params).join(", ")
};
},
setupOptions: function(paramSize, params) {
var options = [], contexts = [], types = [], param, inverse, program;
setupOptions: function(helper, paramSize, params) {
var options = {}, contexts = [], types = [], ids = [], param, inverse, program;
options.push("hash:" + this.popStack());
options.name = this.quotedString(helper);
options.hash = this.popStack();
if (this.options.stringParams) {
options.push("hashTypes:" + this.popStack());
options.push("hashContexts:" + this.popStack());
if (this.trackIds) {
options.hashIds = this.popStack();
}
if (this.stringParams) {
options.hashTypes = this.popStack();
options.hashContexts = this.popStack();
}
inverse = this.popStack();
@@ -854,36 +873,43 @@ JavaScriptCompiler.prototype = {
// helpers to do a check for `if (options.fn)`
if (program || inverse) {
if (!program) {
this.context.aliases.self = "this";
program = "self.noop";
program = 'this.noop';
}
if (!inverse) {
this.context.aliases.self = "this";
inverse = "self.noop";
inverse = 'this.noop';
}
options.push("inverse:" + inverse);
options.push("fn:" + program);
options.fn = program;
options.inverse = inverse;
}
for(var i=0; i<paramSize; i++) {
// The parameters go on to the stack in order (making sure that they are evaluated in order)
// so we need to pop them off the stack in reverse order
var i = paramSize;
while (i--) {
param = this.popStack();
params.push(param);
params[i] = param;
if(this.options.stringParams) {
types.push(this.popStack());
contexts.push(this.popStack());
if (this.trackIds) {
ids[i] = this.popStack();
}
if (this.stringParams) {
types[i] = this.popStack();
contexts[i] = this.popStack();
}
}
if (this.options.stringParams) {
options.push("contexts:[" + contexts.join(",") + "]");
options.push("types:[" + types.join(",") + "]");
if (this.trackIds) {
options.ids = "[" + ids.join(",") + "]";
}
if (this.stringParams) {
options.types = "[" + types.join(",") + "]";
options.contexts = "[" + contexts.join(",") + "]";
}
if(this.options.data) {
options.push("data:data");
if (this.options.data) {
options.data = "data";
}
return options;
@@ -891,8 +917,8 @@ JavaScriptCompiler.prototype = {
// the params and contexts arguments are passed in arrays
// to fill in
setupParams: function(paramSize, params, useRegister) {
var options = '{' + this.setupOptions(paramSize, params).join(',') + '}';
setupParams: function(helperName, paramSize, params, useRegister) {
var options = this.objectLiteral(this.setupOptions(helperName, paramSize, params));
if (useRegister) {
this.useRegister('options');
@@ -930,10 +956,7 @@ for(var i=0, l=reservedWords.length; i<l; i++) {
}
JavaScriptCompiler.isValidJavaScriptVariableName = function(name) {
if(!JavaScriptCompiler.RESERVED_WORDS[name] && /^[a-zA-Z_$][0-9a-zA-Z_$]*$/.test(name)) {
return true;
}
return false;
return !JavaScriptCompiler.RESERVED_WORDS[name] && /^[a-zA-Z_$][0-9a-zA-Z_$]*$/.test(name);
};
export default JavaScriptCompiler;
+10 -7
View File
@@ -10,16 +10,14 @@ export function PrintVisitor() {
PrintVisitor.prototype = new Visitor();
PrintVisitor.prototype.pad = function(string, newline) {
PrintVisitor.prototype.pad = function(string) {
var out = "";
for(var i=0,l=this.padding; i<l; i++) {
out = out + " ";
}
out = out + string;
if(newline !== false) { out = out + "\n"; }
out = out + string + "\n";
return out;
};
@@ -82,7 +80,12 @@ PrintVisitor.prototype.mustache = function(mustache) {
PrintVisitor.prototype.partial = function(partial) {
var content = this.accept(partial.partialName);
if(partial.context) { content = content + " " + this.accept(partial.context); }
if(partial.context) {
content += " " + this.accept(partial.context);
}
if (partial.hash) {
content += " " + this.accept(partial.hash);
}
return this.pad("{{> " + content + " }}");
};
@@ -103,8 +106,8 @@ PrintVisitor.prototype.STRING = function(string) {
return '"' + string.string + '"';
};
PrintVisitor.prototype.INTEGER = function(integer) {
return "INTEGER{" + integer.integer + "}";
PrintVisitor.prototype.NUMBER = function(number) {
return "NUMBER{" + number.number + "}";
};
PrintVisitor.prototype.BOOLEAN = function(bool) {
+110 -53
View File
@@ -1,6 +1,6 @@
module Utils from "./utils";
import Exception from "./exception";
import { COMPILER_REVISION, REVISION_CHANGES } from "./base";
import { COMPILER_REVISION, REVISION_CHANGES, createFrame } from "./base";
export function checkRevision(compilerInfo) {
var compilerRevision = compilerInfo && compilerInfo[0] || 1,
@@ -23,20 +23,43 @@ export function checkRevision(compilerInfo) {
// 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");
}
if (!templateSpec || !templateSpec.main) {
throw new Exception('Unknown template object: ' + typeof templateSpec);
}
// Note: Using env.VM references rather than local var references throughout this section to allow
// for external users to override these as psuedo-supported APIs.
var invokePartialWrapper = function(partial, name, context, helpers, partials, data) {
var result = env.VM.invokePartial.apply(this, arguments);
if (result != null) { return result; }
env.VM.checkRevision(templateSpec.compiler);
if (env.compile) {
var options = { helpers: helpers, partials: partials, data: data };
partials[name] = env.compile(partial, { data: data !== undefined }, env);
return partials[name](context, options);
var invokePartialWrapper = function(partial, indent, name, context, hash, helpers, partials, data, depths) {
if (hash) {
context = Utils.extend({}, context, hash);
}
var result = env.VM.invokePartial.call(this, partial, name, context, helpers, partials, data, depths);
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);
}
if (result != null) {
if (indent) {
var lines = result.split('\n');
for (var i = 0, l = lines.length; i < l; i++) {
if (!lines[i] && i + 1 === l) {
break;
}
lines[i] = 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");
}
@@ -44,84 +67,110 @@ export function template(templateSpec, env) {
// Just add water
var container = {
lookup: function(depths, name) {
var len = depths.length;
for (var i = 0; i < len; i++) {
if (depths[i] && depths[i][name] != null) {
return depths[i][name];
}
}
},
lambda: function(current, context) {
return typeof current === 'function' ? current.call(context) : current;
},
escapeExpression: Utils.escapeExpression,
invokePartial: invokePartialWrapper,
fn: function(i) {
return templateSpec[i];
},
programs: [],
program: function(i, fn, data) {
var programWrapper = this.programs[i];
if(data) {
programWrapper = program(i, fn, data);
program: function(i, data, depths) {
var programWrapper = this.programs[i],
fn = this.fn(i);
if (data || depths) {
programWrapper = program(this, i, fn, data, depths);
} else if (!programWrapper) {
programWrapper = this.programs[i] = program(i, fn);
programWrapper = this.programs[i] = program(this, i, fn);
}
return programWrapper;
},
data: function(data, depth) {
while (data && depth--) {
data = data._parent;
}
return data;
},
merge: function(param, common) {
var ret = param || common;
if (param && common && (param !== common)) {
ret = {};
Utils.extend(ret, common);
Utils.extend(ret, param);
ret = Utils.extend({}, common, param);
}
return ret;
},
programWithDepth: env.VM.programWithDepth,
noop: env.VM.noop,
compilerInfo: null
compilerInfo: templateSpec.compiler
};
return function(context, options) {
var ret = function(context, options) {
options = options || {};
var namespace = options.partial ? options : env,
helpers,
partials;
var data = options.data;
if (!options.partial) {
helpers = options.helpers;
partials = options.partials;
ret._setup(options);
if (!options.partial && templateSpec.useData) {
data = initData(context, data);
}
var result = templateSpec.call(
container,
namespace, context,
helpers,
partials,
options.data);
if (!options.partial) {
env.VM.checkRevision(container.compilerInfo);
var depths;
if (templateSpec.useDepths) {
depths = options.depths ? [context].concat(options.depths) : [context];
}
return result;
return templateSpec.main.call(container, context, container.helpers, container.partials, data, depths);
};
ret.isTop = true;
ret._setup = function(options) {
if (!options.partial) {
container.helpers = container.merge(options.helpers, env.helpers);
if (templateSpec.usePartial) {
container.partials = container.merge(options.partials, env.partials);
}
} else {
container.helpers = options.helpers;
container.partials = options.partials;
}
};
ret._child = function(i, data, depths) {
if (templateSpec.useDepths && !depths) {
throw new Exception('must pass parent depths');
}
return program(container, i, templateSpec[i], data, depths);
};
return ret;
}
export function programWithDepth(i, fn, data /*, $depth */) {
var args = Array.prototype.slice.call(arguments, 3);
export function program(container, i, fn, data, depths) {
var prog = function(context, options) {
options = options || {};
return fn.apply(this, [context, options.data || data].concat(args));
return fn.call(container, context, container.helpers, container.partials, options.data || data, depths && [context].concat(depths));
};
prog.program = i;
prog.depth = args.length;
prog.depth = depths ? depths.length : 0;
return prog;
}
export function program(i, fn, data) {
var prog = function(context, options) {
options = options || {};
return fn(context, options.data || data);
};
prog.program = i;
prog.depth = 0;
return prog;
}
export function invokePartial(partial, name, context, helpers, partials, data) {
var options = { partial: true, helpers: helpers, partials: partials, data: data };
export function invokePartial(partial, name, context, helpers, partials, data, depths) {
var options = { partial: true, helpers: helpers, partials: partials, data: data, depths: depths };
if(partial === undefined) {
throw new Exception("The partial " + name + " could not be found");
@@ -131,3 +180,11 @@ export function invokePartial(partial, name, context, helpers, partials, data) {
}
export function noop() { return ""; }
function initData(context, data) {
if (!data || !('root' in data)) {
data = data ? createFrame(data) : {};
data.root = context;
}
return data;
}
+18 -6
View File
@@ -14,15 +14,19 @@ var badChars = /[&<>"'`]/g;
var possible = /[&<>"'`]/;
function escapeChar(chr) {
return escape[chr] || "&amp;";
return escape[chr];
}
export function extend(obj, value) {
for(var key in value) {
if(Object.prototype.hasOwnProperty.call(value, key)) {
obj[key] = value[key];
export function extend(obj /* , ...source */) {
for (var i = 1; i < arguments.length; i++) {
for (var key in arguments[i]) {
if (Object.prototype.hasOwnProperty.call(arguments[i], key)) {
obj[key] = arguments[i][key];
}
}
}
return obj;
}
export var toString = Object.prototype.toString;
@@ -33,6 +37,7 @@ var isFunction = function(value) {
return typeof value === 'function';
};
// fallback for older versions of Chrome and Safari
/* istanbul ignore next */
if (isFunction(/x/)) {
isFunction = function(value) {
return typeof value === 'function' && toString.call(value) === '[object Function]';
@@ -40,6 +45,7 @@ if (isFunction(/x/)) {
}
export var isFunction;
/* istanbul ignore next */
export var isArray = Array.isArray || function(value) {
return (value && typeof value === 'object') ? toString.call(value) === '[object Array]' : false;
};
@@ -49,8 +55,10 @@ export function escapeExpression(string) {
// don't escape SafeStrings, since they're already safe
if (string instanceof SafeString) {
return string.toString();
} else if (!string && string !== 0) {
} else if (string == null) {
return "";
} else if (!string) {
return string + '';
}
// Force a string conversion as this will be done by the append regardless and
@@ -71,3 +79,7 @@ export function isEmpty(value) {
return false;
}
}
export function appendContextPath(contextPath, id) {
return (contextPath ? contextPath + '.' : '') + id;
}
+1
View File
@@ -14,6 +14,7 @@ handlebars.print = printer.print;
module.exports = handlebars;
// Publish a Node.js require() handler for .handlebars and .hbs files
/* istanbul ignore else */
if (typeof require !== 'undefined' && require.extensions) {
var extension = function(module, filename) {
var fs = require("fs");
+144
View File
@@ -0,0 +1,144 @@
var fs = require('fs'),
Handlebars = require('./index'),
basename = require('path').basename,
uglify = require('uglify-js');
module.exports.cli = function(opts) {
if (opts.version) {
console.log(Handlebars.VERSION);
return;
}
var template = [0];
if (!opts.templates.length) {
throw new Handlebars.Exception('Must define at least one template or directory.');
}
opts.templates.forEach(function(template) {
try {
fs.statSync(template);
} catch (err) {
throw new Handlebars.Exception('Unable to open template file "' + template + '"');
}
});
if (opts.simple && opts.min) {
throw new Handlebars.Exception('Unable to minimze 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 = {};
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++) {
known[opts.known[i]] = true;
}
}
// Build file extension pattern
var extension = opts.extension.replace(/[\\^$*+?.():=!|{}\-\[\]]/g, function(arg) { return '\\' + arg; });
extension = new RegExp('\\.' + extension + '$');
var output = [];
if (!opts.simple) {
if (opts.amd) {
output.push('define([\'' + opts.handlebarPath + 'handlebars.runtime\'], function(Handlebars) {\n Handlebars = Handlebars["default"];');
} else if (opts.commonjs) {
output.push('var Handlebars = require("' + opts.commonjs + '");');
} else {
output.push('(function() {\n');
}
output.push(' var template = Handlebars.template, templates = ');
output.push(opts.namespace);
output.push(' = ');
output.push(opts.namespace);
output.push(' || {};\n');
}
function processTemplate(template, root) {
var path = template,
stat = fs.statSync(path);
if (stat.isDirectory()) {
fs.readdirSync(template).map(function(file) {
var path = template + '/' + file;
if (extension.test(path) || fs.statSync(path).isDirectory()) {
processTemplate(path, root || template);
}
});
} else {
var data = fs.readFileSync(path, 'utf8');
if (opts.bom && data.indexOf('\uFEFF') === 0) {
data = data.substring(1);
}
var options = {
knownHelpers: known,
knownHelpersOnly: opts.o
};
if (opts.data) {
options.data = true;
}
// Clean the template name
if (!root) {
template = basename(template);
} else if (template.indexOf(root) === 0) {
template = template.substring(root.length+1);
}
template = template.replace(extension, '');
if (opts.simple) {
output.push(Handlebars.precompile(data, options) + '\n');
} else if (opts.partial) {
if(opts.amd && (opts.templates.length == 1 && !fs.statSync(opts.templates[0]).isDirectory())) {
output.push('return ');
}
output.push('Handlebars.partials[\'' + template + '\'] = template(' + Handlebars.precompile(data, options) + ');\n');
} else {
if(opts.amd && (opts.templates.length == 1 && !fs.statSync(opts.templates[0]).isDirectory())) {
output.push('return ');
}
output.push('templates[\'' + template + '\'] = template(' + Handlebars.precompile(data, options) + ');\n');
}
}
}
opts.templates.forEach(function(template) {
processTemplate(template, opts.root);
});
// 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');
} else {
output.push('return templates;\n');
}
}
output.push('});');
} else if (!opts.commonjs) {
output.push('})();');
}
}
output = output.join('');
if (opts.min) {
output = uglify.minify(output, {fromString: true}).code;
}
if (opts.output) {
fs.writeFileSync(opts.output, output, 'utf8');
} else {
console.log(output);
}
};
+5 -4
View File
@@ -1,7 +1,7 @@
{
"name": "handlebars",
"barename": "handlebars",
"version": "1.3.0",
"version": "2.0.0",
"description": "Handlebars provides the power necessary to let you build semantic templates effectively with no frustration",
"homepage": "http://www.handlebarsjs.com/",
"keywords": [
@@ -32,6 +32,7 @@
"benchmark": "~1.0",
"dustjs-linkedin": "~2.0.2",
"eco": "~1.1.0-rc-3",
"es6-module-packager": "1.x",
"grunt": "~0.4.1",
"grunt-cli": "~0.1.10",
"grunt-contrib-clean": "~0.4.1",
@@ -42,11 +43,11 @@
"grunt-contrib-requirejs": "~0.4.1",
"grunt-contrib-uglify": "~0.2.2",
"grunt-contrib-watch": "~0.5.3",
"grunt-saucelabs": "~4.1.2",
"es6-module-packager": "0.x",
"grunt-saucelabs": "8.x",
"istanbul": "^0.3.0",
"jison": "~0.3.0",
"keen.io": "0.0.3",
"mocha": "*",
"mocha": "~1.20.0",
"mustache": "~0.7.2",
"semver": "~2.1.0",
"underscore": "~1.5.1"
+111 -1
View File
@@ -2,7 +2,117 @@
## Development
[Commits](https://github.com/wycats/handlebars.js/compare/v1.3.0...master)
[Commits](https://github.com/wycats/handlebars.js/compare/v2.0.0...master)
## 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))
- [#827](https://github.com/wycats/handlebars.js/issues/827) - Render false literal as “false” ([@scoot557](https://api.github.com/users/scoot557))
- [#767](https://github.com/wycats/handlebars.js/issues/767) - Subexpressions bug with hash and context ([@evensoul](https://api.github.com/users/evensoul))
- Changes to 0/undefined handling
- [#731](https://github.com/wycats/handlebars.js/pull/731) - Strange behavior for {{#foo}} {{bar}} {{/foo}} when foo is 0 ([@kpdecker](https://api.github.com/users/kpdecker))
- [#820](https://github.com/wycats/handlebars.js/issues/820) - strange behavior for {{foo.bar}} when foo is 0 or null or false ([@zordius](https://api.github.com/users/zordius))
- [#837](https://github.com/wycats/handlebars.js/issues/837) - Strange input for custom helper ( foo.bar == false when foo is undefined ) ([@zordius](https://api.github.com/users/zordius))
- [#819](https://github.com/wycats/handlebars.js/pull/819) - Implement recursive field lookup ([@kpdecker](https://api.github.com/users/kpdecker))
- [#764](https://github.com/wycats/handlebars.js/issues/764) - This reference not working for helpers ([@kpdecker](https://api.github.com/users/kpdecker))
- [#773](https://github.com/wycats/handlebars.js/issues/773) - Implicit parameters in {{#each}} introduces a peculiarity in helpers calling convention ([@Bertrand](https://api.github.com/users/Bertrand))
- [#783](https://github.com/wycats/handlebars.js/issues/783) - helperMissing and consistency for different expression types ([@ErisDS](https://api.github.com/users/ErisDS))
- [#795](https://github.com/wycats/handlebars.js/pull/795) - Turn the precompile script into a wrapper around a module. ([@jwietelmann](https://api.github.com/users/jwietelmann))
- [#823](https://github.com/wycats/handlebars.js/pull/823) - Support inverse sections on the with helper ([@dan-manges](https://api.github.com/users/dan-manges))
- [#834](https://github.com/wycats/handlebars.js/pull/834) - Refactor blocks, programs and inverses ([@mmun](https://api.github.com/users/mmun))
- [#852](https://github.com/wycats/handlebars.js/issues/852) - {{foo~}} space control behavior is different from older version ([@zordius](https://api.github.com/users/zordius))
- [#835](https://github.com/wycats/handlebars.js/issues/835) - Templates overwritten if file is loaded twice
- Expose escapeExpression on the root object - 980c38c
- Remove nested function eval in blockHelperMissing - 6f22ec1
- Fix compiler program de-duping - 9e3f824
Compatibility notes:
- The default build now outputs a generic UMD wrapper. This should be transparent change but may cause issues in some environments.
- Runtime compatibility breaks in both directions. Ensure that both compiler and client are upgraded to 2.0.0-beta.1 or higher at the same time.
- `programWithDepth` has been removed an instead an array of context values is passed to fields needing depth lookups.
- `false` values are now printed to output rather than silently dropped
- Lines containing only block statements and whitespace are now removed. This matches the Mustache spec but may cause issues with code that expects whitespace to exist but would not otherwise.
- Partials that are standalone will now indent their rendered content
- `AST.ProgramNode`'s signature has changed.
- Numerious methods/features removed from psuedo-API classes
- `JavaScriptCompiler.register`
- `JavaScriptCompiler.replaceStack` no longer supports non-inline replace
- `Compiler.disassemble`
- `DECLARE` opcode
- `strip` opcode
- `lookup` opcode
- Content nodes may have their `string` values mutated over time. `original` field provides the unmodified value.
- Removed unused `Handlebars.registerHelper` `inverse` parameter
- `each` helper requires iterator parameter
[Commits](https://github.com/wycats/handlebars.js/compare/v2.0.0-alpha.4...v2.0.0-beta.1)
## v2.0.0-alpha.4 - May 19th, 2014
- Expose setup wrappers for compiled templates - 3638874
[Commits](https://github.com/wycats/handlebars.js/compare/v2.0.0-alpha.3...v2.0.0-alpha.4)
## v2.0.0-alpha.3 - May 19th, 2014
- [#797](https://github.com/wycats/handlebars.js/pull/797) - Pass full helper ID to helperMissing when options are provided ([@tomdale](https://api.github.com/users/tomdale))
- [#793](https://github.com/wycats/handlebars.js/pull/793) - Ensure isHelper is coerced to a boolean ([@mmun](https://api.github.com/users/mmun))
- Refactor template init logic - 085e5e1
[Commits](https://github.com/wycats/handlebars.js/compare/v2.0.0-alpha.2...v2.0.0-alpha.3)
## v2.0.0-alpha.2 - March 6th, 2014
- [#756](https://github.com/wycats/handlebars.js/pull/756) - fix bug in IE<=8 (no Array::map), closes #751 ([@jenseng](https://api.github.com/users/jenseng))
- [#749](https://github.com/wycats/handlebars.js/pull/749) - properly handle multiple subexpressions in the same hash, fixes #748 ([@jenseng](https://api.github.com/users/jenseng))
- [#743](https://github.com/wycats/handlebars.js/issues/743) - subexpression confusion/problem? ([@waynedpj](https://api.github.com/users/waynedpj))
- [#746](https://github.com/wycats/handlebars.js/issues/746) - [CLI] support `handlebars --version` ([@apfelbox](https://api.github.com/users/apfelbox))
- [#747](https://github.com/wycats/handlebars.js/pull/747) - updated grunt-saucelabs, failing tests revealed ([@Jonahss](https://api.github.com/users/Jonahss))
- Make JSON a requirement for the compiler. - 058c0fb
- Temporarily kill the AWS publish CI step - 8347ee2
Compatibility notes:
- A JSON polyfill is required to run the compiler under IE8 and below. It's recommended that the precompiler be used in lieu of running the compiler on these legacy environments.
[Commits](https://github.com/wycats/handlebars.js/compare/v2.0.0-alpha.1...v2.0.0-alpha.2)
## v2.0.0-alpha.1 - February 10th, 2014
- [#182](https://github.com/wycats/handlebars.js/pull/182) - Allow passing hash parameters to partials ([@kpdecker](https://api.github.com/users/kpdecker))
- [#392](https://github.com/wycats/handlebars.js/pull/392) - Access to root context in partials and helpers ([@kpdecker](https://api.github.com/users/kpdecker))
- [#472](https://github.com/wycats/handlebars.js/issues/472) - Helpers cannot have decimal parameters ([@kayleg](https://api.github.com/users/kayleg))
- [#569](https://github.com/wycats/handlebars.js/pull/569) - Unable to lookup array values using @index ([@kpdecker](https://api.github.com/users/kpdecker))
- [#491](https://github.com/wycats/handlebars.js/pull/491) - For nested helpers: get the @ variables of the outer helper from the inner one ([@kpdecker](https://api.github.com/users/kpdecker))
- [#669](https://github.com/wycats/handlebars.js/issues/669) - Ability to unregister a helper ([@dbachrach](https://api.github.com/users/dbachrach))
- [#730](https://github.com/wycats/handlebars.js/pull/730) - Raw block helpers ([@kpdecker](https://api.github.com/users/kpdecker))
- [#634](https://github.com/wycats/handlebars.js/pull/634) - It would be great to have the helper name passed to `blockHelperMissing` ([@kpdecker](https://api.github.com/users/kpdecker))
- [#729](https://github.com/wycats/handlebars.js/pull/729) - Convert template spec to object literal ([@kpdecker](https://api.github.com/users/kpdecker))
- [#658](https://github.com/wycats/handlebars.js/issues/658) - Depthed helpers do not work after an upgrade from 1.0.0 ([@xibxor](https://api.github.com/users/xibxor))
- [#671](https://github.com/wycats/handlebars.js/issues/671) - Crashes on no-parameter {{#each}} ([@stepancheg](https://api.github.com/users/stepancheg))
- [#689](https://github.com/wycats/handlebars.js/issues/689) - broken template precompilation ([@AAS](https://api.github.com/users/AAS))
- [#698](https://github.com/wycats/handlebars.js/pull/698) - Fix parser generation under windows ([@osiris43](https://api.github.com/users/osiris43))
- [#699](https://github.com/wycats/handlebars.js/issues/699) - @DATA not compiles to invalid JS in stringParams mode ([@kpdecker](https://api.github.com/users/kpdecker))
- [#705](https://github.com/wycats/handlebars.js/issues/705) - 1.3.0 can not be wrapped in an IIFE ([@craigteegarden](https://api.github.com/users/craigteegarden))
- [#706](https://github.com/wycats/handlebars.js/pull/706) - README: Use with helper instead of relying on blockHelperMissing ([@scottgonzalez](https://api.github.com/users/scottgonzalez))
- [#700](https://github.com/wycats/handlebars.js/pull/700) - Remove redundant conditions ([@blakeembrey](https://api.github.com/users/blakeembrey))
- [#704](https://github.com/wycats/handlebars.js/pull/704) - JavaScript Compiler Cleanup ([@blakeembrey](https://api.github.com/users/blakeembrey))
Compatibility notes:
- `helperMissing` helper no longer has the indexed name argument. Helper name is now available via `options.name`.
- Precompiler output has changed, which breaks compatibility with prior versions of the runtime and precompiled output.
- `JavaScriptCompiler.compilerInfo` now returns generic objects rather than javascript source.
- AST changes
- INTEGER -> NUMBER
- Additional PartialNode hash parameter
- New RawBlockNode type
- Data frames now have a `_parent` field. This is internal but is enumerable for performance/compatability reasons.
[Commits](https://github.com/wycats/handlebars.js/compare/v1.3.0...v2.0.0-alpha.1)
## v1.3.0 - January 1st, 2014
- [#690](https://github.com/wycats/handlebars.js/pull/690) - Added support for subexpressions ([@machty](https://api.github.com/users/machty))
+89
View File
@@ -0,0 +1,89 @@
<html>
<head>
<title>Mocha</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="/node_modules/mocha/mocha.css" />
<style>
.headless .suite > h1,
.headless .test.pass {
display: none;
}
</style>
<script>
// Show only errors in "headless", non-interactive mode.
if (/headless=true/.test(location.href)) {
document.documentElement.className = 'headless';
}
</script>
<script src="/node_modules/mocha/mocha.js"></script>
<script>
mocha.setup('bdd');
</script>
<script src="/spec/env/json2.js"></script>
<script src="/spec/env/require.js"></script>
<script src="/spec/env/common.js"></script>
<script>
requirejs.config({
paths: {
'handlebars.runtime': '/dist/handlebars.runtime.amd'
}
});
</script>
<script>
onload = function(){
require(['handlebars.runtime'], function(Handlebars) {
describe('runtime', function() {
it('should load', function() {
equal(!!Handlebars['default'].template, true);
equal(!!Handlebars['default'].VERSION, true);
});
});
mocha.globals(['mochaResults'])
// The test harness leaks under FF. We should have decent global leak coverage from other tests
if (!navigator.userAgent.match(/Firefox\/([\d.]+)/)) {
mocha.checkLeaks();
}
var runner = mocha.run();
//Reporting for saucelabs
var failedTests = [];
runner.on('end', function(){
window.mochaResults = runner.stats;
window.mochaResults.reports = failedTests;
});
runner.on('fail', logFailure);
function logFailure(test, err){
var flattenTitles = function(test){
var titles = [];
while (test.parent.title){
titles.push(test.parent.title);
test = test.parent;
}
return titles.reverse();
};
failedTests.push({
name: test.title,
result: false,
message: err.message,
stack: err.stack,
titles: flattenTitles(test)
});
}
});
};
</script>
</head>
<body>
<div id="mocha"></div>
</body>
</html>
+40 -4
View File
@@ -23,6 +23,7 @@
mocha.setup('bdd');
</script>
<script src="/spec/env/json2.js"></script>
<script src="/spec/env/require.js"></script>
<script src="/spec/env/common.js"></script>
@@ -45,9 +46,14 @@
};
function safeEval(templateSpec) {
var ret;
eval('ret = ' + templateSpec);
return ret;
try {
var ret;
eval('ret = ' + templateSpec);
return ret;
} catch (err) {
console.error(templateSpec);
throw err;
}
}
</script>
<script>
@@ -57,11 +63,41 @@
window.Handlebars = Handlebars['default'];
require(['tests'], function(Handlebars) {
mocha.globals(['mochaResults'])
// The test harness leaks under FF. We should have decent global leak coverage from other tests
if (!navigator.userAgent.match(/Firefox\/([\d.]+)/)) {
mocha.checkLeaks();
}
mocha.run();
var runner = mocha.run();
//Reporting for saucelabs
var failedTests = [];
runner.on('end', function(){
window.mochaResults = runner.stats;
window.mochaResults.reports = failedTests;
});
runner.on('fail', logFailure);
function logFailure(test, err){
var flattenTitles = function(test){
var titles = [];
while (test.parent.title){
titles.push(test.parent.title);
test = test.parent;
}
return titles.reverse();
};
failedTests.push({
name: test.title,
result: false,
message: err.message,
stack: err.stack,
titles: flattenTitles(test)
});
};
});
});
};
+138 -41
View File
@@ -68,17 +68,9 @@ describe('ast', function() {
});
});
describe('BlockNode', function() {
it('should throw on mustache mismatch (old sexpr-less version)', function() {
shouldThrow(function() {
var mustacheNode = new handlebarsEnv.AST.MustacheNode([{ original: 'foo'}], null, '{{', {});
new handlebarsEnv.AST.BlockNode(mustacheNode, {}, {}, {path: {original: 'bar'}});
}, Handlebars.Exception, "foo doesn't match bar");
});
it('should throw on mustache mismatch', function() {
shouldThrow(function() {
var sexprNode = new handlebarsEnv.AST.SexprNode([{ original: 'foo'}], null);
var mustacheNode = new handlebarsEnv.AST.MustacheNode(sexprNode, null, '{{', {});
new handlebarsEnv.AST.BlockNode(mustacheNode, {}, {}, {path: {original: 'bar'}}, {first_line: 2, first_column: 2});
handlebarsEnv.parse("\n {{#foo}}{{/bar}}");
}, Handlebars.Exception, "foo doesn't match bar - 2:2");
});
@@ -86,7 +78,7 @@ describe('ast', 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,
{strip: {}}, {strip: {}},
{statements: [], strip: {}}, {statements: [], strip: {}},
{
strip: {},
path: {original: 'foo'}
@@ -150,10 +142,10 @@ describe('ast', function() {
});
});
describe("IntegerNode", function(){
describe("NumberNode", function(){
it('stores location info', function(){
var integer = new handlebarsEnv.AST.IntegerNode("6", LOCATION_INFO);
var integer = new handlebarsEnv.AST.NumberNode("6", LOCATION_INFO);
testLocationInfoStorage(integer);
});
});
@@ -193,36 +185,15 @@ describe('ast', function() {
describe("PartialNode", function(){
it('stores location info', function(){
var pn = new handlebarsEnv.AST.PartialNode("so_partial", {}, {}, LOCATION_INFO);
var pn = new handlebarsEnv.AST.PartialNode("so_partial", {}, {}, {}, LOCATION_INFO);
testLocationInfoStorage(pn);
});
});
describe("ProgramNode", function(){
describe("storing location info", function(){
it("stores when `inverse` argument isn't passed", function(){
var pn = new handlebarsEnv.AST.ProgramNode([], LOCATION_INFO);
testLocationInfoStorage(pn);
});
it("stores when `inverse` or `stripInverse` arguments passed", function(){
var pn = new handlebarsEnv.AST.ProgramNode([], {strip: {}}, undefined, LOCATION_INFO);
testLocationInfoStorage(pn);
var clone = {
strip: {},
firstLine: 0,
lastLine: 0,
firstColumn: 0,
lastColumn: 0
};
pn = new handlebarsEnv.AST.ProgramNode([], {strip: {}}, [ clone ], LOCATION_INFO);
testLocationInfoStorage(pn);
// Assert that the newly created ProgramNode has the same location
// information as the inverse
testLocationInfoStorage(pn.inverse);
});
describe('ProgramNode', function(){
it('storing location info', function(){
var pn = new handlebarsEnv.AST.ProgramNode([], {}, LOCATION_INFO);
testLocationInfoStorage(pn);
});
});
@@ -251,8 +222,7 @@ describe('ast', function() {
});
it('gets line numbers correct when newlines appear', function(){
var secondContentNode = statements[2];
testColumns(secondContentNode, 1, 2, 21, 8);
testColumns(statements[2], 1, 2, 21, 8);
});
it('gets MustacheNode line numbers correct across newlines', function(){
@@ -265,12 +235,139 @@ describe('ast', function() {
testColumns(blockHelperNode, 3, 7, 8, 23);
});
it('correctly records the line numbers the program of a block helper', function(){
var blockHelperNode = statements[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],
inverse = blockHelperNode.inverse;
testColumns(inverse, 5, 6, 13, 0);
testColumns(inverse, 5, 7, 5, 0);
});
});
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);
});
});
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];
equals(ast.statements[0].string, '');
equals(block.program.statements[0].string, 'foo\n');
equals(block.inverse.statements[0].string, ' bar \n');
equals(ast.statements[2].string, '');
});
it('marks initial block mustaches as standalone', function() {
var ast = Handlebars.parse('{{# comment}} \nfoo\n {{/comment}}'),
block = ast.statements[0];
equals(block.program.statements[0].string, 'foo\n');
});
it('marks mustaches with children as standalone', function() {
var ast = Handlebars.parse('{{# comment}} \n{{foo}}\n {{/comment}}'),
block = ast.statements[0];
equals(block.program.statements[0].string, '');
equals(block.program.statements[1].id.original, 'foo');
equals(block.program.statements[2].string, '\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];
equals(statements[0].string, '');
equals(block.program.statements[0].string, 'foo\n');
equals(block.inverse.statements[0].string, ' bar \n');
equals(statements[0].string, '');
});
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];
equals(statements[0].omit, undefined);
equals(block.program.statements[0].string, ' \nfoo\n');
equals(block.inverse.statements[0].string, ' bar \n ');
equals(statements[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];
equals(block.program.statements[0].string, ' \nfoo\n');
equals(block.inverse.statements[0].string, ' bar \n ');
equals(statements[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];
equals(ast.statements[0].omit, undefined);
equals(block.program.statements[0].string, 'foo\n');
equals(block.inverse.statements[0].string, ' bar \n');
equals(ast.statements[2].string, '');
});
});
describe('partials', function() {
it('marks partial as standalone', function() {
var ast = Handlebars.parse('{{> partial }} ');
equals(ast.statements[1].string, '');
});
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, '');
});
it('marks those around content as not standalone', function() {
var ast = Handlebars.parse('a{{> partial }}');
equals(ast.statements[0].omit, undefined);
ast = Handlebars.parse('{{> partial }}a');
equals(ast.statements[1].omit, undefined);
});
});
describe('comments', function() {
it('marks comment as standalone', function() {
var ast = Handlebars.parse('{{! comment }} ');
equals(ast.statements[1].string, '');
});
it('marks indented comment as standalone', function() {
var ast = Handlebars.parse(' {{! comment }} ');
equals(ast.statements[0].string, '');
equals(ast.statements[2].string, '');
});
it('marks those around content as not standalone', function() {
var ast = Handlebars.parse('a{{! comment }}');
equals(ast.statements[0].omit, undefined);
ast = Handlebars.parse('{{! comment }}a');
equals(ast.statements[1].omit, undefined);
});
});
});
});
+36
View File
@@ -50,6 +50,14 @@ describe("basic context", function() {
shouldCompileTo("num: {{.}}", 0, "num: 0");
shouldCompileTo("num: {{num1/num2}}", {num1: {num2: 0}}, "num: 0");
});
it('false', function() {
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');
});
it("newlines", function() {
shouldCompileTo("Alan's\nTest", {}, "Alan's\nTest");
@@ -97,6 +105,18 @@ describe("basic context", function() {
frank: "Frank"},
"Frank", "functions are called with context arguments");
});
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");
});
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");
});
it("block functions with context argument", function() {
shouldCompileTo("{{#awesome 1}}inner {{.}}{{/awesome}}",
@@ -104,11 +124,27 @@ describe("basic context", function() {
"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}}",
{value: true, awesome: function(context, options) { return options.fn(context); }},
"inner 1", "block functions are called with context and options");
});
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");
});
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("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("paths with hyphens", function() {
+52 -5
View File
@@ -1,4 +1,4 @@
/*global CompilerContext, shouldCompileTo */
/*global CompilerContext, shouldCompileTo, shouldThrow */
describe('blocks', function() {
it("array", function() {
var string = "{{#goodbyes}}{{text}}! {{/goodbyes}}cruel {{world}}!";
@@ -8,7 +8,12 @@ describe('blocks', function() {
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');
});
it("array with @index", function() {
@@ -20,7 +25,7 @@ describe('blocks', function() {
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"};
@@ -39,6 +44,13 @@ describe('blocks', function() {
"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'}]};
shouldCompileTo(string, hash, 'AlanAlanAlanAlanAlanAlan');
});
it("block with complex lookup using nested context", function() {
var string = "{{#goodbyes}}{{text}} cruel {{foo/../name}}! {{/goodbyes}}";
@@ -48,10 +60,10 @@ describe('blocks', function() {
});
it("block with deep nested complex lookup", function() {
var string = "{{#outer}}Goodbye {{#inner}}cruel {{../../omg}}{{/inner}}{{/outer}}";
var hash = {omg: "OMG!", outer: [{ inner: [{ text: "goodbye" }] }] };
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 OMG!");
shouldCompileTo(string, hash, "Goodbye cruel sad OMG!");
});
describe('inverted sections', function() {
@@ -83,4 +95,39 @@ describe('blocks', function() {
"No people");
});
});
describe('standalone sections', function() {
it('block standalone else sections', function() {
shouldCompileTo('{{#people}}\n{{name}}\n{{^}}\n{{none}}\n{{/people}}\n', {none: 'No people'},
'No people\n');
shouldCompileTo('{{#none}}\n{{.}}\n{{^}}\n{{none}}\n{{/none}}\n', {none: 'No people'},
'No people\n');
shouldCompileTo('{{#people}}\n{{name}}\n{{^}}\n{{none}}\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" }] }] };
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" }] }] };
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" }] }] };
shouldCompileTo(string, [hash, undefined, undefined, true], "Goodbye cruel ");
});
});
});
+125 -10
View File
@@ -1,4 +1,4 @@
/*global CompilerContext, shouldCompileTo, compileWithPartials */
/*global CompilerContext, shouldCompileTo, shouldThrow, compileWithPartials, handlebarsEnv */
describe('builtin helpers', function() {
describe('#if', function() {
it("if", function() {
@@ -44,6 +44,10 @@ describe('builtin helpers', 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");
});
});
describe('#each', function() {
@@ -62,9 +66,29 @@ describe('builtin helpers', function() {
"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!');
hash = {goodbyes: 'cruel', world: 'world'};
shouldCompileTo('{{#each .}}{{.}}{{/each}}', [hash,,,,false], 'cruelworld');
});
it('each without context', function() {
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}}!";
var hash = {goodbyes: {"<b>#1</b>": {text: "goodbye"}, 2: {text: "GOODBYE"}}, world: "world"};
function Clazz() {
this['<b>#1</b>'] = {text: 'goodbye'};
this[2] = {text: 'GOODBYE'};
}
Clazz.prototype.foo = 'fail';
var hash = {goodbyes: new Clazz(), world: 'world'};
// Object property iteration order is undefined according to ECMA spec,
// so we need to check both possible orders
@@ -181,19 +205,110 @@ describe('builtin helpers', function() {
equal(result, 'a!b!c!', 'should output data');
});
it("each on implicit context", function() {
shouldThrow(function() {
var template = CompilerContext.compile("{{#each}}{{text}}! {{/each}}cruel world!");
template({});
}, handlebarsEnv.Exception, 'Must pass iterator to #each');
});
});
it("#log", function() {
describe("#log", function() {
if (typeof console === 'undefined') {
return;
}
var string = "{{log blah}}";
var hash = { blah: "whee" };
var info,
error;
beforeEach(function() {
info = console.info;
error = console.error;
});
afterEach(function() {
console.info = info;
console.error = error;
});
var levelArg, logArg;
handlebarsEnv.log = function(level, arg){ levelArg = level, logArg = arg; };
it('should call logger at default level', function() {
var string = "{{log blah}}";
var hash = { blah: "whee" };
shouldCompileTo(string, hash, "", "log should not display");
equals(1, levelArg, "should call log with 1");
equals("whee", logArg, "should call log with 'whee'");
var levelArg, logArg;
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'");
});
it('should call logger at data level', function() {
var string = "{{log blah}}";
var hash = { blah: "whee" };
var levelArg, logArg;
handlebarsEnv.log = function(level, arg){
levelArg = level;
logArg = arg;
};
shouldCompileTo(string, [hash,,,,{level: '03'}], "");
equals(3, levelArg);
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" };
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" };
console.error = undefined;
shouldCompileTo(string, [hash,,,,{level: '03'}], "");
});
});
describe('#lookup', function() {
it('should lookup arbitrary content', function() {
var string = '{{#each goodbyes}}{{lookup ../data .}}{{/each}}',
hash = {goodbyes: [0, 1], data: ['foo', 'bar']};
var template = CompilerContext.compile(string);
var result = template(hash);
equal(result, 'foobar');
});
it('should not fail on undefined value', function() {
var string = '{{#each goodbyes}}{{lookup ../bar .}}{{/each}}',
hash = {goodbyes: [0, 1], data: ['foo', 'bar']};
var template = CompilerContext.compile(string);
var result = template(hash);
equal(result, '');
});
});
});
+70
View File
@@ -0,0 +1,70 @@
/*global Handlebars, shouldThrow */
describe('compiler', function() {
if (!Handlebars.compile) {
return;
}
describe('#equals', function() {
function compile(string) {
var ast = Handlebars.parse(string);
return new Handlebars.Compiler().compile(ast, {});
}
it('should treat as equal', function() {
equal(compile('foo').equals(compile('foo')), true);
equal(compile('{{foo}}').equals(compile('{{foo}}')), true);
equal(compile('{{foo.bar}}').equals(compile('{{foo.bar}}')), true);
equal(compile('{{foo.bar baz "foo" true false bat=1}}').equals(compile('{{foo.bar baz "foo" true false bat=1}}')), true);
equal(compile('{{foo.bar (baz bat=1)}}').equals(compile('{{foo.bar (baz bat=1)}}')), true);
equal(compile('{{#foo}} {{/foo}}').equals(compile('{{#foo}} {{/foo}}')), true);
});
it('should treat as not equal', function() {
equal(compile('foo').equals(compile('bar')), false);
equal(compile('{{foo}}').equals(compile('{{bar}}')), false);
equal(compile('{{foo.bar}}').equals(compile('{{bar.bar}}')), false);
equal(compile('{{foo.bar baz bat=1}}').equals(compile('{{foo.bar bar bat=1}}')), false);
equal(compile('{{foo.bar (baz bat=1)}}').equals(compile('{{foo.bar (bar bat=1)}}')), false);
equal(compile('{{#foo}} {{/foo}}').equals(compile('{{#bar}} {{/bar}}')), false);
equal(compile('{{#foo}} {{/foo}}').equals(compile('{{#foo}} {{foo}}{{/foo}}')), false);
});
});
describe('#compile', function() {
it('should fail with invalid input', function() {
shouldThrow(function() {
Handlebars.compile(null);
}, Error, 'You must pass a string or Handlebars AST to Handlebars.compile. You passed null');
shouldThrow(function() {
Handlebars.compile({});
}, Error, 'You must pass a string or Handlebars AST to Handlebars.compile. You passed [object Object]');
});
it('can utilize AST instance', function() {
equal(Handlebars.compile(new Handlebars.AST.ProgramNode([ new Handlebars.AST.ContentNode("Hello")], {}))(), 'Hello');
});
it("can pass through an empty string", function() {
equal(Handlebars.compile('')(), '');
});
});
describe('#precompile', function() {
it('should fail with invalid input', function() {
shouldThrow(function() {
Handlebars.precompile(null);
}, Error, 'You must pass a string or Handlebars AST to Handlebars.precompile. You passed null');
shouldThrow(function() {
Handlebars.precompile({});
}, Error, 'You must pass a string or Handlebars AST to Handlebars.precompile. You passed [object Object]');
});
it('can utilize AST instance', function() {
equal(/return "Hello"/.test(Handlebars.precompile(new Handlebars.AST.ProgramNode([ new Handlebars.AST.ContentNode("Hello")]), {})), true);
});
it("can pass through an empty string", function() {
equal(/return ""/.test(Handlebars.precompile('')), true);
});
});
});
+34 -16
View File
@@ -85,22 +85,6 @@ describe('data', function() {
equals("Hello undefined", result, "@foo as a parameter retrieves template data");
});
it("parameter data throws when using this scope references", function() {
var string = "{{#goodbyes}}{{text}} cruel {{@./name}}! {{/goodbyes}}";
shouldThrow(function() {
CompilerContext.compile(string);
}, Error);
});
it("parameter data throws when using parent scope references", function() {
var string = "{{#goodbyes}}{{text}} cruel {{@../name}}! {{/goodbyes}}";
shouldThrow(function() {
CompilerContext.compile(string);
}, Error);
});
it("parameter data throws when using complex scope references", function() {
var string = "{{#goodbyes}}{{text}} cruel {{@foo/../name}}! {{/goodbyes}}";
@@ -236,4 +220,38 @@ describe('data', function() {
equals("sad world?", result, "Overriden data output by helper");
});
describe('@root', function() {
it('the root context can be looked up via @root', function() {
var template = CompilerContext.compile('{{@root.foo}}');
var result = template({foo: 'hello'}, { data: {} });
equals('hello', result);
result = template({foo: 'hello'}, {});
equals('hello', result);
});
it('passed root values take priority', function() {
var template = CompilerContext.compile('{{@root.foo}}');
var result = template({}, { data: {root: {foo: 'hello'} } });
equals('hello', result);
});
});
describe('nesting', function() {
it('the root context can be looked up via @root', function() {
var template = CompilerContext.compile('{{#helper}}{{#helper}}{{@./depth}} {{@../depth}} {{@../../depth}}{{/helper}}{{/helper}}');
var result = template({foo: 'hello'}, {
helpers: {
helper: function(options) {
var frame = Handlebars.createFrame(options.data);
frame.depth = options.data.depth + 1;
return options.fn(this, {data: frame});
}
},
data: {
depth: 0
}
});
equals('2 1 0', result);
});
});
});
+6 -1
View File
@@ -19,5 +19,10 @@ global.CompilerContext = {
};
function safeEval(templateSpec) {
return eval('(' + templateSpec + ')');
try {
return eval('(' + templateSpec + ')');
} catch (err) {
console.error(templateSpec);
throw err;
}
}
+13 -4
View File
@@ -1,3 +1,4 @@
/*global CompilerContext, compileWithPartials, shouldCompileToWithPartials */
global.shouldCompileTo = function(string, hashOrArray, expected, message) {
shouldCompileToWithPartials(string, hashOrArray, false, expected, message);
};
@@ -5,27 +6,35 @@ global.shouldCompileTo = function(string, hashOrArray, expected, message) {
global.shouldCompileToWithPartials = function(string, hashOrArray, partials, expected, message) {
var result = compileWithPartials(string, hashOrArray, partials);
if (result !== expected) {
throw new Error("'" + expected + "' should === '" + result + "': " + message);
throw new Error("'" + result + "' should === '" + expected + "': " + message);
}
};
global.compileWithPartials = function(string, hashOrArray, partials) {
var template = CompilerContext[partials ? 'compileWithPartial' : 'compile'](string), ary;
var template,
ary,
options;
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]};
if (hashOrArray[4] != null) {
options.data = !!hashOrArray[4];
ary[1].data = hashOrArray[4];
}
} else {
ary = [hashOrArray];
}
template = CompilerContext[partials ? 'compileWithPartial' : 'compile'](string, options);
return template.apply(this, ary);
};
global.equals = global.equal = function(a, b, msg) {
if (a !== b) {
throw new Error("'" + b + "' should === '" + a + "'" + (msg ? ": " + msg : ''));
throw new Error("'" + a + "' should === '" + b + "'" + (msg ? ": " + msg : ''));
}
};
@@ -39,7 +48,7 @@ global.shouldThrow = function(callback, type, msg) {
throw new Error('Type failure');
}
if (msg && !(msg.test ? msg.test(err.message) : msg === err.message)) {
throw new Error('Message failure');
equal(msg, err.message);
}
}
if (failed) {
+489
View File
@@ -0,0 +1,489 @@
/*
json2.js
2014-02-04
Public Domain.
NO WARRANTY EXPRESSED OR IMPLIED. USE AT YOUR OWN RISK.
See http://www.JSON.org/js.html
This code should be minified before deployment.
See http://javascript.crockford.com/jsmin.html
USE YOUR OWN COPY. IT IS EXTREMELY UNWISE TO LOAD CODE FROM SERVERS YOU DO
NOT CONTROL.
This file creates a global JSON object containing two methods: stringify
and parse.
JSON.stringify(value, replacer, space)
value any JavaScript value, usually an object or array.
replacer an optional parameter that determines how object
values are stringified for objects. It can be a
function or an array of strings.
space an optional parameter that specifies the indentation
of nested structures. If it is omitted, the text will
be packed without extra whitespace. If it is a number,
it will specify the number of spaces to indent at each
level. If it is a string (such as '\t' or '&nbsp;'),
it contains the characters used to indent at each level.
This method produces a JSON text from a JavaScript value.
When an object value is found, if the object contains a toJSON
method, its toJSON method will be called and the result will be
stringified. A toJSON method does not serialize: it returns the
value represented by the name/value pair that should be serialized,
or undefined if nothing should be serialized. The toJSON method
will be passed the key associated with the value, and this will be
bound to the value
For example, this would serialize Dates as ISO strings.
Date.prototype.toJSON = function (key) {
function f(n) {
// Format integers to have at least two digits.
return n < 10 ? '0' + n : n;
}
return this.getUTCFullYear() + '-' +
f(this.getUTCMonth() + 1) + '-' +
f(this.getUTCDate()) + 'T' +
f(this.getUTCHours()) + ':' +
f(this.getUTCMinutes()) + ':' +
f(this.getUTCSeconds()) + 'Z';
};
You can provide an optional replacer method. It will be passed the
key and value of each member, with this bound to the containing
object. The value that is returned from your method will be
serialized. If your method returns undefined, then the member will
be excluded from the serialization.
If the replacer parameter is an array of strings, then it will be
used to select the members to be serialized. It filters the results
such that only members with keys listed in the replacer array are
stringified.
Values that do not have JSON representations, such as undefined or
functions, will not be serialized. Such values in objects will be
dropped; in arrays they will be replaced with null. You can use
a replacer function to replace those with JSON values.
JSON.stringify(undefined) returns undefined.
The optional space parameter produces a stringification of the
value that is filled with line breaks and indentation to make it
easier to read.
If the space parameter is a non-empty string, then that string will
be used for indentation. If the space parameter is a number, then
the indentation will be that many spaces.
Example:
text = JSON.stringify(['e', {pluribus: 'unum'}]);
// text is '["e",{"pluribus":"unum"}]'
text = JSON.stringify(['e', {pluribus: 'unum'}], null, '\t');
// text is '[\n\t"e",\n\t{\n\t\t"pluribus": "unum"\n\t}\n]'
text = JSON.stringify([new Date()], function (key, value) {
return this[key] instanceof Date ?
'Date(' + this[key] + ')' : value;
});
// text is '["Date(---current time---)"]'
JSON.parse(text, reviver)
This method parses a JSON text to produce an object or array.
It can throw a SyntaxError exception.
The optional reviver parameter is a function that can filter and
transform the results. It receives each of the keys and values,
and its return value is used instead of the original value.
If it returns what it received, then the structure is not modified.
If it returns undefined then the member is deleted.
Example:
// Parse the text. Values that look like ISO date strings will
// be converted to Date objects.
myData = JSON.parse(text, function (key, value) {
var a;
if (typeof value === 'string') {
a =
/^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2}(?:\.\d*)?)Z$/.exec(value);
if (a) {
return new Date(Date.UTC(+a[1], +a[2] - 1, +a[3], +a[4],
+a[5], +a[6]));
}
}
return value;
});
myData = JSON.parse('["Date(09/09/2001)"]', function (key, value) {
var d;
if (typeof value === 'string' &&
value.slice(0, 5) === 'Date(' &&
value.slice(-1) === ')') {
d = new Date(value.slice(5, -1));
if (d) {
return d;
}
}
return value;
});
This is a reference implementation. You are free to copy, modify, or
redistribute.
*/
/*jslint evil: true, regexp: true */
/*members "", "\b", "\t", "\n", "\f", "\r", "\"", JSON, "\\", apply,
call, charCodeAt, getUTCDate, getUTCFullYear, getUTCHours,
getUTCMinutes, getUTCMonth, getUTCSeconds, hasOwnProperty, join,
lastIndex, length, parse, prototype, push, replace, slice, stringify,
test, toJSON, toString, valueOf
*/
// Create a JSON object only if one does not already exist. We create the
// methods in a closure to avoid creating global variables.
if (typeof JSON !== 'object') {
JSON = {};
}
(function () {
'use strict';
function f(n) {
// Format integers to have at least two digits.
return n < 10 ? '0' + n : n;
}
if (typeof Date.prototype.toJSON !== 'function') {
Date.prototype.toJSON = function () {
return isFinite(this.valueOf())
? this.getUTCFullYear() + '-' +
f(this.getUTCMonth() + 1) + '-' +
f(this.getUTCDate()) + 'T' +
f(this.getUTCHours()) + ':' +
f(this.getUTCMinutes()) + ':' +
f(this.getUTCSeconds()) + 'Z'
: null;
};
String.prototype.toJSON =
Number.prototype.toJSON =
Boolean.prototype.toJSON = function () {
return this.valueOf();
};
}
var cx,
escapable,
gap,
indent,
meta,
rep;
function quote(string) {
// If the string contains no control characters, no quote characters, and no
// backslash characters, then we can safely slap some quotes around it.
// Otherwise we must also replace the offending characters with safe escape
// sequences.
escapable.lastIndex = 0;
return escapable.test(string) ? '"' + string.replace(escapable, function (a) {
var c = meta[a];
return typeof c === 'string'
? c
: '\\u' + ('0000' + a.charCodeAt(0).toString(16)).slice(-4);
}) + '"' : '"' + string + '"';
}
function str(key, holder) {
// Produce a string from holder[key].
var i, // The loop counter.
k, // The member key.
v, // The member value.
length,
mind = gap,
partial,
value = holder[key];
// If the value has a toJSON method, call it to obtain a replacement value.
if (value && typeof value === 'object' &&
typeof value.toJSON === 'function') {
value = value.toJSON(key);
}
// If we were called with a replacer function, then call the replacer to
// obtain a replacement value.
if (typeof rep === 'function') {
value = rep.call(holder, key, value);
}
// What happens next depends on the value's type.
switch (typeof value) {
case 'string':
return quote(value);
case 'number':
// JSON numbers must be finite. Encode non-finite numbers as null.
return isFinite(value) ? String(value) : 'null';
case 'boolean':
case 'null':
// If the value is a boolean or null, convert it to a string. Note:
// typeof null does not produce 'null'. The case is included here in
// the remote chance that this gets fixed someday.
return String(value);
// If the type is 'object', we might be dealing with an object or an array or
// null.
case 'object':
// Due to a specification blunder in ECMAScript, typeof null is 'object',
// so watch out for that case.
if (!value) {
return 'null';
}
// Make an array to hold the partial results of stringifying this object value.
gap += indent;
partial = [];
// Is the value an array?
if (Object.prototype.toString.apply(value) === '[object Array]') {
// The value is an array. Stringify every element. Use null as a placeholder
// for non-JSON values.
length = value.length;
for (i = 0; i < length; i += 1) {
partial[i] = str(i, value) || 'null';
}
// Join all of the elements together, separated with commas, and wrap them in
// brackets.
v = partial.length === 0
? '[]'
: gap
? '[\n' + gap + partial.join(',\n' + gap) + '\n' + mind + ']'
: '[' + partial.join(',') + ']';
gap = mind;
return v;
}
// If the replacer is an array, use it to select the members to be stringified.
if (rep && typeof rep === 'object') {
length = rep.length;
for (i = 0; i < length; i += 1) {
if (typeof rep[i] === 'string') {
k = rep[i];
v = str(k, value);
if (v) {
partial.push(quote(k) + (gap ? ': ' : ':') + v);
}
}
}
} else {
// Otherwise, iterate through all of the keys in the object.
for (k in value) {
if (Object.prototype.hasOwnProperty.call(value, k)) {
v = str(k, value);
if (v) {
partial.push(quote(k) + (gap ? ': ' : ':') + v);
}
}
}
}
// Join all of the member texts together, separated with commas,
// and wrap them in braces.
v = partial.length === 0
? '{}'
: gap
? '{\n' + gap + partial.join(',\n' + gap) + '\n' + mind + '}'
: '{' + partial.join(',') + '}';
gap = mind;
return v;
}
}
// If the JSON object does not yet have a stringify method, give it one.
if (typeof JSON.stringify !== 'function') {
escapable = /[\\\"\x00-\x1f\x7f-\x9f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g;
meta = { // table of character substitutions
'\b': '\\b',
'\t': '\\t',
'\n': '\\n',
'\f': '\\f',
'\r': '\\r',
'"' : '\\"',
'\\': '\\\\'
};
JSON.stringify = function (value, replacer, space) {
// The stringify method takes a value and an optional replacer, and an optional
// space parameter, and returns a JSON text. The replacer can be a function
// that can replace values, or an array of strings that will select the keys.
// A default replacer method can be provided. Use of the space parameter can
// produce text that is more easily readable.
var i;
gap = '';
indent = '';
// If the space parameter is a number, make an indent string containing that
// many spaces.
if (typeof space === 'number') {
for (i = 0; i < space; i += 1) {
indent += ' ';
}
// If the space parameter is a string, it will be used as the indent string.
} else if (typeof space === 'string') {
indent = space;
}
// If there is a replacer, it must be a function or an array.
// Otherwise, throw an error.
rep = replacer;
if (replacer && typeof replacer !== 'function' &&
(typeof replacer !== 'object' ||
typeof replacer.length !== 'number')) {
throw new Error('JSON.stringify');
}
// Make a fake root object containing our value under the key of ''.
// Return the result of stringifying the value.
return str('', {'': value});
};
}
// If the JSON object does not yet have a parse method, give it one.
if (typeof JSON.parse !== 'function') {
cx = /[\u0000\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g;
JSON.parse = function (text, reviver) {
// The parse method takes a text and an optional reviver function, and returns
// a JavaScript value if the text is a valid JSON text.
var j;
function walk(holder, key) {
// The walk method is used to recursively walk the resulting structure so
// that modifications can be made.
var k, v, value = holder[key];
if (value && typeof value === 'object') {
for (k in value) {
if (Object.prototype.hasOwnProperty.call(value, k)) {
v = walk(value, k);
if (v !== undefined) {
value[k] = v;
} else {
delete value[k];
}
}
}
}
return reviver.call(holder, key, value);
}
// Parsing happens in four stages. In the first stage, we replace certain
// Unicode characters with escape sequences. JavaScript handles many characters
// incorrectly, either silently deleting them, or treating them as line endings.
text = String(text);
cx.lastIndex = 0;
if (cx.test(text)) {
text = text.replace(cx, function (a) {
return '\\u' +
('0000' + a.charCodeAt(0).toString(16)).slice(-4);
});
}
// In the second stage, we run the text against regular expressions that look
// for non-JSON patterns. We are especially concerned with '()' and 'new'
// because they can cause invocation, and '=' because it can cause mutation.
// But just to be safe, we want to reject all unexpected forms.
// We split the second stage into 4 regexp operations in order to work around
// crippling inefficiencies in IE's and Safari's regexp engines. First we
// replace the JSON backslash pairs with '@' (a non-JSON character). Second, we
// replace all simple value tokens with ']' characters. Third, we delete all
// open brackets that follow a colon or comma or that begin the text. Finally,
// we look to see that the remaining characters are only whitespace or ']' or
// ',' or ':' or '{' or '}'. If that is so, then the text is safe for eval.
if (/^[\],:{}\s]*$/
.test(text.replace(/\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g, '@')
.replace(/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g, ']')
.replace(/(?:^|:|,)(?:\s*\[)+/g, ''))) {
// In the third stage we use the eval function to compile the text into a
// JavaScript structure. The '{' operator is subject to a syntactic ambiguity
// in JavaScript: it can begin a block or an object literal. We wrap the text
// in parens to eliminate the ambiguity.
j = eval('(' + text + ')');
// In the optional fourth stage, we recursively walk the new structure, passing
// each name/value pair to a reviver function for possible transformation.
return typeof reviver === 'function'
? walk({'': j}, '')
: j;
}
// If the text is not JSON parseable, then a SyntaxError is thrown.
throw new SyntaxError('JSON.parse');
};
}
}());
+6 -1
View File
@@ -14,5 +14,10 @@ global.CompilerContext = {
};
function safeEval(templateSpec) {
return eval('(' + templateSpec + ')');
try {
return eval('(' + templateSpec + ')');
} catch (err) {
console.error(templateSpec);
throw err;
}
}
+6 -1
View File
@@ -39,5 +39,10 @@ global.CompilerContext = {
};
function safeEval(templateSpec) {
return eval('(' + templateSpec + ')');
try {
return eval('(' + templateSpec + ')');
} catch (err) {
console.error(templateSpec);
throw err;
}
}
+3 -8
View File
@@ -1,11 +1,6 @@
define(['handlebars.runtime'], function(Handlebars) {
Handlebars = Handlebars["default"]; var template = Handlebars.template, templates = Handlebars.templates = Handlebars.templates || {};
return templates['empty'] = template(function (Handlebars,depth0,helpers,partials,data) {
this.compilerInfo = [4,'>= 1.0.0'];
helpers = this.merge(helpers, Handlebars.helpers); data = data || {};
var buffer = "";
return buffer;
});
return templates['empty'] = template({"compiler":[6,">= 2.0.0-beta.1"],"main":function(depth0,helpers,partials,data) {
return "";
},"useData":true});
});
+142 -14
View File
@@ -9,6 +9,26 @@ describe('helpers', function() {
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" };
var helpers = { raw: function(options) {
return options.fn();
} };
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" };
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");
});
it("helper block with complex lookup expression", function() {
var string = "{{#goodbyes}}{{../name}}{{/goodbyes}}";
var hash = {name: "Alan"};
@@ -138,6 +158,22 @@ describe('helpers', function() {
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() {
var hash = {
helper: function() {
return 'winning';
}
};
hash.hash = hash;
var helpers = {
'./helper': function() {
return 'fail';
}
};
shouldCompileTo('{{./helper 1}}', [hash, helpers], 'winning');
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!",
@@ -172,20 +208,52 @@ describe('helpers', function() {
});
});
it("Multiple global helper registration", function() {
var helpers = handlebarsEnv.helpers;
handlebarsEnv.helpers = {};
describe('registration', function() {
it('unregisters', function() {
var helpers = handlebarsEnv.helpers;
handlebarsEnv.helpers = {};
handlebarsEnv.registerHelper({
'if': helpers['if'],
world: function() { return "world!"; },
test_helper: function() { return 'found it!'; }
handlebarsEnv.registerHelper('foo', function() {
return 'fail';
});
handlebarsEnv.unregisterHelper('foo');
equals(handlebarsEnv.helpers.foo, undefined);
});
shouldCompileTo(
"{{test_helper}} {{#if cruel}}Goodbye {{cruel}} {{world}}!{{/if}}",
[{cruel: "cruel"}],
"found it! Goodbye cruel world!!");
it('allows multiple globals', function() {
var helpers = handlebarsEnv.helpers;
handlebarsEnv.helpers = {};
handlebarsEnv.registerHelper({
'if': helpers['if'],
world: function() { return "world!"; },
test_helper: function() { return 'found it!'; }
});
shouldCompileTo(
"{{test_helper}} {{#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!'; }
}, {});
}, 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";
}};
shouldCompileTo(string, [hash, helpers], "Message: Hello -1.2 1.2 times", "template with a negative integer literal");
});
it("negative number literals work", function() {
@@ -260,6 +328,7 @@ describe('helpers', function() {
shouldCompileTo(string, [hash, helpers], "Message: Goodbye cruel world", "block helpers with multiple params");
});
});
describe('hash', function() {
it("helpers can take an optional hash", function() {
var template = CompilerContext.compile('{{goodbye cruel="CRUEL" world="WORLD" times=12}}');
@@ -362,15 +431,30 @@ describe('helpers', function() {
var context = { hello: "Hello", world: "world" };
var helpers = {
helperMissing: function(helper, context) {
if(helper === "link_to") {
return new Handlebars.SafeString("<a>" + context + "</a>");
helperMissing: function(mesg, options) {
if(options.name === "link_to") {
return new Handlebars.SafeString("<a>" + mesg + "</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" };
var helpers = {
helperMissing: function(options) {
if(options.name === "link_to") {
return new Handlebars.SafeString("<a>winning</a>");
}
}
};
shouldCompileTo(string, [context, helpers], "Hello <a>winning</a>");
});
});
describe("knownHelpers", function() {
@@ -436,6 +520,50 @@ describe('helpers', function() {
});
});
describe('name field', function() {
var context = {};
var helpers = {
blockHelperMissing: function() {
return 'missing: ' + arguments[arguments.length-1].name;
},
helperMissing: function() {
return 'helper missing: ' + arguments[arguments.length-1].name;
},
helper: function() {
return 'ran: ' + arguments[arguments.length-1].name;
}
};
it('should include in ambiguous mustache calls', function() {
shouldCompileTo('{{helper}}', [context, helpers], 'ran: helper');
});
it('should include in helper mustache calls', function() {
shouldCompileTo('{{helper 1}}', [context, helpers], 'ran: helper');
});
it('should include in ambiguous block calls', function() {
shouldCompileTo('{{#helper}}{{/helper}}', [context, helpers], 'ran: helper');
});
it('should include in simple block calls', function() {
shouldCompileTo('{{#./helper}}{{/./helper}}', [context, helpers], 'missing: ./helper');
});
it('should include in helper block calls', 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});
equal(template({}, {helpers: helpers}), 'ran: helper');
});
it('should include full id', function() {
shouldCompileTo('{{#foo.helper}}{{/foo.helper}}', [{foo: {}}, helpers], 'missing: foo.helper');
});
it('should include full id if a hash is passed', function() {
shouldCompileTo('{{#foo.helper bar=baz}}{{/foo.helper}}', [{foo: {}}, helpers], 'helper missing: foo.helper');
});
});
describe('name conflicts', function() {
it("helpers take precedence over same-named context properties", function() {
var template = CompilerContext.compile("{{goodbye}} {{cruel world}}");
+40 -4
View File
@@ -24,6 +24,7 @@
</script>
<script src="/dist/handlebars.js"></script>
<script src="/spec/env/json2.js"></script>
<script src="/spec/env/common.js"></script>
<script>
var CompilerContext = {
@@ -37,19 +38,54 @@
};
function safeEval(templateSpec) {
var ret;
eval('ret = ' + templateSpec);
return ret;
try {
var ret;
eval('ret = ' + templateSpec);
return ret;
} catch (err) {
console.error(templateSpec);
throw err;
}
}
</script>
<script src="/tmp/tests.js"></script>
<script>
onload = function(){
mocha.globals(['mochaResults'])
// The test harness leaks under FF. We should have decent global leak coverage from other tests
if (!navigator.userAgent.match(/Firefox\/([\d.]+)/)) {
mocha.checkLeaks();
}
mocha.run();
var runner = mocha.run();
//Reporting for saucelabs
var failedTests = [];
runner.on('end', function(){
window.mochaResults = runner.stats;
window.mochaResults.reports = failedTests;
});
runner.on('fail', logFailure);
function logFailure(test, err){
var flattenTitles = function(test){
var titles = [];
while (test.parent.title){
titles.push(test.parent.title);
test = test.parent;
}
return titles.reverse();
};
failedTests.push({
name: test.title,
result: false,
message: err.message,
stack: err.stack,
titles: flattenTitles(test)
});
};
};
</script>
</head>
+3 -1
View File
@@ -32,7 +32,7 @@ describe('javascript-compiler api', function() {
});
it('should allow compilerInfo override', function() {
handlebarsEnv.JavaScriptCompiler.prototype.compilerInfo = function() {
return 'this.compilerInfo = "crazy";';
return 'crazy';
};
handlebarsEnv.VM.checkRevision = function(compilerInfo) {
if (compilerInfo !== 'crazy') {
@@ -45,10 +45,12 @@ describe('javascript-compiler api', function() {
describe('buffer', function() {
var $superAppend, $superCreate;
beforeEach(function() {
handlebarsEnv.JavaScriptCompiler.prototype.forceBuffer = true;
$superAppend = handlebarsEnv.JavaScriptCompiler.prototype.appendToBuffer;
$superCreate = handlebarsEnv.JavaScriptCompiler.prototype.initializeBuffer;
});
afterEach(function() {
handlebarsEnv.JavaScriptCompiler.prototype.forceBuffer = false;
handlebarsEnv.JavaScriptCompiler.prototype.appendToBuffer = $superAppend;
handlebarsEnv.JavaScriptCompiler.prototype.initializeBuffer = $superCreate;
});
Submodule
+1
Submodule spec/mustache added at 72233f3ffd
+26 -7
View File
@@ -1,4 +1,4 @@
/*global Handlebars */
/*global Handlebars, shouldThrow */
describe('parser', function() {
if (!Handlebars.print) {
return;
@@ -21,6 +21,10 @@ describe('parser', function() {
equals(ast_for("{{@foo}}"), "{{ @ID:foo [] }}\n");
});
it('parses simple mustaches with data paths', function() {
equals(ast_for("{{@../foo}}"), "{{ @ID:foo [] }}\n");
});
it('parses mustaches with paths', function() {
equals(ast_for("{{foo/bar}}"), "{{ PATH:foo/bar [] }}\n");
});
@@ -41,8 +45,8 @@ describe('parser', function() {
equals(ast_for("{{foo bar \"baz\" }}"), '{{ ID:foo [ID:bar, "baz"] }}\n');
});
it('parses mustaches with INTEGER parameters', function() {
equals(ast_for("{{foo 1}}"), "{{ ID:foo [INTEGER{1}] }}\n");
it('parses mustaches with NUMBER parameters', function() {
equals(ast_for("{{foo 1}}"), "{{ ID:foo [NUMBER{1}] }}\n");
});
it('parses mustaches with BOOLEAN parameters', function() {
@@ -56,7 +60,7 @@ describe('parser', function() {
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=INTEGER{1}} }}\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");
@@ -67,7 +71,7 @@ describe('parser', function() {
equals(ast_for("{{foo bat='bam'}}"), '{{ ID: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=INTEGER{1}} }}\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');
});
@@ -84,6 +88,14 @@ describe('parser', function() {
equals(ast_for("{{> foo bar}}"), "{{> PARTIAL:foo ID:bar }}\n");
});
it('parses a partial with hash', function() {
equals(ast_for("{{> foo bar=bat}}"), "{{> PARTIAL:foo HASH{bar=ID: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");
});
it('parses a partial with a complex name', function() {
equals(ast_for("{{> shared/partial?.bar}}"), "{{> PARTIAL:shared/partial?.bar }}\n");
});
@@ -109,11 +121,11 @@ describe('parser', function() {
});
it('parses empty blocks with empty inverse section', function() {
equals(ast_for("{{#foo}}{{^}}{{/foo}}"), "BLOCK:\n {{ ID:foo [] }}\n PROGRAM:\n");
equals(ast_for("{{#foo}}{{^}}{{/foo}}"), "BLOCK:\n {{ ID: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");
equals(ast_for("{{#foo}}{{else}}{{/foo}}"), "BLOCK:\n {{ ID:foo [] }}\n PROGRAM:\n {{^}}\n");
});
it('parses non-empty blocks with empty inverse section', function() {
@@ -135,6 +147,9 @@ describe('parser', function() {
it('parses a standalone inverse section', function() {
equals(ast_for("{{^foo}}bar{{/foo}}"), "BLOCK:\n {{ ID: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("raises if there's a Parse error", function() {
shouldThrow(function() {
@@ -149,6 +164,10 @@ describe('parser', function() {
shouldThrow(function() {
ast_for("{{#goodbyes}}{{/hellos}}");
}, Error, /goodbyes doesn't match hellos/);
shouldThrow(function() {
ast_for("{{{{goodbyes}}}} {{{{/hellos}}}}");
}, Error, /goodbyes doesn't match hellos/);
});
it('knows how to report the correct line number in errors', function() {
+68 -8
View File
@@ -1,11 +1,11 @@
/*global CompilerContext, shouldCompileTo, shouldCompileToWithPartials */
/*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) ",
"Basic partials output based on current context.");
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) ');
});
it("partials with context", function() {
@@ -23,6 +23,14 @@ describe('partials', function() {
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}} ";
@@ -83,6 +91,9 @@ describe('partials', function() {
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");
handlebarsEnv.unregisterPartial('global_test');
equals(handlebarsEnv.partials.global_test, undefined);
});
it("Multiple partial registration", function() {
@@ -128,5 +139,54 @@ describe('partials', 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: "); });
shouldCompileToWithPartials(string, [hash, {}, {dude: partial}], true, "Dudes: ");
});
it("throw on missing partial", function() {
var compile = handlebarsEnv.compile;
handlebarsEnv.compile = undefined;
shouldThrow(function() {
shouldCompileTo('{{> dude}}', [{}, {}, {dude: 'fail'}], '');
}, Error, /The partial dude could not be compiled/);
handlebarsEnv.compile = compile;
});
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"}]};
shouldCompileToWithPartials(string, [hash, {}, {dude: dude}], true,
"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"}]};
shouldCompileToWithPartials(string, [hash, {}, {dude: dude, url: url}], true,
"Dudes:\n Yehuda\n http://yehuda!\n Alan\n http://alan!\n");
});
});
describe('compat mode', function() {
it('partials can access parents', function() {
var string = 'Dudes: {{#dudes}}{{> dude}}{{/dudes}}';
var partial = '{{name}} ({{url}}) {{root}} ';
var hash = {root: 'yes', dudes: [{name: 'Yehuda', url: 'http://yehuda'}, {name: 'Alan', url: 'http://alan'}]};
shouldCompileToWithPartials(string, [hash, {}, {dude: partial}, true], true, 'Dudes: Yehuda (http://yehuda) yes Alan (http://alan) yes ');
});
it('partials can access parents without data', function() {
var string = 'Dudes: {{#dudes}}{{> dude}}{{/dudes}}';
var partial = '{{name}} ({{url}}) {{root}} ';
var hash = {root: 'yes', dudes: [{name: 'Yehuda', url: 'http://yehuda'}, {name: 'Alan', url: 'http://alan'}]};
shouldCompileToWithPartials(string, [hash, {}, {dude: partial}, true, false], true, 'Dudes: Yehuda (http://yehuda) yes Alan (http://alan) yes ');
});
it('partials inherit compat', function() {
var string = 'Dudes: {{> dude}}';
var partial = '{{#dudes}}{{name}} ({{url}}) {{root}} {{/dudes}}';
var hash = {root: 'yes', dudes: [{name: 'Yehuda', url: 'http://yehuda'}, {name: 'Alan', url: 'http://alan'}]};
shouldCompileToWithPartials(string, [hash, {}, {dude: partial}, true], true, 'Dudes: Yehuda (http://yehuda) yes Alan (http://alan) yes ');
});
});
});
+130
View File
@@ -0,0 +1,130 @@
/*global shouldThrow */
describe('precompiler', function() {
// NOP Under non-node environments
if (typeof process === 'undefined') {
return;
}
var Handlebars = require('../lib'),
Precompiler = require('../lib/precompiler'),
uglify = require('uglify-js');
var log,
logFunction,
precompile,
minify;
beforeEach(function() {
precompile = Handlebars.precompile;
minify = uglify.minify;
logFunction = console.log;
log = '';
console.log = function() {
log += Array.prototype.join.call(arguments, '');
};
});
afterEach(function() {
Handlebars.precompile = precompile;
uglify.minify = minify;
console.log = logFunction;
});
it('should output version', function() {
Precompiler.cli({templates: [], version: true});
equals(log, Handlebars.VERSION);
});
it('should throw if lacking templates', function() {
shouldThrow(function() {
Precompiler.cli({templates: []});
}, Handlebars.Exception, 'Must define at least one template or directory.');
});
it('should throw on missing template', function() {
shouldThrow(function() {
Precompiler.cli({templates: ['foo']});
}, Handlebars.Exception, 'Unable to open template file "foo"');
});
it('should throw when combining simple and minimized', function() {
shouldThrow(function() {
Precompiler.cli({templates: [__dirname], simple: true, min: true});
}, Handlebars.Exception, 'Unable to minimze simple output');
});
it('should throw when combining simple and multiple templates', function() {
shouldThrow(function() {
Precompiler.cli({templates: [__dirname + '/artifacts/empty.handlebars', __dirname + '/artifacts/empty.handlebars'], simple: true});
}, Handlebars.Exception, 'Unable to output multiple templates in simple mode');
});
it('should throw when combining simple and directories', function() {
shouldThrow(function() {
Precompiler.cli({templates: [__dirname], simple: true});
}, Handlebars.Exception, 'Unable to output multiple templates in simple mode');
});
it('should enumerate directories by extension', function() {
Precompiler.cli({templates: [__dirname + '/artifacts'], extension: 'hbs'});
equal(/'example_2'/.test(log), true);
log = '';
Precompiler.cli({templates: [__dirname + '/artifacts'], extension: 'handlebars'});
equal(/'empty'/.test(log), true);
equal(/'example_1'/.test(log), true);
});
it('should protect from regexp patterns', function() {
Precompiler.cli({templates: [__dirname + '/artifacts'], extension: 'hb(s'});
// Success is not throwing
});
it('should output simple templates', function() {
Handlebars.precompile = function() { return 'simple'; };
Precompiler.cli({templates: [__dirname + '/artifacts/empty.handlebars'], simple: true, extension: 'handlebars'});
equal(log, 'simple\n');
});
it('should output amd templates', function() {
Handlebars.precompile = function() { return 'amd'; };
Precompiler.cli({templates: [__dirname + '/artifacts/empty.handlebars'], amd: true, extension: 'handlebars'});
equal(/template\(amd\)/.test(log), true);
});
it('should output multiple amd', function() {
Handlebars.precompile = function() { return 'amd'; };
Precompiler.cli({templates: [__dirname + '/artifacts'], amd: true, extension: 'handlebars'});
equal(/return templates/.test(log), true);
equal(/template\(amd\)/.test(log), true);
});
it('should output amd partials', function() {
Handlebars.precompile = function() { return 'amd'; };
Precompiler.cli({templates: [__dirname + '/artifacts/empty.handlebars'], amd: true, partial: true, extension: 'handlebars'});
equal(/return Handlebars\.partials\['empty'\]/.test(log), true);
equal(/template\(amd\)/.test(log), true);
});
it('should output multiple amd partials', function() {
Handlebars.precompile = function() { return 'amd'; };
Precompiler.cli({templates: [__dirname + '/artifacts'], amd: true, partial: true, extension: 'handlebars'});
equal(/return Handlebars\.partials\[/.test(log), false);
equal(/template\(amd\)/.test(log), true);
});
it('should output commonjs templates', function() {
Handlebars.precompile = function() { return 'commonjs'; };
Precompiler.cli({templates: [__dirname + '/artifacts/empty.handlebars'], commonjs: true, extension: 'handlebars'});
equal(/template\(commonjs\)/.test(log), true);
});
it('should set data flag', function() {
Handlebars.precompile = function(data, options) { equal(options.data, true); return 'simple'; };
Precompiler.cli({templates: [__dirname + '/artifacts/empty.handlebars'], simple: true, extension: 'handlebars', data: true});
equal(log, 'simple\n');
});
it('should set known helpers', function() {
Handlebars.precompile = function(data, options) { equal(options.knownHelpers.foo, true); return 'simple'; };
Precompiler.cli({templates: [__dirname + '/artifacts/empty.handlebars'], simple: true, extension: 'handlebars', known: 'foo'});
equal(log, 'simple\n');
});
it('should 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');
});
});
+32 -14
View File
@@ -24,7 +24,19 @@ describe('Regressions', function() {
});
it("bug reported by @fat where lambdas weren't being properly resolved", function() {
var string = "<strong>This is a slightly more complicated {{thing}}.</strong>.\n{{! Just ignore this business. }}\nCheck this out:\n{{#hasThings}}\n<ul>\n{{#things}}\n<li class={{className}}>{{word}}</li>\n{{/things}}</ul>.\n{{/hasThings}}\n{{^hasThings}}\n\n<small>Nothing to check out...</small>\n{{/hasThings}}";
var string = '<strong>This is a slightly more complicated {{thing}}.</strong>.\n'
+ '{{! Just ignore this business. }}\n'
+ 'Check this out:\n'
+ '{{#hasThings}}\n'
+ '<ul>\n'
+ '{{#things}}\n'
+ '<li class={{className}}>{{word}}</li>\n'
+ '{{/things}}</ul>.\n'
+ '{{/hasThings}}\n'
+ '{{^hasThings}}\n'
+ '\n'
+ '<small>Nothing to check out...</small>\n'
+ '{{/hasThings}}';
var data = {
thing: function() {
return "blah";
@@ -39,7 +51,13 @@ describe('Regressions', function() {
}
};
var output = "<strong>This is a slightly more complicated blah.</strong>.\n\nCheck this out:\n\n<ul>\n\n<li class=one>@fat</li>\n\n<li class=two>@dhg</li>\n\n<li class=three>@sayrer</li>\n</ul>.\n\n";
var output = '<strong>This is a slightly more complicated blah.</strong>.\n'
+ 'Check this out:\n'
+ '<ul>\n'
+ '<li class=one>@fat</li>\n'
+ '<li class=two>@dhg</li>\n'
+ '<li class=three>@sayrer</li>\n'
+ '</ul>.\n';
shouldCompileTo(string, data, output);
});
@@ -112,19 +130,19 @@ describe('Regressions', function() {
shouldCompileTo(string, data, "Hello Chris. You have just won $10000! Well, $6000, after taxes.", "the hello world mustache example works");
});
it("Passing falsy values to Handlebars.compile throws an error", function() {
shouldThrow(function() {
CompilerContext.compile(null);
}, Error, 'You must pass a string or Handlebars AST to Handlebars.precompile. You passed null');
it('GH-731: zero context rendering', function() {
shouldCompileTo('{{#foo}} This is {{bar}} ~ {{/foo}}', {foo: 0, bar: 'OK'}, ' This is ~ ');
});
if (Handlebars.AST) {
it("can pass through an already-compiled AST via compile/precompile", function() {
equal(Handlebars.compile(new Handlebars.AST.ProgramNode([ new Handlebars.AST.ContentNode("Hello")]))(), 'Hello');
});
it('GH-820: zero pathed rendering', function() {
shouldCompileTo('{{foo.bar}}', {foo: 0}, '');
});
it("can pass through an empty string", function() {
equal(Handlebars.compile('')(), '');
});
}
it('GH-837: undefined values for helpers', function() {
var helpers = {
str: function(value) { return value + ''; }
};
shouldCompileTo('{{str bar.baz}}', [{}, helpers], 'undefined');
});
});
+63
View File
@@ -0,0 +1,63 @@
/*globals Handlebars, shouldThrow */
describe('runtime', function() {
describe('#template', function() {
it('should throw on invalid templates', function() {
shouldThrow(function() {
Handlebars.template({});
}, Error, 'Unknown template object: object');
shouldThrow(function() {
Handlebars.template();
}, Error, 'Unknown template object: undefined');
shouldThrow(function() {
Handlebars.template('');
}, Error, 'Unknown template object: string');
});
it('should throw on version mismatch', function() {
shouldThrow(function() {
Handlebars.template({
main: true,
compiler: [Handlebars.COMPILER_REVISION + 1]
});
}, Error, /Template was precompiled with a newer version of Handlebars than the current runtime/);
shouldThrow(function() {
Handlebars.template({
main: true,
compiler: [Handlebars.COMPILER_REVISION - 1]
});
}, Error, /Template was precompiled with an older version of Handlebars than the current runtime/);
shouldThrow(function() {
Handlebars.template({
main: true
});
}, Error, /Template was precompiled with an older version of Handlebars than the current runtime/);
});
});
describe('#child', function() {
if (!Handlebars.compile) {
return;
}
it('should throw for depthed methods without depths', function() {
shouldThrow(function() {
var template = Handlebars.compile('{{#foo}}{{../bar}}{{/foo}}');
// Calling twice to hit the non-compiled case.
template._setup({});
template._setup({});
template._child(1);
}, Error, 'must pass parent depths');
});
it('should expose child template', function() {
var template = Handlebars.compile('{{#foo}}bar{{/foo}}');
// Calling twice to hit the non-compiled case.
equal(template._child(1)(), 'bar');
equal(template._child(1)(), 'bar');
});
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');
});
});
});
+49
View File
@@ -0,0 +1,49 @@
describe('spec', function() {
// NOP Under non-node environments
if (typeof process === 'undefined') {
return;
}
var _ = require('underscore'),
Handlebars = require('../lib'),
fs = require('fs');
var specDir =__dirname + '/mustache/specs/';
var specs = _.filter(fs.readdirSync(specDir), function(name) {
return /.*\.json$/.test(name);
});
_.each(specs, function(name) {
var spec = require(specDir + name);
_.each(spec.tests, function(test) {
// Our lambda implementation knowingly deviates from the optional Mustace lambda spec
// We also do not support alternative delimeters
if (name === '~lambdas.json'
// We also choose to throw if paritals are not found
|| (name === 'partials.json' && test.name === 'Failed Lookup')
// We nest the entire response from partials, not just the literals
|| (name === 'partials.json' && test.name === 'Standalone Indentation')
|| /\{\{\=/.test(test.template)
|| _.any(test.partials, function(partial) { return /\{\{\=/.test(partial); })) {
it.skip(name + ' - ' + test.name);
return;
}
var data = _.clone(test.data);
if (data.lambda) {
// Blergh
data.lambda = eval('(' + data.lambda.js + ')');
}
it(name + ' - ' + test.name, function() {
if (test.partials) {
shouldCompileToWithPartials(test.template, [data, {}, test.partials, true], true, test.expected, test.desc + ' "' + test.template + '"');
} else {
shouldCompileTo(test.template, [data, {}, {}, true], test.expected, test.desc + ' "' + test.template + '"');
}
});
});
});
});
+16 -1
View File
@@ -147,7 +147,7 @@ describe('string params mode', function() {
var template = CompilerContext.compile('{{#with content}}{{#view}}{{firstName}} {{lastName}}{{/view}}{{/with}}', {stringParams: true});
var helpers = {
with: function(options) {
'with': function(options) {
return "WITH";
},
view: function() {
@@ -158,4 +158,19 @@ describe('string params mode', function() {
var result = template({}, {helpers: helpers});
equals(result, "WITH");
});
it('should handle DATA', function() {
var template = CompilerContext.compile('{{foo @bar}}', { stringParams: true });
var helpers = {
foo: function(bar, options) {
equal(bar, 'bar');
equal(options.types[0], 'DATA');
return 'Foo!';
}
};
var result = template({}, { helpers: helpers });
equal(result, 'Foo!');
});
});
+74
View File
@@ -29,6 +29,21 @@ describe('subexpressions', function() {
shouldCompileTo(string, [context, helpers], "val is true");
});
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 = {
blog: function(val, that, 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!");
});
it("supports much nesting", function() {
var string = '{{blog (equal (equal true true) true)}}';
@@ -44,6 +59,24 @@ describe('subexpressions', function() {
shouldCompileTo(string, [context, helpers], "val is true");
});
it('GH-800 : Complex subexpressions', function() {
var context = {a: 'a', b:'b', c:{c:'c'}, d:'d', e: {e: 'e'}};
var helpers = {
dash: function(a, b) {
return a + "-" + b;
},
concat: function(a, b) {
return a + b;
}
};
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() {
var string = '{{equal (equal true true) true}}';
@@ -89,6 +122,47 @@ describe('subexpressions', function() {
shouldCompileTo(string, [{}, helpers], "val is true");
});
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 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() {
var string = '{{input aria-label=(t item.field) placeholder=(t item.placeholder)}}';
var context = {
item: {
field: "Name",
placeholder: "Example User"
}
};
var helpers = {
input: function(options) {
var hash = options.hash;
var ariaLabel = Handlebars.Utils.escapeExpression(hash['aria-label']);
var placeholder = Handlebars.Utils.escapeExpression(hash.placeholder);
return new Handlebars.SafeString('<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() {
var template = CompilerContext.compile('{{snog (blorg foo x=y) yeah a=b}}', {stringParams: true});
+18 -10
View File
@@ -237,10 +237,10 @@ describe('Tokenizer', function() {
shouldMatchTokens(result, ['OPEN_BLOCK', 'ID', 'CLOSE', 'CONTENT', 'OPEN_ENDBLOCK', 'ID', 'CLOSE']);
});
it('tokenizes inverse sections as "OPEN_INVERSE CLOSE"', function() {
shouldMatchTokens(tokenize("{{^}}"), ['OPEN_INVERSE', 'CLOSE']);
shouldMatchTokens(tokenize("{{else}}"), ['OPEN_INVERSE', 'CLOSE']);
shouldMatchTokens(tokenize("{{ else }}"), ['OPEN_INVERSE', 'CLOSE']);
it('tokenizes inverse sections as "INVERSE"', function() {
shouldMatchTokens(tokenize("{{^}}"), ['INVERSE']);
shouldMatchTokens(tokenize("{{else}}"), ['INVERSE']);
shouldMatchTokens(tokenize("{{ else }}"), ['INVERSE']);
});
it('tokenizes inverse sections with ID as "OPEN_INVERSE ID CLOSE"', function() {
@@ -295,12 +295,20 @@ describe('Tokenizer', function() {
it('tokenizes numbers', function() {
var result = tokenize('{{ foo 1 }}');
shouldMatchTokens(result, ['OPEN', 'ID', 'INTEGER', 'CLOSE']);
shouldBeToken(result[2], "INTEGER", "1");
shouldMatchTokens(result, ['OPEN', 'ID', 'NUMBER', 'CLOSE']);
shouldBeToken(result[2], "NUMBER", "1");
result = tokenize('{{ foo 1.1 }}');
shouldMatchTokens(result, ['OPEN', 'ID', 'NUMBER', 'CLOSE']);
shouldBeToken(result[2], "NUMBER", "1.1");
result = tokenize('{{ foo -1 }}');
shouldMatchTokens(result, ['OPEN', 'ID', 'INTEGER', 'CLOSE']);
shouldBeToken(result[2], "INTEGER", "-1");
shouldMatchTokens(result, ['OPEN', 'ID', 'NUMBER', 'CLOSE']);
shouldBeToken(result[2], "NUMBER", "-1");
result = tokenize('{{ foo -1.1 }}');
shouldMatchTokens(result, ['OPEN', 'ID', 'NUMBER', 'CLOSE']);
shouldBeToken(result[2], "NUMBER", "-1.1");
});
it('tokenizes booleans', function() {
@@ -321,7 +329,7 @@ describe('Tokenizer', function() {
shouldMatchTokens(result, ['OPEN', 'ID', 'ID', 'ID', 'EQUALS', 'ID', 'CLOSE']);
result = tokenize("{{ foo bar baz=1 }}");
shouldMatchTokens(result, ['OPEN', 'ID', 'ID', 'ID', 'EQUALS', 'INTEGER', 'CLOSE']);
shouldMatchTokens(result, ['OPEN', 'ID', 'ID', 'ID', 'EQUALS', 'NUMBER', 'CLOSE']);
result = tokenize("{{ foo bar baz=true }}");
shouldMatchTokens(result, ['OPEN', 'ID', 'ID', 'ID', 'EQUALS', 'BOOLEAN', 'CLOSE']);
@@ -389,6 +397,6 @@ describe('Tokenizer', function() {
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', 'INTEGER', 'CLOSE_SEXPR', 'OPEN_SEXPR', 'ID', 'STRING', 'CLOSE_SEXPR', 'OPEN_SEXPR', 'ID', 'STRING', 'CLOSE_SEXPR', 'CLOSE']);
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']);
});
});
+163
View File
@@ -0,0 +1,163 @@
/*global CompilerContext */
describe('track ids', function() {
var context;
beforeEach(function() {
context = {is: {a: 'foo'}, slave: {driver: 'bar'}};
});
it('should not include anything without the flag', function() {
var template = CompilerContext.compile('{{wycats is.a slave.driver}}');
var helpers = {
wycats: function(passiveVoice, noun, options) {
equal(options.ids, undefined);
equal(options.hashIds, undefined);
return 'success';
}
};
equals(template({}, {helpers: helpers}), 'success');
});
it('should include argument ids', function() {
var template = CompilerContext.compile('{{wycats is.a slave.driver}}', {trackIds: true});
var helpers = {
wycats: function(passiveVoice, noun, options) {
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;
}
};
equals(template(context, {helpers: helpers}), 'HELP ME MY BOSS is.a:foo slave.driver:bar');
});
it('should include hash ids', function() {
var template = CompilerContext.compile('{{wycats bat=is.a baz=slave.driver}}', {trackIds: true});
var helpers = {
wycats: function(options) {
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;
}
};
equals(template(context, {helpers: helpers}), 'HELP ME MY BOSS is.a:foo slave.driver:bar');
});
it('should note ../ and ./ references', function() {
var template = CompilerContext.compile('{{wycats ./is.a ../slave.driver}}', {trackIds: true});
var helpers = {
wycats: function(passiveVoice, noun, options) {
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;
}
};
equals(template(context, {helpers: helpers}), 'HELP ME MY BOSS is.a:foo ../slave.driver:undefined');
});
it('should note @data references', function() {
var template = CompilerContext.compile('{{wycats @is.a @slave.driver}}', {trackIds: true});
var helpers = {
wycats: function(passiveVoice, noun, options) {
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;
}
};
equals(template({}, {helpers: helpers, data:context}), 'HELP ME MY BOSS @is.a:foo @slave.driver:bar');
});
it('should return null for constants', function() {
var template = CompilerContext.compile('{{wycats 1 "foo" key=false}}', {trackIds: true});
var helpers = {
wycats: function(passiveVoice, noun, options) {
equal(options.ids[0], null);
equal(options.ids[1], null);
equal(options.hashIds.key, null);
return "HELP ME MY BOSS " + passiveVoice + ' ' + noun + ' ' + options.hash.key;
}
};
equals(template(context, {helpers: helpers}), 'HELP ME MY BOSS 1 foo false');
});
it('should return true for subexpressions', function() {
var template = CompilerContext.compile('{{wycats (sub)}}', {trackIds: true});
var helpers = {
sub: function() { return 1; },
wycats: function(passiveVoice, options) {
equal(options.ids[0], true);
return "HELP ME MY BOSS " + passiveVoice;
}
};
equals(template(context, {helpers: helpers}), 'HELP ME MY BOSS 1');
});
describe('builtin helpers', function() {
var helpers = {
wycats: function(name, options) {
return name + ':' + options.data.contextPath + '\n';
}
};
describe('#each', function() {
it('should track contextPath for arrays', function() {
var template = CompilerContext.compile('{{#each array}}{{wycats name}}{{/each}}', {trackIds: true});
equals(template({array: [{name: 'foo'}, {name: 'bar'}]}, {helpers: helpers}), 'foo:array.0\nbar:array.1\n');
});
it('should track contextPath for keys', function() {
var template = CompilerContext.compile('{{#each object}}{{wycats name}}{{/each}}', {trackIds: true});
equals(template({object: {foo: {name: 'foo'}, bar: {name: 'bar'}}}, {helpers: helpers}), 'foo:object.foo\nbar:object.bar\n');
});
it('should handle nesting', function() {
var template = CompilerContext.compile('{{#each .}}{{#each .}}{{wycats name}}{{/each}}{{/each}}', {trackIds: true});
equals(template({array: [{name: 'foo'}, {name: 'bar'}]}, {helpers: helpers}), 'foo:.array..0\nbar:.array..1\n');
});
});
describe('#with', function() {
it('should track contextPath', function() {
var template = CompilerContext.compile('{{#with field}}{{wycats name}}{{/with}}', {trackIds: true});
equals(template({field: {name: 'foo'}}, {helpers: helpers}), 'foo:field\n');
});
it('should handle nesting', function() {
var template = CompilerContext.compile('{{#with bat}}{{#with field}}{{wycats name}}{{/with}}{{/with}}', {trackIds: true});
equals(template({bat: {field: {name: 'foo'}}}, {helpers: helpers}), 'foo:bat.field\n');
});
});
describe('#blockHelperMissing', function() {
it('should track contextPath for arrays', function() {
var template = CompilerContext.compile('{{#field}}{{wycats name}}{{/field}}', {trackIds: true});
equals(template({field: [{name: 'foo'}]}, {helpers: helpers}), 'foo:field.0\n');
});
it('should track contextPath for keys', function() {
var template = CompilerContext.compile('{{#field}}{{wycats name}}{{/field}}', {trackIds: true});
equals(template({field: {name: 'foo'}}, {helpers: helpers}), 'foo:field\n');
});
it('should handle nesting', function() {
var template = CompilerContext.compile('{{#bat}}{{#field}}{{wycats name}}{{/field}}{{/bat}}', {trackIds: true});
equals(template({bat: {field: {name: 'foo'}}}, {helpers: helpers}), 'foo:bat.field\n');
});
});
});
});
+89
View File
@@ -0,0 +1,89 @@
<html>
<head>
<title>Mocha</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="/node_modules/mocha/mocha.css" />
<style>
.headless .suite > h1,
.headless .test.pass {
display: none;
}
</style>
<script>
// Show only errors in "headless", non-interactive mode.
if (/headless=true/.test(location.href)) {
document.documentElement.className = 'headless';
}
</script>
<script src="/node_modules/mocha/mocha.js"></script>
<script>
mocha.setup('bdd');
</script>
<script src="/spec/env/json2.js"></script>
<script src="/spec/env/require.js"></script>
<script src="/spec/env/common.js"></script>
<script>
requirejs.config({
paths: {
'handlebars.runtime': '/dist/handlebars.runtime'
}
});
</script>
<script>
onload = function(){
require(['handlebars.runtime'], function(Handlebars) {
describe('runtime', function() {
it('should load', function() {
equal(!!Handlebars.template, true);
equal(!!Handlebars.VERSION, true);
});
});
mocha.globals(['mochaResults'])
// The test harness leaks under FF. We should have decent global leak coverage from other tests
if (!navigator.userAgent.match(/Firefox\/([\d.]+)/)) {
mocha.checkLeaks();
}
var runner = mocha.run();
//Reporting for saucelabs
var failedTests = [];
runner.on('end', function(){
window.mochaResults = runner.stats;
window.mochaResults.reports = failedTests;
});
runner.on('fail', logFailure);
function logFailure(test, err){
var flattenTitles = function(test){
var titles = [];
while (test.parent.title){
titles.push(test.parent.title);
test = test.parent;
}
return titles.reverse();
};
failedTests.push({
name: test.title,
result: false,
message: err.message,
stack: err.stack,
titles: flattenTitles(test)
});
}
});
};
</script>
</head>
<body>
<div id="mocha"></div>
</body>
</html>
+109
View File
@@ -0,0 +1,109 @@
<html>
<head>
<title>Mocha</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="/node_modules/mocha/mocha.css" />
<style>
.headless .suite > h1,
.headless .test.pass {
display: none;
}
</style>
<script>
// Show only errors in "headless", non-interactive mode.
if (/headless=true/.test(location.href)) {
document.documentElement.className = 'headless';
}
</script>
<script src="/node_modules/mocha/mocha.js"></script>
<script>
mocha.setup('bdd');
</script>
<script src="/spec/env/json2.js"></script>
<script src="/spec/env/require.js"></script>
<script src="/spec/env/common.js"></script>
<script>
requirejs.config({
paths: {
handlebars: '/dist/handlebars',
tests: '/tmp/tests'
}
});
var CompilerContext = {
compile: function(template, options) {
var templateSpec = handlebarsEnv.precompile(template, options);
return handlebarsEnv.template(safeEval(templateSpec));
},
compileWithPartial: function(template, options) {
return handlebarsEnv.compile(template, options);
}
};
function safeEval(templateSpec) {
try {
var ret;
eval('ret = ' + templateSpec);
return ret;
} catch (err) {
console.error(templateSpec);
throw err;
}
}
</script>
<script>
onload = function(){
require(['handlebars'], function(Handlebars) {
window.Handlebars = Handlebars;
require(['tests'], function() {
mocha.globals(['mochaResults'])
// The test harness leaks under FF. We should have decent global leak coverage from other tests
if (!navigator.userAgent.match(/Firefox\/([\d.]+)/)) {
mocha.checkLeaks();
}
var runner = mocha.run();
//Reporting for saucelabs
var failedTests = [];
runner.on('end', function(){
window.mochaResults = runner.stats;
window.mochaResults.reports = failedTests;
});
runner.on('fail', logFailure);
function logFailure(test, err){
var flattenTitles = function(test){
var titles = [];
while (test.parent.title){
titles.push(test.parent.title);
test = test.parent;
}
return titles.reverse();
};
failedTests.push({
name: test.title,
result: false,
message: err.message,
stack: err.stack,
titles: flattenTitles(test)
});
};
});
});
};
</script>
</head>
<body>
<div id="mocha"></div>
</body>
</html>
+17 -1
View File
@@ -30,8 +30,8 @@ describe('utils', function() {
equals(Handlebars.Utils.escapeExpression(''), '');
equals(Handlebars.Utils.escapeExpression(undefined), '');
equals(Handlebars.Utils.escapeExpression(null), '');
equals(Handlebars.Utils.escapeExpression(false), '');
equals(Handlebars.Utils.escapeExpression(false), 'false');
equals(Handlebars.Utils.escapeExpression(0), '0');
});
it('should handle empty objects', function() {
@@ -56,4 +56,20 @@ describe('utils', function() {
equals(Handlebars.Utils.isEmpty({bar: 1}), false);
});
});
describe('#extend', function() {
it('should ignore prototype values', function() {
function A() {
this.a = 1;
}
A.prototype.b = 4;
var b = {b: 2};
Handlebars.Utils.extend(b, new A());
equals(b.a, 1);
equals(b.b, 2);
});
});
});
+14
View File
@@ -1,3 +1,5 @@
/*global shouldCompileTo, shouldCompileToWithPartials */
describe('whitespace control', function() {
it('should strip whitespace around mustache calls', function() {
var hash = {foo: 'bar<'};
@@ -8,6 +10,8 @@ describe('whitespace control', function() {
shouldCompileTo(' {{~&foo~}} ', hash, 'bar<');
shouldCompileTo(' {{~{foo}~}} ', hash, 'bar<');
shouldCompileTo('1\n{{foo~}} \n\n 23\n{{bar}}4', {}, '1\n23\n4');
});
describe('blocks', function() {
@@ -18,6 +22,9 @@ describe('whitespace control', function() {
shouldCompileTo(' {{#if foo~}} bar {{/if~}} ', hash, ' bar ');
shouldCompileTo(' {{~#if foo}} bar {{~/if}} ', hash, ' bar ');
shouldCompileTo(' {{#if foo}} bar {{/if}} ', hash, ' bar ');
shouldCompileTo(' \n\n{{~#if foo~}} \n\nbar \n\n{{~/if~}}\n\n ', hash, 'bar');
shouldCompileTo(' a\n\n{{~#if foo~}} \n\nbar \n\n{{~/if~}}\n\na ', hash, ' abara ');
});
it('should strip whitespace around inverse block calls', function() {
var hash = {};
@@ -26,6 +33,8 @@ describe('whitespace control', function() {
shouldCompileTo(' {{^if foo~}} bar {{/if~}} ', hash, ' bar ');
shouldCompileTo(' {{~^if foo}} bar {{~/if}} ', hash, ' bar ');
shouldCompileTo(' {{^if foo}} bar {{/if}} ', hash, ' bar ');
shouldCompileTo(' \n\n{{~^if foo~}} \n\nbar \n\n{{~/if~}}\n\n ', hash, 'bar');
});
it('should strip whitespace around complex block calls', function() {
var hash = {foo: 'bar<'};
@@ -37,6 +46,9 @@ describe('whitespace control', function() {
shouldCompileTo('{{#if foo~}} bar {{~else~}} baz {{~/if}}', hash, 'bar');
shouldCompileTo('\n\n{{~#if foo~}} \n\nbar \n\n{{~^~}} \n\nbaz \n\n{{~/if~}}\n\n', hash, 'bar');
shouldCompileTo('\n\n{{~#if foo~}} \n\n{{{foo}}} \n\n{{~^~}} \n\nbaz \n\n{{~/if~}}\n\n', hash, 'bar<');
hash = {};
shouldCompileTo('{{#if foo~}} bar {{~^~}} baz {{~/if}}', hash, 'baz');
@@ -45,6 +57,8 @@ describe('whitespace control', function() {
shouldCompileTo('{{#if foo~}} bar {{~^}} baz {{/if}}', hash, ' baz ');
shouldCompileTo('{{#if foo~}} bar {{~else~}} baz {{~/if}}', hash, 'baz');
shouldCompileTo('\n\n{{~#if foo~}} \n\nbar \n\n{{~^~}} \n\nbaz \n\n{{~/if~}}\n\n', hash, 'baz');
});
});
+18 -3
View File
@@ -1,5 +1,5 @@
%x mu emu com
%x mu emu com raw
%{
@@ -28,7 +28,7 @@ ID [^\s!"#%-,\.\/;->@\[-\^`\{-~]+/{LOOKAHEAD}
%%
[^\x00]*?/("{{") {
[^\x00]*?/("{{") {
if(yytext.slice(-2) === "\\\\") {
strip(0,1);
this.begin("mu");
@@ -49,14 +49,29 @@ ID [^\s!"#%-,\.\/;->@\[-\^`\{-~]+/{LOOKAHEAD}
return 'CONTENT';
}
<raw>"{{{{/"[^\s!"#%-,\.\/;->@\[-\^`\{-~]+/[=}\s\/.]"}}}}" {
yytext = yytext.substr(5, yyleng-9);
this.popState();
return 'END_RAW_BLOCK';
}
<raw>[^\x00]*?/("{{{{/") { return 'CONTENT'; }
<com>[\s\S]*?"--}}" strip(0,4); this.popState(); return 'COMMENT';
<mu>"(" return 'OPEN_SEXPR';
<mu>")" return 'CLOSE_SEXPR';
<mu>"{{{{" { return 'OPEN_RAW_BLOCK'; }
<mu>"}}}}" {
this.popState();
this.begin('raw');
return 'CLOSE_RAW_BLOCK';
}
<mu>"{{"{LEFT_STRIP}?">" return 'OPEN_PARTIAL';
<mu>"{{"{LEFT_STRIP}?"#" return 'OPEN_BLOCK';
<mu>"{{"{LEFT_STRIP}?"/" return 'OPEN_ENDBLOCK';
<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}?"{" return 'OPEN_UNESCAPED';
@@ -77,7 +92,7 @@ ID [^\s!"#%-,\.\/;->@\[-\^`\{-~]+/{LOOKAHEAD}
<mu>"@" return 'DATA';
<mu>"true"/{LITERAL_LOOKAHEAD} return 'BOOLEAN';
<mu>"false"/{LITERAL_LOOKAHEAD} return 'BOOLEAN';
<mu>\-?[0-9]+/{LITERAL_LOOKAHEAD} return 'INTEGER';
<mu>\-?[0-9]+(?:\.[0-9]+)?/{LITERAL_LOOKAHEAD} return 'NUMBER';
<mu>{ID} return 'ID';
+31 -39
View File
@@ -2,72 +2,64 @@
%ebnf
%{
function stripFlags(open, close) {
return {
left: open.charAt(2) === '~',
right: close.charAt(0) === '~' || close.charAt(1) === '~'
};
}
%}
%%
root
: statements EOF { return new yy.ProgramNode($1, @$); }
| EOF { return new yy.ProgramNode([], @$); }
: program EOF { yy.prepareProgram($1.statements, true); return $1; }
;
program
: simpleInverse statements -> new yy.ProgramNode([], $1, $2, @$)
| statements simpleInverse statements -> new yy.ProgramNode($1, $2, $3, @$)
| statements simpleInverse -> new yy.ProgramNode($1, $2, [], @$)
| statements -> new yy.ProgramNode($1, @$)
| simpleInverse -> new yy.ProgramNode([], @$)
| "" -> new yy.ProgramNode([], @$)
;
statements
: statement -> [$1]
| statements statement { $1.push($2); $$ = $1; }
: statement* -> new yy.ProgramNode(yy.prepareProgram($1), {}, @$)
;
statement
: openInverse program closeBlock -> new yy.BlockNode($1, $2.inverse, $2, $3, @$)
| openBlock program closeBlock -> new yy.BlockNode($1, $2, $2.inverse, $3, @$)
| mustache -> $1
: mustache -> $1
| block -> $1
| rawBlock -> $1
| partial -> $1
| CONTENT -> new yy.ContentNode($1, @$)
| COMMENT -> new yy.CommentNode($1, @$)
;
rawBlock
: openRawBlock CONTENT END_RAW_BLOCK -> new yy.RawBlockNode($1, $2, $3, @$)
;
openRawBlock
: OPEN_RAW_BLOCK sexpr CLOSE_RAW_BLOCK -> new yy.MustacheNode($2, null, '', '', @$)
;
block
: openBlock program inverseAndProgram? 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, stripFlags($1, $3), @$)
: OPEN_BLOCK sexpr CLOSE -> new yy.MustacheNode($2, null, $1, yy.stripFlags($1, $3), @$)
;
openInverse
: OPEN_INVERSE sexpr CLOSE -> new yy.MustacheNode($2, null, $1, stripFlags($1, $3), @$)
: OPEN_INVERSE sexpr CLOSE -> new yy.MustacheNode($2, null, $1, yy.stripFlags($1, $3), @$)
;
inverseAndProgram
: INVERSE program -> { strip: yy.stripFlags($1, $1), program: $2 }
;
closeBlock
: OPEN_ENDBLOCK path CLOSE -> {path: $2, strip: stripFlags($1, $3)}
: OPEN_ENDBLOCK path 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, stripFlags($1, $3), @$)
| OPEN_UNESCAPED sexpr CLOSE_UNESCAPED -> new yy.MustacheNode($2, null, $1, stripFlags($1, $3), @$)
: 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), @$)
;
partial
: OPEN_PARTIAL partialName path? CLOSE -> new yy.PartialNode($2, $3, stripFlags($1, $4), @$)
;
simpleInverse
: OPEN_INVERSE CLOSE -> stripFlags($1, $2)
: 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
@@ -78,7 +70,7 @@ sexpr
param
: path -> $1
| STRING -> new yy.StringNode($1, @$)
| INTEGER -> new yy.IntegerNode($1, @$)
| NUMBER -> new yy.NumberNode($1, @$)
| BOOLEAN -> new yy.BooleanNode($1, @$)
| dataName -> $1
| OPEN_SEXPR sexpr CLOSE_SEXPR {$2.isHelper = true; $$ = $2;}
@@ -95,7 +87,7 @@ hashSegment
partialName
: path -> new yy.PartialNameNode($1, @$)
| STRING -> new yy.PartialNameNode(new yy.StringNode($1, @$), @$)
| INTEGER -> new yy.PartialNameNode(new yy.IntegerNode($1, @$))
| NUMBER -> new yy.PartialNameNode(new yy.NumberNode($1, @$))
;
dataName
+1
View File
@@ -1 +1,2 @@
/* jshint ignore:start */
/* istanbul ignore next */
+7 -1
View File
@@ -4,7 +4,13 @@ module.exports = function(grunt) {
grunt.registerTask('parser', 'Generate jison parser.', function() {
var done = this.async();
var child = childProcess.spawn('./node_modules/.bin/jison', ['-m', 'js', 'src/handlebars.yy', 'src/handlebars.l'], {stdio: 'inherit'});
var cmd = './node_modules/.bin/jison';
if(process.platform === 'win32'){
cmd = 'node_modules\\.bin\\jison.cmd';
}
var child = childProcess.spawn(cmd, ['-m', 'js', 'src/handlebars.yy', 'src/handlebars.l'], {stdio: 'inherit'});
child.on('exit', function(code) {
if (code != 0) {
grunt.fatal('Jison failure: ' + code);
+15 -3
View File
@@ -10,8 +10,9 @@ module.exports = function(grunt) {
throw err;
}
if (stdout.toString() !== fs.readFileSync('./spec/expected/empty.amd.js').toString()) {
throw new Error('Expected binary output differed');
var expected = fs.readFileSync('./spec/expected/empty.amd.js');
if (stdout.toString() !== expected.toString()) {
throw new Error('Expected binary output differed:\n\n"' + stdout + '"\n\n"' + expected + '"');
}
done();
@@ -28,5 +29,16 @@ module.exports = function(grunt) {
done();
});
});
grunt.registerTask('test', ['test:bin', 'test:mocha']);
grunt.registerTask('test:cov', function() {
var done = this.async();
var runner = childProcess.fork('node_modules/.bin/istanbul', ['cover', '--', './spec/env/runner.js'], {stdio: 'inherit'});
runner.on('close', function(code) {
if (code != 0) {
grunt.fatal(code + ' tests failed');
}
done();
});
});
grunt.registerTask('test', ['test:bin', 'test:cov']);
};
+11 -4
View File
@@ -80,20 +80,27 @@ module.exports = {
childProcess.exec('git tag -a --message=' + name + ' ' + name, {}, function(err, stdout, stderr) {
if (err) {
throw new Error('git.tag: ' + err.message);
throw err;
}
callback();
});
},
tagName: function(callback) {
childProcess.exec('git tag -l --points-at HEAD', {}, function(err, stdout) {
childProcess.exec('git describe --tags', {}, function(err, stdout) {
if (err) {
throw new Error('git.tagName: ' + err.message);
}
var tags = stdout.trim().split(/\n/),
versionTags = tags.filter(function(tag) { return /^v/.test(tag); });
var tags = stdout.trim().split(/\n/);
tags = tags.filter(function(info) {
info = info.split('-');
return info.length == 1;
});
var versionTags = tags.filter(function(info) {
return /^v/.test(info[0]);
});
callback(undefined, versionTags[0] || tags[0]);
});
}