Compare commits
213 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| bef909f5cb | |||
| 0b97fea56f | |||
| 2ea95ca08d | |||
| cb9db98642 | |||
| 58e2c2410f | |||
| 6040149cf8 | |||
| 17cdb0091b | |||
| 730c2617af | |||
| 757d5250dc | |||
| 163a92ebe0 | |||
| c79c761460 | |||
| e474e56b80 | |||
| 69307d0e2b | |||
| 9852502636 | |||
| 7a0bf9d332 | |||
| 2e7ea96818 | |||
| 932e2970ad | |||
| 373d10edb1 | |||
| bd0f74fbd9 | |||
| e57e626480 | |||
| cb04822812 | |||
| 647d5adf85 | |||
| 6df51733d3 | |||
| 9c9d92413e | |||
| c6eec286a8 | |||
| f6070f0084 | |||
| 87e6f21765 | |||
| 3afc572f6b | |||
| fea981a3f6 | |||
| 799c7ed3ce | |||
| b2a27347e8 | |||
| 215472dda2 | |||
| 8338bc888a | |||
| 5d15ef9625 | |||
| 42e31ef6db | |||
| deac4f5141 | |||
| 79b730ccf3 | |||
| ec3e5e3914 | |||
| 5f9919dcf7 | |||
| 608aa94fc3 | |||
| 9998d9d9bc | |||
| 924cc3e86c | |||
| 25b8e314af | |||
| 43431d2a78 | |||
| b36776f4c6 | |||
| f23fbb84e2 | |||
| 87599daef1 | |||
| 374a38ed36 | |||
| b501c380af | |||
| 7dd11fa3f2 | |||
| defc2f3403 | |||
| 9a27aa98b7 | |||
| 4458b5fa13 | |||
| 7cd34edcc2 | |||
| 7b18e51bf8 | |||
| cd927e9cda | |||
| b832c85923 | |||
| 33bb89beda | |||
| 33bde69750 | |||
| 2422a6518f | |||
| 0768f07b17 | |||
| 2f3d5977d1 | |||
| 8d38ebd251 | |||
| d630894d9b | |||
| 8f617ca624 | |||
| 57e3bd72a8 | |||
| 6b18873d23 | |||
| c1c455acc8 | |||
| f2f1f5f9bd | |||
| 7212e2b7d7 | |||
| fc84308cc9 | |||
| 91bbc4fd2c | |||
| 696dc2dc36 | |||
| 96571b93c1 | |||
| 3db5fb742e | |||
| c561fdf8b1 | |||
| f7480d5b42 | |||
| 93ebee8faf | |||
| 20350a3c07 | |||
| 8cd4d939f3 | |||
| 98e66f898e | |||
| 85a32a18e2 | |||
| 90648c0e2d | |||
| 6ef3d1316f | |||
| f3a2374dda | |||
| cbadb33d51 | |||
| 17e1b1fab7 | |||
| 6eea3e4d44 | |||
| 48726c7fed | |||
| 47c5cd71ea | |||
| a9bcd369f3 | |||
| b3e70414d8 | |||
| c9d851e9a4 | |||
| 86d71e6fc8 | |||
| 542f98190b | |||
| 16c0907607 | |||
| 292e32292b | |||
| 3c5cac3f3f | |||
| 2f7b724d5a | |||
| 318c08a97e | |||
| d6b97cf34d | |||
| 82e3344aed | |||
| 6349c1a0c7 | |||
| f53737ef23 | |||
| 1a88356dc3 | |||
| 5d4b549dc3 | |||
| d044ada7aa | |||
| 77a20a3a9f | |||
| 9821da8df7 | |||
| 2d538baf72 | |||
| c7e8ddd6b5 | |||
| 6fcebec713 | |||
| 993c793565 | |||
| f8edc59025 | |||
| 3af9de7407 | |||
| 72ea816cf7 | |||
| d641dae5f1 | |||
| ccf32821df | |||
| 8e23ce87d3 | |||
| 5a3522cf2e | |||
| aaec001931 | |||
| 3d9e5070f5 | |||
| 82a1b0e85c | |||
| 600a669d12 | |||
| 5aa12b5e09 | |||
| 6a6edf5ae6 | |||
| aeda7e389f | |||
| 09e79f0587 | |||
| 4a9270aa38 | |||
| 59f5331db4 | |||
| 74bd8cac60 | |||
| 542d64f0be | |||
| 5e611205ac | |||
| 2e77f1c777 | |||
| 8c49721c70 | |||
| 2fc01ff73a | |||
| bba08ad95f | |||
| 034dafddac | |||
| 6aa9a30b02 | |||
| f2dccb753f | |||
| 471f3b9748 | |||
| a927125909 | |||
| 9062cac3f0 | |||
| 059a80661d | |||
| 37e36bf137 | |||
| 3c2086eb2a | |||
| 1ff0b657be | |||
| 17dbfa7004 | |||
| 52900c89f7 | |||
| be8f4f6fb9 | |||
| 1482f1ae72 | |||
| 0f78345d0c | |||
| 4eeda34ad2 | |||
| ed1be562e2 | |||
| 04b359d15b | |||
| f07f70ca42 | |||
| 28d35180bd | |||
| 0e22301e8f | |||
| 4e5cf17311 | |||
| a4ca50533d | |||
| 1aa512d227 | |||
| 2f4644529c | |||
| d109e31f62 | |||
| 5168037b5a | |||
| 92535a9158 | |||
| 4f309bb3aa | |||
| b4c949cc07 | |||
| 90bb568faf | |||
| 60040e60fb | |||
| a00054e26b | |||
| 81071dd22c | |||
| c29223a98b | |||
| 5d99572006 | |||
| d83e1c7484 | |||
| 201ecbd7bc | |||
| 9333d1210d | |||
| 2615fa4bd1 | |||
| 3e5bc84058 | |||
| 5d8480fa8a | |||
| 56b0ce56f8 | |||
| 9dc4bd47e6 | |||
| 4fb42d7682 | |||
| b0df69ddec | |||
| b324d002ca | |||
| b291a1ad8c | |||
| dec196b4d9 | |||
| aba636ec38 | |||
| 038d9b3fee | |||
| 6a80531513 | |||
| a1b1b160cf | |||
| f368f9177c | |||
| a7bb51d8a5 | |||
| 357710f136 | |||
| ca9b9671a5 | |||
| e0aa705f71 | |||
| 51d2543ba1 | |||
| e5899cad1e | |||
| 0397f875c5 | |||
| 2cf88d4e77 | |||
| 32d7e52182 | |||
| 6fbdf4b445 | |||
| 0d4d8813ac | |||
| 935053cc33 | |||
| 2b319bef2f | |||
| 76e1bb4c06 | |||
| 7f8a323efe | |||
| ee388dc149 | |||
| 54948aad6f | |||
| 5e18cb6298 | |||
| ad50cb7efe | |||
| ed6b999623 | |||
| e7d5e72a87 | |||
| 57e990e6e0 |
+3
-1
@@ -1,4 +1,6 @@
|
||||
dist
|
||||
lib/handlebars/parser.js
|
||||
vendor
|
||||
.rvmrc
|
||||
.DS_Store
|
||||
lib/handlebars/compiler/parser.js
|
||||
node_modules
|
||||
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
.DS_Store
|
||||
.gitignore
|
||||
.rvmrc
|
||||
Gemfile
|
||||
Gemfile.lock
|
||||
Rakefile
|
||||
bench/*
|
||||
dist/*
|
||||
spec/*
|
||||
src/*
|
||||
vendor/*
|
||||
@@ -1,4 +1,5 @@
|
||||
source "http://rubygems.org"
|
||||
|
||||
gem "therubyracer", ">= 0.8.0"
|
||||
gem "rake"
|
||||
gem "therubyracer", ">= 0.9.8"
|
||||
gem "rspec"
|
||||
|
||||
+14
-10
@@ -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)
|
||||
|
||||
@@ -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
@@ -1,8 +1,13 @@
|
||||
[](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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -112,6 +112,8 @@ BenchWarmer.prototype = {
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
print("\n");
|
||||
},
|
||||
startLine: function(name) {
|
||||
var winners = Benchmark.map(this.winners(this.currentBenches), function(bench) {
|
||||
|
||||
+1
-4
@@ -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
@@ -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
@@ -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
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
@@ -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
@@ -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 = {
|
||||
"<": "<",
|
||||
">": ">"
|
||||
">": ">",
|
||||
'"': """,
|
||||
"'": "'",
|
||||
"`": "`"
|
||||
};
|
||||
|
||||
var badChars = /&(?!\w+;)|[<>]/g;
|
||||
var possible = /[&<>]/
|
||||
var badChars = /&(?!\w+;)|[<>"'`]/g;
|
||||
var possible = /[&<>"'`]/;
|
||||
|
||||
var escapeChar = function(chr) {
|
||||
return escape[chr] || "&"
|
||||
return escape[chr] || "&";
|
||||
};
|
||||
|
||||
Handlebars.Utils = {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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: "&\"\\<>"}, '&\"\\<>',
|
||||
"by default expressions should be escaped");
|
||||
|
||||
shouldCompileTo("{{&awesome}}", {awesome: "&\"\\<>"}, '&\"\\<>',
|
||||
"expressions with {{& handlebars aren't escaped");
|
||||
|
||||
shouldCompileTo("{{awesome}}", {awesome: "&\"'`\\<>"}, '&"'`\\<>',
|
||||
"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'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
@@ -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
@@ -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
@@ -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
@@ -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]; }
|
||||
;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user