Compare commits

...

213 Commits

Author SHA1 Message Date
kpdecker bef909f5cb 1.0.5beta 2012-02-09 11:06:11 -06:00
Peter Wagenet 0b97fea56f Fixed error in lexer 2012-02-08 20:49:50 -08:00
Peter Wagenet 2ea95ca08d Fixed whitespace and semi-colons 2012-02-08 20:49:49 -08:00
Peter Wagenet cb9db98642 Merge pull request #165 from spadgos/patch-1
Added a missing `var` statement.
2012-02-08 18:08:26 -08:00
Nick Fisher 58e2c2410f Added a missing var statement. 2012-01-17 07:16:19 +01:00
Yehuda Katz 6040149cf8 I'm an idiot 2011-12-31 16:04:11 -08:00
Yehuda Katz 17cdb0091b Missing some reserved words 2011-12-31 15:41:16 -08:00
Yehuda Katz 730c2617af Differentiate between lambdas in the context (which should have mustache semantics) and helpers (which have Handlebars helper semantics). 2011-12-31 13:51:54 -08:00
Yehuda Katz 757d5250dc Rename "vm" to "runtime" for clarity 2011-12-27 18:15:44 -08:00
Yehuda Katz 163a92ebe0 Update Gemfile.lock with latest TRR with lion support 2011-12-27 17:05:22 -08:00
Yehuda Katz c79c761460 Add support for escaping mustaches 2011-12-27 17:04:59 -08:00
Yehuda Katz e474e56b80 data should be passed through to partials. closes #111. 2011-12-27 15:57:08 -08:00
Yehuda Katz 69307d0e2b Allow function parameters to #if. This was originally disallowed because it is decidedly not idiomatic Handlebars, which would prefer that you use helpers for these cases, not pollute your context objects with functions to call from your template, but enough people asked for it that I'll give in.
I will personally not be using this feature and still strongly recommend that people use helpers in this case.
2011-12-27 15:47:45 -08:00
Yehuda Katz 9852502636 Bump version 2011-12-27 13:34:37 -08:00
Yehuda Katz 7a0bf9d332 Handlebars errors should copy the message over 2011-12-27 13:18:54 -08:00
Yehuda Katz 2e7ea96818 Add Ember.js 2011-12-27 13:13:57 -08:00
Yehuda Katz 932e2970ad Realign lexer actions 2011-12-27 01:23:28 -08:00
Yehuda Katz 373d10edb1 Literal square-bracket path segments cannot have ] in them. TODO: Allow escaped path segments. 2011-12-27 01:21:53 -08:00
Yehuda Katz bd0f74fbd9 Test that the hello world mustache example works 2011-12-27 00:43:30 -08:00
Yehuda Katz e57e626480 Fix bug where inverse section was being passed to subsequent helpers 2011-12-27 00:30:46 -08:00
Yehuda Katz cb04822812 Merge pull request #156 from broady/master
Fix support for Rhino
2011-12-23 16:57:51 -08:00
Yehuda Katz 647d5adf85 Merge pull request #121 from tablatom/master
Added a noEscape option to Handlebars.compile
2011-12-23 16:57:39 -08:00
Yehuda Katz 6df51733d3 Merge pull request #157 from kpdecker/npm-update
Npm update
2011-12-22 23:52:22 -08:00
kpdecker 9c9d92413e Update package version number 2011-12-22 22:52:41 -06:00
kpdecker c6eec286a8 Update package dependencies 2011-12-22 22:52:20 -06:00
Chris Broadfoot f6070f0084 Fix support for Rhino 2011-12-20 10:43:37 +11:00
Yehuda Katz 87e6f21765 Add a status button 2011-12-16 16:13:07 -08:00
Yehuda Katz 3afc572f6b Compile after installing jison 2011-12-16 16:07:54 -08:00
Yehuda Katz fea981a3f6 Try to install jison if it's not already installed 2011-12-16 15:55:12 -08:00
Peter Wagenet 799c7ed3ce Merge pull request #155 from twe4ked/patch-1
Add syntax highlighting to the README.
2011-12-05 22:15:47 -08:00
Odin Dutton b2a27347e8 Add syntax highlighting to the README. 2011-12-06 14:59:48 +10:00
Yehuda Katz 215472dda2 Merge pull request #149 from SlexAxton/patch-1
Update the version!
2011-11-30 12:46:13 -08:00
Alex Sexton 8338bc888a Update the version! 2011-11-30 02:06:49 -06:00
Yehuda Katz 5d15ef9625 Merge pull request #144 from wagenet/rhino-fix
Compatibility with Mozilla Rhino
2011-11-22 23:04:40 -08:00
Mike Moulton 42e31ef6db Escaping a '/' in the ID regex. This was preventing handelbars.js from loading in Rhino. 2011-11-22 22:46:29 -08:00
Yehuda Katz deac4f5141 Merge pull request #135 from leshill/handlebars_assets
Add handlebars_assets to README
2011-11-12 05:51:45 -08:00
Yehuda Katz 79b730ccf3 Merge pull request #132 from steevel/master
Broken code when putting js after generated block.
2011-11-12 05:50:29 -08:00
kpdecker ec3e5e3914 Merge remote-tracking branch 'upstream/master' 2011-11-04 14:15:46 -04:00
Les Hill 5f9919dcf7 Add handlebars_assets to README 2011-11-01 23:20:12 -07:00
Yehuda Katz 608aa94fc3 Update Gemfile.lock 2011-10-28 17:39:49 -07:00
Yehuda Katz 9998d9d9bc Allow subclasses to customize the namespace 2011-10-28 17:39:42 -07:00
Peter Wagenet 924cc3e86c Goodbye poor hack. You were broken before your time. 2011-10-27 22:32:07 -07:00
Steeve Lennmark 25b8e314af Add semicolon to the generated anonymous javascript function since it could break code following it.
Example: (function(){})()/* this is the same as undefined() */(function(){})()
2011-10-27 17:21:46 +02:00
Peter Wagenet 43431d2a78 Added 'log' helper - See #97 2011-10-24 19:41:41 -07:00
Peter Wagenet b36776f4c6 Better errors for 'equals' test 2011-10-24 19:12:54 -07:00
Peter Wagenet f23fbb84e2 Updated Gemfile to therubyracer 0.9.8 - Fixes #126 2011-10-24 17:54:11 -07:00
Peter Wagenet 87599daef1 Merge pull request #127 from SlexAxton/master
Added in comment docs
2011-10-24 17:43:29 -07:00
Peter Wagenet 374a38ed36 Merge pull request #130 from kpdecker/defer-compile
Defer compile
2011-10-24 17:27:54 -07:00
Peter Wagenet b501c380af Handle 'jison' failing in 'rake compile' 2011-10-24 17:27:14 -07:00
kpdecker 7dd11fa3f2 Merge branch 'defer-compile' 2011-10-21 08:34:33 -05:00
kpdecker defc2f3403 Refactor deferred compile to use helper 2011-10-21 08:34:24 -05:00
kpdecker 9a27aa98b7 Merge branch 'defer-compile' 2011-10-21 08:25:30 -05:00
kpdecker 4458b5fa13 Defer compilation of templates until needed. 2011-10-21 08:24:55 -05:00
kpdecker 7cd34edcc2 Defer compilation of templates until needed. 2011-10-20 14:52:21 -07:00
Alex Sexton 7b18e51bf8 Added in docs for template comments 2011-10-09 20:29:03 -05:00
Tom Locke cd927e9cda Add noEscape option to Handlebars.compile 2011-09-26 09:47:30 +01:00
Alan Johnson b832c85923 Forgot === for falsy check. 2011-09-05 14:20:57 -04:00
Alan Johnson 33bb89beda Merge branch 'master' of github.com:wycats/handlebars.js 2011-09-05 11:08:44 -04:00
Alan Johnson 33bde69750 Fixed bug in falsy check on lookups. 2011-09-05 11:08:22 -04:00
Alan Johnson 2422a6518f Merge pull request #116 from jroes/master
Add rake to Gemfile
2011-09-04 05:26:32 -07:00
Alan Johnson 0768f07b17 Merge pull request #117 from kpdecker/precompiled-readme
Precompiled readme
2011-09-04 05:21:06 -07:00
kpdecker 2f3d5977d1 Precompiled templates info in README 2011-09-04 02:33:14 -05:00
kpdecker 8d38ebd251 Fix copy in command line utility 2011-09-04 02:29:25 -05:00
kpdecker d630894d9b Merge remote-tracking branch 'upstream/master' 2011-09-03 17:52:06 -05:00
Jonathan Roes 8f617ca624 Add rake to Gemfile 2011-09-02 21:57:07 -04:00
Alan Johnson 57e3bd72a8 Added link to tryhandlebarsjs.com. 2011-09-02 12:13:42 -03:00
Alan Johnson 6b18873d23 Fixed errors occurring because of undefined properties on nested paths. 2011-09-02 10:56:37 -04:00
Alan Johnson c1c455acc8 Reproduced undefined property error. 2011-09-02 10:47:05 -04:00
Alan Johnson f2f1f5f9bd Wrote a quick test for partials with literal paths. 2011-09-02 10:36:59 -04:00
Alan Johnson 7212e2b7d7 Got more complex literal expressions working. 2011-09-02 09:10:20 -04:00
Alan Johnson fc84308cc9 Got simple literal expressions added into paths. 2011-09-02 09:04:41 -04:00
Alan Johnson 91bbc4fd2c Merge pull request #114 from markmarkoh/patch-1
The missing semicolon is breaking our code when we minify.
2011-09-01 18:49:12 -07:00
Mark DiMarco 696dc2dc36 The missing semicolon is breaking our code when we minify. 2011-09-01 20:37:51 -05:00
Alan Johnson 96571b93c1 Merge pull request #113 from kpdecker/npm
Npm package declaration
2011-09-01 05:15:56 -07:00
Alan Johnson 3db5fb742e Merge pull request #112 from kpdecker/command-line
Enhanced precompiler command line support
2011-09-01 05:15:37 -07:00
kpdecker c561fdf8b1 Merge branch 'npm' 2011-08-22 02:41:59 -05:00
kpdecker f7480d5b42 Add main target to packageinfo 2011-08-22 02:41:53 -05:00
kpdecker 93ebee8faf Exclude directory children from npm 2011-08-22 02:34:04 -05:00
kpdecker 20350a3c07 Merge branch 'command-line' 2011-08-22 02:27:23 -05:00
kpdecker 8cd4d939f3 Use proper uglify package name 2011-08-22 02:27:15 -05:00
kpdecker 98e66f898e Merge branch 'npm' 2011-08-22 02:00:43 -05:00
kpdecker 85a32a18e2 Update package info 2011-08-22 02:00:35 -05:00
kpdecker 90648c0e2d Merge branch 'npm' 2011-08-22 01:52:09 -05:00
kpdecker 6ef3d1316f npmignore file 2011-08-22 01:51:08 -05:00
kpdecker f3a2374dda Add simple output mode. 2011-08-22 01:49:10 -05:00
kpdecker cbadb33d51 Cleanup stored template names. 2011-08-22 01:40:33 -05:00
kpdecker 17e1b1fab7 Output multiple templates, storing into the Handlebars.template object 2011-08-22 01:38:41 -05:00
kpdecker 6eea3e4d44 Examine all passed names in the template check 2011-08-22 01:35:26 -05:00
kpdecker 48726c7fed Minimize js commandline option 2011-08-22 00:15:06 -05:00
kpdecker 47c5cd71ea File output commandline option 2011-08-22 00:14:53 -05:00
kpdecker a9bcd369f3 Known helpers command line options. 2011-08-22 00:11:22 -05:00
kpdecker b3e70414d8 Use optimist to perform validation. 2011-08-22 00:07:35 -05:00
kpdecker c9d851e9a4 Merge branch 'master' of https://github.com/wycats/handlebars.js 2011-08-21 16:29:31 -05:00
kpdecker 86d71e6fc8 Properly init eco for rake bench. 2011-08-21 16:28:57 -05:00
Alan Johnson 542f98190b Added newline to end of benchmark output, just to clean it up. 2011-08-15 23:38:35 -04:00
Alan Johnson 16c0907607 Added some more notes to the README for contributors. 2011-08-15 23:31:29 -04:00
Alan Johnson 292e32292b Updated README. 2011-08-15 23:14:40 -04:00
Alan Johnson 3c5cac3f3f Fixed missing var in compiler code causing compilerWords to be global. 2011-08-15 23:10:27 -04:00
tomhuda 2f7b724d5a Make sure options.hash is an empty {} if no hash is present to eliminate necessary guards in helpers 2011-08-02 16:32:57 -07:00
kpdecker 318c08a97e Do not perform unnecessary self-assign 2011-07-31 21:44:27 -05:00
kpdecker d6b97cf34d Do not buffer for simple programs (1 statement) 2011-07-31 21:44:16 -05:00
kpdecker 82e3344aed Update the eco benchmark handler for the latest API 2011-07-31 21:02:40 -05:00
kpdecker 6349c1a0c7 Optimize the populate call logic for the simple cases 2011-07-31 21:01:40 -05:00
kpdecker f53737ef23 knownHandlers and knownHandlersOnly compile options. 2011-07-31 21:00:44 -05:00
kpdecker 1a88356dc3 Access context objects directly rather than using currentContext var. 2011-07-31 17:17:14 -05:00
kpdecker 5d4b549dc3 Add template call to the bin output 2011-07-31 16:17:57 -05:00
kpdecker d044ada7aa Convert children to nested functions. Reuse identifiers by closure where possible. 2011-07-31 16:17:39 -05:00
kpdecker 77a20a3a9f Alias self -> this 2011-07-31 14:58:37 -05:00
kpdecker 9821da8df7 Move aliases and registers into context object. 2011-07-31 14:10:32 -05:00
kpdecker 2d538baf72 Use x = a[y] || b[y] rather than if exists lookup 2011-07-31 11:27:47 -05:00
kpdecker c7e8ddd6b5 Minimizable id aliases. 2011-07-31 11:25:40 -05:00
kpdecker 6fcebec713 buffer += 2011-07-31 11:21:18 -05:00
kpdecker 993c793565 Avoid eval when running in VM+Compiler mode 2011-07-30 16:55:26 -05:00
kpdecker f8edc59025 Update the bin to use the precompile API 2011-07-30 16:20:26 -05:00
kpdecker 3af9de7407 Update partial testing for the various modes 2011-07-30 16:12:20 -05:00
kpdecker 72ea816cf7 Remove debug code 2011-07-30 16:11:37 -05:00
kpdecker d641dae5f1 Full context and compileWithPartials test helper 2011-07-30 16:02:11 -05:00
kpdecker ccf32821df Throw a handlebars exception when attempting to use template partials in VM mode. 2011-07-30 16:01:43 -05:00
kpdecker 8e23ce87d3 Use CompilerContext.compile for tests rather than Handlebars.compile.
This prevents false negatives on the partial tests.
2011-07-30 15:54:57 -05:00
kpdecker 5a3522cf2e Builder for handlebars.vm.js 2011-07-30 15:26:34 -05:00
kpdecker aaec001931 Split the tests into compiler and vm tests 2011-07-30 15:26:21 -05:00
kpdecker 3d9e5070f5 Move mismatched block test from quint to parser tests. 2011-07-30 15:25:25 -05:00
kpdecker 82a1b0e85c Temporary Handlebars.compile implementation in compiler 2011-07-30 15:24:49 -05:00
kpdecker 600a669d12 Expose Handlebars.precompile method from the compiler. 2011-07-30 15:24:14 -05:00
kpdecker 5aa12b5e09 Rename Handlebars.VM.compile to Handelbars.VM.template 2011-07-30 15:23:01 -05:00
kpdecker 6a6edf5ae6 Simple node template compiler 2011-07-30 12:25:41 -05:00
kpdecker aeda7e389f Ignores update 2011-07-30 12:19:30 -05:00
kpdecker 09e79f0587 Package info 2011-07-30 12:19:17 -05:00
kpdecker 4a9270aa38 Fix module loading within node 2011-07-30 12:18:56 -05:00
kpdecker 59f5331db4 Phase 2 of precompiled extraction:
Use string representations to pass around the function context
2011-07-30 11:23:24 -05:00
kpdecker 74bd8cac60 Phase 1 of precompiled extraction:
Separate template logic from container construction.
2011-07-30 11:10:13 -05:00
kpdecker 542d64f0be Remove unnecessary var declaration. 2011-07-30 10:44:07 -05:00
kpdecker 5e611205ac Create compiler base file. 2011-07-30 10:38:36 -05:00
kpdecker 2e77f1c777 Remove the debug file (Suspect broken Due to missing class references) 2011-07-30 10:33:40 -05:00
kpdecker 8c49721c70 Move visitor into compiler subdir 2011-07-30 10:28:31 -05:00
kpdecker 2fc01ff73a Move art into compiler dir 2011-07-30 10:25:30 -05:00
kpdecker bba08ad95f Move printer into compiler dir 2011-07-30 10:23:37 -05:00
kpdecker 034dafddac Move parser file into compiler subdir 2011-07-30 10:14:59 -05:00
kpdecker 6aa9a30b02 Update node loader for compiler/vm split 2011-07-30 10:12:37 -05:00
kpdecker f2dccb753f Break compiler and vm logic into separate files. 2011-07-30 10:11:13 -05:00
kpdecker 471f3b9748 Reduce scope of global variable 2011-07-29 22:45:16 -05:00
kpdecker a927125909 Can access context variables masked by helpers by scoping with 'this.' 2011-07-29 20:53:47 -05:00
kpdecker 9062cac3f0 Add missing messages in equal tests 2011-07-29 20:45:17 -05:00
tomhuda 059a80661d * Remove legacy support for inverse sections as additional parameters.
* Unify inverse and normal block helpers
* Make Handlebars.Exception inherit from JS Error
2011-07-07 23:09:33 -07:00
Alan Johnson 37e36bf137 Merge pull request #66 from rgrove/fix-empty-array-if
The "if" block helper shouldn't treat empty arrays as truthy
2011-06-27 03:15:50 -07:00
Alan Johnson 3c2086eb2a Merge pull request #98 from Pumpkin/master
Updated README to fix code sample mistake.
2011-06-27 03:11:41 -07:00
inook 1ff0b657be Update README 2011-06-26 10:37:55 +04:00
Yehuda Katz 17dbfa7004 Merge pull request #96 from gleitz/master
Allow boolean calls to work with YUI Compressor
2011-06-22 16:37:26 -07:00
gleitz 52900c89f7 removed references to .boolean, which broke the YUI Compressor 2011-06-22 17:18:07 -04:00
gleitz be8f4f6fb9 Updated the boolean functions to work with YUI compressor. Linted other files for consistency 2011-06-22 13:54:17 -04:00
tomhuda 1482f1ae72 Add BOOLEAN support 2011-06-01 22:57:22 -07:00
tomhuda 0f78345d0c Add support for INTEGER expressions 2011-06-01 22:33:48 -07:00
Yehuda Katz 4eeda34ad2 Merge pull request #79 from schuyler1d/numbers
allow number indexes in paths for array access
2011-05-25 17:08:18 -07:00
Yehuda Katz ed1be562e2 Merge pull request #81 from schuyler1d/google_json_support
support "$" as an ID character: google has a bunch of json that has $t, etc
2011-05-25 17:06:50 -07:00
Yehuda Katz 04b359d15b Merge pull request #86 from jblotus/master
A few bug fixes
2011-05-25 17:06:31 -07:00
Yehuda Katz f07f70ca42 Merge pull request #68 from rgrove/fix-escaping
Add ", ', and ` to the list of chars that need HTML escaping.
2011-05-25 17:05:54 -07:00
James Fuller 28d35180bd fixing partials with int names 2011-05-24 00:40:09 -04:00
James Fuller 0e22301e8f fixed a syntax error in documentation example 2011-05-18 19:26:15 -07:00
Schuyler Duveen 4e5cf17311 support "$" as an ID character: google has a bunch of json that has $t, etc values 2011-05-13 11:40:31 -04:00
wycats a4ca50533d Add support for line-breaks in mustaches 2011-05-12 12:23:15 -07:00
Schuyler Duveen 1aa512d227 allow number indexes in paths for array access 2011-05-10 12:00:40 -04:00
Ryan Grove 2f4644529c Remove / from the list of escaped chars and add `.
It's probably fine not to escape /, since its only danger is in ending
entities (like &amp/). This isn't a problem for us, since the badChars
regex won't allow it and the & will get escaped.

It turns out ` can be used to quote attribute values in IE, so it needs
to be escaped along with " and '.
2011-05-09 15:19:23 -07:00
Ryan Grove d109e31f62 Merge branch 'master' into fix-escaping
Conflicts:
	lib/handlebars/utils.js
2011-05-09 14:59:51 -07:00
Yehuda Katz 5168037b5a Edited README.markdown via GitHub 2011-05-05 11:22:03 -07:00
Yehuda Katz 92535a9158 Edited README.markdown via GitHub 2011-05-05 11:16:22 -07:00
Yehuda Katz 4f309bb3aa Merge pull request #74 from jeroenvandijk/patch-1
Add a link to the Readme to the very nice documentation site
2011-05-05 11:14:29 -07:00
Yehuda Katz b4c949cc07 Merge pull request #73 from nhocki/patch-1
Adding missing ) to the upgrading section...
2011-05-05 11:14:04 -07:00
Jeroen van Dijk 90bb568faf Add a link to the Readme to the very nice documentation site 2011-05-04 01:37:21 -07:00
tomhuda 60040e60fb Update specs and code so that the function passed to block helpers has the same API as regular compiled templates 2011-05-04 00:03:50 -07:00
Nicolás Hock Isaza a00054e26b Adding missing ) to the upgrading section... 2011-05-03 21:09:09 -07:00
tomhuda 81071dd22c Add VERSION constant 2011-05-03 20:39:24 -07:00
tomhuda c29223a98b Update specs for change to template signature 2011-05-03 20:39:14 -07:00
tomhuda 5d99572006 Rename vm.js to compiler.js 2011-05-03 20:38:28 -07:00
tomhuda d83e1c7484 Remove unnecessary original runtime implementation 2011-05-03 20:38:06 -07:00
tomhuda 201ecbd7bc Improve Handlebars subclassability 2011-05-03 19:30:32 -07:00
tomhuda 9333d1210d Add support for passing String parameters to helper 2011-05-03 19:30:32 -07:00
tomhuda 2615fa4bd1 Add a default helperMissing to help catch errors and restore old behavior 2011-05-03 19:30:32 -07:00
Yehuda Katz 3e5bc84058 Merge pull request #69 from rgrove/allow-hyphens.
Allow ids to contain hyphens. Fixes #36.
2011-05-03 19:01:37 -07:00
Yehuda Katz 5d8480fa8a Merge pull request #48 from jasondavies/master.
Fix multi-backslash escaping
2011-05-03 18:59:59 -07:00
Yehuda Katz 56b0ce56f8 Merge pull request #50 from emtrane/spec_patch.
Correct path to vm.js
2011-05-03 18:59:07 -07:00
Yehuda Katz 9dc4bd47e6 Merge pull request #65 from rgrove/master.
Add support for multi-line comments
2011-05-03 18:58:50 -07:00
Yehuda Katz 4fb42d7682 Merge pull request #71 from andrewmbenton/master.
missing semicolon prevents minification
2011-05-03 18:57:57 -07:00
Andrew Benton b0df69ddec add missing semicolon 2011-04-30 19:07:38 -07:00
Ryan Grove b324d002ca Allow ids to contain hyphens. Fixes #36.
Previously, a mustache like {{foo-bar}} would generate a
parse error, which was a departure from the behavior of
other Mustache implementations.
2011-04-28 22:30:21 -07:00
Ryan Grove b291a1ad8c Add ", ', and / to the list of chars that need HTML escaping.
Previously, only < and > were escaped. This meant that any Handlebars
template that used user input in an HTML attribute value was wide open
to a trivial XSS exploit. Note that unquoted attribute values are still
open to attack, but this set of characters at least brings Handlebars in
line with other Mustache implementations and other template languages.

See the OWASP XSS prevention cheat sheet (rule #1) for the rationale
behind escaping these characters:

https://www.owasp.org/index.php/XSS_(Cross_Site_Scripting)_Prevention_Cheat_Sheet
2011-04-25 11:15:53 -07:00
Ryan Grove dec196b4d9 The "if" block helper shouldn't treat empty arrays as truthy.
Given the data {foo: []}, the following template previously considered
foo to be truthy when it shouldn't have:

    {{#if foo}}
      You should not see me!
    {{else}}
      You should see me!
    {{/if}}
2011-04-20 16:56:37 -07:00
Ryan Grove aba636ec38 Support multi-line comments.
{{!
Like this.
}}
2011-04-18 16:08:20 -07:00
wycats 038d9b3fee Check in a LICENSE file 2011-04-04 22:18:54 -07:00
tomhuda 6a80531513 Merge branch 'master' of https://github.com/tf/handlebars.js into tf-master 2011-03-26 14:04:49 -07:00
Tim Fischbach a1b1b160cf inserting missing return statement in unless helper (fixes issue #51) 2011-03-09 11:51:00 +01:00
Martin Davidsson f368f9177c Correct path to vm.js for case-sensitive filesystems
lib/Handlebars/vm.js -> lib/handlebars/vm.js
2011-03-05 21:10:18 -08:00
tomhuda a7bb51d8a5 DRY up some code that converts program AST nodes to opcodes. 2011-03-04 15:17:32 -08:00
tomhuda 357710f136 Add support for block hashes and clean up mustache/program code 2011-03-04 15:10:22 -08:00
tomhuda ca9b9671a5 Add Hash arguments to simple mustaches (TODO: add Hash args to block helpers) 2011-03-04 00:11:03 -08:00
tomhuda e0aa705f71 Add support for hash args in the tokenizer and parser 2011-03-03 21:33:28 -08:00
tomhuda 51d2543ba1 Add support for {{foo.bar.baz}} 2011-03-03 20:08:28 -08:00
Jason Davies e5899cad1e Test multi-backslash-escaping 2011-02-28 22:28:11 +00:00
Jason Davies 0397f875c5 Merge branch 'master' of http://github.com/wycats/handlebars.js 2011-02-28 22:23:11 +00:00
Jason Davies 2cf88d4e77 Fix backslash escaping 2011-02-28 22:23:02 +00:00
tomhuda 32d7e52182 Helpers take precedence over context properties with the same name. This is useful in scenarios where your context object is inherited from another system (such as a framework or JSON API) that may contain properties that conflict with helpers you explicitly define. 2011-02-25 23:01:11 -08:00
tomhuda 6fbdf4b445 Merge branch 'master' of github.com:wycats/handlebars.js 2011-02-25 22:36:49 -08:00
Jason Davies 0d4d8813ac Merge branch 'master' of http://github.com/wycats/handlebars.js
Conflicts:
	lib/handlebars.js
	test/index.html
2011-02-21 01:51:31 +00:00
Alan Johnson 935053cc33 Added .DS_Store to gitignore. 2011-02-15 16:44:48 -05:00
tomhuda 2b319bef2f Make the function passed to a block helper have an identical signature to top-level template methods 2011-02-14 16:05:01 -08:00
Bryan Woods 76e1bb4c06 Checks to see if jison is installed before attempting to run it in Rakefile 2011-02-13 12:37:08 -05:00
Jason Davies 7f8a323efe Add nicer header and title to QUnit test page 2010-11-29 21:39:35 +00:00
Jason Davies ee388dc149 Merge branch 'master' of http://github.com/wycats/handlebars.js 2010-11-29 15:05:11 +00:00
Jason Davies 54948aad6f Merge branch 'master' of http://github.com/wycats/handlebars.js 2010-11-11 23:28:01 +00:00
Jason Davies 5e18cb6298 Remove extraneous breaks 2010-10-19 12:25:47 +01:00
Jason Davies ad50cb7efe Merge branch 'master' of http://github.com/wycats/handlebars.js 2010-10-19 12:25:05 +01:00
Jason Davies ed6b999623 Remove backslash escapes from default escaping 2010-10-19 12:24:43 +01:00
Jason Davies e7d5e72a87 Merge branch 'master' of http://github.com/wycats/handlebars.js 2010-09-27 17:38:41 +01:00
Jason Davies 57e990e6e0 Fix disappearing backslash bug
Backslashes weren't being escaped during compilation, causing templates
containing backslashes to mysteriously lose them.
2010-09-20 23:48:25 +01:00
30 changed files with 2382 additions and 1343 deletions
+3 -1
View File
@@ -1,4 +1,6 @@
dist
lib/handlebars/parser.js
vendor
.rvmrc
.DS_Store
lib/handlebars/compiler/parser.js
node_modules
+11
View File
@@ -0,0 +1,11 @@
.DS_Store
.gitignore
.rvmrc
Gemfile
Gemfile.lock
Rakefile
bench/*
dist/*
spec/*
src/*
vendor/*
+2 -1
View File
@@ -1,4 +1,5 @@
source "http://rubygems.org"
gem "therubyracer", ">= 0.8.0"
gem "rake"
gem "therubyracer", ">= 0.9.8"
gem "rspec"
+14 -10
View File
@@ -1,20 +1,24 @@
GEM
remote: http://rubygems.org/
specs:
diff-lcs (1.1.2)
rspec (2.1.0)
rspec-core (~> 2.1.0)
rspec-expectations (~> 2.1.0)
rspec-mocks (~> 2.1.0)
rspec-core (2.1.0)
rspec-expectations (2.1.0)
diff-lcs (1.1.3)
libv8 (3.3.10.4)
rake (0.9.2.2)
rspec (2.7.0)
rspec-core (~> 2.7.0)
rspec-expectations (~> 2.7.0)
rspec-mocks (~> 2.7.0)
rspec-core (2.7.1)
rspec-expectations (2.7.0)
diff-lcs (~> 1.1.2)
rspec-mocks (2.1.0)
therubyracer (0.8.0)
rspec-mocks (2.7.0)
therubyracer (0.9.9)
libv8 (~> 3.3.10)
PLATFORMS
ruby
DEPENDENCIES
rake
rspec
therubyracer (>= 0.8.0)
therubyracer (>= 0.9.8)
+20
View File
@@ -0,0 +1,20 @@
Copyright (C) 2011 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
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
+192 -82
View File
@@ -1,8 +1,13 @@
[![Build Status](https://secure.travis-ci.org/wycats/handlebars.js.png)](http://travis-ci.org/wycats/handlebars.js)
Handlebars.js
=============
Handlebars.js is an extension to the [Mustache templating language](http://mustache.github.com/) created by Chris Wanstrath. Handlebars.js and Mustache are both logicless templating languages that keep the view and the code separated like we all know they should be.
Checkout the official Handlebars docs site at [http://www.handlebarsjs.com](http://www.handlebarsjs.com).
Installing
----------
Installing Handlebars is easy. Simply [download the package from GitHub](https://github.com/wycats/handlebars.js/archives/master) and add it to your web pages (you should usually use the most recent version).
@@ -13,21 +18,23 @@ In general, the syntax of Handlebars.js templates is a superset of Mustache temp
Once you have a template, use the Handlebars.compile method to compile the template into a function. The generated function takes a context argument, which will be used to render the template.
var source = "<p>Hello, my name is {{name}}. I am from {{hometown}}. I have " +
"{{kids/length}} kids:</p>" +
"<ul>{{#kids}}<li>{{name}} is {{age}}</li>{{/kids}}</ul>";
var template = Handlebars.compile(source);
```js
var source = "<p>Hello, my name is {{name}}. I am from {{hometown}}. I have " +
"{{kids.length}} kids:</p>" +
"<ul>{{#kids}}<li>{{name}} is {{age}}</li>{{/kids}}</ul>";
var template = Handlebars.compile(source);
var data = { "name": "Alan", "hometown": "Somewhere, TX",
"kids": [{"name": "Jimmy", "age": "12"}, {"name": "Sally", "age": "4"}]};
var result = template(data);
var data = { "name": "Alan", "hometown": "Somewhere, TX",
"kids": [{"name": "Jimmy", "age": "12"}, {"name": "Sally", "age": "4"}]};
var result = template(data);
// Would render:
// <p>Hello, my name is Alan. I am from Somewhere, TX. I have 2 kids:</p>
// <ul>
// <li>Jimmy is 12</li>
// <li>Sally is 4</li>
// </ul>
// Would render:
// <p>Hello, my name is Alan. I am from Somewhere, TX. I have 2 kids:</p>
// <ul>
// <li>Jimmy is 12</li>
// <li>Sally is 4</li>
// </ul>
```
Registering Helpers
@@ -37,22 +44,23 @@ You can register helpers that Handlebars will use when evaluating your
template. Here's an example, which assumes that your objects have a URL
embedded in them, as well as the text for a link:
Handlebars.registerHelper('link_to', function(context) {
return "<a href='" + context.url + "'>" + context.body + "</a>";
});
```js
Handlebars.registerHelper('link_to', function(context) {
return "<a href='" + context.url + "'>" + context.body + "</a>";
});
var context = { posts: [{url: "/hello-world", body: "Hello World!"}] };
var source = "<ul>{{#posts}}<li>{{{link_to this}}}</li></ul>"
var context = { posts: [{url: "/hello-world", body: "Hello World!"}] };
var source = "<ul>{{#posts}}<li>{{{link_to this}}}</li>{{/posts}}</ul>"
var template = Handlebars.compile(source);
template(context);
// Would render:
//
// <ul>
// <li><a href='/hello-world'>Hello World!</a></li>
// </ul>
var template = Handlebars.compile(source);
template(context);
// Would render:
//
// <ul>
// <li><a href='/hello-world'>Hello World!</a></li>
// </ul>
```
Escaping
--------
@@ -71,44 +79,54 @@ Handlebars.js adds a couple of additional features to make writing templates eas
### Paths
Handlebars.js supports an extended expression syntax that we call paths. Paths are made up of typical expressions and / characters. Expressions allow you to not only display data from the current context, but to display data from contexts that are descendents and ancestors of the current context.
Handlebars.js supports an extended expression syntax that we call paths. Paths are made up of typical expressions and . characters. Expressions allow you to not only display data from the current context, but to display data from contexts that are descendents and ancestors of the current context.
To display data from descendent contexts, use the `/` character. So, for example, if your data were structured like:
To display data from descendent contexts, use the `.` character. So, for example, if your data were structured like:
var data = {"person": { "name": "Alan" }, company: {"name": "Rad, Inc." } };
```js
var data = {"person": { "name": "Alan" }, company: {"name": "Rad, Inc." } };
```
you could display the person's name from the top-level context with the following expression:
{{person/name}}
```
{{person.name}}
```
Similarly, if already traversed into the person object you could still display the company's name with an expression like `{{../company/name}}`, so:
You can backtrack using `../`. For example, if you've already traversed into the person object you could still display the company's name with an expression like `{{../company.name}}`, so:
{{#person}}{{name}} - {{../company/name}}{{/person}}
```
{{#person}}{{name}} - {{../company.name}}{{/person}}
```
would render:
Alan - Rad, Inc.
```
Alan - Rad, Inc.
```
### Strings
When calling a helper, you can pass paths or Strings as parameters. For
instance:
Handlebars.registerHelper('link_to', function(title, context) {
return "<a href='/posts" + context.id + "'>" + title + "</a>"
});
```js
Handlebars.registerHelper('link_to', function(title, context) {
return "<a href='/posts" + context.id + "'>" + title + "</a>"
});
var context = { posts: [{url: "/hello-world", body: "Hello World!"}] };
var source = '<ul>{{#posts}}<li>{{{link_to "Post" this}}}</li></ul>'
var context = { posts: [{url: "/hello-world", body: "Hello World!"}] };
var source = '<ul>{{#posts}}<li>{{{link_to "Post" this}}}</li>{{/posts}}</ul>'
var template = Handlebars.compile(source);
template(context);
var template = Handlebars.compile(source);
template(context);
// Would render:
//
// <ul>
// <li><a href='/hello-world'>Post!</a></li>
// </ul>
// Would render:
//
// <ul>
// <li><a href='/hello-world'>Post!</a></li>
// </ul>
```
When you pass a String as a parameter to a helper, the literal String
gets passed to the helper function.
@@ -118,23 +136,25 @@ gets passed to the helper function.
Handlebars.js also adds the ability to define block helpers. Block helpers are functions that can be called from anywhere in the template. Here's an example:
var source = "<ul>{{#people}}<li>{{{#link}}}{{name}}{{/link}}</li>{{/people}}</ul>";
Handlebars.registerHelper('link', function(context, fn) {
return '<a href="/people/' + this.__get__("id") + '">' + fn(this) + '</a>';
});
var template = Handlebars.compile(source);
```js
var source = "<ul>{{#people}}<li>{{{#link}}}{{name}}{{/link}}</li>{{/people}}</ul>";
Handlebars.registerHelper('link', function(context, fn) {
return '<a href="/people/' + this.__get__("id") + '">' + fn(this) + '</a>';
});
var template = Handlebars.compile(source);
var data = { "people": [
{ "name": "Alan", "id": 1 },
{ "name": "Yehuda", "id": 2 }
]};
template(data);
var data = { "people": [
{ "name": "Alan", "id": 1 },
{ "name": "Yehuda", "id": 2 }
]};
template(data);
// Should render:
// <ul>
// <li><a href="/people/1">Alan</a></li>
// <li><a href="/people/2">Yehuda</a></li>
// </ul>
// Should render:
// <ul>
// <li><a href="/people/1">Alan</a></li>
// <li><a href="/people/2">Yehuda</a></li>
// </ul>
```
Whenever the block helper is called it is given two parameters, the argument that is passed to the helper, or the current context if no argument is passed and the compiled contents of the block. Inside of the block helper the value of `this` is the current context, wrapped to include a method named `__get__` that helps translate paths into values within the helpers.
@@ -145,28 +165,88 @@ Handlebars when it encounters a partial (`{{> partialName}}`). Partials
can either be String templates or compiled template functions. Here's an
example:
var source = "<ul>{{#people}}<li>{{> link}}</li>{{/people}}</ul>";
```js
var source = "<ul>{{#people}}<li>{{> link}}</li>{{/people}}</ul>";
Handlebars.registerPartial('link', '<a href="/people/{{id}}">{{name}}</a>')
var template = Handlebars.compile(source);
Handlebars.registerPartial('link', '<a href="/people/{{id}}">{{name}}</a>')
var template = Handlebars.compile(source);
var data = { "people": [
{ "name": "Alan", "id": 1 },
{ "name": "Yehuda", "id": 2 }
]};
var data = { "people": [
{ "name": "Alan", "id": 1 },
{ "name": "Yehuda", "id": 2 }
]};
template(data);
template(data);
// Should render:
// <ul>
// <li><a href="/people/1">Alan</a></li>
// <li><a href="/people/2">Yehuda</a></li>
// </ul>
```
### Comments
You can add comments to your templates with the following syntax:
```js
{{! This is a comment }}
```
You can also use real html comments if you want them to end up in the output.
```html
<div>
{{! This comment will not end up in the output }}
<!-- This comment will show up in the output -->
</div>
```
// Should render:
// <ul>
// <li><a href="/people/1">Alan</a></li>
// <li><a href="/people/2">Yehuda</a></li>
// </ul>
Precompiling Templates
----------------------
TODO in the rewrite. This will use RubyRacer and not node.
Handlebars allows templates to be precompiled and included as javascript
code rather than the handlebars template allowing for faster startup time.
### Installation
The precompiler script may be installed via npm using the `npm install -g handlebars`
command.
### Usage
<pre>
Precompile handlebar templates.
Usage: handlebars template...
Options:
-f, --output Output File [string]
-k, --known Known helpers [string]
-o, --knownOnly Known helpers only [boolean]
-m, --min Minimize output [boolean]
-s, --simple Output template function only. [boolean]
-r, --root Template root. Base value that will be stripped from template names. [string]
</pre>
If using the precompiler's normal mode, the resulting templates will be stored
to the `Handlebars.templates` object using the relative template name sans the
extension. These templates may be executed in the same manner as templates.
If using the simple mode the precompiler will generate a single javascript method.
To execute this method it must be passed to the using the `Handlebars.template`
method and the resulting object may be as normal.
### Optimizations
- Rather than using the full _handlebars.js_ library, implementations that
do not need to compile templates at runtime may include _handlebars.vm.js_
whose min+gzip size is approximately 1k.
- If a helper is known to exist in the target environment they may be defined
using the `--known name` argument may be used to optimize accesses to these
helpers for size and speed.
- When all helpers are known in advance the `--knownOnly` argument may be used
to optimize all block helper references.
Performance
-----------
@@ -178,26 +258,56 @@ Building
--------
To build handlebars, just run `rake release`, and you will get two files
in the `dist` directory. The debug version comes with stack trace
annotations for webkit browsers, but will slightly increase startup
time.
in the `dist` directory.
Upgrading
---------
When upgrading from the Handlebars 0.9 series, be aware that the
signature for passing custom helpers or partials to templates has
changed.
Instead of:
```js
template(context, helpers, partials, [data])
```
Use:
```js
template(context, {helpers: helpers, partials: partials, data: data})
```
Known Issues
------------
* Handlebars.js can be cryptic when there's an error while rendering.
Handlebars Contrib
------------------
Alan Johnson, a.k.a. commondream, keeps a repository of useful helpers in the [handlebars-contrib](http://github.com/commondream/handlebars-contrib) repository. Feel free to copy those, or add some of your own.
Handlebars in the Wild
-----------------
* Don Park wrote an Express.js view engine adapter for Handlebars.js called [hbs](http://github.com/donpark/hbs)
* [jblotus](http://github.com/jblotus) created [http://tryhandlebarsjs.com](http://tryhandlebarsjs.com) for anyone who would
like to try out Handlebars.js in their browser.
* Don Park wrote an Express.js view engine adapter for Handlebars.js called [hbs](http://github.com/donpark/hbs).
* [sammy.js](http://github.com/quirkey/sammy) by Aaron Quint, a.k.a. quirkey, supports Handlebars.js as one of its template plugins.
* [SproutCore](http://www.sproutcore.com) uses Handlebars.js as its main templating engine, extending it with automatic data binding support.
* [Ember.js](http://www.emberjs.com) makes Handlebars.js the primary way to structure your views, also with automatic data binding support.
* Les Hill (@leshill) wrote a Rails Asset Pipeline gem named [handlebars_assets](http://github.com/leshill/handlebars_assets).
Helping Out
-----------
To build Handlebars.js you'll need a few things installed.
* Node.js
* Jison, for building the compiler - `npm install jison`
* Ruby
* therubyracer, for running tests - `gem install therubyracer`
* rspec, for running tests - `gem install rspec`
There's a Gemfile in the repo, so you can run `bundle` to install rspec and therubyracer if you've got bundler installed.
To build Handlebars.js from scratch, you'll want to run `rake compile` in the root of the project. That will build Handlebars and output the results to the dist/ folder. To run tests, run `rake spec`. You can also run our set of benchmarks with `rake bench`.
If you notice any problems, please report them to the GitHub issue tracker at [http://github.com/wycats/handlebars.js/issues](http://github.com/wycats/handlebars.js/issues). Feel free to contact commondream or wycats through GitHub with any other questions or feature requests. To submit changes fork the project and send a pull request.
License
+47 -40
View File
@@ -1,12 +1,30 @@
require "rubygems"
require "bundler/setup"
file "lib/handlebars/parser.js" => ["src/handlebars.yy","src/handlebars.l"] do
def compile_parser
system "jison src/handlebars.yy src/handlebars.l"
sh "mv handlebars.js lib/handlebars/parser.js"
if $?.success?
File.open("lib/handlebars/compiler/parser.js", "w") do |file|
file.puts File.read("handlebars.js") + ";"
end
sh "rm handlebars.js"
else
puts "Failed to run Jison."
end
end
task :compile => "lib/handlebars/parser.js"
file "lib/handlebars/compiler/parser.js" => ["src/handlebars.yy","src/handlebars.l"] do
if ENV['PATH'].split(':').any? {|folder| File.exists?(folder+'/jison')}
compile_parser
else
puts "Jison is not installed. Trying `npm install jison`."
sh "npm install jison -g"
compile_parser
end
end
task :compile => "lib/handlebars/compiler/parser.js"
desc "run the spec suite"
task :spec => [:release] do
@@ -20,23 +38,17 @@ def remove_exports(string)
match ? match[1] : string
end
minimal_deps = %w(parser base ast visitor utils vm).map do |file|
minimal_deps = %w(base compiler/parser compiler/base compiler/ast utils compiler/compiler runtime).map do |file|
"lib/handlebars/#{file}.js"
end
base_deps = %w(parser base ast visitor runtime utils vm).map do |file|
"lib/handlebars/#{file}.js"
end
debug_deps = %w(parser base ast visitor printer runtime utils vm debug).map do |file|
runtime_deps = %w(base utils runtime).map do |file|
"lib/handlebars/#{file}.js"
end
directory "dist"
minimal_deps.unshift "dist"
base_deps.unshift "dist"
debug_deps.unshift "dist"
def build_for_task(task)
FileUtils.rm_rf("dist/*") if File.directory?("dist")
@@ -58,52 +70,47 @@ file "dist/handlebars.js" => minimal_deps do |task|
build_for_task(task)
end
file "dist/handlebars.base.js" => base_deps do |task|
build_for_task(task)
end
file "dist/handlebars.debug.js" => debug_deps do |task|
file "dist/handlebars.runtime.js" => runtime_deps do |task|
build_for_task(task)
end
task :build => [:compile, "dist/handlebars.js"]
task :base => [:compile, "dist/handlebars.base.js"]
task :debug => [:compile, "dist/handlebars.debug.js"]
task :runtime => [:compile, "dist/handlebars.runtime.js"]
desc "build the build, debug and base versions of handlebars"
task :release => [:build, :debug, :base]
desc "build the build and runtime version of handlebars"
task :release => [:build, :runtime]
directory "vendor"
desc "benchmark against dust.js and mustache.js"
task :bench => "vendor" do
require "open-uri"
File.open("vendor/mustache.js", "w") do |file|
file.puts open("https://github.com/janl/mustache.js/raw/master/mustache.js").read
file.puts "module.exports = Mustache;"
end
#File.open("vendor/mustache.js", "w") do |file|
#file.puts open("https://github.com/janl/mustache.js/raw/master/mustache.js").read
#file.puts "module.exports = Mustache;"
#end
File.open("vendor/benchmark.js", "w") do |file|
file.puts open("https://github.com/mathiasbynens/benchmark.js/raw/master/benchmark.js").read
file.puts open("https://raw.github.com/bestiejs/benchmark.js/master/benchmark.js").read
end
if File.directory?("vendor/dustjs")
system "cd vendor/dustjs && git pull"
else
system "git clone git://github.com/akdubya/dustjs.git vendor/dustjs"
end
#if File.directory?("vendor/dustjs")
#system "cd vendor/dustjs && git pull"
#else
#system "git clone git://github.com/akdubya/dustjs.git vendor/dustjs"
#end
if File.directory?("vendor/coffee")
system "cd vendor/coffee && git pull"
else
system "git clone git://github.com/jashkenas/coffee-script.git vendor/coffee"
end
#if File.directory?("vendor/coffee")
#system "cd vendor/coffee && git pull"
#else
#system "git clone git://github.com/jashkenas/coffee-script.git vendor/coffee"
#end
if File.directory?("vendor/eco")
system "cd vendor/eco && git pull"
else
system "git clone git://github.com/sstephenson/eco.git vendor/eco"
end
#if File.directory?("vendor/eco")
#system "cd vendor/eco && git pull && npm update"
#else
#system "git clone git://github.com/sstephenson/eco.git vendor/eco && cd vendor/eco && npm update"
#end
system "node bench/handlebars.js"
end
+2
View File
@@ -112,6 +112,8 @@ BenchWarmer.prototype = {
}
}
});
print("\n");
},
startLine: function(name) {
var winners = Benchmark.map(this.winners(this.currentBenches), function(bench) {
+1 -4
View File
@@ -13,10 +13,7 @@ var Mustache = require("mustache");
var ecoExports = require("eco");
eco = function(str) {
var module = {};
var template = new Function("module", ecoExports.compile(str));
template(module);
return module.exports;
return ecoExports(str);
}
var benchDetails = {
Executable
+139
View File
@@ -0,0 +1,139 @@
#!/usr/bin/env node
var optimist = require('optimist')
.usage('Precompile handlebar templates.\nUsage: $0 template...', {
'f': {
'type': 'string',
'description': 'Output File',
'alias': 'output'
},
'k': {
'type': 'string',
'description': 'Known helpers',
'alias': 'known'
},
'o': {
'type': 'boolean',
'description': 'Known helpers only',
'alias': 'knownOnly'
},
'm': {
'type': 'boolean',
'description': 'Minimize output',
'alias': 'min'
},
's': {
'type': 'boolean',
'description': 'Output template function only.',
'alias': 'simple'
},
'r': {
'type': 'string',
'description': 'Template root. Base value that will be stripped from template names.',
'alias': 'root'
}
})
.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';
}
});
var fs = require('fs'),
handlebars = require('../lib/handlebars'),
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;
}
}
var output = [];
if (!argv.simple) {
output.push('(function() {\n var template = Handlebars.template, templates = Handlebars.templates = Handlebars.templates || {};\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 (/\.handlebars$/.test(path) || fs.statSync(path).isDirectory()) {
processTemplate(path, root || template);
}
});
} else {
var data = fs.readFileSync(path, 'utf8');
var options = {
knownHelpers: known,
knownHelpersOnly: argv.o
};
// Clean the template name
if (!root) {
template = basename(template);
} else if (template.indexOf(root) === 0) {
template = template.substring(root.length+1);
}
template = template.replace(/\.handlebars$/, '');
if (argv.simple) {
output.push(handlebars.precompile(data, options) + '\n');
} else {
output.push('templates[\'' + template + '\'] = template(' + handlebars.precompile(data, options) + ');\n');
}
}
}
argv._.forEach(function(template) {
processTemplate(template, argv.root);
});
// Output the content
if (!argv.simple) {
output.push('})();');
}
output = output.join('');
if (argv.min) {
var ast = uglify.parser.parse(output);
ast = uglify.uglify.ast_mangle(ast);
ast = uglify.uglify.ast_squeeze(ast);
output = uglify.uglify.gen_code(ast);
}
if (argv.output) {
fs.writeFileSync(argv.output, output, 'utf8');
} else {
console.log(output);
}
+6 -8
View File
@@ -1,14 +1,12 @@
var Handlebars = require("handlebars/base");
var Handlebars = require("./handlebars/base");
module.exports = Handlebars;
require("handlebars/utils");
// Each of these augment the Handlebars object. No need to setup here.
// (This is done to easily share code between commonjs and browse envs)
require("./handlebars/utils");
require("handlebars/ast");
require("handlebars/printer");
require("handlebars/visitor");
require("handlebars/runtime");
require("handlebars/vm");
require("./handlebars/compiler");
require("./handlebars/runtime");
// BEGIN(BROWSER)
+36 -53
View File
@@ -1,34 +1,7 @@
var handlebars = require("handlebars/parser").parser;
// BEGIN(BROWSER)
var Handlebars = {};
Handlebars.Parser = handlebars;
Handlebars.parse = function(string) {
Handlebars.Parser.yy = Handlebars.AST;
return Handlebars.Parser.parse(string);
};
Handlebars.print = function(ast) {
return new Handlebars.PrintVisitor().accept(ast);
};
Handlebars.Runtime = {};
Handlebars.Runtime.compile = function(string) {
var ast = Handlebars.parse(string);
return function(context, helpers, partials) {
helpers = helpers || Handlebars.helpers;
partials = partials || Handlebars.partials;
var internalContext = new Handlebars.Context(context, helpers, partials);
var runtime = new Handlebars.Runtime(internalContext);
runtime.accept(ast);
return runtime.buffer;
};
};
Handlebars.VERSION = "1.0.beta.5";
Handlebars.helpers = {};
Handlebars.partials = {};
@@ -42,15 +15,24 @@ Handlebars.registerPartial = function(name, str) {
this.partials[name] = str;
};
Handlebars.registerHelper('blockHelperMissing', function(context, fn, inverse) {
inverse = inverse || function() {};
Handlebars.registerHelper('helperMissing', function(arg) {
if(arguments.length === 2) {
return undefined;
} else {
throw new Error("Could not find property '" + arg + "'");
}
});
var toString = Object.prototype.toString, functionType = "[object Function]";
Handlebars.registerHelper('blockHelperMissing', function(context, options) {
var inverse = options.inverse || function() {}, fn = options.fn;
var ret = "";
var type = Object.prototype.toString.call(context);
var type = toString.call(context);
if(type === "[object Function]") {
context = context();
}
if(type === functionType) { context = context.call(this); }
if(context === true) {
return fn(this);
@@ -68,11 +50,10 @@ Handlebars.registerHelper('blockHelperMissing', function(context, fn, inverse) {
} else {
return fn(context);
}
}, function(context, fn) {
return fn(context);
});
Handlebars.registerHelper('each', function(context, fn, inverse) {
Handlebars.registerHelper('each', function(context, options) {
var fn = options.fn, inverse = options.inverse;
var ret = "";
if(context && context.length > 0) {
@@ -85,30 +66,32 @@ Handlebars.registerHelper('each', function(context, fn, inverse) {
return ret;
});
Handlebars.registerHelper('if', function(context, fn, inverse) {
if(!context || context == []) {
return inverse(this);
Handlebars.registerHelper('if', function(context, options) {
var type = toString.call(context);
if(type === functionType) { context = context.call(this); }
if(!context || Handlebars.Utils.isEmpty(context)) {
return options.inverse(this);
} else {
return fn(this);
return options.fn(this);
}
});
Handlebars.registerHelper('unless', function(context, fn, inverse) {
Handlebars.helpers['if'].call(this, context, inverse, fn);
Handlebars.registerHelper('unless', function(context, options) {
var fn = options.fn, inverse = options.inverse;
options.fn = inverse;
options.inverse = fn;
return Handlebars.helpers['if'].call(this, context, options);
});
Handlebars.registerHelper('with', function(context, fn) {
return fn(context);
Handlebars.registerHelper('with', function(context, options) {
return options.fn(context);
});
Handlebars.logger = {
DEBUG: 0, INFO: 1, WARN: 2, ERROR: 3, level: 3,
// override in the host environment
log: function(level, str) {}
};
Handlebars.log = function(level, str) { Handlebars.logger.log(level, str); };
Handlebars.registerHelper('log', function(context) {
Handlebars.log(context);
});
// END(BROWSER)
@@ -1,4 +1,4 @@
var Handlebars = require("handlebars");
var Handlebars = require('./base');
// BEGIN(BROWSER)
(function() {
@@ -11,10 +11,11 @@ var Handlebars = require("handlebars");
if(inverse) { this.inverse = new Handlebars.AST.ProgramNode(inverse); }
};
Handlebars.AST.MustacheNode = function(params, unescaped) {
Handlebars.AST.MustacheNode = function(params, hash, unescaped) {
this.type = "mustache";
this.id = params[0];
this.params = params.slice(1);
this.hash = hash;
this.escaped = !unescaped;
};
@@ -52,9 +53,14 @@ var Handlebars = require("handlebars");
this.string = string;
};
Handlebars.AST.HashNode = function(pairs) {
this.type = "hash";
this.pairs = pairs;
};
Handlebars.AST.IdNode = function(parts) {
this.type = "ID";
this.original = parts.join("/");
this.original = parts.join(".");
var dig = [], depth = 0;
@@ -62,11 +68,12 @@ var Handlebars = require("handlebars");
var part = parts[i];
if(part === "..") { depth++; }
else if(part === "." || part === "this") { continue; }
else if(part === "." || part === "this") { this.isScoped = true; }
else { dig.push(part); }
}
this.parts = dig;
this.string = dig.join('.');
this.depth = depth;
this.isSimple = (dig.length === 1) && (depth === 0);
};
@@ -76,6 +83,16 @@ var Handlebars = require("handlebars");
this.string = string;
};
Handlebars.AST.IntegerNode = function(integer) {
this.type = "INTEGER";
this.integer = integer;
};
Handlebars.AST.BooleanNode = function(bool) {
this.type = "BOOLEAN";
this.bool = bool;
};
Handlebars.AST.CommentNode = function(comment) {
this.type = "comment";
this.comment = comment;
+27
View File
@@ -0,0 +1,27 @@
var handlebars = require("./parser").parser;
var Handlebars = require("../base");
// BEGIN(BROWSER)
Handlebars.Parser = handlebars;
Handlebars.parse = function(string) {
Handlebars.Parser.yy = Handlebars.AST;
return Handlebars.Parser.parse(string);
};
Handlebars.print = function(ast) {
return new Handlebars.PrintVisitor().accept(ast);
};
Handlebars.logger = {
DEBUG: 0, INFO: 1, WARN: 2, ERROR: 3, level: 3,
// override in the host environment
log: function(level, str) {}
};
Handlebars.log = function(level, str) { Handlebars.logger.log(level, str); };
// END(BROWSER)
module.exports = Handlebars;
+805
View File
@@ -0,0 +1,805 @@
var Handlebars = require("./base");
// BEGIN(BROWSER)
Handlebars.Compiler = function() {};
Handlebars.JavaScriptCompiler = function() {};
(function(Compiler, JavaScriptCompiler) {
Compiler.OPCODE_MAP = {
appendContent: 1,
getContext: 2,
lookupWithHelpers: 3,
lookup: 4,
append: 5,
invokeMustache: 6,
appendEscaped: 7,
pushString: 8,
truthyOrFallback: 9,
functionOrFallback: 10,
invokeProgram: 11,
invokePartial: 12,
push: 13,
assignToHash: 15,
pushStringParam: 16
};
Compiler.MULTI_PARAM_OPCODES = {
appendContent: 1,
getContext: 1,
lookupWithHelpers: 2,
lookup: 1,
invokeMustache: 3,
pushString: 1,
truthyOrFallback: 1,
functionOrFallback: 1,
invokeProgram: 3,
invokePartial: 1,
push: 1,
assignToHash: 1,
pushStringParam: 1
};
Compiler.DISASSEMBLE_MAP = {};
for(var prop in Compiler.OPCODE_MAP) {
var value = Compiler.OPCODE_MAP[prop];
Compiler.DISASSEMBLE_MAP[value] = prop;
}
Compiler.multiParamSize = function(code) {
return Compiler.MULTI_PARAM_OPCODES[Compiler.DISASSEMBLE_MAP[code]];
};
Compiler.prototype = {
compiler: Compiler,
disassemble: function() {
var opcodes = this.opcodes, opcode, nextCode;
var out = [], str, name, value;
for(var i=0, l=opcodes.length; i<l; i++) {
opcode = opcodes[i];
if(opcode === 'DECLARE') {
name = opcodes[++i];
value = opcodes[++i];
out.push("DECLARE " + name + " = " + value);
} else {
str = Compiler.DISASSEMBLE_MAP[opcode];
var extraParams = Compiler.multiParamSize(opcode);
var codes = [];
for(var j=0; j<extraParams; j++) {
nextCode = opcodes[++i];
if(typeof nextCode === "string") {
nextCode = "\"" + nextCode.replace("\n", "\\n") + "\"";
}
codes.push(nextCode);
}
str = str + " " + codes.join(" ");
out.push(str);
}
}
return out.join("\n");
},
guid: 0,
compile: function(program, options) {
this.children = [];
this.depths = {list: []};
this.options = options;
// These changes will propagate to the other compiler components
var knownHelpers = this.options.knownHelpers;
this.options.knownHelpers = {
'helperMissing': true,
'blockHelperMissing': true,
'each': true,
'if': true,
'unless': true,
'with': true,
'log': true
};
if (knownHelpers) {
for (var name in knownHelpers) {
this.options.knownHelpers[name] = knownHelpers[name];
}
}
return this.program(program);
},
accept: function(node) {
return this[node.type](node);
},
program: function(program) {
var statements = program.statements, statement;
this.opcodes = [];
for(var i=0, l=statements.length; i<l; i++) {
statement = statements[i];
this[statement.type](statement);
}
this.isSimple = l === 1;
this.depths.list = this.depths.list.sort(function(a, b) {
return a - b;
});
return this;
},
compileProgram: function(program) {
var result = new this.compiler().compile(program, this.options);
var guid = this.guid++;
this.usePartial = this.usePartial || result.usePartial;
this.children[guid] = result;
for(var i=0, l=result.depths.list.length; i<l; i++) {
depth = result.depths.list[i];
if(depth < 2) { continue; }
else { this.addDepth(depth - 1); }
}
return guid;
},
block: function(block) {
var mustache = block.mustache;
var depth, child, inverse, inverseGuid;
var params = this.setupStackForMustache(mustache);
var programGuid = this.compileProgram(block.program);
if(block.program.inverse) {
inverseGuid = this.compileProgram(block.program.inverse);
this.declare('inverse', inverseGuid);
}
this.opcode('invokeProgram', programGuid, params.length, !!mustache.hash);
this.declare('inverse', null);
this.opcode('append');
},
inverse: function(block) {
var params = this.setupStackForMustache(block.mustache);
var programGuid = this.compileProgram(block.program);
this.declare('inverse', programGuid);
this.opcode('invokeProgram', null, params.length, !!block.mustache.hash);
this.declare('inverse', null);
this.opcode('append');
},
hash: function(hash) {
var pairs = hash.pairs, pair, val;
this.opcode('push', '{}');
for(var i=0, l=pairs.length; i<l; i++) {
pair = pairs[i];
val = pair[1];
this.accept(val);
this.opcode('assignToHash', pair[0]);
}
},
partial: function(partial) {
var id = partial.id;
this.usePartial = true;
if(partial.context) {
this.ID(partial.context);
} else {
this.opcode('push', 'depth0');
}
this.opcode('invokePartial', id.original);
this.opcode('append');
},
content: function(content) {
this.opcode('appendContent', content.string);
},
mustache: function(mustache) {
var params = this.setupStackForMustache(mustache);
this.opcode('invokeMustache', params.length, mustache.id.original, !!mustache.hash);
if(mustache.escaped && !this.options.noEscape) {
this.opcode('appendEscaped');
} else {
this.opcode('append');
}
},
ID: function(id) {
this.addDepth(id.depth);
this.opcode('getContext', id.depth);
this.opcode('lookupWithHelpers', id.parts[0] || null, id.isScoped || false);
for(var i=1, l=id.parts.length; i<l; i++) {
this.opcode('lookup', id.parts[i]);
}
},
STRING: function(string) {
this.opcode('pushString', string.string);
},
INTEGER: function(integer) {
this.opcode('push', integer.integer);
},
BOOLEAN: function(bool) {
this.opcode('push', bool.bool);
},
comment: function() {},
// HELPERS
pushParams: function(params) {
var i = params.length, param;
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.string);
} else {
this[param.type](param);
}
}
},
opcode: function(name, val1, val2, val3) {
this.opcodes.push(Compiler.OPCODE_MAP[name]);
if(val1 !== undefined) { this.opcodes.push(val1); }
if(val2 !== undefined) { this.opcodes.push(val2); }
if(val3 !== undefined) { this.opcodes.push(val3); }
},
declare: function(name, value) {
this.opcodes.push('DECLARE');
this.opcodes.push(name);
this.opcodes.push(value);
},
addDepth: function(depth) {
if(depth === 0) { return; }
if(!this.depths[depth]) {
this.depths[depth] = true;
this.depths.list.push(depth);
}
},
setupStackForMustache: function(mustache) {
var params = mustache.params;
this.pushParams(params);
if(mustache.hash) {
this.hash(mustache.hash);
}
this.ID(mustache.id);
return params;
}
};
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) {
if (/^[0-9]+$/.test(name)) {
return parent + "[" + name + "]";
} else if (JavaScriptCompiler.isValidJavaScriptVariableName(name)) {
return parent + "." + name;
}
else {
return parent + "['" + name + "']";
}
},
appendToBuffer: function(string) {
if (this.environment.isSimple) {
return "return " + string + ";";
} else {
return "buffer += " + string + ";";
}
},
initializeBuffer: function() {
return this.quotedString("");
},
namespace: "Handlebars",
// END PUBLIC API
compile: function(environment, options, context, asObject) {
this.environment = environment;
this.options = options || {};
this.name = this.environment.name;
this.isChild = !!context;
this.context = context || {
programs: [],
aliases: { self: 'this' },
registers: {list: []}
};
this.preamble();
this.stackSlot = 0;
this.stackVars = [];
this.compileChildren(environment, options);
var opcodes = environment.opcodes, opcode;
this.i = 0;
for(l=opcodes.length; this.i<l; this.i++) {
opcode = this.nextOpcode(0);
if(opcode[0] === 'DECLARE') {
this.i = this.i + 2;
this[opcode[1]] = opcode[2];
} else {
this.i = this.i + opcode[1].length;
this[opcode[0]].apply(this, opcode[1]);
}
}
return this.createFunctionContext(asObject);
},
nextOpcode: function(n) {
var opcodes = this.environment.opcodes, opcode = opcodes[this.i + n], name, val;
var extraParams, codes;
if(opcode === 'DECLARE') {
name = opcodes[this.i + 1];
val = opcodes[this.i + 2];
return ['DECLARE', name, val];
} else {
name = Compiler.DISASSEMBLE_MAP[opcode];
extraParams = Compiler.multiParamSize(opcode);
codes = [];
for(var j=0; j<extraParams; j++) {
codes.push(opcodes[this.i + j + 1 + n]);
}
return [name, codes];
}
},
eat: function(opcode) {
this.i = this.i + opcode.length;
},
preamble: function() {
var out = [];
// this register will disambiguate helper lookup from finding a function in
// a context. This is necessary for mustache compatibility, which requires
// that context functions in blocks are evaluated by blockHelperMissing, and
// then proceed as if the resulting value was provided to blockHelperMissing.
this.useRegister('foundHelper');
if (!this.isChild) {
var namespace = this.namespace;
var copies = "helpers = helpers || " + namespace + ".helpers;";
if(this.environment.usePartial) { copies = copies + " partials = partials || " + namespace + ".partials;"; }
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;
},
createFunctionContext: function(asObject) {
var locals = this.stackVars;
if (!this.isChild) {
locals = locals.concat(this.context.registers.list);
}
if(locals.length > 0) {
this.source[1] = this.source[1] + ", " + locals.join(", ");
}
// Generate minimizer alias mappings
if (!this.isChild) {
var aliases = [];
for (var alias in this.context.aliases) {
this.source[1] = this.source[1] + ', ' + alias + '=' + this.context.aliases[alias];
}
}
if (this.source[1]) {
this.source[1] = "var " + this.source[1].substring(2) + ";";
}
// Merge children
if (!this.isChild) {
this.source[1] += '\n' + this.context.programs.join('\n') + '\n';
}
if (!this.environment.isSimple) {
this.source.push("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 (asObject) {
params.push(this.source.join("\n "));
return Function.apply(this, params);
} else {
var functionSource = 'function ' + (this.name || '') + '(' + params.join(',') + ') {\n ' + this.source.join("\n ") + '}';
Handlebars.log(Handlebars.logger.DEBUG, functionSource + "\n\n");
return functionSource;
}
},
appendContent: function(content) {
this.source.push(this.appendToBuffer(this.quotedString(content)));
},
append: function() {
var local = this.popStack();
this.source.push("if(" + local + " || " + local + " === 0) { " + this.appendToBuffer(local) + " }");
if (this.environment.isSimple) {
this.source.push("else { " + this.appendToBuffer("''") + " }");
}
},
appendEscaped: function() {
var opcode = this.nextOpcode(1), extra = "";
this.context.aliases.escapeExpression = 'this.escapeExpression';
if(opcode[0] === 'appendContent') {
extra = " + " + this.quotedString(opcode[1][0]);
this.eat(opcode);
}
this.source.push(this.appendToBuffer("escapeExpression(" + this.popStack() + ")" + extra));
},
getContext: function(depth) {
if(this.lastContext !== depth) {
this.lastContext = depth;
}
},
lookupWithHelpers: function(name, isScoped) {
if(name) {
var topStack = this.nextStack();
this.usingKnownHelper = false;
var toPush;
if (!isScoped && this.options.knownHelpers[name]) {
toPush = topStack + " = " + this.nameLookup('helpers', name, 'helper');
this.usingKnownHelper = true;
} else if (isScoped || this.options.knownHelpersOnly) {
toPush = topStack + " = " + this.nameLookup('depth' + this.lastContext, name, 'context');
} else {
this.register('foundHelper', this.nameLookup('helpers', name, 'helper'));
toPush = topStack + " = foundHelper || " + this.nameLookup('depth' + this.lastContext, name, 'context');
}
toPush += ';';
this.source.push(toPush);
} else {
this.pushStack('depth' + this.lastContext);
}
},
lookup: function(name) {
var topStack = this.topStack();
this.source.push(topStack + " = (" + topStack + " === null || " + topStack + " === undefined || " + topStack + " === false ? " +
topStack + " : " + this.nameLookup(topStack, name, 'context') + ");");
},
pushStringParam: function(string) {
this.pushStack('depth' + this.lastContext);
this.pushString(string);
},
pushString: function(string) {
this.pushStack(this.quotedString(string));
},
push: function(name) {
this.pushStack(name);
},
invokeMustache: function(paramSize, original, hasHash) {
this.populateParams(paramSize, this.quotedString(original), "{}", null, hasHash, function(nextStack, helperMissingString, id) {
if (!this.usingKnownHelper) {
this.context.aliases.helperMissing = 'helpers.helperMissing';
this.context.aliases.undef = 'void 0';
this.source.push("else if(" + id + "=== undef) { " + nextStack + " = helperMissing.call(" + helperMissingString + "); }");
if (nextStack !== id) {
this.source.push("else { " + nextStack + " = " + id + "; }");
}
}
});
},
invokeProgram: function(guid, paramSize, hasHash) {
var inverse = this.programExpression(this.inverse);
var mainProgram = this.programExpression(guid);
this.populateParams(paramSize, null, mainProgram, inverse, hasHash, function(nextStack, helperMissingString, id) {
if (!this.usingKnownHelper) {
this.context.aliases.blockHelperMissing = 'helpers.blockHelperMissing';
this.source.push("else { " + nextStack + " = blockHelperMissing.call(" + helperMissingString + "); }");
}
});
},
populateParams: function(paramSize, helperId, program, inverse, hasHash, fn) {
var needsRegister = hasHash || this.options.stringParams || inverse || this.options.data;
var id = this.popStack(), nextStack;
var params = [], param, stringParam, stringOptions;
if (needsRegister) {
this.register('tmp1', program);
stringOptions = 'tmp1';
} else {
stringOptions = '{ hash: {} }';
}
if (needsRegister) {
var hash = (hasHash ? this.popStack() : '{}');
this.source.push('tmp1.hash = ' + hash + ';');
}
if(this.options.stringParams) {
this.source.push('tmp1.contexts = [];');
}
for(var i=0; i<paramSize; i++) {
param = this.popStack();
params.push(param);
if(this.options.stringParams) {
this.source.push('tmp1.contexts.push(' + this.popStack() + ');');
}
}
if(inverse) {
this.source.push('tmp1.fn = tmp1;');
this.source.push('tmp1.inverse = ' + inverse + ';');
}
if(this.options.data) {
this.source.push('tmp1.data = data;');
}
params.push(stringOptions);
this.populateCall(params, id, helperId || id, fn, program !== '{}');
},
populateCall: function(params, id, helperId, fn, program) {
var paramString = ["depth0"].concat(params).join(", ");
var helperMissingString = ["depth0"].concat(helperId).concat(params).join(", ");
var nextStack = this.nextStack();
if (this.usingKnownHelper) {
this.source.push(nextStack + " = " + id + ".call(" + paramString + ");");
} else {
this.context.aliases.functionType = '"function"';
var condition = program ? "foundHelper && " : "";
this.source.push("if(" + condition + "typeof " + id + " === functionType) { " + nextStack + " = " + id + ".call(" + paramString + "); }");
}
fn.call(this, nextStack, helperMissingString, id);
this.usingKnownHelper = false;
},
invokePartial: function(context) {
params = [this.nameLookup('partials', context, 'partial'), "'" + context + "'", this.popStack(), "helpers", "partials"];
if (this.options.data) {
params.push("data");
}
this.pushStack("self.invokePartial(" + params.join(", ") + ");");
},
assignToHash: function(key) {
var value = this.popStack();
var hash = this.topStack();
this.source.push(hash + "['" + key + "'] = " + value + ";");
},
// HELPERS
compiler: JavaScriptCompiler,
compileChildren: function(environment, options) {
var children = environment.children, child, compiler;
for(var i=0, l=children.length; i<l; i++) {
child = children[i];
compiler = new this.compiler();
this.context.programs.push(''); // Placeholder to prevent name conflicts for nested children
var index = this.context.programs.length;
child.index = index;
child.name = 'program' + index;
this.context.programs[index] = compiler.compile(child, options, this.context);
}
},
programExpression: function(guid) {
if(guid == null) { return "self.noop"; }
var child = this.environment.children[guid],
depths = child.depths.list;
var programParams = [child.index, child.name, "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(depths.length === 0) {
return "self.program(" + programParams.join(", ") + ")";
} else {
programParams.shift();
return "self.programWithDepth(" + programParams.join(", ") + ")";
}
},
register: function(name, val) {
this.useRegister(name);
this.source.push(name + " = " + val + ";");
},
useRegister: function(name) {
if(!this.context.registers[name]) {
this.context.registers[name] = true;
this.context.registers.list.push(name);
}
},
pushStack: function(item) {
this.source.push(this.nextStack() + " = " + item + ";");
return "stack" + this.stackSlot;
},
nextStack: function() {
this.stackSlot++;
if(this.stackSlot > this.stackVars.length) { this.stackVars.push("stack" + this.stackSlot); }
return "stack" + this.stackSlot;
},
popStack: function() {
return "stack" + this.stackSlot--;
},
topStack: function() {
return "stack" + this.stackSlot;
},
quotedString: function(str) {
return '"' + str
.replace(/\\/g, '\\\\')
.replace(/"/g, '\\"')
.replace(/\n/g, '\\n')
.replace(/\r/g, '\\r') + '"';
}
};
var reservedWords = (
"break else new var" +
" case finally return void" +
" catch for switch while" +
" continue function this with" +
" default if throw" +
" delete in try" +
" do instanceof typeof" +
" abstract enum int short" +
" boolean export interface static" +
" byte extends long super" +
" char final native synchronized" +
" class float package throws" +
" const goto private transient" +
" debugger implements protected volatile" +
" double import public let yield"
).split(" ");
var compilerWords = JavaScriptCompiler.RESERVED_WORDS = {};
for(var i=0, l=reservedWords.length; i<l; i++) {
compilerWords[reservedWords[i]] = true;
}
JavaScriptCompiler.isValidJavaScriptVariableName = function(name) {
if(!JavaScriptCompiler.RESERVED_WORDS[name] && /^[a-zA-Z_$][0-9a-zA-Z_$]+$/.test(name)) {
return true;
}
return false;
};
})(Handlebars.Compiler, Handlebars.JavaScriptCompiler);
Handlebars.precompile = function(string, options) {
options = options || {};
var ast = Handlebars.parse(string);
var environment = new Handlebars.Compiler().compile(ast, options);
return new Handlebars.JavaScriptCompiler().compile(environment, options);
};
Handlebars.compile = function(string, options) {
options = options || {};
var compiled;
function compile() {
var ast = Handlebars.parse(string);
var environment = new Handlebars.Compiler().compile(ast, options);
var templateSpec = new Handlebars.JavaScriptCompiler().compile(environment, options, undefined, true);
return Handlebars.template(templateSpec);
}
// Template is only compiled on first use and cached after that point.
return function(context, options) {
if (!compiled) {
compiled = compile();
}
return compiled.call(this, context, options);
};
};
// END(BROWSER)
+7
View File
@@ -0,0 +1,7 @@
// Each of these module will augment the Handlebars object as it loads. No need to perform addition operations
module.exports = require("./base");
require("./visitor");
require("./printer");
require("./ast");
require("./compiler");
@@ -1,5 +1,4 @@
var Handlebars = require("handlebars");
require("handlebars/visitor");
var Handlebars = require("./base");
// BEGIN(BROWSER)
Handlebars.PrintVisitor = function() { this.padding = 0; };
@@ -73,14 +72,17 @@ Handlebars.PrintVisitor.prototype.inverse = function(block) {
Handlebars.PrintVisitor.prototype.mustache = function(mustache) {
var params = mustache.params, paramStrings = [];
var params = mustache.params, paramStrings = [], hash;
for(var i=0, l=params.length; i<l; i++) {
paramStrings.push(this.accept(params[i]));
}
params = "[" + paramStrings.join(", ") + "]";
return this.pad("{{ " + this.accept(mustache.id) + " " + params + " }}");
hash = mustache.hash ? " " + this.accept(mustache.hash) : "";
return this.pad("{{ " + this.accept(mustache.id) + " " + params + hash + " }}");
};
Handlebars.PrintVisitor.prototype.partial = function(partial) {
@@ -89,10 +91,31 @@ Handlebars.PrintVisitor.prototype.partial = function(partial) {
return this.pad("{{> " + content + " }}");
};
Handlebars.PrintVisitor.prototype.hash = function(hash) {
var pairs = hash.pairs;
var joinedPairs = [], left, right;
for(var i=0, l=pairs.length; i<l; i++) {
left = pairs[i][0];
right = this.accept(pairs[i][1]);
joinedPairs.push( left + "=" + right );
}
return "HASH{" + joinedPairs.join(", ") + "}";
};
Handlebars.PrintVisitor.prototype.STRING = function(string) {
return '"' + string.string + '"';
};
Handlebars.PrintVisitor.prototype.INTEGER = function(integer) {
return "INTEGER{" + integer.integer + "}";
};
Handlebars.PrintVisitor.prototype.BOOLEAN = function(bool) {
return "BOOLEAN{" + bool.bool + "}";
};
Handlebars.PrintVisitor.prototype.ID = function(id) {
var path = id.parts.join("/");
if(id.parts.length > 1) {
@@ -1,4 +1,4 @@
var Handlebars = require("handlebars");
var Handlebars = require("./base");
// BEGIN(BROWSER)
-29
View File
@@ -1,29 +0,0 @@
var Handlebars = require("handlebars");
// BEGIN(BROWSER)
(function() {
var classes = ["Lexer", "PrintVisitor", "Context", "Runtime", "Exception"];
var prop;
for(var i=0, l=classes.length; i<l; i++) {
var className = classes[i], klass = Handlebars[className];
klass.displayName = "new Handlebars." + className;
for(prop in klass) {
if(klass.hasOwnProperty(prop)) {
klass[prop].displayName = "Handlebars." + className + "#" + prop;
}
}
}
for(prop in Handlebars.Utils) {
if(Handlebars.Utils.hasOwnProperty(prop)) {
Handlebars.Utils[prop].displayName = "Handlebars.Utils." + prop;
}
}
Handlebars.parse.displayName = "Handlebars.parse";
Handlebars.print.displayName = "Handlebars.print";
Handlebars.compile.displayName = "Handlebars.compile";
})();
// END(BROWSER)
+54 -253
View File
@@ -1,267 +1,68 @@
var inspect = function(obj) {
require("sys").print(require("sys").inspect(obj) + "\n");
};
var Handlebars = require("handlebars");
var Handlebars = require("./base");
// BEGIN(BROWSER)
// A Context wraps data, and makes it possible to extract a
// new Context given a path. For instance, if the data
// is { person: { name: "Alan" } }, a Context wrapping
// "Alan" can be extracted by searching for "person/name"
Handlebars.Context = function(data, helpers, partials) {
this.data = data;
this.helpers = helpers || {};
this.partials = partials || {};
};
Handlebars.VM = {
template: function(templateSpec) {
// Just add water
var container = {
escapeExpression: Handlebars.Utils.escapeExpression,
invokePartial: Handlebars.VM.invokePartial,
programs: [],
program: function(i, fn, data) {
var programWrapper = this.programs[i];
if(data) {
return Handlebars.VM.program(fn, data);
} else if(programWrapper) {
return programWrapper;
} else {
programWrapper = this.programs[i] = Handlebars.VM.program(fn);
return programWrapper;
}
},
programWithDepth: Handlebars.VM.programWithDepth,
noop: Handlebars.VM.noop
};
Handlebars.Context.prototype = {
isContext: true,
// Make a shallow copy of the Context
clone: function() {
return new Handlebars.Context(this.data, this.helpers, this.partials);
return function(context, options) {
options = options || {};
return templateSpec.call(container, Handlebars, context, options.helpers, options.partials, options.data);
};
},
// Search for an object inside the Context's data. The
// path parameter is an object with parts
// ("person/name" represented as ["person", "name"]),
// and depth (the amount of levels to go up the stack,
// originally represented as ..). The stack parameter
// is the objects already searched from the root of
// the original Context in order to get to this point.
//
// Return a new Context wrapping the data found in
// the search.
evaluate: function(path, stack) {
var context = this.clone();
var depth = path.depth, parts = path.parts;
programWithDepth: function(fn, data, $depth) {
var args = Array.prototype.slice.call(arguments, 2);
if(depth > stack.length) { context.data = null; }
else if(depth > 0) { context = stack[stack.length - depth].clone(); }
return function(context, options) {
options = options || {};
for(var i=0,l=parts.length; i<l && context.data != null; i++) {
context.data = context.data[parts[i]];
return fn.apply(this, [context, options.data || data].concat(args));
};
},
program: function(fn, data) {
return function(context, options) {
options = options || {};
return fn(context, options.data || data);
};
},
noop: function() { return ""; },
invokePartial: function(partial, name, context, helpers, partials, data) {
var options = { helpers: helpers, partials: partials, data: data };
if(partial === undefined) {
throw new Handlebars.Exception("The partial " + name + " could not be found");
} else if(partial instanceof Function) {
return partial(context, options);
} else if (!Handlebars.compile) {
throw new Handlebars.Exception("The partial " + name + " could not be compiled when running in runtime-only mode");
} else {
partials[name] = Handlebars.compile(partial);
return partials[name](context, options);
}
if(context.data !== undefined) { return context; }
if(parts.length === 1 && context.data === undefined) {
context.data = context.helpers[parts[0]];
}
return context;
}
};
Handlebars.K = function() { return this; };
Handlebars.template = Handlebars.VM.template;
Handlebars.proxy = function(obj) {
var Proxy = this.K;
Proxy.prototype = obj;
return new Proxy();
};
Handlebars.Runtime = function(context, stack) {
this.stack = stack || [];
this.buffer = "";
this.context = context;
};
Handlebars.Runtime.prototype = {
accept: Handlebars.Visitor.prototype.accept,
ID: function(path) {
return this.context.evaluate(path, this.stack);
},
STRING: function(string) {
return { data: string.string };
},
program: function(program) {
var statements = program.statements;
for(var i=0, l=statements.length; i<l; i++) {
var statement = statements[i];
this[statement.type](statement);
}
return this.buffer;
},
mustache: function(mustache) {
var idObj = this.ID(mustache.id);
var params = mustache.params.slice(0);
var buf;
for(var i=0, l=params.length; i<l; i++) {
var param = params[i];
params[i] = this[param.type](param).data;
}
var data = idObj.data;
var type = Object.prototype.toString.call(data);
var functionType = (type === "[object Function]");
if(!functionType && params.length) {
params = params.slice(0);
params.unshift(data || mustache.id.original);
data = this.context.helpers.helperMissing;
functionType = true;
}
if(functionType) {
buf = data.apply(this.wrapContext(), params);
} else {
buf = data;
}
if(buf && mustache.escaped) { buf = Handlebars.Utils.escapeExpression(buf); }
this.buffer = this.buffer + ((!buf && buf !== 0) ? '' : buf);
},
block: function(block) {
var mustache = block.mustache, data;
var id = mustache.id,
idObj = this.ID(mustache.id),
data = idObj.data;
var result;
if(typeof data === "function") {
params = this.evaluateParams(mustache.params);
} else {
params = [data];
data = this.context.helpers.blockHelperMissing;
}
params.push(this.wrapProgram(block.program));
result = data.apply(this.wrapContext(), params);
this.buffer = this.buffer + ((result === undefined) ? "" : result);
if(block.program.inverse) {
params.pop();
params.push(this.wrapProgram(block.program.inverse));
result = data.not ? data.not.apply(this.wrapContext(), params) : "";
this.buffer = this.buffer + result;
}
},
partial: function(partial) {
var partials = this.context.partials || {};
var id = partial.id.original;
var partialBody = partials[partial.id.original];
var program, context;
if(!partialBody) {
throw new Handlebars.Exception("The partial " + partial.id.original + " does not exist");
}
if(typeof partialBody === "string") {
program = Handlebars.parse(partialBody);
partials[id] = program;
} else {
program = partialBody;
}
if(partial.context) {
context = this.ID(partial.context);
} else {
context = this.context;
}
var runtime = new Handlebars.Runtime(context, this.stack);
this.buffer = this.buffer + runtime.program(program);
},
not: function(context, fn) {
return fn(context);
},
// TODO: Write down the actual spec for inverse sections...
inverse: function(block) {
var mustache = block.mustache,
id = mustache.id,
not;
var idObj = this.ID(id),
data = idObj.data,
isInverse = Handlebars.Utils.isEmpty(data);
var context = this.wrapContext();
if(Object.prototype.toString.call(data) === "[object Function]") {
params = this.evaluateParams(mustache.params);
id = id.parts.join("/");
data = data.apply(context, params);
if(Handlebars.Utils.isEmpty(data)) { isInverse = true; }
if(data.not) { not = data.not; } else { not = this.not; }
} else {
not = this.not;
}
var result = not(context, this.wrapProgram(block.program));
if(result != null) { this.buffer = this.buffer + result; }
return;
},
content: function(content) {
this.buffer += content.string;
},
comment: function() {},
evaluateParams: function(params) {
var ret = [];
for(var i=0, l=params.length; i<l; i++) {
var param = params[i];
ret[i] = this[param.type](param).data;
}
if(ret.length === 0) { ret = [this.wrapContext()]; }
return ret;
},
wrapContext: function() {
var data = this.context.data;
var proxy = Handlebars.proxy(data);
var context = proxy.__context__ = this.context;
var stack = proxy.__stack__ = this.stack.slice(0);
proxy.__get__ = function(path) {
path = new Handlebars.AST.IdNode(path.split("/"));
return context.evaluate(path, stack).data;
};
proxy.isWrappedContext = true;
proxy.__data__ = data;
return proxy;
},
wrapProgram: function(program) {
var currentContext = this.context;
var stack = this.stack.slice(0);
return function(context) {
if(context && context.isWrappedContext) { context = context.__data__; }
stack.push(currentContext);
var newContext = new Handlebars.Context(context, currentContext.helpers, currentContext.partials);
var runtime = new Handlebars.Runtime(newContext, stack);
runtime.program(program);
return runtime.buffer;
};
}
};
// END(BROWSER)
+16 -6
View File
@@ -1,9 +1,16 @@
var Handlebars = require("handlebars");
var Handlebars = require("./base");
// BEGIN(BROWSER)
Handlebars.Exception = function(message) {
this.message = message;
var tmp = Error.prototype.constructor.apply(this, arguments);
for (var p in tmp) {
if (tmp.hasOwnProperty(p)) { this[p] = tmp[p]; }
}
this.message = tmp.message;
};
Handlebars.Exception.prototype = new Error();
// Build out our basic SafeString type
Handlebars.SafeString = function(string) {
@@ -16,14 +23,17 @@ Handlebars.SafeString.prototype.toString = function() {
(function() {
var escape = {
"<": "&lt;",
">": "&gt;"
">": "&gt;",
'"': "&quot;",
"'": "&#x27;",
"`": "&#x60;"
};
var badChars = /&(?!\w+;)|[<>]/g;
var possible = /[&<>]/
var badChars = /&(?!\w+;)|[<>"'`]/g;
var possible = /[&<>"'`]/;
var escapeChar = function(chr) {
return escape[chr] || "&amp;"
return escape[chr] || "&amp;";
};
Handlebars.Utils = {
-659
View File
@@ -1,659 +0,0 @@
var Handlebars = require("handlebars");
// BEGIN(BROWSER)
Handlebars.Compiler = function() {};
Handlebars.JavaScriptCompiler = function() {};
(function(Compiler, JavaScriptCompiler) {
Compiler.OPCODE_MAP = {
appendContent: 1,
getContext: 2,
lookupWithFallback: 3,
lookup: 4,
append: 5,
invokeMustache: 6,
appendEscaped: 7,
pushString: 8,
truthyOrFallback: 9,
functionOrFallback: 10,
invokeProgram: 11,
invokePartial: 12,
push: 13,
invokeInverse: 14
};
Compiler.MULTI_PARAM_OPCODES = {
appendContent: 1,
getContext: 1,
lookupWithFallback: 1,
lookup: 1,
invokeMustache: 2,
pushString: 1,
truthyOrFallback: 1,
functionOrFallback: 1,
invokeProgram: 2,
invokePartial: 1,
push: 1,
invokeInverse: 1
};
Compiler.DISASSEMBLE_MAP = {};
for(var prop in Compiler.OPCODE_MAP) {
var value = Compiler.OPCODE_MAP[prop];
Compiler.DISASSEMBLE_MAP[value] = prop;
}
Compiler.multiParamSize = function(code) {
return Compiler.MULTI_PARAM_OPCODES[Compiler.DISASSEMBLE_MAP[code]];
};
Compiler.prototype = {
disassemble: function() {
var opcodes = this.opcodes, opcode, nextCode;
var out = [], str, name, value;
for(var i=0, l=opcodes.length; i<l; i++) {
opcode = opcodes[i];
if(opcode === 'DECLARE') {
name = opcodes[++i];
value = opcodes[++i];
out.push("DECLARE " + name + " = " + value);
} else {
str = Compiler.DISASSEMBLE_MAP[opcode];
var extraParams = Compiler.multiParamSize(opcode);
var codes = [];
for(var j=0; j<extraParams; j++) {
nextCode = opcodes[++i];
if(typeof nextCode === "string") {
nextCode = "\"" + nextCode.replace("\n", "\\n") + "\"";
}
codes.push(nextCode);
}
str = str + " " + codes.join(" ");
out.push(str);
}
}
return out.join("\n");
},
guid: 0,
compile: function(program) {
this.children = [];
this.depths = {list: []};
return this.program(program);
},
accept: function(node) {
return this[node.type](node);
},
program: function(program) {
var statements = program.statements, statement;
this.opcodes = [];
for(var i=0, l=statements.length; i<l; i++) {
statement = statements[i];
this[statement.type](statement);
}
this.depths.list = this.depths.list.sort(function(a, b) {
return a - b;
});
return this;
},
compileProgram: function(program) {
var result = new Compiler().compile(program);
var guid = this.guid++;
this.usePartial = this.usePartial || result.usePartial;
this.children[guid] = result;
for(var i=0, l=result.depths.list.length; i<l; i++) {
depth = result.depths.list[i];
if(depth < 2) { continue; }
else { this.addDepth(depth - 1); }
}
return guid;
},
block: function(block) {
var mustache = block.mustache;
var params = mustache.params, depth, child, inverse, inverseGuid;
this.pushParams(params);
// ID lookup is now on the stack
this.ID(mustache.id);
var programGuid = this.compileProgram(block.program);
if(block.program.inverse) {
inverseGuid = this.compileProgram(block.program.inverse);
}
if(block.program.inverse) {
this.declare('inverse', inverseGuid);
}
this.opcode('invokeProgram', programGuid, params.length);
this.declare('inverse', null);
this.opcode('append');
},
inverse: function(block) {
this.ID(block.mustache.id);
var programGuid = this.compileProgram(block.program);
this.opcode('invokeInverse', programGuid);
this.opcode('append');
},
partial: function(partial) {
var id = partial.id;
this.usePartial = true;
if(partial.context) {
this.ID(partial.context);
} else {
this.opcode('push', 'context');
}
this.opcode('invokePartial', id.original);
this.opcode('append');
},
content: function(content) {
this.opcode('appendContent', content.string);
},
mustache: function(mustache) {
var params = mustache.params;
this.pushParams(params);
this.ID(mustache.id);
this.opcode('invokeMustache', params.length, mustache.id.original);
if(mustache.escaped) {
this.opcode('appendEscaped');
} else {
this.opcode('append');
}
},
ID: function(id) {
this.addDepth(id.depth);
this.opcode('getContext', id.depth);
this.opcode('lookupWithFallback', id.parts[0] || null);
for(var i=1, l=id.parts.length; i<l; i++) {
this.opcode('lookup', id.parts[i]);
}
},
STRING: function(string) {
this.opcode('pushString', string.string);
},
comment: function() {},
pushParams: function(params) {
var i = params.length, param;
while(i--) {
param = params[i];
this[param.type](param);
}
},
opcode: function(name, val1, val2) {
this.opcodes.push(Compiler.OPCODE_MAP[name]);
if(val1 !== undefined) { this.opcodes.push(val1); }
if(val2 !== undefined) { this.opcodes.push(val2); }
},
declare: function(name, value) {
this.opcodes.push('DECLARE');
this.opcodes.push(name);
this.opcodes.push(value);
},
addDepth: function(depth) {
if(depth === 0) { return; }
if(!this.depths[depth]) {
this.depths[depth] = true;
this.depths.list.push(depth);
}
}
};
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) {
if(JavaScriptCompiler.RESERVED_WORDS[name]) {
return parent + "['" + name + "']";
} else {
return parent + "." + name;
}
},
appendToBuffer: function(string) {
return "buffer = buffer + " + string + ";";
},
initializeBuffer: function() {
return this.quotedString("");
},
// END PUBLIC API
compile: function(environment, data) {
this.environment = environment;
this.data = data;
this.preamble();
this.stackSlot = 0;
this.stackVars = [];
this.registers = {list: []};
this.compileChildren(environment, data);
Handlebars.log(Handlebars.logger.DEBUG, environment.disassemble() + "\n\n");
var opcodes = environment.opcodes, opcode, name, declareName, declareVal;
this.i = 0;
for(l=opcodes.length; this.i<l; this.i++) {
opcode = this.nextOpcode(0);
if(opcode[0] === 'DECLARE') {
this.i = this.i + 2;
this[opcode[1]] = opcode[2];
} else {
this.i = this.i + opcode[1].length;
this[opcode[0]].apply(this, opcode[1]);
}
}
return this.createFunction();
},
nextOpcode: function(n) {
var opcodes = this.environment.opcodes, opcode = opcodes[this.i + n], name, val;
var extraParams, codes;
if(opcode === 'DECLARE') {
name = opcodes[this.i + 1];
val = opcodes[this.i + 2];
return ['DECLARE', name, val];
} else {
name = Compiler.DISASSEMBLE_MAP[opcode];
extraParams = Compiler.multiParamSize(opcode);
codes = [];
for(var j=0; j<extraParams; j++) {
codes.push(opcodes[this.i + j + 1 + n]);
}
return [name, codes];
}
},
eat: function(opcode) {
this.i = this.i + opcode.length;
},
preamble: function() {
var out = [];
out.push("var buffer = " + this.initializeBuffer() + ", currentContext = context");
var copies = "helpers = helpers || Handlebars.helpers;";
if(this.environment.usePartial) { copies = copies + " partials = partials || Handlebars.partials;"; }
out.push(copies);
// 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;
},
createFunction: function() {
var container = {
escapeExpression: Handlebars.Utils.escapeExpression,
invokePartial: Handlebars.VM.invokePartial,
programs: [],
program: function(i, helpers, partials, data) {
var programWrapper = this.programs[i];
if(data) {
return Handlebars.VM.program(this.children[i], helpers, partials, data);
} else if(programWrapper) {
return programWrapper;
} else {
programWrapper = this.programs[i] = Handlebars.VM.program(this.children[i], helpers, partials);
return programWrapper;
}
},
programWithDepth: Handlebars.VM.programWithDepth,
noop: Handlebars.VM.noop
};
var locals = this.stackVars.concat(this.registers.list);
if(locals.length > 0) {
this.source[0] = this.source[0] + ", " + locals.join(", ");
}
this.source[0] = this.source[0] + ";";
this.source.push("return buffer;");
var params = ["Handlebars", "context", "helpers", "partials"];
if(this.data) { params.push("data"); }
for(var i=0, l=this.environment.depths.list.length; i<l; i++) {
params.push("depth" + this.environment.depths.list[i]);
}
if(params.length === 4 && !this.environment.usePartial) { params.pop(); }
params.push(this.source.join("\n"));
var fn = Function.apply(this, params);
fn.displayName = "Handlebars.js";
Handlebars.log(Handlebars.logger.DEBUG, fn.toString() + "\n\n");
container.render = fn;
container.children = this.environment.children;
return function(context, helpers, partials, data, $depth) {
try {
var args = Array.prototype.slice.call(arguments);
args.unshift(Handlebars);
return container.render.apply(container, args);
} catch(e) {
throw e;
}
};
},
appendContent: function(content) {
this.source.push(this.appendToBuffer(this.quotedString(content)));
},
append: function() {
var local = this.popStack();
this.source.push("if(" + local + " || " + local + " === 0) { " + this.appendToBuffer(local) + " }");
},
appendEscaped: function() {
var opcode = this.nextOpcode(1), extra = "";
if(opcode[0] === 'appendContent') {
extra = " + " + this.quotedString(opcode[1][0]);
this.eat(opcode);
}
this.source.push(this.appendToBuffer("this.escapeExpression(" + this.popStack() + ")" + extra));
},
getContext: function(depth) {
if(this.lastContext !== depth) {
this.lastContext = depth;
if(depth === 0) {
this.source.push("currentContext = context;");
} else {
this.source.push("currentContext = depth" + depth + ";");
}
}
},
lookupWithFallback: function(name) {
if(name) {
this.pushStack(this.nameLookup('currentContext', name, 'context'));
var topStack = this.topStack();
this.source.push("if(" + topStack + " === undefined) { " + topStack + " = " + this.nameLookup('helpers', name, 'helper') + "; }");
} else {
this.pushStack("currentContext");
}
},
lookup: function(name) {
var topStack = this.topStack();
this.source.push(topStack + " = " + this.nameLookup(topStack, name, 'context') + ";");
},
pushString: function(string) {
this.pushStack(this.quotedString(string));
},
push: function(name) {
this.pushStack(name);
},
invokeMustache: function(paramSize, original) {
var params = ["context"], fn, slot;
fn = this.popStack();
for(var i=0; i<paramSize; i++) {
params.push(this.popStack());
}
if(this.data) { params.push("data"); }
var paramString = params.join(", ");
var helperMissing = ["context"].concat(this.quotedString(original)).concat(params.slice(1));
slot = this.nextStack();
if(paramSize === 0) {
// TODO: This case is not listed in the mustache spec. Is it important?
this.source.push("if(typeof " + fn + " === 'function') { " + slot + " = " + fn + ".call(" + paramString + "); }");
} else {
this.source.push("if(typeof " + fn + " === 'function') { " + slot + " = " + fn + ".call(" + paramString + "); } else { " + slot + " = helpers.helperMissing.call(" + helperMissing + ") }");
}
},
invokeProgram: function(guid, paramSize) {
var inverse = this.programExpression(this.inverse);
var fn = this.popStack();
var id = fn;
var params = ["context"];
var blockMissingParams = ["context", id];
for(var i=0; i<paramSize; i++) {
var param = this.popStack();
params.push(param);
blockMissingParams.push(param);
}
var mainProgram = this.programExpression(guid);
params.push(mainProgram, inverse);
blockMissingParams.push(mainProgram, inverse);
if(this.data) {
params.push("data");
blockMissingParams.push("data");
}
var nextStack = this.nextStack();
this.source.push("if(typeof " + id + " === 'function') { " + nextStack + " = " + id + ".call(" + params.join(", ") + "); }");
this.source.push("else { " + nextStack + " = helpers.blockHelperMissing.call(" + blockMissingParams.join(", ") + "); }");
},
invokeInverse: function(guid) {
var program = this.programExpression(guid);
var blockMissingParams = ["context", this.topStack(), "this.noop", program];
this.pushStack("helpers.blockHelperMissing.call(" + blockMissingParams.join(", ") + ")");
},
invokePartial: function(context) {
this.pushStack("this.invokePartial(" + this.nameLookup('partials', context, 'partial') + ", '" + context + "', " + this.popStack() + ", helpers, partials);");
},
// HELPERS
compiler: JavaScriptCompiler,
compileChildren: function(environment, data) {
var children = environment.children, child, compiler;
var compiled = [];
for(var i=0, l=children.length; i<l; i++) {
child = children[i];
compiler = new this.compiler();
compiled[i] = compiler.compile(child, data);
}
environment.rawChildren = children;
environment.children = compiled;
},
programExpression: function(guid) {
if(guid == null) { return "this.noop"; }
var programParams = [guid, "helpers", "partials"];
var depths = this.environment.rawChildren[guid].depths.list;
if(this.data) { programParams.push("data"); }
for(var i=0, l = depths.length; i<l; i++) {
depth = depths[i];
if(depth === 1) { programParams.push("context"); }
else { programParams.push("depth" + (depth - 1)); }
}
if(!this.environment.usePartial) {
if(programParams[3]) {
programParams[2] = "null";
} else {
programParams.pop();
}
}
if(depths.length === 0) {
return "this.program(" + programParams.join(", ") + ")";
} else {
programParams[0] = "this.children[" + guid + "]";
return "this.programWithDepth(" + programParams.join(", ") + ")";
}
},
register: function(name, val) {
this.useRegister(name);
this.source.push(name + " = " + val + ";");
},
useRegister: function(name) {
if(!this.registers[name]) {
this.registers[name] = true;
this.registers.list.push(name);
}
},
pushStack: function(item) {
this.source.push(this.nextStack() + " = " + item + ";");
return "stack" + this.stackSlot;
},
nextStack: function() {
this.stackSlot++;
if(this.stackSlot > this.stackVars.length) { this.stackVars.push("stack" + this.stackSlot); }
return "stack" + this.stackSlot;
},
popStack: function() {
return "stack" + this.stackSlot--;
},
topStack: function() {
return "stack" + this.stackSlot;
},
quotedString: function(str) {
return '"' + str
.replace(/\\/, '\\\\')
.replace(/"/g, '\\"')
.replace(/\n/g, '\\n')
.replace(/\r/g, '\\r') + '"';
}
};
var reservedWords = ("break case catch continue default delete do else finally " +
"for function if in instanceof new return switch this throw " +
"try typeof var void while with null true false").split(" ");
compilerWords = JavaScriptCompiler.RESERVED_WORDS = {};
for(var i=0, l=reservedWords.length; i<l; i++) {
compilerWords[reservedWords[i]] = true;
}
})(Handlebars.Compiler, Handlebars.JavaScriptCompiler);
Handlebars.VM = {
programWithDepth: function(fn) {
var args = Array.prototype.slice.call(arguments, 1);
return function(context) {
return fn.apply(this, [context].concat(args));
};
},
program: function(fn, helpers, partials, data) {
return function(context) {
return fn(context, helpers, partials, data);
};
},
noop: function() { return ""; },
compile: function(string, data) {
var ast = Handlebars.parse(string);
var environment = new Handlebars.Compiler().compile(ast);
return new Handlebars.JavaScriptCompiler().compile(environment, data);
},
invokePartial: function(partial, name, context, helpers, partials) {
if(partial === undefined) {
throw new Handlebars.Exception("The partial " + name + " could not be found");
} else if(partial instanceof Function) {
return partial(context, helpers, partials);
} else {
partials[name] = Handlebars.VM.compile(partial);
return partials[name](context, helpers, partials);
}
}
};
Handlebars.compile = Handlebars.VM.compile;
// END(BROWSER)
+25
View File
@@ -0,0 +1,25 @@
{
"name": "handlebars",
"description": "Extension of the Mustache logicless template language",
"version": "1.0.5beta",
"homepage": "http://www.handlebarsjs.com/",
"keywords": [
"handlebars mustache template html"
],
"repository": {
"type": "git",
"url": "git://github.com/kpdecker/handlebars.js.git"
},
"engines": {
"node": ">=0.4.7"
},
"dependencies": {
"optimist": "~0.3",
"uglify-js": "~1.2"
},
"devDependencies": {},
"main": "lib/handlebars.js",
"bin": {
"handlebars": "bin/handlebars"
}
}
+2 -1
View File
@@ -64,7 +64,8 @@ Module.new do
result = js_context.eval("$$RSPEC1$$ == $$RSPEC2$$")
message ||= "#{first} did not == #{second}"
additional_message = "#{first.inspect} did not == #{second.inspect}"
message = message ? "#{message} (#{additional_message})" : additional_message
unless result
backtrace = js_backtrace(js_context)
+87 -4
View File
@@ -3,6 +3,10 @@ require "spec_helper"
describe "Parser" do
let(:handlebars) { @context["Handlebars"] }
before(:all) do
@compiles = true
end
def program(&block)
ASTBuilder.build do
program do
@@ -65,8 +69,9 @@ describe "Parser" do
with_padding { yield }
end
def mustache(id, *params)
pad("{{ #{id} [#{params.join(", ")}] }}")
def mustache(id, params = [], hash = nil)
hash = " #{hash}" if hash
pad("{{ #{id} [#{params.join(", ")}]#{hash} }}")
end
def partial(id, context = nil)
@@ -79,6 +84,10 @@ describe "Parser" do
pad("{{! '#{comment}' }}")
end
def multiline_comment(comment)
pad("{{! '\n#{comment}\n' }}")
end
def content(string)
pad("CONTENT[ '#{string}' ]")
end
@@ -87,6 +96,18 @@ describe "Parser" do
string.inspect
end
def integer(string)
"INTEGER{#{string}}"
end
def boolean(string)
"BOOLEAN{#{string}}"
end
def hash(*pairs)
"HASH{" + pairs.map {|k,v| "#{k}=#{v}" }.join(", ") + "}"
end
def id(id)
"ID:#{id}"
end
@@ -108,12 +129,67 @@ describe "Parser" do
ast_for("{{this/foo}}").should == program { mustache id("foo") }
end
it "parses mustaches with - in a path" do
ast_for("{{foo-bar}}").should == program { mustache id("foo-bar") }
end
it "parses mustaches with parameters" do
ast_for("{{foo bar}}").should == program { mustache id("foo"), id("bar") }
ast_for("{{foo bar}}").should == program { mustache id("foo"), [id("bar")] }
end
it "parses mustaches with hash arguments" do
ast_for("{{foo bar=baz}}").should == program do
mustache id("foo"), [], hash(["bar", id("baz")])
end
ast_for("{{foo bar=1}}").should == program do
mustache id("foo"), [], hash(["bar", integer("1")])
end
ast_for("{{foo bar=true}}").should == program do
mustache id("foo"), [], hash(["bar", boolean("true")])
end
ast_for("{{foo bar=false}}").should == program do
mustache id("foo"), [], hash(["bar", boolean("false")])
end
ast_for("{{foo bar=baz bat=bam}}").should == program do
mustache id("foo"), [], hash(["bar", "ID:baz"], ["bat", "ID:bam"])
end
ast_for("{{foo bar=baz bat=\"bam\"}}").should == program do
mustache id("foo"), [], hash(["bar", "ID:baz"], ["bat", "\"bam\""])
end
ast_for("{{foo omg bar=baz bat=\"bam\"}}").should == program do
mustache id("foo"), [id("omg")], hash(["bar", id("baz")], ["bat", string("bam")])
end
ast_for("{{foo omg bar=baz bat=\"bam\" baz=1}}").should == program do
mustache id("foo"), [id("omg")], hash(["bar", id("baz")], ["bat", string("bam")], ["baz", integer("1")])
end
ast_for("{{foo omg bar=baz bat=\"bam\" baz=true}}").should == program do
mustache id("foo"), [id("omg")], hash(["bar", id("baz")], ["bat", string("bam")], ["baz", boolean("true")])
end
ast_for("{{foo omg bar=baz bat=\"bam\" baz=false}}").should == program do
mustache id("foo"), [id("omg")], hash(["bar", id("baz")], ["bat", string("bam")], ["baz", boolean("false")])
end
end
it "parses mustaches with string parameters" do
ast_for("{{foo bar \"baz\" }}").should == program { mustache id("foo"), id("bar"), string("baz")}
ast_for("{{foo bar \"baz\" }}").should == program { mustache id("foo"), [id("bar"), string("baz")] }
end
it "parses mustaches with INTEGER parameters" do
ast_for("{{foo 1}}").should == program { mustache id("foo"), [integer("1")] }
end
it "parses mustaches with BOOLEAN parameters" do
ast_for("{{foo true}}").should == program { mustache id("foo"), [boolean("true")] }
ast_for("{{foo false}}").should == program { mustache id("foo"), [boolean("false")] }
end
it "parses contents followed by a mustache" do
@@ -137,6 +213,12 @@ describe "Parser" do
end
end
it "parses a multi-line comment" do
ast_for("{{!\nthis is a multi-line comment\n}}").should == program do
multiline_comment "this is a multi-line comment"
end
end
it "parses an inverse section" do
ast_for("{{#foo}} bar {{^}} baz {{/foo}}").should == program do
block do
@@ -168,6 +250,7 @@ describe "Parser" do
it "raises if there's a Parse error" do
lambda { ast_for("{{foo}") }.should raise_error(V8::JSError, /Parse error on line 1/)
lambda { ast_for("{{foo &}}")}.should raise_error(V8::JSError, /Parse error on line 1/)
lambda { ast_for("{{#goodbyes}}{{/hellos}}") }.should raise_error(V8::JSError, /goodbyes doesn't match hellos/)
end
it "knows how to report the correct line number in errors" do
+582 -118
View File
@@ -6,19 +6,28 @@ Handlebars.registerHelper('helperMissing', function(helper, context) {
}
});
var shouldCompileTo = function(string, hash, expected, message) {
var template = Handlebars.compile(string);
if(Object.prototype.toString.call(hash) === "[object Array]") {
if(hash[1]) {
var shouldCompileTo = function(string, hashOrArray, expected, message) {
shouldCompileToWithPartials(string, hashOrArray, false, expected, message);
};
var shouldCompileToWithPartials = function(string, hashOrArray, partials, expected, message) {
var template = CompilerContext[partials ? 'compileWithPartial' : 'compile'](string), ary;
if(Object.prototype.toString.call(hashOrArray) === "[object Array]") {
helpers = hashOrArray[1];
if(helpers) {
for(var prop in Handlebars.helpers) {
hash[1][prop] = Handlebars.helpers[prop];
helpers[prop] = Handlebars.helpers[prop];
}
}
ary = [];
ary.push(hashOrArray[0]);
ary.push({ helpers: hashOrArray[1], partials: hashOrArray[2] });
} else {
hash = [hash];
ary = [hashOrArray];
}
result = template.apply(this, hash)
result = template.apply(this, ary);
equal(result, expected, "'" + expected + "' should === '" + result + "': " + message);
};
@@ -72,7 +81,8 @@ test("newlines", function() {
test("escaping text", function() {
shouldCompileTo("Awesome's", {}, "Awesome's", "text is escaped so that it doesn't get caught on single quotes");
shouldCompileTo("Awesome\\", {}, "Awesome\\", "text is escaped so that the closing quote can't be ignored");
shouldCompileTo("Awesome\\ foo", {}, "Awesome\\ foo", "text is escaped so that it doesn't mess up backslashes");
shouldCompileTo("Awesome\\\\ foo", {}, "Awesome\\\\ foo", "text is escaped so that it doesn't mess up backslashes");
shouldCompileTo("Awesome {{foo}}", {foo: '\\'}, "Awesome \\", "text is escaped so that it doesn't mess up backslashes");
shouldCompileTo(' " " ', {}, ' " " ', "double quotes never produce invalid javascript");
});
@@ -80,12 +90,12 @@ test("escaping expressions", function() {
shouldCompileTo("{{{awesome}}}", {awesome: "&\"\\<>"}, '&\"\\<>',
"expressions with 3 handlebars aren't escaped");
shouldCompileTo("{{awesome}}", {awesome: "&\"\\<>"}, '&amp;\"\\&lt;&gt;',
"by default expressions should be escaped");
shouldCompileTo("{{&awesome}}", {awesome: "&\"\\<>"}, '&\"\\<>',
"expressions with {{& handlebars aren't escaped");
shouldCompileTo("{{awesome}}", {awesome: "&\"'`\\<>"}, '&amp;&quot;&#x27;&#x60;\\&lt;&gt;',
"by default expressions should be escaped");
});
test("functions returning safestrings shouldn't be escaped", function() {
@@ -106,6 +116,10 @@ test("functions with context argument", function() {
"Frank", "functions are called with context arguments");
});
test("paths with hyphens", function() {
shouldCompileTo("{{foo-bar}}", {"foo-bar": "baz"}, "baz", "Paths can contain hyphens (-)");
});
test("nested paths", function() {
shouldCompileTo("Goodbye {{alan/expression}} world!", {alan: {expression: "beautiful"}},
"Goodbye beautiful world!", "Nested paths access nested objects");
@@ -116,11 +130,16 @@ test("nested paths with empty string value", function() {
"Goodbye world!", "Nested paths access nested objects with empty string");
});
test("literal paths", function() {
shouldCompileTo("Goodbye {{[@alan]/expression}} world!", {"@alan": {expression: "beautiful"}},
"Goodbye beautiful world!", "Literal paths can be used");
});
test("--- TODO --- bad idea nested paths", function() {
return;
var hash = {goodbyes: [{text: "goodbye"}, {text: "Goodbye"}, {text: "GOODBYE"}], world: "world"};
shouldThrow(function() {
Handlebars.compile("{{#goodbyes}}{{../name/../name}}{{/goodbyes}}")(hash);
CompilerContext.compile("{{#goodbyes}}{{../name/../name}}{{/goodbyes}}")(hash);
}, Handlebars.Exception,
"Cannot jump (..) into previous context after moving into a context.");
@@ -128,7 +147,7 @@ test("--- TODO --- bad idea nested paths", function() {
shouldCompileTo(string, hash, "world world world ", "Same context (.) is ignored in paths");
});
test("that current context path ({{.}}) doesn't hit fallback", function() {
test("that current context path ({{.}}) doesn't hit helpers", function() {
shouldCompileTo("test: {{.}}", [null, {helper: "awesome"}], "test: ");
});
@@ -168,12 +187,6 @@ test("inverted section with empty set", function() {
shouldCompileTo(string, hash, "Right On!", "Inverted section rendered when value is empty set.");
});
test("inverted section using result of function call", function() {
var string = "{{goodbyes}}{{^goodbyes}}Right On!{{/goodbyes}}";
var hash = {goodbyes: function() { return false; }}
shouldCompileTo(string, hash, "Right On!", "Inverted section rendered when result of function in expression is false.");
});
module("blocks");
test("array", function() {
@@ -197,14 +210,6 @@ test("empty block", function() {
"Arrays ignore the contents when empty");
});
test("incorrectly matched blocks", function() {
var string = "{{#goodbyes}}{{/hellos}}";
shouldThrow(function() {
Handlebars.compile(string);
}, Handlebars.Exception, "Incorrectly matched blocks return an exception at compile time.");
});
test("nested iteration", function() {
});
@@ -220,16 +225,16 @@ test("block with complex lookup", function() {
test("helper with complex lookup", function() {
var string = "{{#goodbyes}}{{{link ../prefix}}}{{/goodbyes}}"
var hash = {prefix: "/root", goodbyes: [{text: "Goodbye", url: "goodbye"}]};
var fallback = {link: function(prefix) {
var helpers = {link: function(prefix) {
return "<a href='" + prefix + "/" + this.url + "'>" + this.text + "</a>"
}};
shouldCompileTo(string, [hash, fallback], "<a href='/root/goodbye'>Goodbye</a>")
shouldCompileTo(string, [hash, helpers], "<a href='/root/goodbye'>Goodbye</a>")
});
test("helper block with complex lookup expression", function() {
var string = "{{#goodbyes}}{{../name}}{{/goodbyes}}"
var hash = {name: "Alan"};
var fallback = {goodbyes: function(fn) {
var helpers = {goodbyes: function(fn) {
var out = "";
var byes = ["Goodbye", "goodbye", "GOODBYE"];
for (var i = 0,j = byes.length; i < j; i++) {
@@ -237,16 +242,25 @@ test("helper block with complex lookup expression", function() {
}
return out;
}};
shouldCompileTo(string, [hash, fallback], "Goodbye Alan! goodbye Alan! GOODBYE Alan! ");
shouldCompileTo(string, [hash, helpers], "Goodbye Alan! goodbye Alan! GOODBYE Alan! ");
});
test("helper with complex lookup and nested template", function() {
var string = "{{#goodbyes}}{{#link ../prefix}}{{text}}{{/link}}{{/goodbyes}}";
var hash = {prefix: '/root', goodbyes: [{text: "Goodbye", url: "goodbye"}]};
var fallback = {link: function (prefix, fn) {
var helpers = {link: function (prefix, fn) {
return "<a href='" + prefix + "/" + this.url + "'>" + fn(this) + "</a>";
}};
shouldCompileTo(string, [hash, fallback], "<a href='/root/goodbye'>Goodbye</a>")
shouldCompileToWithPartials(string, [hash, helpers], false, "<a href='/root/goodbye'>Goodbye</a>");
});
test("helper with complex lookup and nested template in VM+Compiler", function() {
var string = "{{#goodbyes}}{{#link ../prefix}}{{text}}{{/link}}{{/goodbyes}}";
var hash = {prefix: '/root', goodbyes: [{text: "Goodbye", url: "goodbye"}]};
var helpers = {link: function (prefix, fn) {
return "<a href='" + prefix + "/" + this.url + "'>" + fn(this) + "</a>";
}};
shouldCompileToWithPartials(string, [hash, helpers], true, "<a href='/root/goodbye'>Goodbye</a>");
});
test("block with deep nested complex lookup", function() {
@@ -258,18 +272,18 @@ test("block with deep nested complex lookup", function() {
test("block helper", function() {
var string = "{{#goodbyes}}{{text}}! {{/goodbyes}}cruel {{world}}!";
var template = Handlebars.compile(string);
var template = CompilerContext.compile(string);
result = template({goodbyes: function(fn) { return fn({text: "GOODBYE"}); }, world: "world"});
equal(result, "GOODBYE! cruel world!");
result = template({world: "world"}, { helpers: {goodbyes: function(fn) { return fn({text: "GOODBYE"}); }}});
equal(result, "GOODBYE! cruel world!", "Block helper executed");
});
test("block helper staying in the same context", function() {
var string = "{{#form}}<p>{{name}}</p>{{/form}}"
var template = Handlebars.compile(string);
var template = CompilerContext.compile(string);
result = template({form: function(fn) { return "<form>" + fn(this) + "</form>" }, name: "Yehuda"});
equal(result, "<form><p>Yehuda</p></form>");
result = template({name: "Yehuda"}, {helpers: {form: function(fn) { return "<form>" + fn(this) + "</form>" } }});
equal(result, "<form><p>Yehuda</p></form>", "Block helper executed with current context");
});
test("block helper should have context in this", function() {
@@ -291,31 +305,33 @@ test("block helper for undefined value", function() {
test("block helper passing a new context", function() {
var string = "{{#form yehuda}}<p>{{name}}</p>{{/form}}"
var template = Handlebars.compile(string);
var template = CompilerContext.compile(string);
result = template({form: function(context, fn) { return "<form>" + fn(context) + "</form>" }, yehuda: {name: "Yehuda"}});
equal(result, "<form><p>Yehuda</p></form>");
result = template({yehuda: {name: "Yehuda"}}, { helpers: {form: function(context, fn) { return "<form>" + fn(context) + "</form>" }}});
equal(result, "<form><p>Yehuda</p></form>", "Context variable resolved");
});
test("block helper passing a complex path context", function() {
var string = "{{#form yehuda/cat}}<p>{{name}}</p>{{/form}}"
var template = Handlebars.compile(string);
var template = CompilerContext.compile(string);
result = template({form: function(context, fn) { return "<form>" + fn(context) + "</form>" }, yehuda: {name: "Yehuda", cat: {name: "Harold"}}});
equal(result, "<form><p>Harold</p></form>");
result = template({yehuda: {name: "Yehuda", cat: {name: "Harold"}}}, { helpers: {form: function(context, fn) { return "<form>" + fn(context) + "</form>" }}});
equal(result, "<form><p>Harold</p></form>", "Complex path variable resolved");
});
test("nested block helpers", function() {
var string = "{{#form yehuda}}<p>{{name}}</p>{{#link}}Hello{{/link}}{{/form}}"
var template = Handlebars.compile(string);
var template = CompilerContext.compile(string);
result = template({
form: function(context, fn) { return "<form>" + fn(context) + "</form>" },
yehuda: {name: "Yehuda",
link: function(fn) { return "<a href='" + this.name + "'>" + fn(this) + "</a>"; }
}
yehuda: {name: "Yehuda" }
}, {
helpers: {
link: function(fn) { return "<a href='" + this.name + "'>" + fn(this) + "</a>" },
form: function(context, fn) { return "<form>" + fn(context) + "</form>" }
}
});
equal(result, "<form><p>Yehuda</p><a href='Yehuda'>Hello</a></form>");
equal(result, "<form><p>Yehuda</p><a href='Yehuda'>Hello</a></form>", "Both blocks executed");
});
test("block inverted sections", function() {
@@ -330,25 +346,24 @@ test("block inverted sections with empty arrays", function() {
test("block helper inverted sections", function() {
var string = "{{#list people}}{{name}}{{^}}<em>Nobody's here</em>{{/list}}"
var list = function(context, fn, inverse) {
var list = function(context, options) {
if (context.length > 0) {
var out = "<ul>";
for(var i = 0,j=context.length; i < j; i++) {
out += "<li>";
out += fn(context[i]);
out += options.fn(context[i]);
out += "</li>";
}
out += "</ul>";
return out;
} else {
return "<p>" + inverse(this) + "</p>";
return "<p>" + options.inverse(this) + "</p>";
}
};
var hash = {list: list, people: [{name: "Alan"}, {name: "Yehuda"}]};
var empty = {list: list, people: []};
var hash = {people: [{name: "Alan"}, {name: "Yehuda"}]};
var empty = {people: []};
var rootMessage = {
list: function(context, fn, inverse) { if(context.length === 0) { return "<p>" + inverse(this) + "</p>"; } },
people: [],
message: "Nobody's here"
}
@@ -357,26 +372,26 @@ test("block helper inverted sections", function() {
// the meaning here may be kind of hard to catch, but list.not is always called,
// so we should see the output of both
shouldCompileTo(string, hash, "<ul><li>Alan</li><li>Yehuda</li></ul>", "an inverse wrapper is passed in as a new context");
shouldCompileTo(string, empty, "<p><em>Nobody's here</em></p>", "an inverse wrapper can be optionally called");
shouldCompileTo(messageString, rootMessage, "<p>Nobody's here</p>", "the context of an inverse is the parent of the block");
shouldCompileTo(string, [hash, { list: list }], "<ul><li>Alan</li><li>Yehuda</li></ul>", "an inverse wrapper is passed in as a new context");
shouldCompileTo(string, [empty, { list: list }], "<p><em>Nobody's here</em></p>", "an inverse wrapper can be optionally called");
shouldCompileTo(messageString, [rootMessage, { list: list }], "<p>Nobody&#x27;s here</p>", "the context of an inverse is the parent of the block");
});
module("fallback hash");
module("helpers hash");
test("providing a fallback hash", function() {
test("providing a helpers hash", function() {
shouldCompileTo("Goodbye {{cruel}} {{world}}!", [{cruel: "cruel"}, {world: "world"}], "Goodbye cruel world!",
"Fallback hash is available");
"helpers hash is available");
shouldCompileTo("Goodbye {{#iter}}{{cruel}} {{world}}{{/iter}}!", [{iter: [{cruel: "cruel"}]}, {world: "world"}],
"Goodbye cruel world!", "Fallback hash is available inside other blocks");
"Goodbye cruel world!", "helpers hash is available inside other blocks");
});
test("in cases of conflict, the explicit hash wins", function() {
});
test("the fallback hash is available is nested contexts", function() {
test("the helpers hash is available is nested contexts", function() {
});
@@ -386,7 +401,7 @@ test("basic partials", function() {
var string = "Dudes: {{#dudes}}{{> dude}}{{/dudes}}";
var partial = "{{name}} ({{url}}) ";
var hash = {dudes: [{name: "Yehuda", url: "http://yehuda"}, {name: "Alan", url: "http://alan"}]};
shouldCompileTo(string, [hash, {}, {dude: partial}], "Dudes: Yehuda (http://yehuda) Alan (http://alan) ",
shouldCompileToWithPartials(string, [hash, {}, {dude: partial}], true, "Dudes: Yehuda (http://yehuda) Alan (http://alan) ",
"Basic partials output based on current context.");
});
@@ -394,7 +409,7 @@ test("partials with context", function() {
var string = "Dudes: {{>dude dudes}}";
var partial = "{{#this}}{{name}} ({{url}}) {{/this}}";
var hash = {dudes: [{name: "Yehuda", url: "http://yehuda"}, {name: "Alan", url: "http://alan"}]};
shouldCompileTo(string, [hash, {}, {dude: partial}], "Dudes: Yehuda (http://yehuda) Alan (http://alan) ",
shouldCompileToWithPartials(string, [hash, {}, {dude: partial}], true, "Dudes: Yehuda (http://yehuda) Alan (http://alan) ",
"Partials can be passed a context");
});
@@ -403,58 +418,83 @@ test("partial in a partial", function() {
var dude = "{{name}} {{> url}} ";
var url = "<a href='{{url}}'>{{url}}</a>";
var hash = {dudes: [{name: "Yehuda", url: "http://yehuda"}, {name: "Alan", url: "http://alan"}]};
shouldCompileTo(string, [hash, {}, {dude: dude, url: url}], "Dudes: Yehuda <a href='http://yehuda'>http://yehuda</a> Alan <a href='http://alan'>http://alan</a> ", "Partials are rendered inside of other partials");
shouldCompileToWithPartials(string, [hash, {}, {dude: dude, url: url}], true, "Dudes: Yehuda <a href='http://yehuda'>http://yehuda</a> Alan <a href='http://alan'>http://alan</a> ", "Partials are rendered inside of other partials");
});
test("rendering undefined partial throws an exception", function() {
shouldThrow(function() {
var template = Handlebars.compile("{{> whatever}}");
var template = CompilerContext.compile("{{> whatever}}");
template();
}, Handlebars.Exception, "Should throw exception");
});
test("rendering template partial in vm mode throws an exception", function() {
shouldThrow(function() {
var template = CompilerContext.compile("{{> whatever}}");
var string = "Dudes: {{>dude}} {{another_dude}}";
var dude = "{{name}}";
var hash = {name:"Jeepers", another_dude:"Creepers"};
template();
}, Handlebars.Exception, "Should throw exception");
});
test("rendering function partial in vm mode", function() {
var string = "Dudes: {{#dudes}}{{> dude}}{{/dudes}}";
var partial = function(context) {
return context.name + ' (' + context.url + ') ';
};
var hash = {dudes: [{name: "Yehuda", url: "http://yehuda"}, {name: "Alan", url: "http://alan"}]};
shouldCompileTo(string, [hash, {}, {dude: partial}], "Dudes: Yehuda (http://yehuda) Alan (http://alan) ",
"Function partials output based in VM.");
});
test("GH-14: a partial preceding a selector", function() {
var string = "Dudes: {{>dude}} {{another_dude}}";
var dude = "{{name}}";
var hash = {name:"Jeepers", another_dude:"Creepers"};
shouldCompileTo(string, [hash, {}, {dude:dude}], "Dudes: Jeepers Creepers", "Regular selectors can follow a partial");
shouldCompileToWithPartials(string, [hash, {}, {dude:dude}], true, "Dudes: Jeepers Creepers", "Regular selectors can follow a partial");
});
test("Partial containing complex expression", function() {
var template = "Dudes: {{#dudes}}{{> dude}} {{/dudes}}";
var dude = "{{../salutation}} {{name}}";
var hash = {salutation: "Mr.", dudes: [{name: "Yehuda"}, {name: "Alan"}]};
shouldCompileTo(template, [hash, {}, {dude: dude}], "Dudes: Mr. Yehuda Mr. Alan ");
test("Partials with literal paths", function() {
var string = "Dudes: {{> [dude]}}";
var dude = "{{name}}";
var hash = {name:"Jeepers", another_dude:"Creepers"};
shouldCompileToWithPartials(string, [hash, {}, {dude:dude}], true, "Dudes: Jeepers", "Partials can use literal paths");
});
module("String literal parameters");
test("simple literals work", function() {
var string = 'Message: {{hello "world"}}';
var string = 'Message: {{hello "world" 12 true false}}';
var hash = {};
var fallback = {hello: function(param) { return "Hello " + param; }}
shouldCompileTo(string, [hash, fallback], "Message: Hello world", "template with a simple String literal");
var helpers = {hello: function(param, times, bool1, bool2) {
if(typeof times !== 'number') { times = "NaN"; }
if(typeof bool1 !== 'boolean') { bool1 = "NaB"; }
if(typeof bool2 !== 'boolean') { bool2 = "NaB"; }
return "Hello " + param + " " + times + " times: " + bool1 + " " + bool2;
}}
shouldCompileTo(string, [hash, helpers], "Message: Hello world 12 times: true false", "template with a simple String literal");
});
test("using a quote in the middle of a parameter raises an error", function() {
shouldThrow(function() {
var string = 'Message: {{hello wo"rld"}}';
Handlebars.compile(string);
CompilerContext.compile(string);
}, Error, "should throw exception");
});
test("escaping a String is possible", function(){
var string = 'Message: {{hello "\\"world\\""}}';
var string = 'Message: {{{hello "\\"world\\""}}}';
var hash = {}
var fallback = {hello: function(param) { return "Hello " + param; }}
shouldCompileTo(string, [hash, fallback], "Message: Hello \"world\"", "template with an escaped String literal");
var helpers = {hello: function(param) { return "Hello " + param; }}
shouldCompileTo(string, [hash, helpers], "Message: Hello \"world\"", "template with an escaped String literal");
});
test("it works with ' marks", function() {
var string = 'Message: {{hello "Alan\'s world"}}';
var string = 'Message: {{{hello "Alan\'s world"}}}';
var hash = {}
var fallback = {hello: function(param) { return "Hello " + param; }}
shouldCompileTo(string, [hash, fallback], "Message: Hello Alan's world", "template with a ' mark");
var helpers = {hello: function(param) { return "Hello " + param; }}
shouldCompileTo(string, [hash, helpers], "Message: Hello Alan's world", "template with a ' mark");
});
module("multiple parameters");
@@ -462,17 +502,17 @@ module("multiple parameters");
test("simple multi-params work", function() {
var string = 'Message: {{goodbye cruel world}}';
var hash = {cruel: "cruel", world: "world"}
var fallback = {goodbye: function(cruel, world) { return "Goodbye " + cruel + " " + world; }}
shouldCompileTo(string, [hash, fallback], "Message: Goodbye cruel world", "regular helpers with multiple params");
var helpers = {goodbye: function(cruel, world) { return "Goodbye " + cruel + " " + world; }}
shouldCompileTo(string, [hash, helpers], "Message: Goodbye cruel world", "regular helpers with multiple params");
});
test("block multi-params work", function() {
var string = 'Message: {{#goodbye cruel world}}{{greeting}} {{adj}} {{noun}}{{/goodbye}}';
var hash = {cruel: "cruel", world: "world"}
var fallback = {goodbye: function(cruel, world, fn) {
var helpers = {goodbye: function(cruel, world, fn) {
return fn({greeting: "Goodbye", adj: cruel, noun: world});
}}
shouldCompileTo(string, [hash, fallback], "Message: Goodbye cruel world", "block helpers with multiple params");
shouldCompileTo(string, [hash, helpers], "Message: Goodbye cruel world", "block helpers with multiple params");
})
module("safestring");
@@ -492,7 +532,59 @@ test("if a context is not found, helperMissing is used", function() {
shouldCompileTo(string, context, "Hello <a>world</a>")
});
module("built-in helpers");
module("knownHelpers");
test("Known helper should render helper", function() {
var template = CompilerContext.compile("{{hello}}", {knownHelpers: {"hello" : true}})
var result = template({}, {helpers: {hello: function() { return "foo"; }}});
equal(result, "foo", "'foo' should === '" + result);
});
test("Unknown helper in knownHelpers only mode should be passed as undefined", function() {
var template = CompilerContext.compile("{{typeof hello}}", {knownHelpers: {'typeof': true}, knownHelpersOnly: true})
var result = template({}, {helpers: {'typeof': function(arg) { return typeof arg; }, hello: function() { return "foo"; }}});
equal(result, "undefined", "'undefined' should === '" + result);
});
test("Builtin helpers available in knownHelpers only mode", function() {
var template = CompilerContext.compile("{{#unless foo}}bar{{/unless}}", {knownHelpersOnly: true})
var result = template({});
equal(result, "bar", "'bar' should === '" + result);
});
test("Field lookup works in knownHelpers only mode", function() {
var template = CompilerContext.compile("{{foo}}", {knownHelpersOnly: true})
var result = template({foo: 'bar'});
equal(result, "bar", "'bar' should === '" + result);
});
test("Conditional blocks work in knownHelpers only mode", function() {
var template = CompilerContext.compile("{{#foo}}bar{{/foo}}", {knownHelpersOnly: true})
var result = template({foo: 'baz'});
equal(result, "bar", "'bar' should === '" + result);
});
test("Invert blocks work in knownHelpers only mode", function() {
var template = CompilerContext.compile("{{^foo}}bar{{/foo}}", {knownHelpersOnly: true})
var result = template({foo: false});
equal(result, "bar", "'bar' should === '" + result);
});
module("blockHelperMissing");
test("lambdas are resolved by blockHelperMissing, not handlebars proper", function() {
var string = "{{#truthy}}yep{{/truthy}}";
var data = { truthy: function() { return true; } };
shouldCompileTo(string, data, "yep");
});
var teardown;
module("built-in helpers", {
setup: function(){ teardown = null; },
teardown: function(){ if (teardown) { teardown(); } }
});
test("with", function() {
var string = "{{#with person}}{{first}} {{last}}{{/with}}";
@@ -509,6 +601,22 @@ test("if", function() {
"if with boolean argument does not show the contents when false");
shouldCompileTo(string, {world: "world"}, "cruel world!",
"if with undefined does not show the contents");
shouldCompileTo(string, {goodbye: ['foo'], world: "world"}, "GOODBYE cruel world!",
"if with non-empty array shows the contents");
shouldCompileTo(string, {goodbye: [], world: "world"}, "cruel world!",
"if with empty array does not show the contents");
});
test("if with function argument", function() {
var string = "{{#if goodbye}}GOODBYE {{/if}}cruel {{world}}!";
shouldCompileTo(string, {goodbye: function() {return true}, world: "world"}, "GOODBYE cruel world!",
"if with function shows the contents when function returns true");
shouldCompileTo(string, {goodbye: function() {return this.world}, world: "world"}, "GOODBYE cruel world!",
"if with function shows the contents when function returns string");
shouldCompileTo(string, {goodbye: function() {return false}, world: "world"}, "cruel world!",
"if with function does not show the contents when returns false");
shouldCompileTo(string, {goodbye: function() {return this.foo}, world: "world"}, "cruel world!",
"if with function does not show the contents when returns undefined");
});
test("each", function() {
@@ -520,84 +628,440 @@ test("each", function() {
"each with array argument ignores the contents when empty");
});
test("log", function() {
var string = "{{log blah}}"
var hash = { blah: "whee" };
var logArg;
var originalLog = Handlebars.log;
Handlebars.log = function(arg){ logArg = arg; }
teardown = function(){ Handlebars.log = originalLog; }
shouldCompileTo(string, hash, "", "log should not display");
equals("whee", logArg, "should call log with 'whee'");
});
test("overriding property lookup", function() {
});
test("passing in data to a compiled function that expects data - works with helpers", function() {
var template = Handlebars.compile("{{hello}}", true);
var template = CompilerContext.compile("{{hello}}", {data: true});
var helpers = {
hello: function(data) {
return data.adjective + " " + this.noun;
hello: function(options) {
return options.data.adjective + " " + this.noun;
}
};
var result = template({noun: "cat"}, helpers, null, {adjective: "happy"});
equals("happy cat", result);
var result = template({noun: "cat"}, {helpers: helpers, data: {adjective: "happy"}});
equals("happy cat", result, "Data output by helper");
});
test("passing in data to a compiled function that expects data - works with helpers in partials", function() {
var template = CompilerContext.compile("{{>my_partial}}", {data: true});
var partials = {
my_partial: CompilerContext.compile("{{hello}}", {data: true})
};
var helpers = {
hello: function(options) {
return options.data.adjective + " " + this.noun;
}
};
var result = template({noun: "cat"}, {helpers: helpers, partials: partials, data: {adjective: "happy"}});
equals("happy cat", result, "Data output by helper inside partial");
});
test("passing in data to a compiled function that expects data - works with helpers and parameters", function() {
var template = Handlebars.compile("{{hello world}}", true);
var template = CompilerContext.compile("{{hello world}}", {data: true});
var helpers = {
hello: function(noun, data) {
return data.adjective + " " + noun + (this.exclaim ? "!" : "");
hello: function(noun, options) {
return options.data.adjective + " " + noun + (this.exclaim ? "!" : "");
}
};
var result = template({exclaim: true, world: "world"}, helpers, null, {adjective: "happy"});
equals("happy world!", result);
var result = template({exclaim: true, world: "world"}, {helpers: helpers, data: {adjective: "happy"}});
equals("happy world!", result, "Data output by helper");
});
test("passing in data to a compiled function that expects data - works with block helpers", function() {
var template = Handlebars.compile("{{#hello}}{{world}}{{/hello}}", true);
var template = CompilerContext.compile("{{#hello}}{{world}}{{/hello}}", {data: true});
var helpers = {
hello: function(fn) {
return fn(this);
},
world: function(data) {
return data.adjective + " world" + (this.exclaim ? "!" : "");
world: function(options) {
return options.data.adjective + " world" + (this.exclaim ? "!" : "");
}
};
var result = template({exclaim: true}, helpers, null, {adjective: "happy"});
equals("happy world!", result);
var result = template({exclaim: true}, {helpers: helpers, data: {adjective: "happy"}});
equals("happy world!", result, "Data output by helper");
});
test("passing in data to a compiled function that expects data - works with block helpers that use ..", function() {
var template = Handlebars.compile("{{#hello}}{{world ../zomg}}{{/hello}}", true);
var template = CompilerContext.compile("{{#hello}}{{world ../zomg}}{{/hello}}", {data: true});
var helpers = {
hello: function(fn) {
return fn({exclaim: "?"});
},
world: function(thing, data) {
return data.adjective + " " + thing + (this.exclaim || "");
world: function(thing, options) {
return options.data.adjective + " " + thing + (this.exclaim || "");
}
};
var result = template({exclaim: true, zomg: "world"}, helpers, null, {adjective: "happy"});
equals("happy world?", result);
var result = template({exclaim: true, zomg: "world"}, {helpers: helpers, data: {adjective: "happy"}});
equals("happy world?", result, "Data output by helper");
});
test("passing in data to a compiled function that expects data - works with block helpers that use ..", function() {
var template = Handlebars.compile("{{#hello}}{{world ../zomg}}{{/hello}}", true);
test("passing in data to a compiled function that expects data - data is passed to with block helpers where children use ..", function() {
var template = CompilerContext.compile("{{#hello}}{{world ../zomg}}{{/hello}}", {data: true});
var helpers = {
hello: function(fn, inverse, data) {
return data.accessData + " " + fn({exclaim: "?"});
hello: function(fn, inverse) {
return fn.data.accessData + " " + fn({exclaim: "?"});
},
world: function(thing, data) {
return data.adjective + " " + thing + (this.exclaim || "");
world: function(thing, options) {
return options.data.adjective + " " + thing + (this.exclaim || "");
}
};
var result = template({exclaim: true, zomg: "world"}, helpers, null, {adjective: "happy", accessData: "#win"});
equals("#win happy world?", result);
var result = template({exclaim: true, zomg: "world"}, {helpers: helpers, data: {adjective: "happy", accessData: "#win"}});
equals("#win happy world?", result, "Data output by helper");
});
test("you can override inherited data when invoking a helper", function() {
var template = CompilerContext.compile("{{#hello}}{{world zomg}}{{/hello}}", {data: true});
var helpers = {
hello: function(fn) {
return fn({exclaim: "?", zomg: "world"}, { data: {adjective: "sad"} });
},
world: function(thing, options) {
return options.data.adjective + " " + thing + (this.exclaim || "");
}
};
var result = template({exclaim: true, zomg: "planet"}, {helpers: helpers, data: {adjective: "happy"}});
equals("sad world?", result, "Overriden data output by helper");
});
test("you can override inherited data when invoking a helper with depth", function() {
var template = CompilerContext.compile("{{#hello}}{{world ../zomg}}{{/hello}}", {data: true});
var helpers = {
hello: function(fn) {
return fn({exclaim: "?"}, { data: {adjective: "sad"} });
},
world: function(thing, options) {
return options.data.adjective + " " + thing + (this.exclaim || "");
}
};
var result = template({exclaim: true, zomg: "world"}, {helpers: helpers, data: {adjective: "happy"}});
equals("sad world?", result, "Overriden data output by helper");
});
test("helpers take precedence over same-named context properties", function() {
var template = CompilerContext.compile("{{goodbye}} {{cruel world}}");
var helpers = {
goodbye: function() {
return this.goodbye.toUpperCase();
}
};
var context = {
cruel: function(world) {
return "cruel " + world.toUpperCase();
},
goodbye: "goodbye",
world: "world"
};
var result = template(context, {helpers: helpers});
equals(result, "GOODBYE cruel WORLD", "Helper executed");
});
test("helpers take precedence over same-named context properties", function() {
var template = CompilerContext.compile("{{#goodbye}} {{cruel world}}{{/goodbye}}");
var helpers = {
goodbye: function(fn) {
return this.goodbye.toUpperCase() + fn(this);
}
};
var context = {
cruel: function(world) {
return "cruel " + world.toUpperCase();
},
goodbye: "goodbye",
world: "world"
};
var result = template(context, {helpers: helpers});
equals(result, "GOODBYE cruel WORLD", "Helper executed");
});
test("Scoped names take precedence over helpers", function() {
var template = CompilerContext.compile("{{this.goodbye}} {{cruel world}} {{cruel this.goodbye}}");
var helpers = {
goodbye: function() {
return this.goodbye.toUpperCase();
}
};
var context = {
cruel: function(world) {
return "cruel " + world.toUpperCase();
},
goodbye: "goodbye",
world: "world"
};
var result = template(context, {helpers: helpers});
equals(result, "goodbye cruel WORLD cruel GOODBYE", "Helper not executed");
});
test("Scoped names take precedence over block helpers", function() {
var template = CompilerContext.compile("{{#goodbye}} {{cruel world}}{{/goodbye}} {{this.goodbye}}");
var helpers = {
goodbye: function(fn) {
return this.goodbye.toUpperCase() + fn(this);
}
};
var context = {
cruel: function(world) {
return "cruel " + world.toUpperCase();
},
goodbye: "goodbye",
world: "world"
};
var result = template(context, {helpers: helpers});
equals(result, "GOODBYE cruel WORLD goodbye", "Helper executed");
});
test("helpers can take an optional hash", function() {
var template = CompilerContext.compile('{{goodbye cruel="CRUEL" world="WORLD" times=12}}');
var helpers = {
goodbye: function(options) {
return "GOODBYE " + options.hash.cruel + " " + options.hash.world + " " + options.hash.times + " TIMES";
}
};
var context = {};
var result = template(context, {helpers: helpers});
equals(result, "GOODBYE CRUEL WORLD 12 TIMES", "Helper output hash");
});
test("helpers can take an optional hash with booleans", function() {
var helpers = {
goodbye: function(options) {
if (options.hash.print === true) {
return "GOODBYE " + options.hash.cruel + " " + options.hash.world;
} else if (options.hash.print === false) {
return "NOT PRINTING";
} else {
return "THIS SHOULD NOT HAPPEN";
}
}
};
var context = {};
var template = CompilerContext.compile('{{goodbye cruel="CRUEL" world="WORLD" print=true}}');
var result = template(context, {helpers: helpers});
equals(result, "GOODBYE CRUEL WORLD", "Helper output hash");
var template = CompilerContext.compile('{{goodbye cruel="CRUEL" world="WORLD" print=false}}');
var result = template(context, {helpers: helpers});
equals(result, "NOT PRINTING", "Boolean helper parameter honored");
});
test("block helpers can take an optional hash", function() {
var template = CompilerContext.compile('{{#goodbye cruel="CRUEL" times=12}}world{{/goodbye}}');
var helpers = {
goodbye: function(options) {
return "GOODBYE " + options.hash.cruel + " " + options.fn(this) + " " + options.hash.times + " TIMES";
}
};
var result = template({}, {helpers: helpers});
equals(result, "GOODBYE CRUEL world 12 TIMES", "Hash parameters output");
});
test("block helpers can take an optional hash with booleans", function() {
var helpers = {
goodbye: function(options) {
if (options.hash.print === true) {
return "GOODBYE " + options.hash.cruel + " " + options.fn(this);
} else if (options.hash.print === false) {
return "NOT PRINTING";
} else {
return "THIS SHOULD NOT HAPPEN";
}
}
};
var template = CompilerContext.compile('{{#goodbye cruel="CRUEL" print=true}}world{{/goodbye}}');
var result = template({}, {helpers: helpers});
equals(result, "GOODBYE CRUEL world", "Boolean hash parameter honored");
var template = CompilerContext.compile('{{#goodbye cruel="CRUEL" print=false}}world{{/goodbye}}');
var result = template({}, {helpers: helpers});
equals(result, "NOT PRINTING", "Boolean hash parameter honored");
});
test("arguments to helpers can be retrieved from options hash in string form", function() {
var template = CompilerContext.compile('{{wycats is.a slave.driver}}', {stringParams: true});
var helpers = {
wycats: function(passiveVoice, noun, options) {
return "HELP ME MY BOSS " + passiveVoice + ' ' + noun;
}
};
var result = template({}, {helpers: helpers});
equals(result, "HELP ME MY BOSS is.a slave.driver", "String parameters output");
});
test("when using block form, arguments to helpers can be retrieved from options hash in string form", function() {
var template = CompilerContext.compile('{{#wycats is.a slave.driver}}help :({{/wycats}}', {stringParams: true});
var helpers = {
wycats: function(passiveVoice, noun, options) {
return "HELP ME MY BOSS " + passiveVoice + ' ' +
noun + ': ' + options.fn(this);
}
};
var result = template({}, {helpers: helpers});
equals(result, "HELP ME MY BOSS is.a slave.driver: help :(", "String parameters output");
});
test("when inside a block in String mode, .. passes the appropriate context in the options hash", function() {
var template = CompilerContext.compile('{{#with dale}}{{tomdale ../need dad.joke}}{{/with}}', {stringParams: true});
var helpers = {
tomdale: function(desire, noun, options) {
return "STOP ME FROM READING HACKER NEWS I " +
options.contexts[0][desire] + " " + noun;
},
"with": function(context, options) {
return options.fn(options.contexts[0][context]);
}
};
var result = template({
dale: {},
need: 'need-a'
}, {helpers: helpers});
equals(result, "STOP ME FROM READING HACKER NEWS I need-a dad.joke", "Proper context variable output");
});
test("when inside a block in String mode, .. passes the appropriate context in the options hash to a block helper", function() {
var template = CompilerContext.compile('{{#with dale}}{{#tomdale ../need dad.joke}}wot{{/tomdale}}{{/with}}', {stringParams: true});
var helpers = {
tomdale: function(desire, noun, options) {
return "STOP ME FROM READING HACKER NEWS I " +
options.contexts[0][desire] + " " + noun + " " +
options.fn(this);
},
"with": function(context, options) {
return options.fn(options.contexts[0][context]);
}
};
var result = template({
dale: {},
need: 'need-a'
}, {helpers: helpers});
equals(result, "STOP ME FROM READING HACKER NEWS I need-a dad.joke wot", "Proper context variable output");
});
module("Regressions")
test("GH-94: Cannot read property of undefined", function() {
var data = {"books":[{"title":"The origin of species","author":{"name":"Charles Darwin"}},{"title":"Lazarillo de Tormes"}]};
var string = "{{#books}}{{title}}{{author.name}}{{/books}}";
shouldCompileTo(string, data, "The origin of speciesCharles DarwinLazarillo de Tormes",
"Renders without an undefined property error");
});
test("GH-150: Inverted sections print when they shouldn't", function() {
var string = "{{^set}}not set{{/set}} :: {{#set}}set{{/set}}";
shouldCompileTo(string, {}, "not set :: ", "inverted sections run when property isn't present in context");
shouldCompileTo(string, {set: undefined}, "not set :: ", "inverted sections run when property is undefined");
shouldCompileTo(string, {set: false}, "not set :: ", "inverted sections run when property is false");
shouldCompileTo(string, {set: true}, " :: set", "inverted sections don't run when property is true");
});
test("Mustache man page", function() {
var string = "Hello {{name}}. You have just won ${{value}}!{{#in_ca}} Well, ${{taxed_value}}, after taxes.{{/in_ca}}"
var data = {
"name": "Chris",
"value": 10000,
"taxed_value": 10000 - (10000 * 0.4),
"in_ca": true
}
shouldCompileTo(string, data, "Hello Chris. You have just won $10000! Well, $6000, after taxes.", "the hello world mustache example works");
});
test("GH-158: Using array index twice, breaks the template", function() {
var string = "{{arr.[0]}}, {{arr.[1]}}";
var data = { "arr": [1,2] };
shouldCompileTo(string, data, "1, 2", "it works as expected");
});
test("bug reported by @fat where lambdas weren't being properly resolved", function() {
var string = "<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 data = {
thing: function() {
return "blah";
},
things: [
{className: "one", word: "@fat"},
{className: "two", word: "@dhg"},
{className: "three", word:"@sayrer"}
],
hasThings: function() {
return true;
}
};
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";
shouldCompileTo(string, data, output);
});
+68 -18
View File
@@ -44,13 +44,7 @@ module Handlebars
("\n" * prelines) + ret
end
def self.js_load(file)
str = File.read(file)
CONTEXT.eval(remove_exports(str), file)
end
CONTEXT = V8::Context.new
CONTEXT.instance_eval do |context|
def self.load_helpers(context)
context["exports"] = nil
context["p"] = proc do |val|
@@ -71,16 +65,71 @@ module Handlebars
puts Handlebars::Spec.js_backtrace(context)
puts
end
end
Handlebars::Spec.js_load('lib/handlebars/parser.js')
Handlebars::Spec.js_load('lib/handlebars/base.js');
Handlebars::Spec.js_load('lib/handlebars/ast.js');
Handlebars::Spec.js_load('lib/handlebars/visitor.js');
Handlebars::Spec.js_load('lib/handlebars/printer.js')
Handlebars::Spec.js_load('lib/handlebars/runtime.js')
Handlebars::Spec.js_load('lib/handlebars/utils.js')
Handlebars::Spec.js_load('lib/Handlebars/vm.js')
Handlebars::Spec.js_load('lib/handlebars.js')
def self.js_load(context, file)
str = File.read(file)
context.eval(remove_exports(str), file)
end
CONTEXT = V8::Context.new
CONTEXT.instance_eval do |context|
Handlebars::Spec.load_helpers(context);
Handlebars::Spec.js_load(context, 'lib/handlebars/base.js');
Handlebars::Spec.js_load(context, 'lib/handlebars/utils.js');
Handlebars::Spec.js_load(context, 'lib/handlebars/runtime.js');
context["CompilerContext"] = {}
CompilerContext = context["CompilerContext"]
CompilerContext["compile"] = proc do |*args|
template, options = args[0], args[1] || nil
templateSpec = COMPILE_CONTEXT["Handlebars"]["precompile"].call(template, options);
context["Handlebars"]["template"].call(context.eval("(#{templateSpec})"));
end
CompilerContext["compileWithPartial"] = proc do |*args|
template, options = args[0], args[1] || nil
FULL_CONTEXT["Handlebars"]["compile"].call(template, options);
end
end
COMPILE_CONTEXT = V8::Context.new
COMPILE_CONTEXT.instance_eval do |context|
Handlebars::Spec.load_helpers(context);
Handlebars::Spec.js_load(context, 'lib/handlebars/base.js');
Handlebars::Spec.js_load(context, 'lib/handlebars/utils.js');
Handlebars::Spec.js_load(context, 'lib/handlebars/compiler/parser.js');
Handlebars::Spec.js_load(context, 'lib/handlebars/compiler/base.js');
Handlebars::Spec.js_load(context, 'lib/handlebars/compiler/ast.js');
Handlebars::Spec.js_load(context, 'lib/handlebars/compiler/visitor.js');
Handlebars::Spec.js_load(context, 'lib/handlebars/compiler/printer.js');
Handlebars::Spec.js_load(context, 'lib/handlebars/compiler/compiler.js');
context["Handlebars"]["logger"]["level"] = ENV["DEBUG_JS"] ? context["Handlebars"]["logger"][ENV["DEBUG_JS"]] : 4
context["Handlebars"]["logger"]["log"] = proc do |level, str|
logger_level = context["Handlebars"]["logger"]["level"].to_i
if logger_level <= level
puts str
end
end
end
FULL_CONTEXT = V8::Context.new
FULL_CONTEXT.instance_eval do |context|
Handlebars::Spec.load_helpers(context);
Handlebars::Spec.js_load(context, 'lib/handlebars/base.js');
Handlebars::Spec.js_load(context, 'lib/handlebars/utils.js');
Handlebars::Spec.js_load(context, 'lib/handlebars/compiler/parser.js');
Handlebars::Spec.js_load(context, 'lib/handlebars/compiler/base.js');
Handlebars::Spec.js_load(context, 'lib/handlebars/compiler/ast.js');
Handlebars::Spec.js_load(context, 'lib/handlebars/compiler/visitor.js');
Handlebars::Spec.js_load(context, 'lib/handlebars/compiler/printer.js');
Handlebars::Spec.js_load(context, 'lib/handlebars/compiler/compiler.js');
Handlebars::Spec.js_load(context, 'lib/handlebars/runtime.js');
context["Handlebars"]["logger"]["level"] = ENV["DEBUG_JS"] ? context["Handlebars"]["logger"][ENV["DEBUG_JS"]] : 4
@@ -101,7 +150,8 @@ require "test/unit/assertions"
RSpec.configure do |config|
config.include Test::Unit::Assertions
config.before(:all) do
@context = Handlebars::Spec::CONTEXT
# Each is required to allow classes to mark themselves as compiler tests
config.before(:each) do
@context = @compiles ? Handlebars::Spec::COMPILE_CONTEXT : Handlebars::Spec::CONTEXT
end
end
+108 -2
View File
@@ -5,13 +5,20 @@ describe "Tokenizer" do
let(:parser) { @context["handlebars"] }
let(:lexer) { @context["handlebars"]["lexer"] }
before(:all) do
@compiles = true
end
Token = Struct.new(:name, :text)
def tokenize(string)
lexer.setInput(string)
out = []
while result = parser.terminals_[lexer.lex] and result != "EOF"
while token = lexer.lex
# p token
result = parser.terminals_[token] || token
# p result
break if !result || result == "EOF" || result == "INVALID"
out << Token.new(result, lexer.yytext)
end
@@ -37,12 +44,59 @@ describe "Tokenizer" do
result[1].should be_token("ID", "foo")
end
it "supports escaping delimiters" do
result = tokenize("{{foo}} \\{{bar}} {{baz}}")
result.should match_tokens(%w(OPEN ID CLOSE CONTENT CONTENT OPEN ID CLOSE))
result[4].should be_token("CONTENT", "{{bar}} ")
end
it "supports escaping a triple stash" do
result = tokenize("{{foo}} \\{{{bar}}} {{baz}}")
result.should match_tokens(%w(OPEN ID CLOSE CONTENT CONTENT OPEN ID CLOSE))
result[4].should be_token("CONTENT", "{{{bar}}} ")
end
it "tokenizes a simple path" do
result = tokenize("{{foo/bar}}")
result.should match_tokens(%w(OPEN ID SEP ID CLOSE))
end
it "allows dot notation" do
result = tokenize("{{foo.bar}}")
result.should match_tokens(%w(OPEN ID SEP ID CLOSE))
tokenize("{{foo.bar.baz}}").should match_tokens(%w(OPEN ID SEP ID SEP ID CLOSE))
end
it "allows path literals with []" do
result = tokenize("{{foo.[bar]}}")
result.should match_tokens(%w(OPEN ID SEP ID CLOSE))
end
it "allows multiple path literals on a line with []" do
result = tokenize("{{foo.[bar]}}{{foo.[baz]}}")
result.should match_tokens(%w(OPEN ID SEP ID CLOSE OPEN ID SEP ID CLOSE))
end
it "tokenizes {{.}} as OPEN ID CLOSE" do
result = tokenize("{{.}}")
result.should match_tokens(%w(OPEN ID CLOSE))
end
it "tokenizes a path as 'OPEN (ID SEP)* ID CLOSE'" do
result = tokenize("{{../foo/bar}}")
result.should match_tokens(%w(OPEN ID SEP ID SEP ID CLOSE))
result[1].should be_token("ID", "..")
end
it "tokenizes a path with .. as a parent path" do
result = tokenize("{{../foo.bar}}")
result.should match_tokens(%w(OPEN ID SEP ID SEP ID CLOSE))
result[1].should be_token("ID", "..")
end
it "tokenizes a path with this/foo as OPEN ID SEP ID CLOSE" do
result = tokenize("{{this/foo}}")
result.should match_tokens(%w(OPEN ID SEP ID CLOSE))
@@ -50,12 +104,18 @@ describe "Tokenizer" do
result[3].should be_token("ID", "foo")
end
it "tokenizes a simple mustahe with spaces as 'OPEN ID CLOSE'" do
it "tokenizes a simple mustache with spaces as 'OPEN ID CLOSE'" do
result = tokenize("{{ foo }}")
result.should match_tokens(%w(OPEN ID CLOSE))
result[1].should be_token("ID", "foo")
end
it "tokenizes a simple mustache with line breaks as 'OPEN ID ID CLOSE'" do
result = tokenize("{{ foo \n bar }}")
result.should match_tokens(%w(OPEN ID ID CLOSE))
result[1].should be_token("ID", "foo")
end
it "tokenizes raw content as 'CONTENT'" do
result = tokenize("foo {{ bar }} baz")
result.should match_tokens(%w(CONTENT OPEN ID CLOSE CONTENT))
@@ -138,6 +198,52 @@ describe "Tokenizer" do
result[2].should be_token("STRING", %{bar"baz})
end
it "tokenizes numbers" do
result = tokenize(%|{{ foo 1 }}|)
result.should match_tokens(%w(OPEN ID INTEGER CLOSE))
result[2].should be_token("INTEGER", "1")
end
it "tokenizes booleans" do
result = tokenize(%|{{ foo true }}|)
result.should match_tokens(%w(OPEN ID BOOLEAN CLOSE))
result[2].should be_token("BOOLEAN", "true")
result = tokenize(%|{{ foo false }}|)
result.should match_tokens(%w(OPEN ID BOOLEAN CLOSE))
result[2].should be_token("BOOLEAN", "false")
end
it "tokenizes hash arguments" do
result = tokenize("{{ foo bar=baz }}")
result.should match_tokens %w(OPEN ID ID EQUALS ID CLOSE)
result = tokenize("{{ foo bar baz=bat }}")
result.should match_tokens %w(OPEN ID ID ID EQUALS ID CLOSE)
result = tokenize("{{ foo bar baz=1 }}")
result.should match_tokens %w(OPEN ID ID ID EQUALS INTEGER CLOSE)
result = tokenize("{{ foo bar baz=true }}")
result.should match_tokens %w(OPEN ID ID ID EQUALS BOOLEAN CLOSE)
result = tokenize("{{ foo bar baz=false }}")
result.should match_tokens %w(OPEN ID ID ID EQUALS BOOLEAN CLOSE)
result = tokenize("{{ foo bar\n baz=bat }}")
result.should match_tokens %w(OPEN ID ID ID EQUALS ID CLOSE)
result = tokenize("{{ foo bar baz=\"bat\" }}")
result.should match_tokens %w(OPEN ID ID ID EQUALS STRING CLOSE)
result = tokenize("{{ foo bar baz=\"bat\" bam=wot }}")
result.should match_tokens %w(OPEN ID ID ID EQUALS STRING ID EQUALS ID CLOSE)
result = tokenize("{{foo omg bar=baz bat=\"bam\"}}")
result.should match_tokens %w(OPEN ID ID ID EQUALS ID ID EQUALS STRING CLOSE)
result[2].should be_token("ID", "omg")
end
it "does not time out in a mustache with a single } followed by EOF" do
Timeout.timeout(1) { tokenize("{{foo}").should match_tokens(%w(OPEN ID)) }
end
+33 -19
View File
@@ -1,28 +1,42 @@
%x mu
%x mu emu
%%
[^\x00]*?/("{{") { this.begin("mu"); if (yytext) return 'CONTENT'; }
[^\x00]*?/("{{") {
if(yytext.slice(-1) !== "\\") this.begin("mu");
if(yytext.slice(-1) === "\\") yytext = yytext.substr(0,yyleng-1), this.begin("emu");
if(yytext) return 'CONTENT';
}
[^\x00]+ { return 'CONTENT'; }
<mu>"{{>" { return 'OPEN_PARTIAL'; }
<mu>"{{#" { return 'OPEN_BLOCK'; }
<mu>"{{/" { return 'OPEN_ENDBLOCK'; }
<mu>"{{^" { return 'OPEN_INVERSE'; }
<mu>"{{"\s*"else" { return 'OPEN_INVERSE'; }
<mu>"{{{" { return 'OPEN_UNESCAPED'; }
<mu>"{{&" { return 'OPEN_UNESCAPED'; }
<mu>"{{!".*?"}}" { yytext = yytext.substr(3,yyleng-5); this.begin("INITIAL"); return 'COMMENT'; }
<mu>"{{" { return 'OPEN'; }
<emu>[^\x00]{2,}?/("{{") { this.popState(); return 'CONTENT'; }
<mu>"/" { return 'SEP'; }
<mu>\s+ { /*ignore whitespace*/ }
<mu>"}}}" { this.begin("INITIAL"); return 'CLOSE'; }
<mu>"}}" { this.begin("INITIAL"); return 'CLOSE'; }
<mu>'"'("\\"["]|[^"])*'"' { yytext = yytext.substr(1,yyleng-2).replace(/\\"/g,'"'); return 'STRING'; }
<mu>[a-zA-Z0-9_.]+/[} /] { return 'ID'; }
<mu>. { return 'INVALID'; }
<mu>"{{>" { return 'OPEN_PARTIAL'; }
<mu>"{{#" { return 'OPEN_BLOCK'; }
<mu>"{{/" { return 'OPEN_ENDBLOCK'; }
<mu>"{{^" { return 'OPEN_INVERSE'; }
<mu>"{{"\s*"else" { return 'OPEN_INVERSE'; }
<mu>"{{{" { return 'OPEN_UNESCAPED'; }
<mu>"{{&" { return 'OPEN_UNESCAPED'; }
<mu>"{{!"[\s\S]*?"}}" { yytext = yytext.substr(3,yyleng-5); this.popState(); return 'COMMENT'; }
<mu>"{{" { return 'OPEN'; }
<INITIAL,mu><<EOF>> { return 'EOF'; }
<mu>"=" { return 'EQUALS'; }
<mu>"."/[} ] { return 'ID'; }
<mu>".." { return 'ID'; }
<mu>[\/.] { return 'SEP'; }
<mu>\s+ { /*ignore whitespace*/ }
<mu>"}}}" { this.popState(); return 'CLOSE'; }
<mu>"}}" { this.popState(); return 'CLOSE'; }
<mu>'"'("\\"["]|[^"])*'"' { yytext = yytext.substr(1,yyleng-2).replace(/\\"/g,'"'); return 'STRING'; }
<mu>"true"/[}\s] { return 'BOOLEAN'; }
<mu>"false"/[}\s] { return 'BOOLEAN'; }
<mu>[0-9]+/[}\s] { return 'INTEGER'; }
<mu>[a-zA-Z0-9_$-]+/[=}\s\/.] { return 'ID'; }
<mu>'['[^\]]*']' { yytext = yytext.substr(1, yyleng-2); return 'ID'; }
<mu>. { return 'INVALID'; }
<INITIAL,mu><<EOF>> { return 'EOF'; }
+46 -26
View File
@@ -3,50 +3,50 @@
%%
root
: program EOF { return $1 }
: program EOF { return $1; }
;
program
: statements simpleInverse statements { $$ = new yy.ProgramNode($1, $3) }
| statements { $$ = new yy.ProgramNode($1) }
| "" { $$ = new yy.ProgramNode([]) }
: statements simpleInverse statements { $$ = new yy.ProgramNode($1, $3); }
| statements { $$ = new yy.ProgramNode($1); }
| "" { $$ = new yy.ProgramNode([]); }
;
statements
: statement { $$ = [$1] }
| statements statement { $1.push($2); $$ = $1 }
: statement { $$ = [$1]; }
| statements statement { $1.push($2); $$ = $1; }
;
statement
: openInverse program closeBlock { $$ = new yy.InverseNode($1, $2, $3) }
| openBlock program closeBlock { $$ = new yy.BlockNode($1, $2, $3) }
| mustache { $$ = $1 }
| partial { $$ = $1 }
| CONTENT { $$ = new yy.ContentNode($1) }
| COMMENT { $$ = new yy.CommentNode($1) }
: openInverse program closeBlock { $$ = new yy.InverseNode($1, $2, $3); }
| openBlock program closeBlock { $$ = new yy.BlockNode($1, $2, $3); }
| mustache { $$ = $1; }
| partial { $$ = $1; }
| CONTENT { $$ = new yy.ContentNode($1); }
| COMMENT { $$ = new yy.CommentNode($1); }
;
openBlock
: OPEN_BLOCK inMustache CLOSE { $$ = new yy.MustacheNode($2) }
: OPEN_BLOCK inMustache CLOSE { $$ = new yy.MustacheNode($2[0], $2[1]); }
;
openInverse
: OPEN_INVERSE inMustache CLOSE { $$ = new yy.MustacheNode($2) }
: OPEN_INVERSE inMustache CLOSE { $$ = new yy.MustacheNode($2[0], $2[1]); }
;
closeBlock
: OPEN_ENDBLOCK path CLOSE { $$ = $2 }
: OPEN_ENDBLOCK path CLOSE { $$ = $2; }
;
mustache
: OPEN inMustache CLOSE { $$ = new yy.MustacheNode($2) }
| OPEN_UNESCAPED inMustache CLOSE { $$ = new yy.MustacheNode($2, true) }
: OPEN inMustache CLOSE { $$ = new yy.MustacheNode($2[0], $2[1]); }
| OPEN_UNESCAPED inMustache CLOSE { $$ = new yy.MustacheNode($2[0], $2[1], true); }
;
partial
: OPEN_PARTIAL path CLOSE { $$ = new yy.PartialNode($2) }
| OPEN_PARTIAL path path CLOSE { $$ = new yy.PartialNode($2, $3) }
: OPEN_PARTIAL path CLOSE { $$ = new yy.PartialNode($2); }
| OPEN_PARTIAL path path CLOSE { $$ = new yy.PartialNode($2, $3); }
;
simpleInverse
@@ -54,26 +54,46 @@ simpleInverse
;
inMustache
: path params { $$ = [$1].concat($2) }
| path { $$ = [$1] }
: path params hash { $$ = [[$1].concat($2), $3]; }
| path params { $$ = [[$1].concat($2), null]; }
| path hash { $$ = [[$1], $2]; }
| path { $$ = [[$1], null]; }
;
params
: params param { $1.push($2); $$ = $1; }
| param { $$ = [$1] }
| param { $$ = [$1]; }
;
param
: path { $$ = $1 }
| STRING { $$ = new yy.StringNode($1) }
: path { $$ = $1; }
| STRING { $$ = new yy.StringNode($1); }
| INTEGER { $$ = new yy.IntegerNode($1); }
| BOOLEAN { $$ = new yy.BooleanNode($1); }
;
hash
: hashSegments { $$ = new yy.HashNode($1); }
;
hashSegments
: hashSegments hashSegment { $1.push($2); $$ = $1; }
| hashSegment { $$ = [$1]; }
;
hashSegment
: ID EQUALS path { $$ = [$1, $3]; }
| ID EQUALS STRING { $$ = [$1, new yy.StringNode($3)]; }
| ID EQUALS INTEGER { $$ = [$1, new yy.IntegerNode($3)]; }
| ID EQUALS BOOLEAN { $$ = [$1, new yy.BooleanNode($3)]; }
;
path
: pathSegments { $$ = new yy.IdNode($1) }
: pathSegments { $$ = new yy.IdNode($1); }
;
pathSegments
: pathSegments SEP ID { $1.push($3); $$ = $1; }
| ID { $$ = [$1] }
| ID { $$ = [$1]; }
;