diff --git a/.gitignore b/.gitignore index 6e326b1ba..fc7d79ae6 100644 --- a/.gitignore +++ b/.gitignore @@ -2,7 +2,8 @@ vendor .rvmrc .DS_Store lib/handlebars/compiler/parser.js -dist/*.min.js +/dist/ +/tmp/ node_modules *.sublime-project *.sublime-workspace diff --git a/.jshintrc b/.jshintrc index f37931784..851cf78fa 100644 --- a/.jshintrc +++ b/.jshintrc @@ -21,18 +21,23 @@ "stop", "ok", "strictEqual", - "module" + "module", + + "describe", + "it", + "afterEach" ], "node" : true, - "es5" : true, "browser" : true, + "esnext": true, "boss" : true, "curly": false, "debug": false, "devel": false, - "eqeqeq": true, + "eqeqeq": false, + "eqnull": true, "evil": true, "forin": false, "immed": false, diff --git a/.npmignore b/.npmignore index 2f42c9f44..eeb1ceb5e 100644 --- a/.npmignore +++ b/.npmignore @@ -1,10 +1,22 @@ .DS_Store .gitignore .rvmrc +.jshintrc +.travis.yml +.rspec Gemfile Gemfile.lock Rakefile +Gruntfile.js +*.gemspec +*.nuspec bench/* +configurations/* +components/* +dist/cdnjs/* +dist/components/* spec/* src/* +tasks/* +publish/* vendor/* diff --git a/.rspec b/.rspec deleted file mode 100644 index 62c58f049..000000000 --- a/.rspec +++ /dev/null @@ -1 +0,0 @@ --cfs diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 000000000..1e4e9651f --- /dev/null +++ b/.travis.yml @@ -0,0 +1,27 @@ +language: node_js +node_js: + - "0.8" + - "0.10" + +before_install: + - npm install -g grunt-cli + +script: + - grunt --stack default metrics publish:latest + +email: + on_failure: change + on_success: never +env: + global: + - S3_BUCKET_NAME=builds.handlebarsjs.com + - secure: ! 'PJaukuvkBBsSDOqbIcNSSMgb96VVEaIt/eq9GPjXPeFbSd3hXgwhwVE62Lrq + + tJO8BaUfX+PzpiQjEl4D5/KBmvlFZ057Hqmy0zmPOT5mDZfJe8Ja5zyvTMb+ + + KkCWN/tjAp8kawHojE04pn6jIpPdwXFnAYwPhaHbATFrmdt9fdg=' + - secure: ! 'mBcGL2tnmiRujJdV/4fxrVd8E8wn6AW9IQKVcMv8tvOc7i5dOzZ39rpBKLuT + + MRXDtMV1LyLiuKYb1pHj1IyeadEahcLYFfGygF4LG7Yzp4NWHtRzQ7Q8LXaJ + + V7dXDboYCFkn2a8/Rtx1YSVh/sCONf5UoRC+MUIqrj4UiHN9r3s=' diff --git a/Gemfile b/Gemfile deleted file mode 100644 index cf092ce28..000000000 --- a/Gemfile +++ /dev/null @@ -1,5 +0,0 @@ -source "http://rubygems.org" - -gem "rake" -gem "therubyracer", ">= 0.9.8", "< 0.11" -gem "rspec" diff --git a/Gemfile.lock b/Gemfile.lock deleted file mode 100644 index ce8338f4e..000000000 --- a/Gemfile.lock +++ /dev/null @@ -1,24 +0,0 @@ -GEM - remote: http://rubygems.org/ - specs: - diff-lcs (1.1.3) - libv8 (3.3.10.4) - rake (10.0.3) - rspec (2.12.0) - rspec-core (~> 2.12.0) - rspec-expectations (~> 2.12.0) - rspec-mocks (~> 2.12.0) - rspec-core (2.12.2) - rspec-expectations (2.12.1) - diff-lcs (~> 1.1.3) - rspec-mocks (2.12.1) - therubyracer (0.10.2) - libv8 (~> 3.3.10) - -PLATFORMS - ruby - -DEPENDENCIES - rake - rspec - therubyracer (>= 0.9.8, < 0.11) diff --git a/Gruntfile.js b/Gruntfile.js new file mode 100644 index 000000000..4fe9b0531 --- /dev/null +++ b/Gruntfile.js @@ -0,0 +1,164 @@ +var childProcess = require('child_process'); + +module.exports = function(grunt) { + + grunt.initConfig({ + pkg: grunt.file.readJSON('package.json'), + + jshint: { + options: { + jshintrc: '.jshintrc', + force: true + }, + files: [ + 'lib/**/!(parser).js' + ] + }, + + clean: ["dist", "lib/handlebars/compiler/parser.js"], + + copy: { + dist: { + options: { + processContent: function(content, path) { + return grunt.template.process('/*!\n\n <%= pkg.name %> v<%= pkg.version %>\n\n<%= grunt.file.read("LICENSE") %>\n@license\n*/\n') + + content; + } + }, + files: [ + {expand: true, cwd: 'dist/', src: ['*.js'], dest: 'dist/'} + ] + }, + cdnjs: { + files: [ + {expand: true, cwd: 'dist/', src: ['*.js'], dest: 'dist/cdnjs'} + ] + }, + components: { + files: [ + {expand: true, cwd: 'components/', src: ['**'], dest: 'dist/components'}, + {expand: true, cwd: 'dist/', src: ['*.js'], dest: 'dist/components'} + ] + } + }, + + transpile: { + amd: { + type: "amd", + anonymous: true, + files: [{ + expand: true, + cwd: 'lib/', + src: '**/!(index).js', + dest: 'dist/amd/' + }] + }, + + cjs: { + type: 'cjs', + files: [{ + expand: true, + cwd: 'lib/', + src: '**/!(index).js', + dest: 'dist/cjs/' + }] + } + }, + + packager: { + options: { + export: 'Handlebars' + }, + + global: { + files: [{ + cwd: 'lib/', + expand: true, + src: ['handlebars*.js'], + dest: 'dist/' + }] + } + }, + requirejs: { + options: { + optimize: "none", + baseUrl: "dist/amd/" + }, + dist: { + options: { + name: "handlebars", + out: "dist/handlebars.amd.js" + } + }, + runtime: { + options: { + name: "handlebars.runtime", + out: "dist/handlebars.runtime.amd.js" + } + } + }, + + uglify: { + options: { + mangle: true, + compress: true, + preserveComments: 'some' + }, + dist: { + files: [{ + cwd: 'dist/', + expand: true, + src: ['handlebars*.js'], + dest: 'dist/', + rename: function(dest, src) { + return dest + src.replace(/\.js$/, '.min.js'); + } + }] + } + } + }); + + // Build a new version of the library + this.registerTask('build', "Builds a distributable version of the current project", [ + 'jshint', + 'clean', + 'parser', + 'node', + 'globals']); + + this.registerTask('amd', ['transpile:amd', 'requirejs']); + this.registerTask('node', ['transpile:cjs']); + this.registerTask('globals', ['packager-fork']); + + this.registerTask('release', 'Build final packages', ['amd', 'uglify', 'copy:dist', 'copy:components', 'copy:cdnjs']); + + // Load tasks from npm + grunt.loadNpmTasks('grunt-contrib-clean'); + grunt.loadNpmTasks('grunt-contrib-copy'); + grunt.loadNpmTasks('grunt-contrib-requirejs'); + grunt.loadNpmTasks('grunt-contrib-jshint'); + grunt.loadNpmTasks('grunt-contrib-uglify'); + grunt.loadNpmTasks('grunt-es6-module-transpiler'); + + grunt.task.loadTasks('tasks'); + + grunt.registerTask('packager-fork', function() { + // Allows us to run the packager task out of process to work around the multiple + // traceur exec issues + grunt.util.spawn({grunt: true, args: ['--stack', 'packager'], opts: {stdio: 'inherit'}}, this.async()); + }); + grunt.registerTask('test', function() { + var done = this.async(); + + var runner = childProcess.fork('./spec/env/runner', [], {stdio: 'inherit'}); + runner.on('close', function(code) { + if (code != 0) { + grunt.fatal(code + ' tests failed'); + } + done(); + }); + }); + grunt.registerTask('bench', ['metrics']); + + grunt.registerTask('default', ['build', 'test', 'release']); +}; diff --git a/README.markdown b/README.markdown index f775815e2..6ea94f8fb 100644 --- a/README.markdown +++ b/README.markdown @@ -1,5 +1,6 @@ [![Build Status](https://travis-ci.org/wycats/handlebars.js.png?branch=master)](https://travis-ci.org/wycats/handlebars.js) + Handlebars.js ============= @@ -11,13 +12,23 @@ 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 the official site](http://handlebarsjs.com/) and add it to your web pages (you should usually use the most recent version). +Alternatively, if you prefer having the latest version of handlebars from +the 'master' branch, passing builds of the 'master' branch are automatically +published to S3. You may download the latest passing master build by grabbing +a `handlebars-latest.js` file from the [builds page][builds-page]. When the +build is published, it is also available as a `handlebars-gitSHA.js` file on +the builds page if you need a version to refer to others. +`handlebars-runtime.js` builds are also available. + +**Note**: The S3 builds page is provided as a convenience for the community, +but you should not use it for hosting Handlebars in production. + Usage ----- In general, the syntax of Handlebars.js templates is a superset @@ -108,7 +119,7 @@ To display data from descendant contexts, use the `.` character. So, for example, if your data were structured like: ```js -var data = {"person": { "name": "Alan" }, company: {"name": "Rad, Inc." } }; +var data = {"person": { "name": "Alan" }, "company": {"name": "Rad, Inc." } }; ``` You could display the person's name from the top-level context with the @@ -260,13 +271,20 @@ Precompile handlebar templates. Usage: handlebars template... Options: - -a, --amd Create an AMD format function (allows loading with RequireJS) [boolean] - -f, --output Output File [string] - -k, --known Known helpers [string] - -o, --knownOnly Known helpers only [boolean] - -m, --min Minimize output [boolean] - -s, --simple Output template function only. [boolean] - -r, --root Template root. Base value that will be stripped from template names. [string] + -a, --amd Create an AMD format function (allows loading with RequireJS) [boolean] + -f, --output Output File [string] + -k, --known Known helpers [string] + -o, --knownOnly Known helpers only [boolean] + -m, --min Minimize output [boolean] + -s, --simple Output template function only. [boolean] + -r, --root Template root. Base value that will be stripped from template names. [string] + -c, --commonjs Exports CommonJS style, path to Handlebars module [string] + -h, --handlebarPath Path to handlebar.js (only valid for amd-style) [string] + -n, --namespace Template namespace [string] + -p, --partial Compiling a partial template [boolean] + -d, --data Include data when compiling [boolean] + -e, --extension Template extension. [string] + -b, --bom Removes the BOM (Byte Order Mark) from the beginning of the templates. [boolean] If using the precompiler's normal mode, the resulting templates will be @@ -289,6 +307,8 @@ normal. helpers for size and speed. - When all helpers are known in advance the `--knownOnly` argument may be used to optimize all block helper references. +- Implementations that do not use `@data` variables can improve performance of + iteration centric templates by specifying `{data: false}` in the compiler options. Supported Environments ---------------------- @@ -322,8 +342,7 @@ and we will have some benchmarks in the near future. Building -------- -To build handlebars, just run `rake release`, and you will get two files -in the `dist` directory. +To build handlebars, just run `grunt build`, and the build will output to the `dist` directory. Upgrading @@ -337,47 +356,54 @@ Known Issues * Using a variable, helper, or partial named `class` causes errors in IE browsers. (Instead, use `className`) Handlebars in the Wild ------------------ +---------------------- + +* [Assemble](http://assemble.io), by [@jonschlinkert](https://github.com/jonschlinkert) + and [@doowb](https://github.com/doowb), is a static site generator that uses Handlebars.js + as its template engine. +* [CoSchedule](http://coschedule.com) An editorial calendar for WordPress that uses Handlebars.js +* [Ember.js](http://www.emberjs.com) makes Handlebars.js the primary way to + structure your views, also with automatic data binding support. +* [handlebars_assets](http://github.com/leshill/handlebars_assets): A Rails Asset Pipeline gem + from Les Hill (@leshill). +* [handlebars-helpers](https://github.com/assemble/handlebars-helpers) is an extensive library + with 100+ handlebars helpers. +* [hbs](http://github.com/donpark/hbs): An Express.js view engine adapter for Handlebars.js, + from Don Park. * [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). +* [jQuery plugin](http://71104.github.io/jquery-handlebars/): allows you to use + Handlebars.js with [jQuery](http://jquery.com/). +* [Lumbar](http://walmartlabs.github.io/lumbar) provides easy module-based template management for + handlebars projects. * [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). -* [Gist about Synchronous and asynchronous loading of external handlebars templates](https://gist.github.com/2287070) -* [Lumbar](walmartlabs.github.io/lumbar) provides easy module-based template management for handlebars projects. * [YUI](http://yuilibrary.com/yui/docs/handlebars/) implements a port of handlebars +* [Swag](https://github.com/elving/swag) by [@elving](https://github.com/elving) is a growing collection of helpers for handlebars.js. Give your handlebars.js templates some swag son! + +External Resources +------------------ + +* [Gist about Synchronous and asynchronous loading of external handlebars templates](https://gist.github.com/2287070) Have a project using Handlebars? Send us a [pull request](https://github.com/wycats/handlebars.js/pull/new/master)! Helping Out ----------- + To build Handlebars.js you'll need a few things installed. * Node.js -* Ruby -* therubyracer, for running tests - `gem install therubyracer` -* rspec, for running tests - `gem install rspec` +* [Grunt](http://gruntjs.com/getting-started) -There's a Gemfile in the repo, so you can run `bundle` to install rspec -and therubyracer if you've got bundler installed. +Project dependencies may be installed via `npm install`. -To build Handlebars.js from scratch, you'll want to run `rake compile` +To build Handlebars.js from scratch, you'll want to run `grunt` in the root of the project. That will build Handlebars and output the -results to the dist/ folder. To run tests, run `rake test`. You can also -run our set of benchmarks with `rake bench`. Node tests can be run with -`npm test` or `rake npm_test`. The default rake target will compile and -run both test suites. - -Some environments, notably Windows, have issues running therubyracer. Under these -envrionments the `rake compile` and `npm test` should be sufficient to test -most handlebars functionality. +results to the dist/ folder. To re-run tests, run `grunt test` or `npm test`. +You can also run our set of benchmarks with `grunt bench`. 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). @@ -389,3 +415,4 @@ License ------- Handlebars.js is released under the MIT license. +[builds-page]: http://builds.handlebarsjs.com.s3.amazonaws.com/index.html diff --git a/Rakefile b/Rakefile deleted file mode 100644 index 794686f1e..000000000 --- a/Rakefile +++ /dev/null @@ -1,136 +0,0 @@ -require "rubygems" -require "bundler/setup" - -def compile_parser - system "./node_modules/.bin/jison -m js src/handlebars.yy src/handlebars.l" - if $?.success? - File.open("lib/handlebars/compiler/parser.js", "w") do |file| - file.puts File.read("src/parser-prefix.js") + File.read("handlebars.js") + File.read("src/parser-suffix.js") - end - - sh "rm handlebars.js" - else - fail "Failed to run Jison." - end -end - -file "lib/handlebars/compiler/parser.js" => ["src/handlebars.yy","src/handlebars.l"] do - if File.exists?('./node_modules/jison') - compile_parser - else - puts "Jison is not installed. Trying `npm install jison`." - sh "npm install" - compile_parser - end -end - -task :compile => "lib/handlebars/compiler/parser.js" - -desc "run the spec suite" -task :spec => [:release] do - rc = system "rspec -cfs spec" - fail "rspec spec failed with exit code #{$?.exitstatus}" if (rc.nil? || ! rc || $?.exitstatus != 0) -end - -desc "run the npm test suite" -task :npm_test => [:release] do - rc = system "npm test" - fail "npm test failed with exit code #{$?.exitstatus}" if (rc.nil? || ! rc || $?.exitstatus != 0) -end - -task :default => [:compile, :spec, :npm_test] - -def remove_exports(string) - match = string.match(%r{^// BEGIN\(BROWSER\)\n(.*)\n^// END\(BROWSER\)}m) - match ? match[1] : string -end - -minimal_deps = %w(browser-prefix base compiler/parser compiler/base compiler/ast utils compiler/compiler runtime browser-suffix).map do |file| - "lib/handlebars/#{file}.js" -end - -runtime_deps = %w(browser-prefix base utils runtime browser-suffix).map do |file| - "lib/handlebars/#{file}.js" -end - -directory "dist" - -minimal_deps.unshift "dist" - -def build_for_task(task) - FileUtils.rm_rf("dist/*") if File.directory?("dist") - FileUtils.mkdir_p("dist") - - contents = ["/*\n\n" + File.read('LICENSE') + "\n*/\n"] - task.prerequisites.each do |filename| - next if filename == "dist" - - contents << "// #{filename}\n" + remove_exports(File.read(filename)) + ";" - end - - File.open(task.name, "w") do |file| - file.puts contents.join("\n") - end -end - -file "dist/handlebars.js" => minimal_deps do |task| - build_for_task(task) -end - -file "dist/handlebars.runtime.js" => runtime_deps do |task| - build_for_task(task) -end - -task :build => [:compile] do |task| - # Since the tests are dependent on this always rebuild. - Rake::Task["dist/handlebars.js"].execute -end -task :runtime => [:compile] do |task| - # Since the tests are dependent on this always rebuild. - Rake::Task["dist/handlebars.runtime.js"].execute -end - -# Updates the various version numbers. -task :version => [] do |task| - # TODO : Pull from package.json when the version numbers are synced - version = File.read("lib/handlebars/base.js").match(/Handlebars.VERSION = "(.*)";/)[1] - - content = File.read("bower.json") - File.open("bower.json", "w") do |file| - file.puts content.gsub(/"version":.*/, "\"version\": \"#{version}\",") - end - - content = File.read("handlebars.js.nuspec") - File.open("handlebars.js.nuspec", "w") do |file| - file.puts content.gsub(/.*<\/version>/, "#{version}") - end -end - -task :minify => [] do |task| - system "./node_modules/.bin/uglifyjs --comments -o dist/handlebars.min.js dist/handlebars.js" - system "./node_modules/.bin/uglifyjs --comments -o dist/handlebars.runtime.min.js dist/handlebars.runtime.js" -end - -desc "build the build and runtime version of handlebars" -task :release => [:version, :build, :runtime, :minify] - -directory "vendor" - -desc "benchmark against dust.js and mustache.js" -task :bench => "vendor" do - require "open-uri" - - #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 && 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 diff --git a/bench/dist-size.js b/bench/dist-size.js new file mode 100644 index 000000000..9e5fdc0f5 --- /dev/null +++ b/bench/dist-size.js @@ -0,0 +1,39 @@ +var _ = require('underscore'), + async = require('async'), + fs = require('fs'), + zlib = require('zlib'); + +module.exports = function(grunt, callback) { + var distFiles = fs.readdirSync('dist'), + distSizes = {}; + + async.each(distFiles, function(file, callback) { + var content; + try { + content = fs.readFileSync('dist/' + file); + } catch (err) { + if (err.code === 'EISDIR') { + callback(); + return; + } else { + throw err; + } + } + + file = file.replace(/\.js/, '').replace(/\./g, '_'); + distSizes[file] = content.length; + + zlib.gzip(content, function(err, data) { + if (err) { + throw err; + } + + distSizes[file + '_gz'] = data.length; + callback(); + }); + }, + function() { + grunt.log.writeln('Distribution sizes: ' + JSON.stringify(distSizes, undefined, 2)); + callback([distSizes]); + }); +}; diff --git a/bench/handlebars.js b/bench/handlebars.js deleted file mode 100644 index c7f6c1058..000000000 --- a/bench/handlebars.js +++ /dev/null @@ -1,172 +0,0 @@ -var BenchWarmer = require("./benchwarmer"); -Handlebars = require("../lib/handlebars"); - -var dust, Mustache, eco; - -try { - dust = require("dust"); -} catch (err) { /* NOP */ } - -try { - Mustache = require("mustache"); -} catch (err) { /* NOP */ } - -try { - var ecoExports = require("eco"); - eco = function(str) { - return ecoExports(str); - } -} catch (err) { /* NOP */ } - -var benchDetails = { - string: { - context: {}, - handlebars: "Hello world", - dust: "Hello world", - mustache: "Hello world", - eco: "Hello world" - }, - variables: { - context: {name: "Mick", count: 30}, - handlebars: "Hello {{name}}! You have {{count}} new messages.", - dust: "Hello {name}! You have {count} new messages.", - mustache: "Hello {{name}}! You have {{count}} new messages.", - eco: "Hello <%= @name %>! You have <%= @count %> new messages." - }, - object: { - context: { person: { name: "Larry", age: 45 } }, - handlebars: "{{#with person}}{{name}}{{age}}{{/with}}", - dust: "{#person}{name}{age}{/person}", - mustache: "{{#person}}{{name}}{{age}}{{/person}}" - }, - array: { - context: { names: [{name: "Moe"}, {name: "Larry"}, {name: "Curly"}, {name: "Shemp"}] }, - handlebars: "{{#each names}}{{name}}{{/each}}", - dust: "{#names}{name}{/names}", - mustache: "{{#names}}{{name}}{{/names}}", - eco: "<% for item in @names: %><%= item.name %><% end %>" - }, - partial: { - context: { peeps: [{name: "Moe", count: 15}, {name: "Larry", count: 5}, {name: "Curly", count: 1}] }, - partials: { - mustache: { variables: "Hello {{name}}! You have {{count}} new messages." }, - handlebars: { variables: "Hello {{name}}! You have {{count}} new messages." } - }, - handlebars: "{{#each peeps}}{{>variables}}{{/each}}", - dust: "{#peeps}{>variables/}{/peeps}", - mustache: "{{#peeps}}{{>variables}}{{/peeps}}" - }, - recursion: { - context: { name: '1', kids: [{ name: '1.1', kids: [{name: '1.1.1', kids: []}] }] }, - partials: { - mustache: { recursion: "{{name}}{{#kids}}{{>recursion}}{{/kids}}" }, - handlebars: { recursion: "{{name}}{{#each kids}}{{>recursion}}{{/each}}" } - }, - handlebars: "{{name}}{{#each kids}}{{>recursion}}{{/each}}", - dust: "{name}{#kids}{>recursion:./}{/kids}", - mustache: "{{name}}{{#kids}}{{>recursion}}{{/kids}}" - }, - complex: { - handlebars: "

{{header}}

{{#if items}}{{^}}

The list is empty.

{{/if}}", - - dust: "

{header}

\n" + - "{?items}\n" + - " \n" + - "{:else}\n" + - "

The list is empty.

\n" + - "{/items}", - context: { - header: function() { - return "Colors"; - }, - items: [ - {name: "red", current: true, url: "#Red"}, - {name: "green", current: false, url: "#Green"}, - {name: "blue", current: false, url: "#Blue"} - ] - } - } - -}; - -handlebarsTemplates = {}; -ecoTemplates = {}; - -var warmer = new BenchWarmer(); - -var makeSuite = function(name) { - warmer.suite(name, function(bench) { - var templateName = name; - var details = benchDetails[templateName]; - var mustachePartials = details.partials && details.partials.mustache; - var mustacheSource = details.mustache; - var context = details.context; - - var error = function() { throw new Error("EWOT"); }; - - if (dust) { - bench("dust", function() { - dust.render(templateName, context, function(err, out) { }); - }); - } - - bench("handlebars", function() { - handlebarsTemplates[templateName](context); - }); - - if (eco) { - if(ecoTemplates[templateName]) { - bench("eco", function() { - ecoTemplates[templateName](context); - }); - } else { - bench("eco", error); - } - } - - if (Mustache && mustacheSource) { - bench("mustache", function() { - Mustache.to_html(mustacheSource, context, mustachePartials); - }); - } else { - bench("mustache", error); - } - }); -} - -for(var name in benchDetails) { - if(benchDetails.hasOwnProperty(name)) { - if (dust) { - dust.loadSource(dust.compile(benchDetails[name].dust, name)); - } - handlebarsTemplates[name] = Handlebars.compile(benchDetails[name].handlebars); - - if (eco && benchDetails[name].eco) { - ecoTemplates[name] = eco(benchDetails[name].eco); - } - - var partials = benchDetails[name].partials; - if(partials) { - for(var partialName in partials.handlebars) { - if(partials.handlebars.hasOwnProperty(partialName)) { - Handlebars.registerPartial(partialName, partials.handlebars[partialName]); - } - } - } - - makeSuite(name); - } -} - -warmer.bench(); diff --git a/bench/index.js b/bench/index.js new file mode 100644 index 000000000..462b046f5 --- /dev/null +++ b/bench/index.js @@ -0,0 +1,14 @@ +var fs = require('fs'); + +var metrics = fs.readdirSync(__dirname); +metrics.forEach(function(metric) { + if (metric === 'index.js' || !/(.*)\.js$/.test(metric)) { + return; + } + + var name = RegExp.$1; + metric = require('./' + name); + if (metric instanceof Function) { + module.exports[name] = metric; + } +}); diff --git a/bench/precompile-size.js b/bench/precompile-size.js new file mode 100644 index 000000000..66eacd025 --- /dev/null +++ b/bench/precompile-size.js @@ -0,0 +1,19 @@ +var _ = require('underscore'), + templates = require('./templates'); + +module.exports = function(grunt, callback) { + // Deferring to here in case we have a build for parser, etc as part of this grunt exec + var Handlebars = require('../lib'); + + var templateSizes = {}; + _.each(templates, function(info, template) { + var src = info.handlebars, + compiled = Handlebars.precompile(src, {}), + knownHelpers = Handlebars.precompile(src, {knownHelpersOnly: true, knownHelpers: info.helpers}); + + templateSizes[template] = compiled.length; + templateSizes['knownOnly_' + template] = knownHelpers.length; + }); + grunt.log.writeln('Precompiled sizes: ' + JSON.stringify(templateSizes, undefined, 2)); + callback([templateSizes]); +}; diff --git a/bench/templates/arguments.js b/bench/templates/arguments.js new file mode 100644 index 000000000..5480c8d8e --- /dev/null +++ b/bench/templates/arguments.js @@ -0,0 +1,12 @@ +module.exports = { + helpers: { + foo: function(options) { + return ''; + } + }, + context: { + bar: true + }, + + handlebars: '{{foo person "person" 1 true foo=bar foo="person" foo=1 foo=true}}' +}; diff --git a/bench/templates/array-each.js b/bench/templates/array-each.js new file mode 100644 index 000000000..f1eb1e8e9 --- /dev/null +++ b/bench/templates/array-each.js @@ -0,0 +1,7 @@ +module.exports = { + context: { names: [{name: "Moe"}, {name: "Larry"}, {name: "Curly"}, {name: "Shemp"}] }, + handlebars: "{{#each names}}{{name}}{{/each}}", + dust: "{#names}{name}{/names}", + mustache: "{{#names}}{{name}}{{/names}}", + eco: "<% for item in @names: %><%= item.name %><% end %>" +}; diff --git a/bench/templates/array-mustache.js b/bench/templates/array-mustache.js new file mode 100644 index 000000000..908f805ea --- /dev/null +++ b/bench/templates/array-mustache.js @@ -0,0 +1,4 @@ +module.exports = { + context: { names: [{name: "Moe"}, {name: "Larry"}, {name: "Curly"}, {name: "Shemp"}] }, + handlebars: "{{#names}}{{name}}{{/names}}" +} diff --git a/bench/templates/complex.dust b/bench/templates/complex.dust new file mode 100644 index 000000000..ff453c2b8 --- /dev/null +++ b/bench/templates/complex.dust @@ -0,0 +1,14 @@ +

{header}

+{?items} + +{:else} +

The list is empty.

+{/items} diff --git a/bench/templates/complex.eco b/bench/templates/complex.eco new file mode 100644 index 000000000..42fb55b48 --- /dev/null +++ b/bench/templates/complex.eco @@ -0,0 +1,14 @@ +

<%= @header() %>

+<% if @items.length: %> + +<% else: %> +

The list is empty.

+<% end %> diff --git a/bench/templates/complex.handlebars b/bench/templates/complex.handlebars new file mode 100644 index 000000000..4a5953445 --- /dev/null +++ b/bench/templates/complex.handlebars @@ -0,0 +1,14 @@ +

{{header}}

+{{#if items}} + +{{^}} +

The list is empty.

+{{/if}} diff --git a/bench/templates/complex.js b/bench/templates/complex.js new file mode 100644 index 000000000..ddf361b59 --- /dev/null +++ b/bench/templates/complex.js @@ -0,0 +1,20 @@ +var fs = require('fs'); + +module.exports = { + context: { + header: function() { + return "Colors"; + }, + hasItems: true, // To make things fairer in mustache land due to no `{{if}}` construct on arrays + items: [ + {name: "red", current: true, url: "#Red"}, + {name: "green", current: false, url: "#Green"}, + {name: "blue", current: false, url: "#Blue"} + ] + }, + + handlebars: fs.readFileSync(__dirname + '/complex.handlebars').toString(), + dust: fs.readFileSync(__dirname + '/complex.dust').toString(), + eco: fs.readFileSync(__dirname + '/complex.eco').toString(), + mustache: fs.readFileSync(__dirname + '/complex.mustache').toString() +}; diff --git a/bench/templates/complex.mustache b/bench/templates/complex.mustache new file mode 100644 index 000000000..9e425872f --- /dev/null +++ b/bench/templates/complex.mustache @@ -0,0 +1,13 @@ +

{{header}}

+{{#hasItems}} + +{{/hasItems}} diff --git a/bench/templates/data.js b/bench/templates/data.js new file mode 100644 index 000000000..f532decd5 --- /dev/null +++ b/bench/templates/data.js @@ -0,0 +1,4 @@ +module.exports = { + context: { names: [{name: "Moe"}, {name: "Larry"}, {name: "Curly"}, {name: "Shemp"}] }, + handlebars: "{{#each names}}{{@index}}{{name}}{{/each}}" +} diff --git a/bench/templates/index.js b/bench/templates/index.js new file mode 100644 index 000000000..a718ea388 --- /dev/null +++ b/bench/templates/index.js @@ -0,0 +1,9 @@ +var fs = require('fs'); + +var templates = fs.readdirSync(__dirname); +templates.forEach(function(template) { + if (template === 'index.js' || !/(.*)\.js$/.test(template)) { + return; + } + module.exports[RegExp.$1] = require('./' + RegExp.$1); +}); diff --git a/bench/templates/object-mustache.js b/bench/templates/object-mustache.js new file mode 100644 index 000000000..52dbc26e3 --- /dev/null +++ b/bench/templates/object-mustache.js @@ -0,0 +1,4 @@ +module.exports = { + context: { person: { name: "Larry", age: 45 } }, + handlebars: "{{#person}}{{name}}{{age}}{{/person}}" +}; diff --git a/bench/templates/object.js b/bench/templates/object.js new file mode 100644 index 000000000..fef127ada --- /dev/null +++ b/bench/templates/object.js @@ -0,0 +1,7 @@ +module.exports = { + context: { person: { name: "Larry", age: 45 } }, + handlebars: "{{#with person}}{{name}}{{age}}{{/with}}", + dust: "{#person}{name}{age}{/person}", + eco: "<%= @person.name %><%= @person.age %>", + mustache: "{{#person}}{{name}}{{age}}{{/person}}" +}; diff --git a/bench/templates/partial-recursion.js b/bench/templates/partial-recursion.js new file mode 100644 index 000000000..9d604fd02 --- /dev/null +++ b/bench/templates/partial-recursion.js @@ -0,0 +1,10 @@ +module.exports = { + context: { name: '1', kids: [{ name: '1.1', kids: [{name: '1.1.1', kids: []}] }] }, + partials: { + mustache: { recursion: "{{name}}{{#kids}}{{>recursion}}{{/kids}}" }, + handlebars: { recursion: "{{name}}{{#each kids}}{{>recursion}}{{/each}}" } + }, + handlebars: "{{name}}{{#each kids}}{{>recursion}}{{/each}}", + dust: "{name}{#kids}{>recursion:./}{/kids}", + mustache: "{{name}}{{#kids}}{{>recursion}}{{/kids}}" +}; diff --git a/bench/templates/partial.js b/bench/templates/partial.js new file mode 100644 index 000000000..a6e663156 --- /dev/null +++ b/bench/templates/partial.js @@ -0,0 +1,11 @@ +module.exports = { + context: { peeps: [{name: "Moe", count: 15}, {name: "Larry", count: 5}, {name: "Curly", count: 1}] }, + partials: { + mustache: { variables: "Hello {{name}}! You have {{count}} new messages." }, + handlebars: { variables: "Hello {{name}}! You have {{count}} new messages." } + }, + + handlebars: "{{#each peeps}}{{>variables}}{{/each}}", + dust: "{#peeps}{>variables/}{/peeps}", + mustache: "{{#peeps}}{{>variables}}{{/peeps}}" +}; diff --git a/bench/templates/paths.js b/bench/templates/paths.js new file mode 100644 index 000000000..0a426dd83 --- /dev/null +++ b/bench/templates/paths.js @@ -0,0 +1,7 @@ +module.exports = { + context: { person: { name: "Larry", age: 45 } }, + handlebars: "{{person.name}}{{person.age}}{{person.foo}}{{animal.age}}", + dust: "{person.name}{person.age}{person.foo}{animal.age}", + eco: "<%= @person.name %><%= @person.age %><%= @person.foo %><% if @animal: %><%= @animal.age %><% end %>", + mustache: "{{person.name}}{{person.age}}{{person.foo}}{{animal.age}}" +}; diff --git a/bench/templates/string.js b/bench/templates/string.js new file mode 100644 index 000000000..335e37cf9 --- /dev/null +++ b/bench/templates/string.js @@ -0,0 +1,7 @@ +module.exports = { + context: {}, + handlebars: "Hello world", + dust: "Hello world", + mustache: "Hello world", + eco: "Hello world" +}; diff --git a/bench/templates/variables.js b/bench/templates/variables.js new file mode 100644 index 000000000..d354238b1 --- /dev/null +++ b/bench/templates/variables.js @@ -0,0 +1,8 @@ +module.exports = { + context: {name: "Mick", count: 30}, + handlebars: "Hello {{name}}! You have {{count}} new messages.", + dust: "Hello {name}! You have {count} new messages.", + mustache: "Hello {{name}}! You have {{count}} new messages.", + eco: "Hello <%= @name %>! You have <%= @count %> new messages." +}; + diff --git a/bench/throughput.js b/bench/throughput.js new file mode 100644 index 000000000..308446a09 --- /dev/null +++ b/bench/throughput.js @@ -0,0 +1,123 @@ +var _ = require('underscore'), + runner = require('./util/template-runner'), + templates = require('./templates'), + + eco, dust, Handlebars, Mustache, eco; + +try { + dust = require("dustjs-linkedin"); +} catch (err) { /* NOP */ } + +try { + Mustache = require("mustache"); +} catch (err) { /* NOP */ } + +try { + eco = require("eco"); +} catch (err) { /* NOP */ } + +function error() { + throw new Error("EWOT"); +} + +function makeSuite(bench, name, template, handlebarsOnly) { + // Create aliases to minimize any impact from having to walk up the closure tree. + var templateName = name, + + context = template.context, + partials = template.partials, + + handlebarsOut, + dustOut, + ecoOut, + mustacheOut; + + var handlebar = Handlebars.compile(template.handlebars, {data: false}), + options = {helpers: template.helpers}; + _.each(template.partials && template.partials.handlebars, function(partial, name) { + Handlebars.registerPartial(name, Handlebars.compile(partial, {data: false})); + }); + + handlebarsOut = handlebar(context, options); + bench("handlebars", function() { + handlebar(context, options); + }); + + if (handlebarsOnly) { + return; + } + + if (dust) { + if (template.dust) { + dustOut = false; + dust.loadSource(dust.compile(template.dust, templateName)); + + dust.render(templateName, context, function(err, out) { dustOut = out; }); + + bench("dust", function() { + dust.render(templateName, context, function(err, out) { }); + }); + } else { + bench('dust', error); + } + } + + if (eco) { + if (template.eco) { + var ecoTemplate = eco.compile(template.eco); + + ecoOut = ecoTemplate(context); + + bench("eco", function() { + ecoTemplate(context); + }); + } else { + bench("eco", error); + } + } + + if (Mustache) { + var mustacheSource = template.mustache, + mustachePartials = partials && partials.mustache; + + if (mustacheSource) { + mustacheOut = Mustache.to_html(mustacheSource, context, mustachePartials); + + bench("mustache", function() { + Mustache.to_html(mustacheSource, context, mustachePartials); + }); + } else { + bench("mustache", error); + } + } + + // Hack around whitespace until we have whitespace control + handlebarsOut = handlebarsOut.replace(/\s/g, ''); + function compare(b, lang) { + if (b == null) { + return; + } + + b = b.replace(/\s/g, ''); + + if (handlebarsOut !== b) { + throw new Error('Template output mismatch: ' + name + + '\n\nHandlebars: ' + handlebarsOut + + '\n\n' + lang + ': ' + b); + } + } + + compare(dustOut, 'dust'); + compare(ecoOut, 'eco'); + compare(mustacheOut, 'mustache'); +} + +module.exports = function(grunt, callback) { + // Deferring load incase we are being run inline with the grunt build + Handlebars = require('../lib'); + + console.log('Execution Throughput'); + runner(grunt, makeSuite, function(times, scaled) { + callback(scaled); + }); +}; diff --git a/bench/benchwarmer.js b/bench/util/benchwarmer.js similarity index 50% rename from bench/benchwarmer.js rename to bench/util/benchwarmer.js index 25a250619..7496a3e9e 100644 --- a/bench/benchwarmer.js +++ b/bench/util/benchwarmer.js @@ -1,10 +1,13 @@ - -var Benchmark = require("benchmark"); +var _ = require('underscore'), + Benchmark = require("benchmark"); var BenchWarmer = function(names) { this.benchmarks = []; this.currentBenches = []; this.names = []; + this.times = {}; + this.minimum = Infinity; + this.maximum = -Infinity; this.errors = {}; }; @@ -12,28 +15,11 @@ var print = require("sys").print; BenchWarmer.prototype = { winners: function(benches) { - var result = Benchmark.filter(benches, function(bench) { return bench.cycles; }); - - if (result.length > 1) { - result.sort(function(a, b) { return b.compare(a); }); - first = result[0]; - last = result[result.length - 1]; - - var winners = []; - - Benchmark.each(result, function(bench) { - if (bench.compare(first) === 0) { - winners.push(bench); - } - }); - - return winners; - } else { - return result; - } + return Benchmark.filter(benches, 'fastest'); }, suite: function(suite, fn) { this.suiteName = suite; + this.times[suite] = {}; this.first = true; var self = this; @@ -50,9 +36,7 @@ BenchWarmer.prototype = { var first = this.first, suiteName = this.suiteName, self = this; this.first = false; - var bench = new Benchmark(function() { - fn(); - }, { + var bench = new Benchmark(fn, { name: this.suiteName + ": " + name, onComplete: function() { if(first) { self.startLine(suiteName); } @@ -62,44 +46,49 @@ BenchWarmer.prototype = { self.errors[this.name] = this; } }); + bench.suiteName = this.suiteName; + bench.benchName = name; this.benchmarks.push(bench); }, - bench: function() { - var benchSize = 0, names = this.names, self = this, i, l; - for(i=0, l=names.length; i handlebars.js - 1.0.0 + 1.1.0 handlebars.js Authors https://github.com/wycats/handlebars.js/blob/master/LICENSE https://github.com/wycats/handlebars.js/ @@ -12,6 +12,6 @@ handlebars mustache template html - + diff --git a/components/lib/handlebars/source.rb b/components/lib/handlebars/source.rb new file mode 100644 index 000000000..4f3f8bfce --- /dev/null +++ b/components/lib/handlebars/source.rb @@ -0,0 +1,11 @@ +module Handlebars + module Source + def self.bundled_path + File.expand_path("../../../handlebars.js", __FILE__) + end + + def self.runtime_bundled_path + File.expand_path("../../../handlebars.runtime.js", __FILE__) + end + end +end diff --git a/dist/handlebars.js b/dist/handlebars.js deleted file mode 100644 index c70f09d1d..000000000 --- a/dist/handlebars.js +++ /dev/null @@ -1,2278 +0,0 @@ -/* - -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. - -*/ - -// lib/handlebars/browser-prefix.js -var Handlebars = {}; - -(function(Handlebars, undefined) { -; -// lib/handlebars/base.js - -Handlebars.VERSION = "1.0.0"; -Handlebars.COMPILER_REVISION = 4; - -Handlebars.REVISION_CHANGES = { - 1: '<= 1.0.rc.2', // 1.0.rc.2 is actually rev2 but doesn't report it - 2: '== 1.0.0-rc.3', - 3: '== 1.0.0-rc.4', - 4: '>= 1.0.0' -}; - -Handlebars.helpers = {}; -Handlebars.partials = {}; - -var toString = Object.prototype.toString, - functionType = '[object Function]', - objectType = '[object Object]'; - -Handlebars.registerHelper = function(name, fn, inverse) { - if (toString.call(name) === objectType) { - if (inverse || fn) { throw new Handlebars.Exception('Arg not supported with multiple helpers'); } - Handlebars.Utils.extend(this.helpers, name); - } else { - if (inverse) { fn.not = inverse; } - this.helpers[name] = fn; - } -}; - -Handlebars.registerPartial = function(name, str) { - if (toString.call(name) === objectType) { - Handlebars.Utils.extend(this.partials, name); - } else { - this.partials[name] = str; - } -}; - -Handlebars.registerHelper('helperMissing', function(arg) { - if(arguments.length === 2) { - return undefined; - } else { - throw new Error("Missing helper: '" + arg + "'"); - } -}); - -Handlebars.registerHelper('blockHelperMissing', function(context, options) { - var inverse = options.inverse || function() {}, fn = options.fn; - - var type = toString.call(context); - - if(type === functionType) { context = context.call(this); } - - if(context === true) { - return fn(this); - } else if(context === false || context == null) { - return inverse(this); - } else if(type === "[object Array]") { - if(context.length > 0) { - return Handlebars.helpers.each(context, options); - } else { - return inverse(this); - } - } else { - return fn(context); - } -}); - -Handlebars.K = function() {}; - -Handlebars.createFrame = Object.create || function(object) { - Handlebars.K.prototype = object; - var obj = new Handlebars.K(); - Handlebars.K.prototype = null; - return obj; -}; - -Handlebars.logger = { - DEBUG: 0, INFO: 1, WARN: 2, ERROR: 3, level: 3, - - methodMap: {0: 'debug', 1: 'info', 2: 'warn', 3: 'error'}, - - // can be overridden in the host environment - log: function(level, obj) { - if (Handlebars.logger.level <= level) { - var method = Handlebars.logger.methodMap[level]; - if (typeof console !== 'undefined' && console[method]) { - console[method].call(console, obj); - } - } - } -}; - -Handlebars.log = function(level, obj) { Handlebars.logger.log(level, obj); }; - -Handlebars.registerHelper('each', function(context, options) { - var fn = options.fn, inverse = options.inverse; - var i = 0, ret = "", data; - - var type = toString.call(context); - if(type === functionType) { context = context.call(this); } - - if (options.data) { - data = Handlebars.createFrame(options.data); - } - - if(context && typeof context === 'object') { - if(context instanceof Array){ - for(var j = context.length; i 2) { - expected.push("'" + this.terminals_[p] + "'"); - } - if (this.lexer.showPosition) { - errStr = "Parse error on line " + (yylineno + 1) + ":\n" + this.lexer.showPosition() + "\nExpecting " + expected.join(", ") + ", got '" + (this.terminals_[symbol] || symbol) + "'"; - } else { - errStr = "Parse error on line " + (yylineno + 1) + ": Unexpected " + (symbol == 1?"end of input":"'" + (this.terminals_[symbol] || symbol) + "'"); - } - this.parseError(errStr, {text: this.lexer.match, token: this.terminals_[symbol] || symbol, line: this.lexer.yylineno, loc: yyloc, expected: expected}); - } - } - if (action[0] instanceof Array && action.length > 1) { - throw new Error("Parse Error: multiple actions possible at state: " + state + ", token: " + symbol); - } - switch (action[0]) { - case 1: - stack.push(symbol); - vstack.push(this.lexer.yytext); - lstack.push(this.lexer.yylloc); - stack.push(action[1]); - symbol = null; - if (!preErrorSymbol) { - yyleng = this.lexer.yyleng; - yytext = this.lexer.yytext; - yylineno = this.lexer.yylineno; - yyloc = this.lexer.yylloc; - if (recovering > 0) - recovering--; - } else { - symbol = preErrorSymbol; - preErrorSymbol = null; - } - break; - case 2: - len = this.productions_[action[1]][1]; - yyval.$ = vstack[vstack.length - len]; - yyval._$ = {first_line: lstack[lstack.length - (len || 1)].first_line, last_line: lstack[lstack.length - 1].last_line, first_column: lstack[lstack.length - (len || 1)].first_column, last_column: lstack[lstack.length - 1].last_column}; - if (ranges) { - yyval._$.range = [lstack[lstack.length - (len || 1)].range[0], lstack[lstack.length - 1].range[1]]; - } - r = this.performAction.call(yyval, yytext, yyleng, yylineno, this.yy, action[1], vstack, lstack); - if (typeof r !== "undefined") { - return r; - } - if (len) { - stack = stack.slice(0, -1 * len * 2); - vstack = vstack.slice(0, -1 * len); - lstack = lstack.slice(0, -1 * len); - } - stack.push(this.productions_[action[1]][0]); - vstack.push(yyval.$); - lstack.push(yyval._$); - newState = table[stack[stack.length - 2]][stack[stack.length - 1]]; - stack.push(newState); - break; - case 3: - return true; - } - } - return true; -} -}; -/* Jison generated lexer */ -var lexer = (function(){ -var lexer = ({EOF:1, -parseError:function parseError(str, hash) { - if (this.yy.parser) { - this.yy.parser.parseError(str, hash); - } else { - throw new Error(str); - } - }, -setInput:function (input) { - this._input = input; - this._more = this._less = this.done = false; - this.yylineno = this.yyleng = 0; - this.yytext = this.matched = this.match = ''; - this.conditionStack = ['INITIAL']; - this.yylloc = {first_line:1,first_column:0,last_line:1,last_column:0}; - if (this.options.ranges) this.yylloc.range = [0,0]; - this.offset = 0; - return this; - }, -input:function () { - var ch = this._input[0]; - this.yytext += ch; - this.yyleng++; - this.offset++; - this.match += ch; - this.matched += ch; - var lines = ch.match(/(?:\r\n?|\n).*/g); - if (lines) { - this.yylineno++; - this.yylloc.last_line++; - } else { - this.yylloc.last_column++; - } - if (this.options.ranges) this.yylloc.range[1]++; - - this._input = this._input.slice(1); - return ch; - }, -unput:function (ch) { - var len = ch.length; - var lines = ch.split(/(?:\r\n?|\n)/g); - - this._input = ch + this._input; - this.yytext = this.yytext.substr(0, this.yytext.length-len-1); - //this.yyleng -= len; - this.offset -= len; - var oldLines = this.match.split(/(?:\r\n?|\n)/g); - this.match = this.match.substr(0, this.match.length-1); - this.matched = this.matched.substr(0, this.matched.length-1); - - if (lines.length-1) this.yylineno -= lines.length-1; - var r = this.yylloc.range; - - this.yylloc = {first_line: this.yylloc.first_line, - last_line: this.yylineno+1, - first_column: this.yylloc.first_column, - last_column: lines ? - (lines.length === oldLines.length ? this.yylloc.first_column : 0) + oldLines[oldLines.length - lines.length].length - lines[0].length: - this.yylloc.first_column - len - }; - - if (this.options.ranges) { - this.yylloc.range = [r[0], r[0] + this.yyleng - len]; - } - return this; - }, -more:function () { - this._more = true; - return this; - }, -less:function (n) { - this.unput(this.match.slice(n)); - }, -pastInput:function () { - var past = this.matched.substr(0, this.matched.length - this.match.length); - return (past.length > 20 ? '...':'') + past.substr(-20).replace(/\n/g, ""); - }, -upcomingInput:function () { - var next = this.match; - if (next.length < 20) { - next += this._input.substr(0, 20-next.length); - } - return (next.substr(0,20)+(next.length > 20 ? '...':'')).replace(/\n/g, ""); - }, -showPosition:function () { - var pre = this.pastInput(); - var c = new Array(pre.length + 1).join("-"); - return pre + this.upcomingInput() + "\n" + c+"^"; - }, -next:function () { - if (this.done) { - return this.EOF; - } - if (!this._input) this.done = true; - - var token, - match, - tempMatch, - index, - col, - lines; - if (!this._more) { - this.yytext = ''; - this.match = ''; - } - var rules = this._currentRules(); - for (var i=0;i < rules.length; i++) { - tempMatch = this._input.match(this.rules[rules[i]]); - if (tempMatch && (!match || tempMatch[0].length > match[0].length)) { - match = tempMatch; - index = i; - if (!this.options.flex) break; - } - } - if (match) { - lines = match[0].match(/(?:\r\n?|\n).*/g); - if (lines) this.yylineno += lines.length; - this.yylloc = {first_line: this.yylloc.last_line, - last_line: this.yylineno+1, - first_column: this.yylloc.last_column, - last_column: lines ? lines[lines.length-1].length-lines[lines.length-1].match(/\r?\n?/)[0].length : this.yylloc.last_column + match[0].length}; - this.yytext += match[0]; - this.match += match[0]; - this.matches = match; - this.yyleng = this.yytext.length; - if (this.options.ranges) { - this.yylloc.range = [this.offset, this.offset += this.yyleng]; - } - this._more = false; - this._input = this._input.slice(match[0].length); - this.matched += match[0]; - token = this.performAction.call(this, this.yy, this, rules[index],this.conditionStack[this.conditionStack.length-1]); - if (this.done && this._input) this.done = false; - if (token) return token; - else return; - } - if (this._input === "") { - return this.EOF; - } else { - return this.parseError('Lexical error on line '+(this.yylineno+1)+'. Unrecognized text.\n'+this.showPosition(), - {text: "", token: null, line: this.yylineno}); - } - }, -lex:function lex() { - var r = this.next(); - if (typeof r !== 'undefined') { - return r; - } else { - return this.lex(); - } - }, -begin:function begin(condition) { - this.conditionStack.push(condition); - }, -popState:function popState() { - return this.conditionStack.pop(); - }, -_currentRules:function _currentRules() { - return this.conditions[this.conditionStack[this.conditionStack.length-1]].rules; - }, -topState:function () { - return this.conditionStack[this.conditionStack.length-2]; - }, -pushState:function begin(condition) { - this.begin(condition); - }}); -lexer.options = {}; -lexer.performAction = function anonymous(yy,yy_,$avoiding_name_collisions,YY_START) { - -var YYSTATE=YY_START -switch($avoiding_name_collisions) { -case 0: yy_.yytext = "\\"; return 14; -break; -case 1: - if(yy_.yytext.slice(-1) !== "\\") this.begin("mu"); - if(yy_.yytext.slice(-1) === "\\") yy_.yytext = yy_.yytext.substr(0,yy_.yyleng-1), this.begin("emu"); - if(yy_.yytext) return 14; - -break; -case 2: return 14; -break; -case 3: - if(yy_.yytext.slice(-1) !== "\\") this.popState(); - if(yy_.yytext.slice(-1) === "\\") yy_.yytext = yy_.yytext.substr(0,yy_.yyleng-1); - return 14; - -break; -case 4: yy_.yytext = yy_.yytext.substr(0, yy_.yyleng-4); this.popState(); return 15; -break; -case 5: return 25; -break; -case 6: return 16; -break; -case 7: return 20; -break; -case 8: return 19; -break; -case 9: return 19; -break; -case 10: return 23; -break; -case 11: return 22; -break; -case 12: this.popState(); this.begin('com'); -break; -case 13: yy_.yytext = yy_.yytext.substr(3,yy_.yyleng-5); this.popState(); return 15; -break; -case 14: return 22; -break; -case 15: return 37; -break; -case 16: return 36; -break; -case 17: return 36; -break; -case 18: return 40; -break; -case 19: /*ignore whitespace*/ -break; -case 20: this.popState(); return 24; -break; -case 21: this.popState(); return 18; -break; -case 22: yy_.yytext = yy_.yytext.substr(1,yy_.yyleng-2).replace(/\\"/g,'"'); return 31; -break; -case 23: yy_.yytext = yy_.yytext.substr(1,yy_.yyleng-2).replace(/\\'/g,"'"); return 31; -break; -case 24: return 38; -break; -case 25: return 33; -break; -case 26: return 33; -break; -case 27: return 32; -break; -case 28: return 36; -break; -case 29: yy_.yytext = yy_.yytext.substr(1, yy_.yyleng-2); return 36; -break; -case 30: return 'INVALID'; -break; -case 31: return 5; -break; -} -}; -lexer.rules = [/^(?:\\\\(?=(\{\{)))/,/^(?:[^\x00]*?(?=(\{\{)))/,/^(?:[^\x00]+)/,/^(?:[^\x00]{2,}?(?=(\{\{|$)))/,/^(?:[\s\S]*?--\}\})/,/^(?:\{\{>)/,/^(?:\{\{#)/,/^(?:\{\{\/)/,/^(?:\{\{\^)/,/^(?:\{\{\s*else\b)/,/^(?:\{\{\{)/,/^(?:\{\{&)/,/^(?:\{\{!--)/,/^(?:\{\{![\s\S]*?\}\})/,/^(?:\{\{)/,/^(?:=)/,/^(?:\.(?=[}\/ ]))/,/^(?:\.\.)/,/^(?:[\/.])/,/^(?:\s+)/,/^(?:\}\}\})/,/^(?:\}\})/,/^(?:"(\\["]|[^"])*")/,/^(?:'(\\[']|[^'])*')/,/^(?:@)/,/^(?:true(?=[}\s]))/,/^(?:false(?=[}\s]))/,/^(?:-?[0-9]+(?=[}\s]))/,/^(?:[^\s!"#%-,\.\/;->@\[-\^`\{-~]+(?=[=}\s\/.]))/,/^(?:\[[^\]]*\])/,/^(?:.)/,/^(?:$)/]; -lexer.conditions = {"mu":{"rules":[5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31],"inclusive":false},"emu":{"rules":[3],"inclusive":false},"com":{"rules":[4],"inclusive":false},"INITIAL":{"rules":[0,1,2,31],"inclusive":true}}; -return lexer;})() -parser.lexer = lexer; -function Parser () { this.yy = {}; }Parser.prototype = parser;parser.Parser = Parser; -return new Parser; -})();; -// lib/handlebars/compiler/base.js - -Handlebars.Parser = handlebars; - -Handlebars.parse = function(input) { - - // Just return if an already-compile AST was passed in. - if(input.constructor === Handlebars.AST.ProgramNode) { return input; } - - Handlebars.Parser.yy = Handlebars.AST; - return Handlebars.Parser.parse(input); -}; -; -// lib/handlebars/compiler/ast.js -Handlebars.AST = {}; - -Handlebars.AST.ProgramNode = function(statements, inverse) { - this.type = "program"; - this.statements = statements; - if(inverse) { this.inverse = new Handlebars.AST.ProgramNode(inverse); } -}; - -Handlebars.AST.MustacheNode = function(rawParams, hash, unescaped) { - this.type = "mustache"; - this.escaped = !unescaped; - this.hash = hash; - - var id = this.id = rawParams[0]; - var params = this.params = rawParams.slice(1); - - // a mustache is an eligible helper if: - // * its id is simple (a single part, not `this` or `..`) - var eligibleHelper = this.eligibleHelper = id.isSimple; - - // a mustache is definitely a helper if: - // * it is an eligible helper, and - // * it has at least one parameter or hash segment - this.isHelper = eligibleHelper && (params.length || hash); - - // if a mustache is an eligible helper but not a definite - // helper, it is ambiguous, and will be resolved in a later - // pass or at runtime. -}; - -Handlebars.AST.PartialNode = function(partialName, context) { - this.type = "partial"; - this.partialName = partialName; - this.context = context; -}; - -Handlebars.AST.BlockNode = function(mustache, program, inverse, close) { - var verifyMatch = function(open, close) { - if(open.original !== close.original) { - throw new Handlebars.Exception(open.original + " doesn't match " + close.original); - } - }; - - verifyMatch(mustache.id, close); - this.type = "block"; - this.mustache = mustache; - this.program = program; - this.inverse = inverse; - - if (this.inverse && !this.program) { - this.isInverse = true; - } -}; - -Handlebars.AST.ContentNode = function(string) { - this.type = "content"; - this.string = string; -}; - -Handlebars.AST.HashNode = function(pairs) { - this.type = "hash"; - this.pairs = pairs; -}; - -Handlebars.AST.IdNode = function(parts) { - this.type = "ID"; - - var original = "", - dig = [], - depth = 0; - - for(var i=0,l=parts.length; i 0) { throw new Handlebars.Exception("Invalid path: " + original); } - else if (part === "..") { depth++; } - else { this.isScoped = true; } - } - else { dig.push(part); } - } - - this.original = original; - this.parts = dig; - this.string = dig.join('.'); - this.depth = depth; - - // an ID is simple if it only has one part, and that part is not - // `..` or `this`. - this.isSimple = parts.length === 1 && !this.isScoped && depth === 0; - - this.stringModeValue = this.string; -}; - -Handlebars.AST.PartialNameNode = function(name) { - this.type = "PARTIAL_NAME"; - this.name = name.original; -}; - -Handlebars.AST.DataNode = function(id) { - this.type = "DATA"; - this.id = id; -}; - -Handlebars.AST.StringNode = function(string) { - this.type = "STRING"; - this.original = - this.string = - this.stringModeValue = string; -}; - -Handlebars.AST.IntegerNode = function(integer) { - this.type = "INTEGER"; - this.original = - this.integer = integer; - this.stringModeValue = Number(integer); -}; - -Handlebars.AST.BooleanNode = function(bool) { - this.type = "BOOLEAN"; - this.bool = bool; - this.stringModeValue = bool === "true"; -}; - -Handlebars.AST.CommentNode = function(comment) { - this.type = "comment"; - this.comment = comment; -}; -; -// lib/handlebars/utils.js - -var errorProps = ['description', 'fileName', 'lineNumber', 'message', 'name', 'number', 'stack']; - -Handlebars.Exception = function(message) { - var tmp = Error.prototype.constructor.apply(this, arguments); - - // Unfortunately errors are not enumerable in Chrome (at least), so `for prop in tmp` doesn't work. - for (var idx = 0; idx < errorProps.length; idx++) { - this[errorProps[idx]] = tmp[errorProps[idx]]; - } -}; -Handlebars.Exception.prototype = new Error(); - -// Build out our basic SafeString type -Handlebars.SafeString = function(string) { - this.string = string; -}; -Handlebars.SafeString.prototype.toString = function() { - return this.string.toString(); -}; - -var escape = { - "&": "&", - "<": "<", - ">": ">", - '"': """, - "'": "'", - "`": "`" -}; - -var badChars = /[&<>"'`]/g; -var possible = /[&<>"'`]/; - -var escapeChar = function(chr) { - return escape[chr] || "&"; -}; - -Handlebars.Utils = { - extend: function(obj, value) { - for(var key in value) { - if(value.hasOwnProperty(key)) { - obj[key] = value[key]; - } - } - }, - - escapeExpression: function(string) { - // don't escape SafeStrings, since they're already safe - if (string instanceof Handlebars.SafeString) { - return string.toString(); - } else if (string == null || string === false) { - return ""; - } - - // Force a string conversion as this will be done by the append regardless and - // the regex test will do this transparently behind the scenes, causing issues if - // an object's to string has escaped characters in it. - string = string.toString(); - - if(!possible.test(string)) { return string; } - return string.replace(badChars, escapeChar); - }, - - isEmpty: function(value) { - if (!value && value !== 0) { - return true; - } else if(toString.call(value) === "[object Array]" && value.length === 0) { - return true; - } else { - return false; - } - } -}; -; -// lib/handlebars/compiler/compiler.js - -/*jshint eqnull:true*/ -var Compiler = Handlebars.Compiler = function() {}; -var JavaScriptCompiler = Handlebars.JavaScriptCompiler = function() {}; - -// the foundHelper 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. - -Compiler.prototype = { - compiler: Compiler, - - disassemble: function() { - var opcodes = this.opcodes, opcode, out = [], params, param; - - for (var i=0, l=opcodes.length; i 0) { - this.source[1] = this.source[1] + ", " + locals.join(", "); - } - - // Generate minimizer alias mappings - if (!this.isChild) { - for (var alias in this.context.aliases) { - if (this.context.aliases.hasOwnProperty(alias)) { - this.source[1] = this.source[1] + ', ' + alias + '=' + this.context.aliases[alias]; - } - } - } - - 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 this.stackVars.length) { this.stackVars.push("stack" + this.stackSlot); } - return this.topStackName(); - }, - topStackName: function() { - return "stack" + this.stackSlot; - }, - flushInline: function() { - var inlineStack = this.inlineStack; - if (inlineStack.length) { - this.inlineStack = []; - for (var i = 0, len = inlineStack.length; i < len; i++) { - var entry = inlineStack[i]; - if (entry instanceof Literal) { - this.compileStack.push(entry); - } else { - this.pushStack(entry); - } - } - } - }, - isInline: function() { - return this.inlineStack.length; - }, - - popStack: function(wrapped) { - var inline = this.isInline(), - item = (inline ? this.inlineStack : this.compileStack).pop(); - - if (!wrapped && (item instanceof Literal)) { - return item.value; - } else { - if (!inline) { - this.stackSlot--; - } - return item; - } - }, - - topStack: function(wrapped) { - var stack = (this.isInline() ? this.inlineStack : this.compileStack), - item = stack[stack.length - 1]; - - if (!wrapped && (item instanceof Literal)) { - return item.value; - } else { - return item; - } - }, - - quotedString: function(str) { - return '"' + str - .replace(/\\/g, '\\\\') - .replace(/"/g, '\\"') - .replace(/\n/g, '\\n') - .replace(/\r/g, '\\r') - .replace(/\u2028/g, '\\u2028') // Per Ecma-262 7.3 + 7.8.4 - .replace(/\u2029/g, '\\u2029') + '"'; - }, - - setupHelper: function(paramSize, name, missingParams) { - var params = []; - this.setupParams(paramSize, params, missingParams); - var foundHelper = this.nameLookup('helpers', name, 'helper'); - - return { - params: params, - name: foundHelper, - callParams: ["depth0"].concat(params).join(", "), - helperMissingParams: missingParams && ["depth0", this.quotedString(name)].concat(params).join(", ") - }; - }, - - // the params and contexts arguments are passed in arrays - // to fill in - setupParams: function(paramSize, params, useRegister) { - var options = [], contexts = [], types = [], param, inverse, program; - - options.push("hash:" + this.popStack()); - - inverse = this.popStack(); - program = this.popStack(); - - // Avoid setting fn and inverse if neither are set. This allows - // helpers to do a check for `if (options.fn)` - if (program || inverse) { - if (!program) { - this.context.aliases.self = "this"; - program = "self.noop"; - } - - if (!inverse) { - this.context.aliases.self = "this"; - inverse = "self.noop"; - } - - options.push("inverse:" + inverse); - options.push("fn:" + program); - } - - for(var i=0; i= 1.0.0' -}; - -Handlebars.helpers = {}; -Handlebars.partials = {}; - -var toString = Object.prototype.toString, - functionType = '[object Function]', - objectType = '[object Object]'; - -Handlebars.registerHelper = function(name, fn, inverse) { - if (toString.call(name) === objectType) { - if (inverse || fn) { throw new Handlebars.Exception('Arg not supported with multiple helpers'); } - Handlebars.Utils.extend(this.helpers, name); - } else { - if (inverse) { fn.not = inverse; } - this.helpers[name] = fn; - } -}; - -Handlebars.registerPartial = function(name, str) { - if (toString.call(name) === objectType) { - Handlebars.Utils.extend(this.partials, name); - } else { - this.partials[name] = str; - } -}; - -Handlebars.registerHelper('helperMissing', function(arg) { - if(arguments.length === 2) { - return undefined; - } else { - throw new Error("Missing helper: '" + arg + "'"); - } -}); - -Handlebars.registerHelper('blockHelperMissing', function(context, options) { - var inverse = options.inverse || function() {}, fn = options.fn; - - var type = toString.call(context); - - if(type === functionType) { context = context.call(this); } - - if(context === true) { - return fn(this); - } else if(context === false || context == null) { - return inverse(this); - } else if(type === "[object Array]") { - if(context.length > 0) { - return Handlebars.helpers.each(context, options); - } else { - return inverse(this); - } - } else { - return fn(context); - } -}); - -Handlebars.K = function() {}; - -Handlebars.createFrame = Object.create || function(object) { - Handlebars.K.prototype = object; - var obj = new Handlebars.K(); - Handlebars.K.prototype = null; - return obj; -}; - -Handlebars.logger = { - DEBUG: 0, INFO: 1, WARN: 2, ERROR: 3, level: 3, - - methodMap: {0: 'debug', 1: 'info', 2: 'warn', 3: 'error'}, - - // can be overridden in the host environment - log: function(level, obj) { - if (Handlebars.logger.level <= level) { - var method = Handlebars.logger.methodMap[level]; - if (typeof console !== 'undefined' && console[method]) { - console[method].call(console, obj); - } - } - } -}; - -Handlebars.log = function(level, obj) { Handlebars.logger.log(level, obj); }; - -Handlebars.registerHelper('each', function(context, options) { - var fn = options.fn, inverse = options.inverse; - var i = 0, ret = "", data; - - var type = toString.call(context); - if(type === functionType) { context = context.call(this); } - - if (options.data) { - data = Handlebars.createFrame(options.data); - } - - if(context && typeof context === 'object') { - if(context instanceof Array){ - for(var j = context.length; i": ">", - '"': """, - "'": "'", - "`": "`" -}; - -var badChars = /[&<>"'`]/g; -var possible = /[&<>"'`]/; - -var escapeChar = function(chr) { - return escape[chr] || "&"; -}; - -Handlebars.Utils = { - extend: function(obj, value) { - for(var key in value) { - if(value.hasOwnProperty(key)) { - obj[key] = value[key]; - } - } - }, - - escapeExpression: function(string) { - // don't escape SafeStrings, since they're already safe - if (string instanceof Handlebars.SafeString) { - return string.toString(); - } else if (string == null || string === false) { - return ""; - } - - // Force a string conversion as this will be done by the append regardless and - // the regex test will do this transparently behind the scenes, causing issues if - // an object's to string has escaped characters in it. - string = string.toString(); - - if(!possible.test(string)) { return string; } - return string.replace(badChars, escapeChar); - }, - - isEmpty: function(value) { - if (!value && value !== 0) { - return true; - } else if(toString.call(value) === "[object Array]" && value.length === 0) { - return true; - } else { - return false; - } - } -}; -; -// lib/handlebars/runtime.js - -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) { - programWrapper = Handlebars.VM.program(i, fn, data); - } else if (!programWrapper) { - programWrapper = this.programs[i] = Handlebars.VM.program(i, fn); - } - return programWrapper; - }, - merge: function(param, common) { - var ret = param || common; - - if (param && common) { - ret = {}; - Handlebars.Utils.extend(ret, common); - Handlebars.Utils.extend(ret, param); - } - return ret; - }, - programWithDepth: Handlebars.VM.programWithDepth, - noop: Handlebars.VM.noop, - compilerInfo: null - }; - - return function(context, options) { - options = options || {}; - var result = templateSpec.call(container, Handlebars, context, options.helpers, options.partials, options.data); - - var compilerInfo = container.compilerInfo || [], - compilerRevision = compilerInfo[0] || 1, - currentRevision = Handlebars.COMPILER_REVISION; - - if (compilerRevision !== currentRevision) { - if (compilerRevision < currentRevision) { - var runtimeVersions = Handlebars.REVISION_CHANGES[currentRevision], - compilerVersions = Handlebars.REVISION_CHANGES[compilerRevision]; - throw "Template was precompiled with an older version of Handlebars than the current runtime. "+ - "Please update your precompiler to a newer version ("+runtimeVersions+") or downgrade your runtime to an older version ("+compilerVersions+")."; - } else { - // Use the embedded version info since the runtime doesn't know about this revision yet - throw "Template was precompiled with a newer version of Handlebars than the current runtime. "+ - "Please update your runtime to a newer version ("+compilerInfo[1]+")."; - } - } - - return result; - }; - }, - - programWithDepth: function(i, fn, data /*, $depth */) { - var args = Array.prototype.slice.call(arguments, 3); - - var program = function(context, options) { - options = options || {}; - - return fn.apply(this, [context, options.data || data].concat(args)); - }; - program.program = i; - program.depth = args.length; - return program; - }, - program: function(i, fn, data) { - var program = function(context, options) { - options = options || {}; - - return fn(context, options.data || data); - }; - program.program = i; - program.depth = 0; - return program; - }, - 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, {data: data !== undefined}); - return partials[name](context, options); - } - } -}; - -Handlebars.template = Handlebars.VM.template; -; -// lib/handlebars/browser-suffix.js -})(Handlebars); -; diff --git a/lib/handlebars.js b/lib/handlebars.js index f82ec3bad..4abdd4a1b 100644 --- a/lib/handlebars.js +++ b/lib/handlebars.js @@ -1,43 +1,30 @@ -var handlebars = require("./handlebars/base"), +import Handlebars from "./handlebars.runtime"; -// Each of these augment the Handlebars object. No need to setup here. -// (This is done to easily share code between commonjs and browse envs) - utils = require("./handlebars/utils"), - compiler = require("./handlebars/compiler"), - runtime = require("./handlebars/runtime"); +// Compiler imports +module AST from "./handlebars/compiler/ast"; +import { parser as Parser, parse } from "./handlebars/compiler/base"; +import { Compiler, compile, precompile } from "./handlebars/compiler/compiler"; +import JavaScriptCompiler from "./handlebars/compiler/javascript-compiler"; +var _create = Handlebars.create; var create = function() { - var hb = handlebars.create(); + var hb = _create(); - utils.attach(hb); - compiler.attach(hb); - runtime.attach(hb); + hb.compile = function(input, options) { + return compile(input, options, hb); + }; + hb.precompile = precompile; + + hb.AST = AST; + hb.Compiler = Compiler; + hb.JavaScriptCompiler = JavaScriptCompiler; + hb.Parser = Parser; + hb.parse = parse; return hb; }; -var Handlebars = create(); +Handlebars = create(); Handlebars.create = create; -module.exports = Handlebars; // instantiate an instance - -// Publish a Node.js require() handler for .handlebars and .hbs files -if (require.extensions) { - var extension = function(module, filename) { - var fs = require("fs"); - var templateString = fs.readFileSync(filename, "utf8"); - module.exports = Handlebars.compile(templateString); - }; - require.extensions[".handlebars"] = extension; - require.extensions[".hbs"] = extension; -} - -// BEGIN(BROWSER) - -// END(BROWSER) - -// USAGE: -// var handlebars = require('handlebars'); - -// var singleton = handlebars.Handlebars, -// local = handlebars.create(); +export default Handlebars; diff --git a/lib/handlebars.runtime.js b/lib/handlebars.runtime.js new file mode 100644 index 000000000..a59101919 --- /dev/null +++ b/lib/handlebars.runtime.js @@ -0,0 +1,30 @@ +module base from "./handlebars/base"; + +// Each of these augment the Handlebars object. No need to setup here. +// (This is done to easily share code between commonjs and browse envs) +import SafeString from "./handlebars/safe-string"; +import Exception from "./handlebars/exception"; +module Utils from "./handlebars/utils"; +module runtime from "./handlebars/runtime"; + +// For compatibility and usage outside of module systems, make the Handlebars object a namespace +var create = function() { + var hb = new base.HandlebarsEnvironment(); + + Utils.extend(hb, base); + hb.SafeString = SafeString; + hb.Exception = Exception; + hb.Utils = Utils; + + hb.VM = runtime; + hb.template = function(spec) { + return runtime.template(spec, hb); + }; + + return hb; +}; + +var Handlebars = create(); +Handlebars.create = create; + +export default Handlebars; diff --git a/lib/handlebars/base.js b/lib/handlebars/base.js index 44a369c5e..236a3c272 100644 --- a/lib/handlebars/base.js +++ b/lib/handlebars/base.js @@ -1,166 +1,189 @@ -/*jshint eqnull: true */ +/*globals Exception, Utils */ +module Utils from "./utils"; +import Exception from "./exception"; -module.exports.create = function() { +export var VERSION = "1.1.0"; +export var COMPILER_REVISION = 4; -var Handlebars = {}; - -// BEGIN(BROWSER) - -Handlebars.VERSION = "1.0.0"; -Handlebars.COMPILER_REVISION = 4; - -Handlebars.REVISION_CHANGES = { +export var REVISION_CHANGES = { 1: '<= 1.0.rc.2', // 1.0.rc.2 is actually rev2 but doesn't report it 2: '== 1.0.0-rc.3', 3: '== 1.0.0-rc.4', 4: '>= 1.0.0' }; -Handlebars.helpers = {}; -Handlebars.partials = {}; - var toString = Object.prototype.toString, - functionType = '[object Function]', objectType = '[object Object]'; -Handlebars.registerHelper = function(name, fn, inverse) { - if (toString.call(name) === objectType) { - if (inverse || fn) { throw new Handlebars.Exception('Arg not supported with multiple helpers'); } - Handlebars.Utils.extend(this.helpers, name); - } else { - if (inverse) { fn.not = inverse; } - this.helpers[name] = fn; - } +// Sourced from lodash +// https://github.com/bestiejs/lodash/blob/master/LICENSE.txt +var isFunction = function(value) { + return typeof value === 'function'; }; +// fallback for older versions of Chrome and Safari +if (isFunction(/x/)) { + isFunction = function(value) { + return typeof value === 'function' && toString.call(value) === '[object Function]'; + }; +} + +function isArray(value) { + return (value && typeof value === 'object') ? toString.call(value) === '[object Array]' : false; +} + +export function HandlebarsEnvironment(helpers, partials) { + this.helpers = helpers || {}; + this.partials = partials || {}; + + registerDefaultHelpers(this); +} + +HandlebarsEnvironment.prototype = { + constructor: HandlebarsEnvironment, + + logger: logger, + log: log, + + registerHelper: function(name, fn, inverse) { + if (toString.call(name) === objectType) { + if (inverse || fn) { throw new Exception('Arg not supported with multiple helpers'); } + Utils.extend(this.helpers, name); + } else { + if (inverse) { fn.not = inverse; } + this.helpers[name] = fn; + } + }, -Handlebars.registerPartial = function(name, str) { - if (toString.call(name) === objectType) { - Handlebars.Utils.extend(this.partials, name); - } else { - this.partials[name] = str; + registerPartial: function(name, str) { + if (toString.call(name) === objectType) { + Utils.extend(this.partials, name); + } else { + this.partials[name] = str; + } } }; -Handlebars.registerHelper('helperMissing', function(arg) { - if(arguments.length === 2) { - return undefined; - } else { - throw new Error("Missing helper: '" + arg + "'"); - } -}); - -Handlebars.registerHelper('blockHelperMissing', function(context, options) { - var inverse = options.inverse || function() {}, fn = options.fn; +function registerDefaultHelpers(instance) { + instance.registerHelper('helperMissing', function(arg) { + if(arguments.length === 2) { + return undefined; + } else { + throw new Error("Missing helper: '" + arg + "'"); + } + }); - var type = toString.call(context); + instance.registerHelper('blockHelperMissing', function(context, options) { + var inverse = options.inverse || function() {}, fn = options.fn; - if(type === functionType) { context = context.call(this); } + if (isFunction(context)) { context = context.call(this); } - if(context === true) { - return fn(this); - } else if(context === false || context == null) { - return inverse(this); - } else if(type === "[object Array]") { - if(context.length > 0) { - return Handlebars.helpers.each(context, options); - } else { + if(context === true) { + return fn(this); + } else if(context === false || context == null) { return inverse(this); + } else if (isArray(context)) { + if(context.length > 0) { + return instance.helpers.each(context, options); + } else { + return inverse(this); + } + } else { + return fn(context); } - } else { - return fn(context); - } -}); - -Handlebars.K = function() {}; + }); -Handlebars.createFrame = Object.create || function(object) { - Handlebars.K.prototype = object; - var obj = new Handlebars.K(); - Handlebars.K.prototype = null; - return obj; -}; + instance.registerHelper('each', function(context, options) { + var fn = options.fn, inverse = options.inverse; + var i = 0, ret = "", data; -Handlebars.logger = { - DEBUG: 0, INFO: 1, WARN: 2, ERROR: 3, level: 3, + if (isFunction(context)) { context = context.call(this); } - methodMap: {0: 'debug', 1: 'info', 2: 'warn', 3: 'error'}, + if (options.data) { + data = createFrame(options.data); + } - // can be overridden in the host environment - log: function(level, obj) { - if (Handlebars.logger.level <= level) { - var method = Handlebars.logger.methodMap[level]; - if (typeof console !== 'undefined' && console[method]) { - console[method].call(console, obj); + if(context && typeof context === 'object') { + if (isArray(context)) { + for(var j = context.length; i 0) { throw new Handlebars.Exception("Invalid path: " + original); } + if (dig.length > 0) { throw new Exception("Invalid path: " + original); } else if (part === "..") { depth++; } else { this.isScoped = true; } } @@ -94,45 +107,39 @@ Handlebars.AST.IdNode = function(parts) { this.isSimple = parts.length === 1 && !this.isScoped && depth === 0; this.stringModeValue = this.string; -}; +} -Handlebars.AST.PartialNameNode = function(name) { +export function PartialNameNode(name) { this.type = "PARTIAL_NAME"; this.name = name.original; -}; +} -Handlebars.AST.DataNode = function(id) { +export function DataNode(id) { this.type = "DATA"; this.id = id; -}; +} -Handlebars.AST.StringNode = function(string) { +export function StringNode(string) { this.type = "STRING"; this.original = this.string = this.stringModeValue = string; -}; +} -Handlebars.AST.IntegerNode = function(integer) { +export function IntegerNode(integer) { this.type = "INTEGER"; this.original = this.integer = integer; this.stringModeValue = Number(integer); -}; +} -Handlebars.AST.BooleanNode = function(bool) { +export function BooleanNode(bool) { this.type = "BOOLEAN"; this.bool = bool; this.stringModeValue = bool === "true"; -}; +} -Handlebars.AST.CommentNode = function(comment) { +export function CommentNode(comment) { this.type = "comment"; this.comment = comment; -}; - -// END(BROWSER) - -return Handlebars; -}; - +} diff --git a/lib/handlebars/compiler/base.js b/lib/handlebars/compiler/base.js index 759445154..d6cb06ee2 100644 --- a/lib/handlebars/compiler/base.js +++ b/lib/handlebars/compiler/base.js @@ -1,21 +1,12 @@ -var handlebars = require("./parser"); +import parser from "./parser"; +module AST from "./ast"; -exports.attach = function(Handlebars) { - -// BEGIN(BROWSER) - -Handlebars.Parser = handlebars; - -Handlebars.parse = function(input) { +export { parser }; +export function parse(input) { // Just return if an already-compile AST was passed in. - if(input.constructor === Handlebars.AST.ProgramNode) { return input; } - - Handlebars.Parser.yy = Handlebars.AST; - return Handlebars.Parser.parse(input); -}; - -// END(BROWSER) + if(input.constructor === AST.ProgramNode) { return input; } -return Handlebars; -}; + parser.yy = AST; + return parser.parse(input); +} diff --git a/lib/handlebars/compiler/compiler.js b/lib/handlebars/compiler/compiler.js index 8bb1fc521..4f232ebce 100644 --- a/lib/handlebars/compiler/compiler.js +++ b/lib/handlebars/compiler/compiler.js @@ -1,14 +1,9 @@ -var compilerbase = require("./base"); +import Exception from "../exception"; +import { parse } from "./base"; +import JavaScriptCompiler from "./javascript-compiler"; +module AST from "./ast"; -exports.attach = function(Handlebars) { - -compilerbase.attach(Handlebars); - -// BEGIN(BROWSER) - -/*jshint eqnull:true*/ -var Compiler = Handlebars.Compiler = function() {}; -var JavaScriptCompiler = Handlebars.JavaScriptCompiler = function() {}; +export function Compiler() {} // the foundHelper register will disambiguate helper lookup from finding a // function in a context. This is necessary for mustache compatibility, which @@ -41,6 +36,7 @@ Compiler.prototype = { return out.join("\n"); }, + equals: function(other) { var len = this.opcodes.length; if (other.opcodes.length !== len) { @@ -76,6 +72,7 @@ Compiler.prototype = { guid: 0, compile: function(program, options) { + this.opcodes = []; this.children = []; this.depths = {list: []}; this.options = options; @@ -97,20 +94,30 @@ Compiler.prototype = { } } - return this.program(program); + return this.accept(program); }, accept: function(node) { - return this[node.type](node); + var strip = node.strip || {}, + ret; + if (strip.left) { + this.opcode('strip'); + } + + ret = this[node.type](node); + + if (strip.right) { + this.opcode('strip'); + } + + return ret; }, program: function(program) { - var statements = program.statements, statement; - this.opcodes = []; + var statements = program.statements; for(var i=0, l=statements.length; i 0) { - this.source[1] = this.source[1] + ", " + locals.join(", "); - } - - // Generate minimizer alias mappings - if (!this.isChild) { - for (var alias in this.context.aliases) { - if (this.context.aliases.hasOwnProperty(alias)) { - this.source[1] = this.source[1] + ', ' + alias + '=' + this.context.aliases[alias]; - } - } - } - - 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 this.stackVars.length) { this.stackVars.push("stack" + this.stackSlot); } - return this.topStackName(); - }, - topStackName: function() { - return "stack" + this.stackSlot; - }, - flushInline: function() { - var inlineStack = this.inlineStack; - if (inlineStack.length) { - this.inlineStack = []; - for (var i = 0, len = inlineStack.length; i < len; i++) { - var entry = inlineStack[i]; - if (entry instanceof Literal) { - this.compileStack.push(entry); - } else { - this.pushStack(entry); - } - } - } - }, - isInline: function() { - return this.inlineStack.length; - }, - - popStack: function(wrapped) { - var inline = this.isInline(), - item = (inline ? this.inlineStack : this.compileStack).pop(); - - if (!wrapped && (item instanceof Literal)) { - return item.value; - } else { - if (!inline) { - this.stackSlot--; - } - return item; - } - }, - - topStack: function(wrapped) { - var stack = (this.isInline() ? this.inlineStack : this.compileStack), - item = stack[stack.length - 1]; - - if (!wrapped && (item instanceof Literal)) { - return item.value; - } else { - return item; - } - }, - - quotedString: function(str) { - return '"' + str - .replace(/\\/g, '\\\\') - .replace(/"/g, '\\"') - .replace(/\n/g, '\\n') - .replace(/\r/g, '\\r') - .replace(/\u2028/g, '\\u2028') // Per Ecma-262 7.3 + 7.8.4 - .replace(/\u2029/g, '\\u2029') + '"'; - }, - - setupHelper: function(paramSize, name, missingParams) { - var params = []; - this.setupParams(paramSize, params, missingParams); - var foundHelper = this.nameLookup('helpers', name, 'helper'); - - return { - params: params, - name: foundHelper, - callParams: ["depth0"].concat(params).join(", "), - helperMissingParams: missingParams && ["depth0", this.quotedString(name)].concat(params).join(", ") - }; - }, - - // the params and contexts arguments are passed in arrays - // to fill in - setupParams: function(paramSize, params, useRegister) { - var options = [], contexts = [], types = [], param, inverse, program; - - options.push("hash:" + this.popStack()); - - inverse = this.popStack(); - program = this.popStack(); - - // Avoid setting fn and inverse if neither are set. This allows - // helpers to do a check for `if (options.fn)` - if (program || inverse) { - if (!program) { - this.context.aliases.self = "this"; - program = "self.noop"; - } - - if (!inverse) { - this.context.aliases.self = "this"; - inverse = "self.noop"; - } - - options.push("inverse:" + inverse); - options.push("fn:" + program); - } - - for(var i=0; i 0) { + this.source[1] = this.source[1] + ", " + locals.join(", "); + } + + // Generate minimizer alias mappings + if (!this.isChild) { + for (var alias in this.context.aliases) { + if (this.context.aliases.hasOwnProperty(alias)) { + this.source[1] = this.source[1] + ', ' + alias + '=' + this.context.aliases[alias]; + } + } + } + + 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.pushSource("return buffer;"); + } + + var params = this.isChild ? ["depth0", "data"] : ["Handlebars", "depth0", "helpers", "partials", "data"]; + + for(var i=0, l=this.environment.depths.list.length; i this.stackVars.length) { this.stackVars.push("stack" + this.stackSlot); } + return this.topStackName(); + }, + topStackName: function() { + return "stack" + this.stackSlot; + }, + flushInline: function() { + var inlineStack = this.inlineStack; + if (inlineStack.length) { + this.inlineStack = []; + for (var i = 0, len = inlineStack.length; i < len; i++) { + var entry = inlineStack[i]; + if (entry instanceof Literal) { + this.compileStack.push(entry); + } else { + this.pushStack(entry); + } + } + } + }, + isInline: function() { + return this.inlineStack.length; + }, + + popStack: function(wrapped) { + var inline = this.isInline(), + item = (inline ? this.inlineStack : this.compileStack).pop(); + + if (!wrapped && (item instanceof Literal)) { + return item.value; + } else { + if (!inline) { + this.stackSlot--; + } + return item; + } + }, + + topStack: function(wrapped) { + var stack = (this.isInline() ? this.inlineStack : this.compileStack), + item = stack[stack.length - 1]; + + if (!wrapped && (item instanceof Literal)) { + return item.value; + } else { + return item; + } + }, + + quotedString: function(str) { + return '"' + str + .replace(/\\/g, '\\\\') + .replace(/"/g, '\\"') + .replace(/\n/g, '\\n') + .replace(/\r/g, '\\r') + .replace(/\u2028/g, '\\u2028') // Per Ecma-262 7.3 + 7.8.4 + .replace(/\u2029/g, '\\u2029') + '"'; + }, + + setupHelper: function(paramSize, name, missingParams) { + var params = []; + this.setupParams(paramSize, params, missingParams); + var foundHelper = this.nameLookup('helpers', name, 'helper'); + + return { + params: params, + name: foundHelper, + callParams: ["depth0"].concat(params).join(", "), + helperMissingParams: missingParams && ["depth0", this.quotedString(name)].concat(params).join(", ") + }; + }, + + // the params and contexts arguments are passed in arrays + // to fill in + setupParams: function(paramSize, params, useRegister) { + var options = [], contexts = [], types = [], param, inverse, program; + + options.push("hash:" + this.popStack()); + + inverse = this.popStack(); + program = this.popStack(); + + // Avoid setting fn and inverse if neither are set. This allows + // helpers to do a check for `if (options.fn)` + if (program || inverse) { + if (!program) { + this.context.aliases.self = "this"; + program = "self.noop"; + } + + if (!inverse) { + this.context.aliases.self = "this"; + inverse = "self.noop"; + } + + options.push("inverse:" + inverse); + options.push("fn:" + program); + } + + for(var i=0; i " + content + " }}"); }; -Handlebars.PrintVisitor.prototype.hash = function(hash) { +PrintVisitor.prototype.hash = function(hash) { var pairs = hash.pairs; var joinedPairs = [], left, right; @@ -95,19 +96,19 @@ Handlebars.PrintVisitor.prototype.hash = function(hash) { return "HASH{" + joinedPairs.join(", ") + "}"; }; -Handlebars.PrintVisitor.prototype.STRING = function(string) { +PrintVisitor.prototype.STRING = function(string) { return '"' + string.string + '"'; }; -Handlebars.PrintVisitor.prototype.INTEGER = function(integer) { +PrintVisitor.prototype.INTEGER = function(integer) { return "INTEGER{" + integer.integer + "}"; }; -Handlebars.PrintVisitor.prototype.BOOLEAN = function(bool) { +PrintVisitor.prototype.BOOLEAN = function(bool) { return "BOOLEAN{" + bool.bool + "}"; }; -Handlebars.PrintVisitor.prototype.ID = function(id) { +PrintVisitor.prototype.ID = function(id) { var path = id.parts.join("/"); if(id.parts.length > 1) { return "PATH:" + path; @@ -116,23 +117,19 @@ Handlebars.PrintVisitor.prototype.ID = function(id) { } }; -Handlebars.PrintVisitor.prototype.PARTIAL_NAME = function(partialName) { +PrintVisitor.prototype.PARTIAL_NAME = function(partialName) { return "PARTIAL:" + partialName.name; }; -Handlebars.PrintVisitor.prototype.DATA = function(data) { +PrintVisitor.prototype.DATA = function(data) { return "@" + this.accept(data.id); }; -Handlebars.PrintVisitor.prototype.content = function(content) { +PrintVisitor.prototype.content = function(content) { return this.pad("CONTENT[ '" + content.string + "' ]"); }; -Handlebars.PrintVisitor.prototype.comment = function(comment) { +PrintVisitor.prototype.comment = function(comment) { return this.pad("{{! '" + comment.comment + "' }}"); }; -// END(BROWSER) - -return Handlebars; -}; diff --git a/lib/handlebars/compiler/visitor.js b/lib/handlebars/compiler/visitor.js index 5d0731407..6a0373e19 100644 --- a/lib/handlebars/compiler/visitor.js +++ b/lib/handlebars/compiler/visitor.js @@ -1,18 +1,11 @@ -exports.attach = function(Handlebars) { +function Visitor() {} -// BEGIN(BROWSER) +Visitor.prototype = { + constructor: Visitor, -Handlebars.Visitor = function() {}; - -Handlebars.Visitor.prototype = { accept: function(object) { return this[object.type](object); } }; -// END(BROWSER) - -return Handlebars; -}; - - +export default Visitor; diff --git a/lib/handlebars/exception.js b/lib/handlebars/exception.js new file mode 100644 index 000000000..6de9cfdb4 --- /dev/null +++ b/lib/handlebars/exception.js @@ -0,0 +1,15 @@ + +var errorProps = ['description', 'fileName', 'lineNumber', 'message', 'name', 'number', 'stack']; + +function Exception(/* message */) { + var tmp = Error.prototype.constructor.apply(this, arguments); + + // Unfortunately errors are not enumerable in Chrome (at least), so `for prop in tmp` doesn't work. + for (var idx = 0; idx < errorProps.length; idx++) { + this[errorProps[idx]] = tmp[errorProps[idx]]; + } +} + +Exception.prototype = new Error(); + +export default Exception; diff --git a/lib/handlebars/runtime.js b/lib/handlebars/runtime.js index 2e845c4cc..fe5fef4ab 100644 --- a/lib/handlebars/runtime.js +++ b/lib/handlebars/runtime.js @@ -1,106 +1,139 @@ -exports.attach = function(Handlebars) { - -// BEGIN(BROWSER) - -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) { - programWrapper = Handlebars.VM.program(i, fn, data); - } else if (!programWrapper) { - programWrapper = this.programs[i] = Handlebars.VM.program(i, fn); - } - return programWrapper; - }, - merge: function(param, common) { - var ret = param || common; - - if (param && common) { - ret = {}; - Handlebars.Utils.extend(ret, common); - Handlebars.Utils.extend(ret, param); - } - return ret; - }, - programWithDepth: Handlebars.VM.programWithDepth, - noop: Handlebars.VM.noop, - compilerInfo: null - }; - - return function(context, options) { - options = options || {}; - var result = templateSpec.call(container, Handlebars, context, options.helpers, options.partials, options.data); - - var compilerInfo = container.compilerInfo || [], - compilerRevision = compilerInfo[0] || 1, - currentRevision = Handlebars.COMPILER_REVISION; - - if (compilerRevision !== currentRevision) { - if (compilerRevision < currentRevision) { - var runtimeVersions = Handlebars.REVISION_CHANGES[currentRevision], - compilerVersions = Handlebars.REVISION_CHANGES[compilerRevision]; - throw "Template was precompiled with an older version of Handlebars than the current runtime. "+ - "Please update your precompiler to a newer version ("+runtimeVersions+") or downgrade your runtime to an older version ("+compilerVersions+")."; - } else { - // Use the embedded version info since the runtime doesn't know about this revision yet - throw "Template was precompiled with a newer version of Handlebars than the current runtime. "+ - "Please update your runtime to a newer version ("+compilerInfo[1]+")."; - } - } +/*global Utils */ +module Utils from "./utils"; +import Exception from "./exception"; +import { COMPILER_REVISION, REVISION_CHANGES } from "./base"; + +function checkRevision(compilerInfo) { + var compilerRevision = compilerInfo && compilerInfo[0] || 1, + currentRevision = COMPILER_REVISION; + + if (compilerRevision !== currentRevision) { + if (compilerRevision < currentRevision) { + var runtimeVersions = REVISION_CHANGES[currentRevision], + compilerVersions = REVISION_CHANGES[compilerRevision]; + throw new Error("Template was precompiled with an older version of Handlebars than the current runtime. "+ + "Please update your precompiler to a newer version ("+runtimeVersions+") or downgrade your runtime to an older version ("+compilerVersions+")."); + } else { + // Use the embedded version info since the runtime doesn't know about this revision yet + throw new Error("Template was precompiled with a newer version of Handlebars than the current runtime. "+ + "Please update your runtime to a newer version ("+compilerInfo[1]+")."); + } + } +} - return result; - }; - }, +// TODO: Remove this line and break up compilePartial - programWithDepth: function(i, fn, data /*, $depth */) { - var args = Array.prototype.slice.call(arguments, 3); +export function template(templateSpec, env) { + if (!env) { + throw new Error("No environment passed to template"); + } - var program = function(context, options) { - options = options || {}; + var invokePartialWrapper; + if (env.compile) { + invokePartialWrapper = function(partial, name, context, helpers, partials, data) { + // TODO : Check this for all inputs and the options handling (partial flag, etc). This feels + // like there should be a common exec path + var result = invokePartial.apply(this, arguments); + if (result) { return result; } - return fn.apply(this, [context, options.data || data].concat(args)); + var options = { helpers: helpers, partials: partials, data: data }; + partials[name] = env.compile(partial, { data: data !== undefined }, env); + return partials[name](context, options); }; - program.program = i; - program.depth = args.length; - return program; - }, - program: function(i, fn, data) { - var program = function(context, options) { - options = options || {}; - - return fn(context, options.data || data); + } else { + invokePartialWrapper = function(partial, name /* , context, helpers, partials, data */) { + var result = invokePartial.apply(this, arguments); + if (result) { return result; } + throw new Exception("The partial " + name + " could not be compiled when running in runtime-only mode"); }; - program.program = i; - program.depth = 0; - return program; - }, - 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, {data: data !== undefined}); - return partials[name](context, options); - } } -}; -Handlebars.template = Handlebars.VM.template; - -// END(BROWSER) + // Just add water + var container = { + escapeExpression: Utils.escapeExpression, + invokePartial: invokePartialWrapper, + programs: [], + program: function(i, fn, data) { + var programWrapper = this.programs[i]; + if(data) { + programWrapper = program(i, fn, data); + } else if (!programWrapper) { + programWrapper = this.programs[i] = program(i, fn); + } + return programWrapper; + }, + merge: function(param, common) { + var ret = param || common; + + if (param && common && (param !== common)) { + ret = {}; + Utils.extend(ret, common); + Utils.extend(ret, param); + } + return ret; + }, + programWithDepth: programWithDepth, + noop: noop, + compilerInfo: null + }; + + return function(context, options) { + options = options || {}; + var namespace = options.partial ? options : env, + helpers, + partials; + + if (!options.partial) { + helpers = options.helpers; + partials = options.partials; + } + var result = templateSpec.call( + container, + namespace, context, + helpers, + partials, + options.data); + + if (!options.partial) { + checkRevision(container.compilerInfo); + } -return Handlebars; + return result; + }; +} + +export function programWithDepth(i, fn, data /*, $depth */) { + var args = Array.prototype.slice.call(arguments, 3); + + var prog = function(context, options) { + options = options || {}; + + return fn.apply(this, [context, options.data || data].concat(args)); + }; + prog.program = i; + prog.depth = args.length; + return prog; +} + +export function program(i, fn, data) { + var prog = function(context, options) { + options = options || {}; + + return fn(context, options.data || data); + }; + prog.program = i; + prog.depth = 0; + return prog; +} + +export function invokePartial(partial, name, context, helpers, partials, data) { + var options = { partial: true, helpers: helpers, partials: partials, data: data }; + + if(partial === undefined) { + throw new Exception("The partial " + name + " could not be found"); + } else if(partial instanceof Function) { + return partial(context, options); + } +} -}; +export function noop() { return ""; } diff --git a/lib/handlebars/safe-string.js b/lib/handlebars/safe-string.js new file mode 100644 index 000000000..2ae49aa89 --- /dev/null +++ b/lib/handlebars/safe-string.js @@ -0,0 +1,10 @@ +// Build out our basic SafeString type +function SafeString(string) { + this.string = string; +} + +SafeString.prototype.toString = function() { + return "" + this.string; +}; + +export default SafeString; diff --git a/lib/handlebars/source.rb b/lib/handlebars/source.rb deleted file mode 100644 index f576885ae..000000000 --- a/lib/handlebars/source.rb +++ /dev/null @@ -1,11 +0,0 @@ -module Handlebars - module Source - def self.bundled_path - File.expand_path("../../../dist/handlebars.js", __FILE__) - end - - def self.runtime_bundled_path - File.expand_path("../../../dist/handlebars.runtime.js", __FILE__) - end - end -end diff --git a/lib/handlebars/utils.js b/lib/handlebars/utils.js index 1e0e4c902..998c9ca6b 100644 --- a/lib/handlebars/utils.js +++ b/lib/handlebars/utils.js @@ -1,28 +1,6 @@ -exports.attach = function(Handlebars) { +import SafeString from "./safe-string"; -var toString = Object.prototype.toString; - -// BEGIN(BROWSER) - -var errorProps = ['description', 'fileName', 'lineNumber', 'message', 'name', 'number', 'stack']; - -Handlebars.Exception = function(message) { - var tmp = Error.prototype.constructor.apply(this, arguments); - - // Unfortunately errors are not enumerable in Chrome (at least), so `for prop in tmp` doesn't work. - for (var idx = 0; idx < errorProps.length; idx++) { - this[errorProps[idx]] = tmp[errorProps[idx]]; - } -}; -Handlebars.Exception.prototype = new Error(); - -// Build out our basic SafeString type -Handlebars.SafeString = function(string) { - this.string = string; -}; -Handlebars.SafeString.prototype.toString = function() { - return this.string.toString(); -}; +var isArray = Array.isArray; var escape = { "&": "&", @@ -36,48 +14,41 @@ var escape = { var badChars = /[&<>"'`]/g; var possible = /[&<>"'`]/; -var escapeChar = function(chr) { +function escapeChar(chr) { return escape[chr] || "&"; -}; +} -Handlebars.Utils = { - extend: function(obj, value) { - for(var key in value) { - if(value.hasOwnProperty(key)) { - obj[key] = value[key]; - } - } - }, - - escapeExpression: function(string) { - // don't escape SafeStrings, since they're already safe - if (string instanceof Handlebars.SafeString) { - return string.toString(); - } else if (string == null || string === false) { - return ""; - } - - // Force a string conversion as this will be done by the append regardless and - // the regex test will do this transparently behind the scenes, causing issues if - // an object's to string has escaped characters in it. - string = string.toString(); - - if(!possible.test(string)) { return string; } - return string.replace(badChars, escapeChar); - }, - - isEmpty: function(value) { - if (!value && value !== 0) { - return true; - } else if(toString.call(value) === "[object Array]" && value.length === 0) { - return true; - } else { - return false; +export function extend(obj, value) { + for(var key in value) { + if(value.hasOwnProperty(key)) { + obj[key] = value[key]; } } -}; - -// END(BROWSER) +} + +export function escapeExpression(string) { + // don't escape SafeStrings, since they're already safe + if (string instanceof SafeString) { + return string.toString(); + } else if (!string && string !== 0) { + return ""; + } -return Handlebars; -}; + // Force a string conversion as this will be done by the append regardless and + // the regex test will do this transparently behind the scenes, causing issues if + // an object's to string has escaped characters in it. + string = "" + string; + + if(!possible.test(string)) { return string; } + return string.replace(badChars, escapeChar); +} + +export function isEmpty(value) { + if (!value && value !== 0) { + return true; + } else if (isArray(value) && value.length === 0) { + return true; + } else { + return false; + } +} diff --git a/lib/index.js b/lib/index.js new file mode 100644 index 000000000..ca51d3a49 --- /dev/null +++ b/lib/index.js @@ -0,0 +1,25 @@ +// USAGE: +// var handlebars = require('handlebars'); + +// var local = handlebars.create(); + +var handlebars = require('../dist/cjs/handlebars').default; + +handlebars.Visitor = require('../dist/cjs/handlebars/compiler/visitor').default; + +var printer = require('../dist/cjs/handlebars/compiler/printer'); +handlebars.PrintVisitor = printer.PrintVisitor; +handlebars.print = printer.print; + +module.exports = handlebars; + +// Publish a Node.js require() handler for .handlebars and .hbs files +if (typeof require !== 'undefined' && require.extensions) { + var extension = function(module, filename) { + var fs = require("fs"); + var templateString = fs.readFileSync(filename, "utf8"); + module.exports = handlebars.compile(templateString); + }; + require.extensions[".handlebars"] = extension; + require.extensions[".hbs"] = extension; +} diff --git a/package.json b/package.json index b469398f2..3e7595b39 100644 --- a/package.json +++ b/package.json @@ -1,35 +1,58 @@ { "name": "handlebars", - "description": "Extension of the Mustache logicless template language", - "version": "1.0.12", + "barename": "handlebars", + "version": "1.1.0", + "description": "Handlebars provides the power necessary to let you build semantic templates effectively with no frustration", "homepage": "http://www.handlebarsjs.com/", "keywords": [ - "handlebars mustache template html" + "handlebars", + "mustache", + "template", + "html" ], "repository": { "type": "git", - "url": "git://github.com/wycats/handlebars.js.git" + "url": "https://github.com/wycats/handlebars.js.git" }, + "author": "Yehuda Katz", + "license": "BSD", + "readmeFilename": "README.md", "engines": { "node": ">=0.4.7" }, "dependencies": { - "optimist": "~0.3", + "optimist": "~0.3" + }, + "optionalDependencies": { "uglify-js": "~2.3" }, "devDependencies": { + "async": "~0.2.9", + "aws-sdk": "~1.5.0", "benchmark": "~1.0", - "dust": "~0.3", - "jison": "~0.3", + "dustjs-linkedin": "~2.0.2", + "eco": "~1.1.0-rc-3", + "grunt": "~0.4.1", + "grunt-contrib-clean": "~0.4.1", + "grunt-contrib-copy": "~0.4.1", + "grunt-contrib-jshint": "~0.6.3", + "grunt-contrib-requirejs": "~0.4.1", + "grunt-contrib-uglify": "~0.2.2", + "grunt-es6-module-transpiler": "joefiorini/grunt-es6-module-transpiler", + "es6-module-packager": "*", + "jison": "~0.3.0", + "keen.io": "0.0.3", "mocha": "*", - "mustache": "~0.7.2" + "mustache": "~0.7.2", + "semver": "~2.1.0", + "should": "~1.2.2", + "underscore": "~1.5.1" }, - "main": "lib/handlebars.js", + "main": "lib/index.js", "bin": { "handlebars": "bin/handlebars" }, "scripts": { - "test": "node_modules/.bin/mocha -u qunit spec/qunit_spec.js" - }, - "optionalDependencies": {} + "test": "grunt" + } } diff --git a/release-notes.md b/release-notes.md index 8e6761944..6040f411f 100644 --- a/release-notes.md +++ b/release-notes.md @@ -1,7 +1,37 @@ # Release Notes ## Development -[Commits](https://github.com/wycats/handlebars.js/compare/v1.0.12...master) + +[Commits](https://github.com/wycats/handlebars.js/compare/v1.1.0...master) + +## v1.1.0 - November 3rd, 2013 + +- [#628](https://github.com/wycats/handlebars.js/pull/628) - Convert code to ES6 modules ([@kpdecker](https://api.github.com/users/kpdecker)) +- [#336](https://github.com/wycats/handlebars.js/pull/336) - Add whitespace control syntax ([@kpdecker](https://api.github.com/users/kpdecker)) +- [#535](https://github.com/wycats/handlebars.js/pull/535) - Fix for probable JIT error under Safari ([@sorentwo](https://api.github.com/users/sorentwo)) +- [#483](https://github.com/wycats/handlebars.js/issues/483) - Add first and last @ vars to each helper ([@denniskuczynski](https://api.github.com/users/denniskuczynski)) +- [#557](https://github.com/wycats/handlebars.js/pull/557) - `\\{{foo}}` escaping only works in some situations ([@dmarcotte](https://api.github.com/users/dmarcotte)) +- [#552](https://github.com/wycats/handlebars.js/pull/552) - Added BOM removal flag. ([@blessenm](https://api.github.com/users/blessenm)) +- [#543](https://github.com/wycats/handlebars.js/pull/543) - publish passing master builds to s3 ([@fivetanley](https://api.github.com/users/fivetanley)) + +- [#608](https://github.com/wycats/handlebars.js/issues/608) - Add `includeZero` flag to `if` conditional +- [#498](https://github.com/wycats/handlebars.js/issues/498) - `Handlebars.compile` fails on empty string although a single blank works fine +- [#599](https://github.com/wycats/handlebars.js/issues/599) - lambda helpers only receive options if used with arguments +- [#592](https://github.com/wycats/handlebars.js/issues/592) - Optimize array and subprogram performance +- [#571](https://github.com/wycats/handlebars.js/issues/571) - uglify upgrade breaks compatibility with older versions of node +- [#587](https://github.com/wycats/handlebars.js/issues/587) - Partial inside partial breaks? + + +Compatibility notes: +- The project now includes separate artifacts for AMD, CommonJS, and global objects. + - AMD: Users may load the bundled `handlebars.amd.js` or `handlebars.runtime.amd.js` files or load individual modules directly. AMD users should also note that the handlebars object is exposed via the `default` field on the imported object. This [gist](https://gist.github.com/wycats/7417be0dc361a69d5916) provides some discussion of possible compatibility shims. + - CommonJS/Node: Node loading occurs as normal via `require` + - Globals: The `handlebars.js` and `handlebars.runtime.js` files should behave in the same manner as the v1.0.12 / 1.0.0 release. +- Build artifacts have been removed from the repository. [npm][npm], [components/handlebars.js][components], [cdnjs][cdnjs-lib], or the [builds page][builds-page] should now be used as the source of built artifacts. +- Context-stored helpers are now always passed the `options` hash. Previously no-argument helpers did not have this argument. + + +[Commits](https://github.com/wycats/handlebars.js/compare/v1.0.12...v1.1.0) ## v1.0.12 / 1.0.0 - May 31 2013 @@ -21,6 +51,7 @@ Compatibility notes: - The parser is now stricter on `{{{`, requiring that the end token be `}}}`. Templates that do not follow this convention should add the additional brace value. - Code that relies on global the namespace being muted when custom helpers or partials are passed will need to explicitly pass an `undefined` value for any helpers that should not be available. +- The compiler version has changed. Precompiled templates with 1.0.12 or higher must use the 1.0.0 or higher runtime. [Commits](https://github.com/wycats/handlebars.js/compare/v1.0.11...v1.0.12) @@ -91,3 +122,8 @@ Use: ```js template(context, {helpers: helpers, partials: partials, data: data}) ``` + +[builds-page]: http://builds.handlebarsjs.com.s3.amazonaws.com/index.html +[cdn-js]: http://cdnjs.com/libraries/handlebars.js/ +[components]: https://github.com/components/handlebars.js +[npm]: https://npmjs.org/package/handlebars diff --git a/spec/acceptance_spec.rb b/spec/acceptance_spec.rb deleted file mode 100644 index 03b23c5de..000000000 --- a/spec/acceptance_spec.rb +++ /dev/null @@ -1,101 +0,0 @@ -require "spec_helper" - -class TestContext - class TestModule - attr_reader :name, :tests - - def initialize(name) - @name = name - @tests = [] - end - end - - attr_reader :modules - - def initialize - @modules = [] - end - - def module(name) - @modules << TestModule.new(name) - end - - def test(name, function) - @modules.last.tests << [name, function] - end -end - -test_context = TestContext.new -js_context = Handlebars::Spec::CONTEXT - -Module.new do - extend Test::Unit::Assertions - - def self.js_backtrace(context) - begin - context.eval("throw") - rescue V8::JSError => e - return e.backtrace(:javascript) - end - end - - js_context["p"] = proc do |this, str| - p str - end - - js_context["ok"] = proc do |this, ok, message| - js_context["$$RSPEC1$$"] = ok - - result = js_context.eval("!!$$RSPEC1$$") - - message ||= "#{ok} was not truthy" - - unless result - backtrace = js_backtrace(js_context) - message << "\n#{backtrace.join("\n")}" - end - - assert result, message - end - - js_context["equals"] = proc do |this, first, second, message| - js_context["$$RSPEC1$$"] = first - js_context["$$RSPEC2$$"] = second - - result = js_context.eval("$$RSPEC1$$ == $$RSPEC2$$") - - additional_message = "#{first.inspect} did not == #{second.inspect}" - message = message ? "#{message} (#{additional_message})" : additional_message - - unless result - backtrace = js_backtrace(js_context) - message << "\n#{backtrace.join("\n")}" - end - - assert result, message - end - - js_context["equal"] = js_context["equals"] - - js_context["suite"] = proc do |this, name| - test_context.module(name) - end - - js_context["test"] = proc do |this, name, function| - test_context.test(name, function) - end - - local = Regexp.escape(File.expand_path(Dir.pwd)) - qunit_spec = File.expand_path("../qunit_spec.js", __FILE__) - js_context.load(qunit_spec.sub(/^#{local}\//, '')) -end - -test_context.modules.each do |mod| - describe mod.name do - mod.tests.each do |name, function| - it name do - function.call - end - end - end -end diff --git a/spec/example_1.handlebars b/spec/artifacts/example_1.handlebars similarity index 100% rename from spec/example_1.handlebars rename to spec/artifacts/example_1.handlebars diff --git a/spec/example_2.hbs b/spec/artifacts/example_2.hbs similarity index 100% rename from spec/example_2.hbs rename to spec/artifacts/example_2.hbs diff --git a/spec/basic.js b/spec/basic.js new file mode 100644 index 000000000..ee154a165 --- /dev/null +++ b/spec/basic.js @@ -0,0 +1,188 @@ +global.handlebarsEnv = null; + +beforeEach(function() { + global.handlebarsEnv = Handlebars.create(); +}); + +describe("basic context", function() { + it("most basic", function() { + shouldCompileTo("{{foo}}", { foo: "foo" }, "foo"); + }); + + it("escaping", function() { + shouldCompileTo("\\{{foo}}", { foo: "food" }, "{{foo}}"); + shouldCompileTo("content \\{{foo}}", { foo: "food" }, "content {{foo}}"); + shouldCompileTo("\\\\{{foo}}", { foo: "food" }, "\\food"); + shouldCompileTo("content \\\\{{foo}}", { foo: "food" }, "content \\food"); + shouldCompileTo("\\\\ {{foo}}", { foo: "food" }, "\\\\ food"); + }); + + it("compiling with a basic context", function() { + shouldCompileTo("Goodbye\n{{cruel}}\n{{world}}!", {cruel: "cruel", world: "world"}, "Goodbye\ncruel\nworld!", + "It works if all the required keys are provided"); + }); + + it("compiling with an undefined context", function() { + shouldCompileTo("Goodbye\n{{cruel}}\n{{world.bar}}!", undefined, "Goodbye\n\n!"); + + shouldCompileTo("{{#unless foo}}Goodbye{{../test}}{{test2}}{{/unless}}", undefined, "Goodbye"); + }); + + it("comments", function() { + shouldCompileTo("{{! Goodbye}}Goodbye\n{{cruel}}\n{{world}}!", + {cruel: "cruel", world: "world"}, "Goodbye\ncruel\nworld!", + "comments are ignored"); + }); + + it("boolean", function() { + var string = "{{#goodbye}}GOODBYE {{/goodbye}}cruel {{world}}!"; + shouldCompileTo(string, {goodbye: true, world: "world"}, "GOODBYE cruel world!", + "booleans show the contents when true"); + + shouldCompileTo(string, {goodbye: false, world: "world"}, "cruel world!", + "booleans do not show the contents when false"); + }); + + it("zeros", function() { + shouldCompileTo("num1: {{num1}}, num2: {{num2}}", {num1: 42, num2: 0}, + "num1: 42, num2: 0"); + shouldCompileTo("num: {{.}}", 0, "num: 0"); + shouldCompileTo("num: {{num1/num2}}", {num1: {num2: 0}}, "num: 0"); + }); + + it("newlines", function() { + shouldCompileTo("Alan's\nTest", {}, "Alan's\nTest"); + shouldCompileTo("Alan's\rTest", {}, "Alan's\rTest"); + }); + + it("escaping text", function() { + shouldCompileTo("Awesome's", {}, "Awesome's", "text is escaped so that it doesn't get caught on single quotes"); + shouldCompileTo("Awesome\\", {}, "Awesome\\", "text is escaped so that the closing quote can't be ignored"); + shouldCompileTo("Awesome\\\\ foo", {}, "Awesome\\\\ foo", "text is escaped so that it doesn't mess up backslashes"); + shouldCompileTo("Awesome {{foo}}", {foo: '\\'}, "Awesome \\", "text is escaped so that it doesn't mess up backslashes"); + shouldCompileTo(' " " ', {}, ' " " ', "double quotes never produce invalid javascript"); + }); + + it("escaping expressions", function() { + shouldCompileTo("{{{awesome}}}", {awesome: "&\"\\<>"}, '&\"\\<>', + "expressions with 3 handlebars aren't escaped"); + + shouldCompileTo("{{&awesome}}", {awesome: "&\"\\<>"}, '&\"\\<>', + "expressions with {{& handlebars aren't escaped"); + + shouldCompileTo("{{awesome}}", {awesome: "&\"'`\\<>"}, '&"'`\\<>', + "by default expressions should be escaped"); + + shouldCompileTo("{{awesome}}", {awesome: "Escaped, looks like: <b>"}, 'Escaped, <b> looks like: &lt;b&gt;', + "escaping should properly handle amperstands"); + }); + + it("functions returning safestrings shouldn't be escaped", function() { + var hash = {awesome: function() { return new Handlebars.SafeString("&\"\\<>"); }}; + shouldCompileTo("{{awesome}}", hash, '&\"\\<>', + "functions returning safestrings aren't escaped"); + }); + + it("functions", function() { + shouldCompileTo("{{awesome}}", {awesome: function() { return "Awesome"; }}, "Awesome", + "functions are called and render their output"); + shouldCompileTo("{{awesome}}", {awesome: function() { return this.more; }, more: "More awesome"}, "More awesome", + "functions are bound to the context"); + }); + + it("functions with context argument", function() { + shouldCompileTo("{{awesome frank}}", + {awesome: function(context) { return context; }, + frank: "Frank"}, + "Frank", "functions are called with context arguments"); + }); + + it("block functions with context argument", function() { + shouldCompileTo("{{#awesome 1}}inner {{.}}{{/awesome}}", + {awesome: function(context, options) { return options.fn(context); }}, + "inner 1", "block functions are called with context and options"); + }); + + it("block functions without context argument", function() { + shouldCompileTo("{{#awesome}}inner{{/awesome}}", + {awesome: function(options) { return options.fn(this); }}, + "inner", "block functions are called with options"); + }); + + + it("paths with hyphens", function() { + shouldCompileTo("{{foo-bar}}", {"foo-bar": "baz"}, "baz", "Paths can contain hyphens (-)"); + shouldCompileTo("{{foo.foo-bar}}", {foo: {"foo-bar": "baz"}}, "baz", "Paths can contain hyphens (-)"); + shouldCompileTo("{{foo/foo-bar}}", {foo: {"foo-bar": "baz"}}, "baz", "Paths can contain hyphens (-)"); + }); + + it("nested paths", function() { + shouldCompileTo("Goodbye {{alan/expression}} world!", {alan: {expression: "beautiful"}}, + "Goodbye beautiful world!", "Nested paths access nested objects"); + }); + + it("nested paths with empty string value", function() { + shouldCompileTo("Goodbye {{alan/expression}} world!", {alan: {expression: ""}}, + "Goodbye world!", "Nested paths access nested objects with empty string"); + }); + + it("literal paths", function() { + shouldCompileTo("Goodbye {{[@alan]/expression}} world!", {"@alan": {expression: "beautiful"}}, + "Goodbye beautiful world!", "Literal paths can be used"); + shouldCompileTo("Goodbye {{[foo bar]/expression}} world!", {"foo bar": {expression: "beautiful"}}, + "Goodbye beautiful world!", "Literal paths can be used"); + }); + + it('literal references', function() { + shouldCompileTo("Goodbye {{[foo bar]}} world!", {"foo bar": "beautiful"}, + "Goodbye beautiful world!", "Literal paths can be used"); + }); + + it("that current context path ({{.}}) doesn't hit helpers", function() { + shouldCompileTo("test: {{.}}", [null, {helper: "awesome"}], "test: "); + }); + + it("complex but empty paths", function() { + shouldCompileTo("{{person/name}}", {person: {name: null}}, ""); + shouldCompileTo("{{person/name}}", {person: {}}, ""); + }); + + it("this keyword in paths", function() { + var string = "{{#goodbyes}}{{this}}{{/goodbyes}}"; + var hash = {goodbyes: ["goodbye", "Goodbye", "GOODBYE"]}; + shouldCompileTo(string, hash, "goodbyeGoodbyeGOODBYE", + "This keyword in paths evaluates to current context"); + + string = "{{#hellos}}{{this/text}}{{/hellos}}"; + hash = {hellos: [{text: "hello"}, {text: "Hello"}, {text: "HELLO"}]}; + shouldCompileTo(string, hash, "helloHelloHELLO", "This keyword evaluates in more complex paths"); + }); + + it("this keyword nested inside path", function() { + var string = "{{#hellos}}{{text/this/foo}}{{/hellos}}"; + (function() { + CompilerContext.compile(string); + }).should.throw(Error); + }); + + it("this keyword in helpers", function() { + var helpers = {foo: function(value) { + return 'bar ' + value; + }}; + var string = "{{#goodbyes}}{{foo this}}{{/goodbyes}}"; + var hash = {goodbyes: ["goodbye", "Goodbye", "GOODBYE"]}; + shouldCompileTo(string, [hash, helpers], "bar goodbyebar Goodbyebar GOODBYE", + "This keyword in paths evaluates to current context"); + + string = "{{#hellos}}{{foo this/text}}{{/hellos}}"; + hash = {hellos: [{text: "hello"}, {text: "Hello"}, {text: "HELLO"}]}; + shouldCompileTo(string, [hash, helpers], "bar hellobar Hellobar HELLO", "This keyword evaluates in more complex paths"); + }); + + it("this keyword nested inside helpers param", function() { + var string = "{{#hellos}}{{foo text/this/foo}}{{/hellos}}"; + (function() { + CompilerContext.compile(string); + }).should.throw(Error); + }); +}); diff --git a/spec/blocks.js b/spec/blocks.js new file mode 100644 index 000000000..1880eb584 --- /dev/null +++ b/spec/blocks.js @@ -0,0 +1,86 @@ +/*global CompilerContext, shouldCompileTo */ +describe('blocks', function() { + it("array", function() { + var string = "{{#goodbyes}}{{text}}! {{/goodbyes}}cruel {{world}}!"; + var hash = {goodbyes: [{text: "goodbye"}, {text: "Goodbye"}, {text: "GOODBYE"}], world: "world"}; + shouldCompileTo(string, hash, "goodbye! Goodbye! GOODBYE! cruel world!", + "Arrays iterate over the contents when not empty"); + + shouldCompileTo(string, {goodbyes: [], world: "world"}, "cruel world!", + "Arrays ignore the contents when empty"); + + }); + + it("array with @index", function() { + var string = "{{#goodbyes}}{{@index}}. {{text}}! {{/goodbyes}}cruel {{world}}!"; + var hash = {goodbyes: [{text: "goodbye"}, {text: "Goodbye"}, {text: "GOODBYE"}], world: "world"}; + + var template = CompilerContext.compile(string); + var result = template(hash); + + equal(result, "0. goodbye! 1. Goodbye! 2. GOODBYE! cruel world!", "The @index variable is used"); + }); + + it("empty block", function() { + var string = "{{#goodbyes}}{{/goodbyes}}cruel {{world}}!"; + var hash = {goodbyes: [{text: "goodbye"}, {text: "Goodbye"}, {text: "GOODBYE"}], world: "world"}; + shouldCompileTo(string, hash, "cruel world!", + "Arrays iterate over the contents when not empty"); + + shouldCompileTo(string, {goodbyes: [], world: "world"}, "cruel world!", + "Arrays ignore the contents when empty"); + }); + + it("block with complex lookup", function() { + var string = "{{#goodbyes}}{{text}} cruel {{../name}}! {{/goodbyes}}"; + var hash = {name: "Alan", goodbyes: [{text: "goodbye"}, {text: "Goodbye"}, {text: "GOODBYE"}]}; + + shouldCompileTo(string, hash, "goodbye cruel Alan! Goodbye cruel Alan! GOODBYE cruel Alan! ", + "Templates can access variables in contexts up the stack with relative path syntax"); + }); + + it("block with complex lookup using nested context", function() { + var string = "{{#goodbyes}}{{text}} cruel {{foo/../name}}! {{/goodbyes}}"; + + (function() { + CompilerContext.compile(string); + }).should.throw(Error); + }); + + it("block with deep nested complex lookup", function() { + var string = "{{#outer}}Goodbye {{#inner}}cruel {{../../omg}}{{/inner}}{{/outer}}"; + var hash = {omg: "OMG!", outer: [{ inner: [{ text: "goodbye" }] }] }; + + shouldCompileTo(string, hash, "Goodbye cruel OMG!"); + }); + + describe('inverted sections', function() { + it("inverted sections with unset value", function() { + var string = "{{#goodbyes}}{{this}}{{/goodbyes}}{{^goodbyes}}Right On!{{/goodbyes}}"; + var hash = {}; + shouldCompileTo(string, hash, "Right On!", "Inverted section rendered when value isn't set."); + }); + + it("inverted section with false value", function() { + var string = "{{#goodbyes}}{{this}}{{/goodbyes}}{{^goodbyes}}Right On!{{/goodbyes}}"; + var hash = {goodbyes: false}; + shouldCompileTo(string, hash, "Right On!", "Inverted section rendered when value is false."); + }); + + it("inverted section with empty set", function() { + var string = "{{#goodbyes}}{{this}}{{/goodbyes}}{{^goodbyes}}Right On!{{/goodbyes}}"; + var hash = {goodbyes: []}; + shouldCompileTo(string, hash, "Right On!", "Inverted section rendered when value is empty set."); + }); + + it("block inverted sections", function() { + shouldCompileTo("{{#people}}{{name}}{{^}}{{none}}{{/people}}", {none: "No people"}, + "No people"); + }); + + it("block inverted sections with empty arrays", function() { + shouldCompileTo("{{#people}}{{name}}{{^}}{{none}}{{/people}}", {none: "No people", people: []}, + "No people"); + }); + }); +}); diff --git a/spec/builtins.js b/spec/builtins.js new file mode 100644 index 000000000..401e789a9 --- /dev/null +++ b/spec/builtins.js @@ -0,0 +1,178 @@ +/*global CompilerContext, shouldCompileTo, compileWithPartials */ +describe('builtin helpers', function() { + describe('#if', function() { + it("if", function() { + var string = "{{#if goodbye}}GOODBYE {{/if}}cruel {{world}}!"; + shouldCompileTo(string, {goodbye: true, world: "world"}, "GOODBYE cruel world!", + "if with boolean argument shows the contents when true"); + shouldCompileTo(string, {goodbye: "dummy", world: "world"}, "GOODBYE cruel world!", + "if with string argument shows the contents"); + shouldCompileTo(string, {goodbye: false, world: "world"}, "cruel world!", + "if with boolean argument does not show the contents when false"); + shouldCompileTo(string, {world: "world"}, "cruel world!", + "if with undefined does not show the contents"); + shouldCompileTo(string, {goodbye: ['foo'], world: "world"}, "GOODBYE cruel world!", + "if with non-empty array shows the contents"); + shouldCompileTo(string, {goodbye: [], world: "world"}, "cruel world!", + "if with empty array does not show the contents"); + shouldCompileTo(string, {goodbye: 0, world: "world"}, "cruel world!", + "if with zero does not show the contents"); + shouldCompileTo("{{#if goodbye includeZero=true}}GOODBYE {{/if}}cruel {{world}}!", + {goodbye: 0, world: "world"}, "GOODBYE cruel world!", + "if with zero does not show the contents"); + }); + + it("if with function argument", function() { + var string = "{{#if goodbye}}GOODBYE {{/if}}cruel {{world}}!"; + shouldCompileTo(string, {goodbye: function() {return true;}, world: "world"}, "GOODBYE cruel world!", + "if with function shows the contents when function returns true"); + shouldCompileTo(string, {goodbye: function() {return this.world;}, world: "world"}, "GOODBYE cruel world!", + "if with function shows the contents when function returns string"); + shouldCompileTo(string, {goodbye: function() {return false;}, world: "world"}, "cruel world!", + "if with function does not show the contents when returns false"); + shouldCompileTo(string, {goodbye: function() {return this.foo;}, world: "world"}, "cruel world!", + "if with function does not show the contents when returns undefined"); + }); + }); + + describe('#with', function() { + it("with", function() { + var string = "{{#with person}}{{first}} {{last}}{{/with}}"; + shouldCompileTo(string, {person: {first: "Alan", last: "Johnson"}}, "Alan Johnson"); + }); + it("with with function argument", function() { + var string = "{{#with person}}{{first}} {{last}}{{/with}}"; + shouldCompileTo(string, {person: function() { return {first: "Alan", last: "Johnson"};}}, "Alan Johnson"); + }); + }); + + describe('#each', function() { + beforeEach(function() { + handlebarsEnv.registerHelper('detectDataInsideEach', function(options) { + return options.data && options.data.exclaim; + }); + }); + + it("each", function() { + var string = "{{#each goodbyes}}{{text}}! {{/each}}cruel {{world}}!"; + var hash = {goodbyes: [{text: "goodbye"}, {text: "Goodbye"}, {text: "GOODBYE"}], world: "world"}; + shouldCompileTo(string, hash, "goodbye! Goodbye! GOODBYE! cruel world!", + "each with array argument iterates over the contents when not empty"); + shouldCompileTo(string, {goodbyes: [], world: "world"}, "cruel world!", + "each with array argument ignores the contents when empty"); + }); + + it("each with an object and @key", function() { + var string = "{{#each goodbyes}}{{@key}}. {{text}}! {{/each}}cruel {{world}}!"; + var hash = {goodbyes: {"#1": {text: "goodbye"}, 2: {text: "GOODBYE"}}, world: "world"}; + + // Object property iteration order is undefined according to ECMA spec, + // so we need to check both possible orders + // @see http://stackoverflow.com/questions/280713/elements-order-in-a-for-in-loop + var actual = compileWithPartials(string, hash); + var expected1 = "<b>#1</b>. goodbye! 2. GOODBYE! cruel world!"; + var expected2 = "2. GOODBYE! <b>#1</b>. goodbye! cruel world!"; + + (actual === expected1 || actual === expected2).should.equal(true, "each with object argument iterates over the contents when not empty"); + shouldCompileTo(string, {goodbyes: [], world: "world"}, "cruel world!", + "each with object argument ignores the contents when empty"); + }); + + it("each with @index", function() { + var string = "{{#each goodbyes}}{{@index}}. {{text}}! {{/each}}cruel {{world}}!"; + var hash = {goodbyes: [{text: "goodbye"}, {text: "Goodbye"}, {text: "GOODBYE"}], world: "world"}; + + var template = CompilerContext.compile(string); + var result = template(hash); + + equal(result, "0. goodbye! 1. Goodbye! 2. GOODBYE! cruel world!", "The @index variable is used"); + }); + + it("each with nested @index", function() { + var string = "{{#each goodbyes}}{{@index}}. {{text}}! {{#each ../goodbyes}}{{@index}} {{/each}}After {{@index}} {{/each}}{{@index}}cruel {{world}}!"; + var hash = {goodbyes: [{text: "goodbye"}, {text: "Goodbye"}, {text: "GOODBYE"}], world: "world"}; + + var template = CompilerContext.compile(string); + var result = template(hash); + + equal(result, "0. goodbye! 0 1 2 After 0 1. Goodbye! 0 1 2 After 1 2. GOODBYE! 0 1 2 After 2 cruel world!", "The @index variable is used"); + }); + + it("each with @first", function() { + var string = "{{#each goodbyes}}{{#if @first}}{{text}}! {{/if}}{{/each}}cruel {{world}}!"; + var hash = {goodbyes: [{text: "goodbye"}, {text: "Goodbye"}, {text: "GOODBYE"}], world: "world"}; + + var template = CompilerContext.compile(string); + var result = template(hash); + + equal(result, "goodbye! cruel world!", "The @first variable is used"); + }); + + it("each with nested @first", function() { + var string = "{{#each goodbyes}}({{#if @first}}{{text}}! {{/if}}{{#each ../goodbyes}}{{#if @first}}{{text}}!{{/if}}{{/each}}{{#if @first}} {{text}}!{{/if}}) {{/each}}cruel {{world}}!"; + var hash = {goodbyes: [{text: "goodbye"}, {text: "Goodbye"}, {text: "GOODBYE"}], world: "world"}; + + var template = CompilerContext.compile(string); + var result = template(hash); + + equal(result, "(goodbye! goodbye! goodbye!) (goodbye!) (goodbye!) cruel world!", "The @first variable is used"); + }); + + it("each with @last", function() { + var string = "{{#each goodbyes}}{{#if @last}}{{text}}! {{/if}}{{/each}}cruel {{world}}!"; + var hash = {goodbyes: [{text: "goodbye"}, {text: "Goodbye"}, {text: "GOODBYE"}], world: "world"}; + + var template = CompilerContext.compile(string); + var result = template(hash); + + equal(result, "GOODBYE! cruel world!", "The @last variable is used"); + }); + + it("each with nested @last", function() { + var string = "{{#each goodbyes}}({{#if @last}}{{text}}! {{/if}}{{#each ../goodbyes}}{{#if @last}}{{text}}!{{/if}}{{/each}}{{#if @last}} {{text}}!{{/if}}) {{/each}}cruel {{world}}!"; + var hash = {goodbyes: [{text: "goodbye"}, {text: "Goodbye"}, {text: "GOODBYE"}], world: "world"}; + + var template = CompilerContext.compile(string); + var result = template(hash); + + equal(result, "(GOODBYE!) (GOODBYE!) (GOODBYE! GOODBYE! GOODBYE!) cruel world!", "The @last variable is used"); + }); + + it("each with function argument", function() { + var string = "{{#each goodbyes}}{{text}}! {{/each}}cruel {{world}}!"; + var hash = {goodbyes: function () { return [{text: "goodbye"}, {text: "Goodbye"}, {text: "GOODBYE"}];}, world: "world"}; + shouldCompileTo(string, hash, "goodbye! Goodbye! GOODBYE! cruel world!", + "each with array function argument iterates over the contents when not empty"); + shouldCompileTo(string, {goodbyes: [], world: "world"}, "cruel world!", + "each with array function argument ignores the contents when empty"); + }); + + it("data passed to helpers", function() { + var string = "{{#each letters}}{{this}}{{detectDataInsideEach}}{{/each}}"; + var hash = {letters: ['a', 'b', 'c']}; + + var template = CompilerContext.compile(string); + var result = template(hash, { + data: { + exclaim: '!' + } + }); + equal(result, 'a!b!c!', 'should output data'); + }); + + }); + + it("#log", function() { + + var string = "{{log blah}}"; + var hash = { blah: "whee" }; + + var levelArg, logArg; + handlebarsEnv.log = function(level, arg){ levelArg = level, logArg = arg; }; + + shouldCompileTo(string, hash, "", "log should not display"); + equals(1, levelArg, "should call log with 1"); + equals("whee", logArg, "should call log with 'whee'"); + }); + +}); diff --git a/spec/data.js b/spec/data.js new file mode 100644 index 000000000..cf9424e97 --- /dev/null +++ b/spec/data.js @@ -0,0 +1,239 @@ +/*global CompilerContext */ +describe('data', function() { + it("passing in data to a compiled function that expects data - works with helpers", function() { + var template = CompilerContext.compile("{{hello}}", {data: true}); + + var helpers = { + hello: function(options) { + return options.data.adjective + " " + this.noun; + } + }; + + var result = template({noun: "cat"}, {helpers: helpers, data: {adjective: "happy"}}); + equals("happy cat", result, "Data output by helper"); + }); + + it("data can be looked up via @foo", function() { + var template = CompilerContext.compile("{{@hello}}"); + var result = template({}, { data: { hello: "hello" } }); + equals("hello", result, "@foo retrieves template data"); + }); + + it("deep @foo triggers automatic top-level data", function() { + var template = CompilerContext.compile('{{#let world="world"}}{{#if foo}}{{#if foo}}Hello {{@world}}{{/if}}{{/if}}{{/let}}'); + + var helpers = Handlebars.createFrame(handlebarsEnv.helpers); + + helpers.let = function(options) { + var frame = Handlebars.createFrame(options.data); + + for (var prop in options.hash) { + frame[prop] = options.hash[prop]; + } + return options.fn(this, { data: frame }); + }; + + var result = template({ foo: true }, { helpers: helpers }); + equals("Hello world", result, "Automatic data was triggered"); + }); + + it("parameter data can be looked up via @foo", function() { + var template = CompilerContext.compile("{{hello @world}}"); + var helpers = { + hello: function(noun) { + return "Hello " + noun; + } + }; + + var result = template({}, { helpers: helpers, data: { world: "world" } }); + equals("Hello world", result, "@foo as a parameter retrieves template data"); + }); + + it("hash values can be looked up via @foo", function() { + var template = CompilerContext.compile("{{hello noun=@world}}"); + var helpers = { + hello: function(options) { + return "Hello " + options.hash.noun; + } + }; + + var result = template({}, { helpers: helpers, data: { world: "world" } }); + equals("Hello world", result, "@foo as a parameter retrieves template data"); + }); + + it("nested parameter data can be looked up via @foo.bar", function() { + var template = CompilerContext.compile("{{hello @world.bar}}"); + var helpers = { + hello: function(noun) { + return "Hello " + noun; + } + }; + + var result = template({}, { helpers: helpers, data: { world: {bar: "world" } } }); + equals("Hello world", result, "@foo as a parameter retrieves template data"); + }); + + it("nested parameter data does not fail with @world.bar", function() { + var template = CompilerContext.compile("{{hello @world.bar}}"); + var helpers = { + hello: function(noun) { + return "Hello " + noun; + } + }; + + var result = template({}, { helpers: helpers, data: { foo: {bar: "world" } } }); + equals("Hello undefined", result, "@foo as a parameter retrieves template data"); + }); + + it("parameter data throws when using this scope references", function() { + var string = "{{#goodbyes}}{{text}} cruel {{@./name}}! {{/goodbyes}}"; + + (function() { + CompilerContext.compile(string); + }).should.throw(Error); + }); + + it("parameter data throws when using parent scope references", function() { + var string = "{{#goodbyes}}{{text}} cruel {{@../name}}! {{/goodbyes}}"; + + (function() { + CompilerContext.compile(string); + }).should.throw(Error); + }); + + it("parameter data throws when using complex scope references", function() { + var string = "{{#goodbyes}}{{text}} cruel {{@foo/../name}}! {{/goodbyes}}"; + + (function() { + CompilerContext.compile(string); + }).should.throw(Error); + }); + + it("data is inherited downstream", function() { + var template = CompilerContext.compile("{{#let foo=1 bar=2}}{{#let foo=bar.baz}}{{@bar}}{{@foo}}{{/let}}{{@foo}}{{/let}}", { data: true }); + var helpers = { + let: function(options) { + var frame = Handlebars.createFrame(options.data); + for (var prop in options.hash) { + frame[prop] = options.hash[prop]; + } + return options.fn(this, {data: frame}); + } + }; + + var result = template({ bar: { baz: "hello world" } }, { helpers: helpers, data: {} }); + equals("2hello world1", result, "data variables are inherited downstream"); + }); + + it("passing in data to a compiled function that expects data - works with helpers in partials", function() { + var template = CompilerContext.compile("{{>my_partial}}", {data: true}); + + var 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"); + }); + + it("passing in data to a compiled function that expects data - works with helpers and parameters", function() { + var template = CompilerContext.compile("{{hello world}}", {data: true}); + + var helpers = { + hello: function(noun, options) { + return options.data.adjective + " " + noun + (this.exclaim ? "!" : ""); + } + }; + + var result = template({exclaim: true, world: "world"}, {helpers: helpers, data: {adjective: "happy"}}); + equals("happy world!", result, "Data output by helper"); + }); + + it("passing in data to a compiled function that expects data - works with block helpers", function() { + var template = CompilerContext.compile("{{#hello}}{{world}}{{/hello}}", {data: true}); + + var helpers = { + hello: function(options) { + return options.fn(this); + }, + world: function(options) { + return options.data.adjective + " world" + (this.exclaim ? "!" : ""); + } + }; + + var result = template({exclaim: true}, {helpers: helpers, data: {adjective: "happy"}}); + equals("happy world!", result, "Data output by helper"); + }); + + it("passing in data to a compiled function that expects data - works with block helpers that use ..", function() { + var template = CompilerContext.compile("{{#hello}}{{world ../zomg}}{{/hello}}", {data: true}); + + var helpers = { + hello: function(options) { + return options.fn({exclaim: "?"}); + }, + world: function(thing, options) { + return options.data.adjective + " " + thing + (this.exclaim || ""); + } + }; + + var result = template({exclaim: true, zomg: "world"}, {helpers: helpers, data: {adjective: "happy"}}); + equals("happy world?", result, "Data output by helper"); + }); + + it("passing in data to a compiled function that expects data - data is passed to with block helpers where children use ..", function() { + var template = CompilerContext.compile("{{#hello}}{{world ../zomg}}{{/hello}}", {data: true}); + + var helpers = { + hello: function(options) { + return options.data.accessData + " " + options.fn({exclaim: "?"}); + }, + world: function(thing, options) { + return options.data.adjective + " " + thing + (this.exclaim || ""); + } + }; + + var result = template({exclaim: true, zomg: "world"}, {helpers: helpers, data: {adjective: "happy", accessData: "#win"}}); + equals("#win happy world?", result, "Data output by helper"); + }); + + it("you can override inherited data when invoking a helper", function() { + var template = CompilerContext.compile("{{#hello}}{{world zomg}}{{/hello}}", {data: true}); + + var helpers = { + hello: function(options) { + return options.fn({exclaim: "?", zomg: "world"}, { data: {adjective: "sad"} }); + }, + 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"); + }); + + + it("you can override inherited data when invoking a helper with depth", function() { + var template = CompilerContext.compile("{{#hello}}{{world ../zomg}}{{/hello}}", {data: true}); + + var helpers = { + hello: function(options) { + return options.fn({exclaim: "?"}, { data: {adjective: "sad"} }); + }, + 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"); + }); + +}); diff --git a/spec/env/browser.js b/spec/env/browser.js new file mode 100644 index 000000000..9f69e74d7 --- /dev/null +++ b/spec/env/browser.js @@ -0,0 +1,23 @@ +/*global handlebarsEnv */ +require('./common'); + +var _ = require('underscore'), + fs = require('fs'), + vm = require('vm'); + +global.Handlebars = undefined; +vm.runInThisContext(fs.readFileSync(__dirname + '/../../dist/handlebars.js'), 'dist/handlebars.js'); + +global.CompilerContext = { + compile: function(template, options) { + var templateSpec = handlebarsEnv.precompile(template, options); + return handlebarsEnv.template(safeEval(templateSpec)); + }, + compileWithPartial: function(template, options) { + return handlebarsEnv.compile(template, options); + } +}; + +function safeEval(templateSpec) { + return eval('(' + templateSpec + ')'); +} diff --git a/spec/env/common.js b/spec/env/common.js new file mode 100644 index 000000000..53ddd61ae --- /dev/null +++ b/spec/env/common.js @@ -0,0 +1,28 @@ +global.should = require('should'); + +global.shouldCompileTo = function(string, hashOrArray, expected, message) { + shouldCompileToWithPartials(string, hashOrArray, false, expected, message); +}; + +global.shouldCompileToWithPartials = function(string, hashOrArray, partials, expected, message) { + var result = compileWithPartials(string, hashOrArray, partials); + result.should.equal(expected, "'" + expected + "' should === '" + result + "': " + message); +}; + +global.compileWithPartials = function(string, hashOrArray, partials) { + var template = CompilerContext[partials ? 'compileWithPartial' : 'compile'](string), ary; + if(Object.prototype.toString.call(hashOrArray) === "[object Array]") { + ary = []; + ary.push(hashOrArray[0]); + ary.push({ helpers: hashOrArray[1], partials: hashOrArray[2] }); + } else { + ary = [hashOrArray]; + } + + return template.apply(this, ary); +}; + + +global.equals = global.equal = function(a, b, msg) { + a.should.equal(b, msg || ''); +}; diff --git a/spec/env/node.js b/spec/env/node.js new file mode 100644 index 000000000..808c07ef4 --- /dev/null +++ b/spec/env/node.js @@ -0,0 +1,18 @@ +/*global handlebarsEnv */ +require('./common'); + +global.Handlebars = require('../../lib'); + +global.CompilerContext = { + compile: function(template, options) { + var templateSpec = handlebarsEnv.precompile(template, options); + return handlebarsEnv.template(safeEval(templateSpec)); + }, + compileWithPartial: function(template, options) { + return handlebarsEnv.compile(template, options); + } +}; + +function safeEval(templateSpec) { + return eval('(' + templateSpec + ')'); +} diff --git a/spec/env/runner.js b/spec/env/runner.js new file mode 100644 index 000000000..143d30023 --- /dev/null +++ b/spec/env/runner.js @@ -0,0 +1,42 @@ +var fs = require('fs'), + Mocha = require('mocha'), + path = require('path'); + +var errors = 0, + testDir = path.dirname(__dirname), + grep = process.argv[2]; + +var files = [ testDir + "/basic.js" ]; + +var files = fs.readdirSync(testDir) + .filter(function(name) { return (/.*\.js$/).test(name); }) + .map(function(name) { return testDir + '/' + name; }); + +run('./node', function() { + run('./browser', function() { + run('./runtime', function() { + process.exit(errors); + }); + }); +}); + + +function run(env, callback) { + var mocha = new Mocha(); + mocha.ui('bdd'); + mocha.files = files.slice(); + if (grep) { + mocha.grep(grep); + } + + files.forEach(function(name) { + delete require.cache[name]; + }); + + console.log('Running env: ' + env); + require(env); + mocha.run(function(errorCount) { + errors += errorCount; + callback(); + }); +} diff --git a/spec/env/runtime.js b/spec/env/runtime.js new file mode 100644 index 000000000..68e40b058 --- /dev/null +++ b/spec/env/runtime.js @@ -0,0 +1,29 @@ +/*global handlebarsEnv */ +require('./common'); + +var _ = require('underscore'), + fs = require('fs'), + vm = require('vm'); + +global.Handlebars = undefined; +vm.runInThisContext(fs.readFileSync(__dirname + '/../../dist/handlebars.runtime.js'), 'dist/handlebars.runtime.js'); + +var compiler = require('../../dist/cjs/handlebars/compiler/compiler'); + +global.CompilerContext = { + compile: function(template, options) { + var templateSpec = compiler.precompile(template, options); + return handlebarsEnv.template(safeEval(templateSpec)); + }, + compileWithPartial: function(template, options) { + // Hack the compiler on to the environment for these specific tests + handlebarsEnv.compile = function(template, options) { + return compiler.compile(template, options, handlebarsEnv); + }; + return handlebarsEnv.compile(template, options); + } +}; + +function safeEval(templateSpec) { + return eval('(' + templateSpec + ')'); +} diff --git a/spec/helpers.js b/spec/helpers.js new file mode 100644 index 000000000..c5ea574bc --- /dev/null +++ b/spec/helpers.js @@ -0,0 +1,528 @@ +/*global CompilerContext, shouldCompileTo, shouldCompileToWithPartials */ +describe('helpers', function() { + it("helper with complex lookup$", function() { + var string = "{{#goodbyes}}{{{link ../prefix}}}{{/goodbyes}}"; + var hash = {prefix: "/root", goodbyes: [{text: "Goodbye", url: "goodbye"}]}; + var helpers = {link: function(prefix) { + return "" + this.text + ""; + }}; + shouldCompileTo(string, [hash, helpers], "Goodbye"); + }); + + it("helper block with complex lookup expression", function() { + var string = "{{#goodbyes}}{{../name}}{{/goodbyes}}"; + var hash = {name: "Alan"}; + var helpers = {goodbyes: function(options) { + var out = ""; + var byes = ["Goodbye", "goodbye", "GOODBYE"]; + for (var i = 0,j = byes.length; i < j; i++) { + out += byes[i] + " " + options.fn(this) + "! "; + } + return out; + }}; + shouldCompileTo(string, [hash, helpers], "Goodbye Alan! goodbye Alan! GOODBYE Alan! "); + }); + + it("helper with complex lookup and nested template", function() { + var string = "{{#goodbyes}}{{#link ../prefix}}{{text}}{{/link}}{{/goodbyes}}"; + var hash = {prefix: '/root', goodbyes: [{text: "Goodbye", url: "goodbye"}]}; + var helpers = {link: function (prefix, options) { + return "" + options.fn(this) + ""; + }}; + shouldCompileToWithPartials(string, [hash, helpers], false, "Goodbye"); + }); + + it("helper with complex lookup and nested template in VM+Compiler", function() { + var string = "{{#goodbyes}}{{#link ../prefix}}{{text}}{{/link}}{{/goodbyes}}"; + var hash = {prefix: '/root', goodbyes: [{text: "Goodbye", url: "goodbye"}]}; + var helpers = {link: function (prefix, options) { + return "" + options.fn(this) + ""; + }}; + shouldCompileToWithPartials(string, [hash, helpers], true, "Goodbye"); + }); + + it("block helper", function() { + var string = "{{#goodbyes}}{{text}}! {{/goodbyes}}cruel {{world}}!"; + var template = CompilerContext.compile(string); + + var result = template({world: "world"}, { helpers: {goodbyes: function(options) { return options.fn({text: "GOODBYE"}); }}}); + equal(result, "GOODBYE! cruel world!", "Block helper executed"); + }); + + it("block helper staying in the same context", function() { + var string = "{{#form}}

{{name}}

{{/form}}"; + var template = CompilerContext.compile(string); + + var result = template({name: "Yehuda"}, {helpers: {form: function(options) { return "
" + options.fn(this) + "
"; } }}); + equal(result, "

Yehuda

", "Block helper executed with current context"); + }); + + it("block helper should have context in this", function() { + var source = "
    {{#people}}
  • {{#link}}{{name}}{{/link}}
  • {{/people}}
"; + var link = function(options) { + return '' + options.fn(this) + ''; + }; + var data = { "people": [ + { "name": "Alan", "id": 1 }, + { "name": "Yehuda", "id": 2 } + ]}; + + shouldCompileTo(source, [data, {link: link}], ""); + }); + + it("block helper for undefined value", function() { + shouldCompileTo("{{#empty}}shouldn't render{{/empty}}", {}, ""); + }); + + it("block helper passing a new context", function() { + var string = "{{#form yehuda}}

{{name}}

{{/form}}"; + var template = CompilerContext.compile(string); + + var result = template({yehuda: {name: "Yehuda"}}, { helpers: {form: function(context, options) { return "
" + options.fn(context) + "
"; }}}); + equal(result, "

Yehuda

", "Context variable resolved"); + }); + + it("block helper passing a complex path context", function() { + var string = "{{#form yehuda/cat}}

{{name}}

{{/form}}"; + var template = CompilerContext.compile(string); + + var result = template({yehuda: {name: "Yehuda", cat: {name: "Harold"}}}, { helpers: {form: function(context, options) { return "
" + options.fn(context) + "
"; }}}); + equal(result, "

Harold

", "Complex path variable resolved"); + }); + + it("nested block helpers", function() { + var string = "{{#form yehuda}}

{{name}}

{{#link}}Hello{{/link}}{{/form}}"; + var template = CompilerContext.compile(string); + + var result = template({ + yehuda: {name: "Yehuda" } + }, { + helpers: { + link: function(options) { return "" + options.fn(this) + ""; }, + form: function(context, options) { return "
" + options.fn(context) + "
"; } + } + }); + equal(result, "

Yehuda

Hello
", "Both blocks executed"); + }); + + it("block helper inverted sections", function() { + var string = "{{#list people}}{{name}}{{^}}Nobody's here{{/list}}"; + var list = function(context, options) { + if (context.length > 0) { + var out = "
    "; + for(var i = 0,j=context.length; i < j; i++) { + out += "
  • "; + out += options.fn(context[i]); + out += "
  • "; + } + out += "
"; + return out; + } else { + return "

" + options.inverse(this) + "

"; + } + }; + + var hash = {people: [{name: "Alan"}, {name: "Yehuda"}]}; + var empty = {people: []}; + var rootMessage = { + people: [], + message: "Nobody's here" + }; + + var messageString = "{{#list people}}Hello{{^}}{{message}}{{/list}}"; + + // the meaning here may be kind of hard to catch, but list.not is always called, + // so we should see the output of both + shouldCompileTo(string, [hash, { list: list }], "
  • Alan
  • Yehuda
", "an inverse wrapper is passed in as a new context"); + shouldCompileTo(string, [empty, { list: list }], "

Nobody's here

", "an inverse wrapper can be optionally called"); + shouldCompileTo(messageString, [rootMessage, { list: list }], "

Nobody's here

", "the context of an inverse is the parent of the block"); + }); + + describe("helpers hash", function() { + it("providing a helpers hash", function() { + shouldCompileTo("Goodbye {{cruel}} {{world}}!", [{cruel: "cruel"}, {world: function() { return "world"; }}], "Goodbye cruel world!", + "helpers hash is available"); + + shouldCompileTo("Goodbye {{#iter}}{{cruel}} {{world}}{{/iter}}!", [{iter: [{cruel: "cruel"}]}, {world: function() { return "world"; }}], + "Goodbye cruel world!", "helpers hash is available inside other blocks"); + }); + + it("in cases of conflict, helpers win", function() { + shouldCompileTo("{{{lookup}}}", [{lookup: 'Explicit'}, {lookup: function() { return 'helpers'; }}], "helpers", + "helpers hash has precedence escaped expansion"); + shouldCompileTo("{{lookup}}", [{lookup: 'Explicit'}, {lookup: function() { return 'helpers'; }}], "helpers", + "helpers hash has precedence simple expansion"); + }); + + it("the helpers hash is available is nested contexts", function() { + shouldCompileTo("{{#outer}}{{#inner}}{{helper}}{{/inner}}{{/outer}}", + [{'outer': {'inner': {'unused':[]}}}, {'helper': function() { return 'helper'; }}], "helper", + "helpers hash is available in nested contexts."); + }); + + it("the helper hash should augment the global hash", function() { + handlebarsEnv.registerHelper('test_helper', function() { return 'found it!'; }); + + shouldCompileTo( + "{{test_helper}} {{#if cruel}}Goodbye {{cruel}} {{world}}!{{/if}}", [ + {cruel: "cruel"}, + {world: function() { return "world!"; }} + ], + "found it! Goodbye cruel world!!"); + }); + }); + + it("Multiple global helper registration", function() { + var helpers = handlebarsEnv.helpers; + handlebarsEnv.helpers = {}; + + handlebarsEnv.registerHelper({ + 'if': helpers['if'], + world: function() { return "world!"; }, + test_helper: function() { return 'found it!'; } + }); + + shouldCompileTo( + "{{test_helper}} {{#if cruel}}Goodbye {{cruel}} {{world}}!{{/if}}", + [{cruel: "cruel"}], + "found it! Goodbye cruel world!!"); + }); + + it("negative number literals work", function() { + var string = 'Message: {{hello -12}}'; + var hash = {}; + var helpers = {hello: function(times) { + if(typeof times !== 'number') { times = "NaN"; } + return "Hello " + times + " times"; + }}; + shouldCompileTo(string, [hash, helpers], "Message: Hello -12 times", "template with a negative integer literal"); + }); + + describe("String literal parameters", function() { + it("simple literals work", function() { + var string = 'Message: {{hello "world" 12 true false}}'; + var hash = {}; + var helpers = {hello: function(param, times, bool1, bool2) { + if(typeof times !== 'number') { times = "NaN"; } + if(typeof bool1 !== 'boolean') { bool1 = "NaB"; } + if(typeof bool2 !== 'boolean') { bool2 = "NaB"; } + return "Hello " + param + " " + times + " times: " + bool1 + " " + bool2; + }}; + shouldCompileTo(string, [hash, helpers], "Message: Hello world 12 times: true false", "template with a simple String literal"); + }); + + it("using a quote in the middle of a parameter raises an error", function() { + (function() { + var string = 'Message: {{hello wo"rld"}}'; + CompilerContext.compile(string); + }).should.throw(Error); + }); + + it("escaping a String is possible", function(){ + var string = 'Message: {{{hello "\\"world\\""}}}'; + var hash = {}; + var helpers = {hello: function(param) { return "Hello " + param; }}; + shouldCompileTo(string, [hash, helpers], "Message: Hello \"world\"", "template with an escaped String literal"); + }); + + it("it works with ' marks", function() { + var string = 'Message: {{{hello "Alan\'s world"}}}'; + var hash = {}; + var helpers = {hello: function(param) { return "Hello " + param; }}; + shouldCompileTo(string, [hash, helpers], "Message: Hello Alan's world", "template with a ' mark"); + }); + }); + + it("negative number literals work", function() { + var string = 'Message: {{hello -12}}'; + var hash = {}; + var helpers = {hello: function(times) { + if(typeof times !== 'number') { times = "NaN"; } + return "Hello " + times + " times"; + }}; + shouldCompileTo(string, [hash, helpers], "Message: Hello -12 times", "template with a negative integer literal"); + }); + + describe("multiple parameters", function() { + it("simple multi-params work", function() { + var string = 'Message: {{goodbye cruel world}}'; + var hash = {cruel: "cruel", world: "world"}; + var helpers = {goodbye: function(cruel, world) { return "Goodbye " + cruel + " " + world; }}; + shouldCompileTo(string, [hash, helpers], "Message: Goodbye cruel world", "regular helpers with multiple params"); + }); + + it("block multi-params work", function() { + var string = 'Message: {{#goodbye cruel world}}{{greeting}} {{adj}} {{noun}}{{/goodbye}}'; + var hash = {cruel: "cruel", world: "world"}; + var helpers = {goodbye: function(cruel, world, options) { + return options.fn({greeting: "Goodbye", adj: cruel, noun: world}); + }}; + shouldCompileTo(string, [hash, helpers], "Message: Goodbye cruel world", "block helpers with multiple params"); + }); + }); + describe('hash', function() { + it("helpers can take an optional hash", function() { + var template = CompilerContext.compile('{{goodbye cruel="CRUEL" world="WORLD" times=12}}'); + + 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"); + }); + + it("helpers can take an optional hash with booleans", function() { + var helpers = { + goodbye: function(options) { + if (options.hash.print === true) { + return "GOODBYE " + options.hash.cruel + " " + options.hash.world; + } 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"); + + template = CompilerContext.compile('{{goodbye cruel="CRUEL" world="WORLD" print=false}}'); + result = template(context, {helpers: helpers}); + equals(result, "NOT PRINTING", "Boolean helper parameter honored"); + }); + + it("block helpers can take an optional hash", function() { + var template = CompilerContext.compile('{{#goodbye cruel="CRUEL" times=12}}world{{/goodbye}}'); + + var helpers = { + goodbye: function(options) { + return "GOODBYE " + options.hash.cruel + " " + options.fn(this) + " " + options.hash.times + " TIMES"; + } + }; + + var result = template({}, {helpers: helpers}); + equals(result, "GOODBYE CRUEL world 12 TIMES", "Hash parameters output"); + }); + + it("block helpers can take an optional hash with single quoted stings", function() { + var template = CompilerContext.compile("{{#goodbye cruel='CRUEL' times=12}}world{{/goodbye}}"); + + 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"); + }); + + it("block helpers can take an optional hash with booleans", function() { + var helpers = { + goodbye: function(options) { + if (options.hash.print === true) { + return "GOODBYE " + options.hash.cruel + " " + options.fn(this); + } 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"); + + template = CompilerContext.compile('{{#goodbye cruel="CRUEL" print=false}}world{{/goodbye}}'); + result = template({}, {helpers: helpers}); + equals(result, "NOT PRINTING", "Boolean hash parameter honored"); + }); + }); + + describe("helperMissing", function() { + it("if a context is not found, helperMissing is used", function() { + (function() { + var template = CompilerContext.compile("{{hello}} {{link_to world}}"); + template({}); + }).should.throw(/Missing helper: 'link_to'/); + }); + + it("if a context is not found, custom helperMissing is used", function() { + var string = "{{hello}} {{link_to world}}"; + var context = { hello: "Hello", world: "world" }; + + var helpers = { + helperMissing: function(helper, context) { + if(helper === "link_to") { + return new Handlebars.SafeString("" + context + ""); + } + } + }; + + shouldCompileTo(string, [context, helpers], "Hello world"); + }); + }); + + describe("knownHelpers", function() { + it("Known helper should render helper", function() { + var template = CompilerContext.compile("{{hello}}", {knownHelpers: {"hello" : true}}); + + var result = template({}, {helpers: {hello: function() { return "foo"; }}}); + equal(result, "foo", "'foo' should === '" + result); + }); + + it("Unknown helper in knownHelpers only mode should be passed as undefined", function() { + var template = CompilerContext.compile("{{typeof hello}}", {knownHelpers: {'typeof': true}, knownHelpersOnly: true}); + + var result = template({}, {helpers: {'typeof': function(arg) { return typeof arg; }, hello: function() { return "foo"; }}}); + equal(result, "undefined", "'undefined' should === '" + result); + }); + it("Builtin helpers available in knownHelpers only mode", function() { + var template = CompilerContext.compile("{{#unless foo}}bar{{/unless}}", {knownHelpersOnly: true}); + + var result = template({}); + equal(result, "bar", "'bar' should === '" + result); + }); + it("Field lookup works in knownHelpers only mode", function() { + var template = CompilerContext.compile("{{foo}}", {knownHelpersOnly: true}); + + var result = template({foo: 'bar'}); + equal(result, "bar", "'bar' should === '" + result); + }); + it("Conditional blocks work in knownHelpers only mode", function() { + var template = CompilerContext.compile("{{#foo}}bar{{/foo}}", {knownHelpersOnly: true}); + + var result = template({foo: 'baz'}); + equal(result, "bar", "'bar' should === '" + result); + }); + it("Invert blocks work in knownHelpers only mode", function() { + var template = CompilerContext.compile("{{^foo}}bar{{/foo}}", {knownHelpersOnly: true}); + + var result = template({foo: false}); + equal(result, "bar", "'bar' should === '" + result); + }); + it("Functions are bound to the context in knownHelpers only mode", function() { + var template = CompilerContext.compile("{{foo}}", {knownHelpersOnly: true}); + var result = template({foo: function() { return this.bar; }, bar: 'bar'}); + equal(result, "bar", "'bar' should === '" + result); + }); + it("Unknown helper call in knownHelpers only mode should throw", function() { + (function() { + CompilerContext.compile("{{typeof hello}}", {knownHelpersOnly: true}); + }).should.throw(Error); + }); + }); + + describe("blockHelperMissing", function() { + it("lambdas are resolved by blockHelperMissing, not handlebars proper", function() { + var string = "{{#truthy}}yep{{/truthy}}"; + var data = { truthy: function() { return true; } }; + shouldCompileTo(string, data, "yep"); + }); + it("lambdas resolved by blockHelperMissing are bound to the context", function() { + var string = "{{#truthy}}yep{{/truthy}}"; + var boundData = { truthy: function() { return this.truthiness(); }, truthiness: function() { return false; } }; + shouldCompileTo(string, boundData, ""); + }); + }); + + describe('name conflicts', function() { + it("helpers take precedence over same-named context properties", function() { + var template = CompilerContext.compile("{{goodbye}} {{cruel world}}"); + + var helpers = { + goodbye: function() { + return this.goodbye.toUpperCase(); + }, + + cruel: function(world) { + return "cruel " + world.toUpperCase(); + } + }; + + var context = { + goodbye: "goodbye", + world: "world" + }; + + var result = template(context, {helpers: helpers}); + equals(result, "GOODBYE cruel WORLD", "Helper executed"); + }); + + it("helpers take precedence over same-named context properties$", function() { + var template = CompilerContext.compile("{{#goodbye}} {{cruel world}}{{/goodbye}}"); + + var helpers = { + goodbye: function(options) { + return this.goodbye.toUpperCase() + options.fn(this); + }, + + cruel: function(world) { + return "cruel " + world.toUpperCase(); + } + }; + + var context = { + goodbye: "goodbye", + world: "world" + }; + + var result = template(context, {helpers: helpers}); + equals(result, "GOODBYE cruel WORLD", "Helper executed"); + }); + + it("Scoped names take precedence over helpers", function() { + var template = CompilerContext.compile("{{this.goodbye}} {{cruel world}} {{cruel this.goodbye}}"); + + var helpers = { + goodbye: function() { + return this.goodbye.toUpperCase(); + }, + + cruel: function(world) { + return "cruel " + world.toUpperCase(); + }, + }; + + var context = { + goodbye: "goodbye", + world: "world" + }; + + var result = template(context, {helpers: helpers}); + equals(result, "goodbye cruel WORLD cruel GOODBYE", "Helper not executed"); + }); + + it("Scoped names take precedence over block helpers", function() { + var template = CompilerContext.compile("{{#goodbye}} {{cruel world}}{{/goodbye}} {{this.goodbye}}"); + + var helpers = { + goodbye: function(options) { + return this.goodbye.toUpperCase() + options.fn(this); + }, + + cruel: function(world) { + return "cruel " + world.toUpperCase(); + }, + }; + + var context = { + goodbye: "goodbye", + world: "world" + }; + + var result = template(context, {helpers: helpers}); + equals(result, "GOODBYE cruel WORLD goodbye", "Helper executed"); + }); + }); +}); diff --git a/spec/parser.js b/spec/parser.js new file mode 100644 index 000000000..3397105fd --- /dev/null +++ b/spec/parser.js @@ -0,0 +1,173 @@ +describe('parser', function() { + if (!Handlebars.print) { + return; + } + + function ast_for(template) { + var ast = Handlebars.parse(template); + return Handlebars.print(ast); + } + + it('parses simple mustaches', function() { + ast_for('{{foo}}').should.equal("{{ ID:foo [] }}\n"); + ast_for('{{foo?}}').should.equal("{{ ID:foo? [] }}\n"); + ast_for('{{foo_}}').should.equal("{{ ID:foo_ [] }}\n"); + ast_for('{{foo-}}').should.equal("{{ ID:foo- [] }}\n"); + ast_for('{{foo:}}').should.equal("{{ ID:foo: [] }}\n"); + }); + + it('parses simple mustaches with data', function() { + ast_for("{{@foo}}").should.equal("{{ @ID:foo [] }}\n"); + }); + + it('parses mustaches with paths', function() { + ast_for("{{foo/bar}}").should.equal("{{ PATH:foo/bar [] }}\n"); + }); + + it('parses mustaches with this/foo', function() { + ast_for("{{this/foo}}").should.equal("{{ ID:foo [] }}\n"); + }); + + it('parses mustaches with - in a path', function() { + ast_for("{{foo-bar}}").should.equal("{{ ID:foo-bar [] }}\n"); + }); + + it('parses mustaches with parameters', function() { + ast_for("{{foo bar}}").should.equal("{{ ID:foo [ID:bar] }}\n"); + }); + + it('parses mustaches with string parameters', function() { + ast_for("{{foo bar \"baz\" }}").should.equal('{{ ID:foo [ID:bar, "baz"] }}\n'); + }); + + it('parses mustaches with INTEGER parameters', function() { + ast_for("{{foo 1}}").should.equal("{{ ID:foo [INTEGER{1}] }}\n"); + }); + + it('parses mustaches with BOOLEAN parameters', function() { + ast_for("{{foo true}}").should.equal("{{ ID:foo [BOOLEAN{true}] }}\n"); + ast_for("{{foo false}}").should.equal("{{ ID:foo [BOOLEAN{false}] }}\n"); + }); + + it('parses mutaches with DATA parameters', function() { + ast_for("{{foo @bar}}").should.equal("{{ ID:foo [@ID:bar] }}\n"); + }); + + it('parses mustaches with hash arguments', function() { + ast_for("{{foo bar=baz}}").should.equal("{{ ID:foo [] HASH{bar=ID:baz} }}\n"); + ast_for("{{foo bar=1}}").should.equal("{{ ID:foo [] HASH{bar=INTEGER{1}} }}\n"); + ast_for("{{foo bar=true}}").should.equal("{{ ID:foo [] HASH{bar=BOOLEAN{true}} }}\n"); + ast_for("{{foo bar=false}}").should.equal("{{ ID:foo [] HASH{bar=BOOLEAN{false}} }}\n"); + ast_for("{{foo bar=@baz}}").should.equal("{{ ID:foo [] HASH{bar=@ID:baz} }}\n"); + + ast_for("{{foo bar=baz bat=bam}}").should.equal("{{ ID:foo [] HASH{bar=ID:baz, bat=ID:bam} }}\n"); + ast_for("{{foo bar=baz bat=\"bam\"}}").should.equal('{{ ID:foo [] HASH{bar=ID:baz, bat="bam"} }}\n'); + + ast_for("{{foo bat='bam'}}").should.equal('{{ ID:foo [] HASH{bat="bam"} }}\n'); + + ast_for("{{foo omg bar=baz bat=\"bam\"}}").should.equal('{{ ID:foo [ID:omg] HASH{bar=ID:baz, bat="bam"} }}\n'); + ast_for("{{foo omg bar=baz bat=\"bam\" baz=1}}").should.equal('{{ ID:foo [ID:omg] HASH{bar=ID:baz, bat="bam", baz=INTEGER{1}} }}\n'); + ast_for("{{foo omg bar=baz bat=\"bam\" baz=true}}").should.equal('{{ ID:foo [ID:omg] HASH{bar=ID:baz, bat="bam", baz=BOOLEAN{true}} }}\n'); + ast_for("{{foo omg bar=baz bat=\"bam\" baz=false}}").should.equal('{{ ID:foo [ID:omg] HASH{bar=ID:baz, bat="bam", baz=BOOLEAN{false}} }}\n'); + }); + + it('parses contents followed by a mustache', function() { + ast_for("foo bar {{baz}}").should.equal("CONTENT[ \'foo bar \' ]\n{{ ID:baz [] }}\n"); + }); + + it('parses a partial', function() { + ast_for("{{> foo }}").should.equal("{{> PARTIAL:foo }}\n"); + }); + + it('parses a partial with context', function() { + ast_for("{{> foo bar}}").should.equal("{{> PARTIAL:foo ID:bar }}\n"); + }); + + it('parses a partial with a complex name', function() { + ast_for("{{> shared/partial?.bar}}").should.equal("{{> PARTIAL:shared/partial?.bar }}\n"); + }); + + it('parses a comment', function() { + ast_for("{{! this is a comment }}").should.equal("{{! ' this is a comment ' }}\n"); + }); + + it('parses a multi-line comment', function() { + ast_for("{{!\nthis is a multi-line comment\n}}").should.equal("{{! \'\nthis is a multi-line comment\n\' }}\n"); + }); + + it('parses an inverse section', function() { + ast_for("{{#foo}} bar {{^}} baz {{/foo}}").should.equal("BLOCK:\n {{ ID:foo [] }}\n PROGRAM:\n CONTENT[ ' bar ' ]\n {{^}}\n CONTENT[ ' baz ' ]\n"); + }); + + it('parses an inverse (else-style) section', function() { + ast_for("{{#foo}} bar {{else}} baz {{/foo}}").should.equal("BLOCK:\n {{ ID:foo [] }}\n PROGRAM:\n CONTENT[ ' bar ' ]\n {{^}}\n CONTENT[ ' baz ' ]\n"); + }); + + it('parses empty blocks', function() { + ast_for("{{#foo}}{{/foo}}").should.equal("BLOCK:\n {{ ID:foo [] }}\n PROGRAM:\n"); + }); + + it('parses empty blocks with empty inverse section', function() { + ast_for("{{#foo}}{{^}}{{/foo}}").should.equal("BLOCK:\n {{ ID:foo [] }}\n PROGRAM:\n"); + }); + + it('parses empty blocks with empty inverse (else-style) section', function() { + ast_for("{{#foo}}{{else}}{{/foo}}").should.equal("BLOCK:\n {{ ID:foo [] }}\n PROGRAM:\n"); + }); + + it('parses non-empty blocks with empty inverse section', function() { + ast_for("{{#foo}} bar {{^}}{{/foo}}").should.equal("BLOCK:\n {{ ID:foo [] }}\n PROGRAM:\n CONTENT[ ' bar ' ]\n {{^}}\n"); + }); + + it('parses non-empty blocks with empty inverse (else-style) section', function() { + ast_for("{{#foo}} bar {{else}}{{/foo}}").should.equal("BLOCK:\n {{ ID:foo [] }}\n PROGRAM:\n CONTENT[ ' bar ' ]\n {{^}}\n"); + }); + + it('parses empty blocks with non-empty inverse section', function() { + ast_for("{{#foo}}{{^}} bar {{/foo}}").should.equal("BLOCK:\n {{ ID:foo [] }}\n PROGRAM:\n {{^}}\n CONTENT[ ' bar ' ]\n"); + }); + + it('parses empty blocks with non-empty inverse (else-style) section', function() { + ast_for("{{#foo}}{{else}} bar {{/foo}}").should.equal("BLOCK:\n {{ ID:foo [] }}\n PROGRAM:\n {{^}}\n CONTENT[ ' bar ' ]\n"); + }); + + it('parses a standalone inverse section', function() { + ast_for("{{^foo}}bar{{/foo}}").should.equal("BLOCK:\n {{ ID:foo [] }}\n {{^}}\n CONTENT[ 'bar' ]\n"); + }); + + it("raises if there's a Parse error", function() { + (function() { + ast_for("foo{{^}}bar"); + }).should.throw(/Parse error on line 1/); + (function() { + ast_for("{{foo}"); + }).should.throw(/Parse error on line 1/); + (function() { + ast_for("{{foo &}}"); + }).should.throw(/Parse error on line 1/); + (function() { + ast_for("{{#goodbyes}}{{/hellos}}"); + }).should.throw(/goodbyes doesn't match hellos/); + }); + + it('knows how to report the correct line number in errors', function() { + (function() { + ast_for("hello\nmy\n{{foo}"); + }).should.throw(/Parse error on line 3/); + (function() { + ast_for("hello\n\nmy\n\n{{foo}"); + }).should.throw(/Parse error on line 5/); + }); + + it('knows how to report the correct line number in errors when the first character is a newline', function() { + (function() { + ast_for("\n\nhello\n\nmy\n\n{{foo}"); + }).should.throw(/Parse error on line 7/); + }); + + describe('externally compiled AST', function() { + it('can pass through an already-compiled AST', function() { + ast_for(new Handlebars.AST.ProgramNode([ new Handlebars.AST.ContentNode("Hello")])).should.equal("CONTENT[ \'Hello\' ]\n"); + }); + }); +}); diff --git a/spec/parser_spec.rb b/spec/parser_spec.rb deleted file mode 100644 index b25e88938..000000000 --- a/spec/parser_spec.rb +++ /dev/null @@ -1,426 +0,0 @@ -require "spec_helper" - -describe "Parser" do - let(:handlebars) { @context["Handlebars"] } - - before(:all) do - @compiles = true - end - - def root(&block) - ASTBuilder.build do - instance_eval(&block) - end - end - - def ast_for(string) - ast = handlebars.parse(string) - handlebars.print(ast) - end - - class ASTBuilder - def self.build(&block) - ret = new - ret.evaluate(&block) - ret.out - end - - attr_reader :out - - def initialize - @padding = 0 - @out = "" - end - - def evaluate(&block) - instance_eval(&block) - end - - def pad(string) - @out << (" " * @padding) + string + "\n" - end - - def with_padding - @padding += 1 - ret = yield - @padding -= 1 - ret - end - - def program - pad("PROGRAM:") - with_padding { yield } - end - - def inverse - pad("{{^}}") - with_padding { yield } - end - - def block - pad("BLOCK:") - with_padding { yield } - end - - def inverted_block - pad("INVERSE:") - with_padding { yield } - end - - def mustache(id, params = [], hash = nil) - hash = " #{hash}" if hash - pad("{{ #{id} [#{params.join(", ")}]#{hash} }}") - end - - def partial(id, context = nil) - content = id.dup - content << " #{context}" if context - pad("{{> #{content} }}") - end - - def comment(comment) - pad("{{! '#{comment}' }}") - end - - def multiline_comment(comment) - pad("{{! '\n#{comment}\n' }}") - end - - def content(string) - pad("CONTENT[ '#{string}' ]") - end - - def string(string) - 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 - - def data(id) - "@ID:#{id}" - end - - def partial_name(name) - "PARTIAL:#{name}" - end - - def path(*parts) - "PATH:#{parts.join("/")}" - end - end - - it "parses simple mustaches" do - ast_for("{{foo}}").should == root { mustache id("foo") } - ast_for("{{foo?}}").should == root { mustache id("foo?") } - ast_for("{{foo_}}").should == root { mustache id("foo_") } - ast_for("{{foo-}}").should == root { mustache id("foo-") } - ast_for("{{foo:}}").should == root { mustache id("foo:") } - end - - it "parses simple mustaches with data" do - ast_for("{{@foo}}").should == root { mustache data("foo") } - end - - it "parses mustaches with paths" do - ast_for("{{foo/bar}}").should == root { mustache path("foo", "bar") } - end - - it "parses mustaches with this/foo" do - ast_for("{{this/foo}}").should == root { mustache id("foo") } - end - - it "parses mustaches with - in a path" do - ast_for("{{foo-bar}}").should == root { mustache id("foo-bar") } - end - - it "parses mustaches with parameters" do - ast_for("{{foo bar}}").should == root { mustache id("foo"), [id("bar")] } - end - - it "parses mustaches with hash arguments" do - ast_for("{{foo bar=baz}}").should == root do - mustache id("foo"), [], hash(["bar", id("baz")]) - end - - ast_for("{{foo bar=1}}").should == root do - mustache id("foo"), [], hash(["bar", integer("1")]) - end - - ast_for("{{foo bar=true}}").should == root do - mustache id("foo"), [], hash(["bar", boolean("true")]) - end - - ast_for("{{foo bar=false}}").should == root do - mustache id("foo"), [], hash(["bar", boolean("false")]) - end - - ast_for("{{foo bar=@baz}}").should == root do - mustache id("foo"), [], hash(["bar", data("baz")]) - end - - ast_for("{{foo bar=baz bat=bam}}").should == root do - mustache id("foo"), [], hash(["bar", "ID:baz"], ["bat", "ID:bam"]) - end - - ast_for("{{foo bar=baz bat=\"bam\"}}").should == root do - mustache id("foo"), [], hash(["bar", "ID:baz"], ["bat", "\"bam\""]) - end - - ast_for("{{foo bat='bam'}}").should == root do - mustache id("foo"), [], hash(["bat", "\"bam\""]) - end - - ast_for("{{foo omg bar=baz bat=\"bam\"}}").should == root 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 == root 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 == root 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 == root 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 == root { mustache id("foo"), [id("bar"), string("baz")] } - end - - it "parses mustaches with INTEGER parameters" do - ast_for("{{foo 1}}").should == root { mustache id("foo"), [integer("1")] } - end - - it "parses mustaches with BOOLEAN parameters" do - ast_for("{{foo true}}").should == root { mustache id("foo"), [boolean("true")] } - ast_for("{{foo false}}").should == root { mustache id("foo"), [boolean("false")] } - end - - it "parses mutaches with DATA parameters" do - ast_for("{{foo @bar}}").should == root { mustache id("foo"), [data("bar")] } - end - - it "parses contents followed by a mustache" do - ast_for("foo bar {{baz}}").should == root do - content "foo bar " - mustache id("baz") - end - end - - it "parses a partial" do - ast_for("{{> foo }}").should == root { partial partial_name("foo") } - end - - it "parses a partial with context" do - ast_for("{{> foo bar}}").should == root { partial partial_name("foo"), id("bar") } - end - - it "parses a partial with a complex name" do - ast_for("{{> shared/partial?.bar}}").should == root { partial partial_name("shared/partial?.bar") } - end - - it "parses a comment" do - ast_for("{{! this is a comment }}").should == root do - comment " this is a comment " - end - end - - it "parses a multi-line comment" do - ast_for("{{!\nthis is a multi-line comment\n}}").should == root do - multiline_comment "this is a multi-line comment" - end - end - - it "parses an inverse section" do - ast_for("{{#foo}} bar {{^}} baz {{/foo}}").should == root do - block do - mustache id("foo") - - program do - content " bar " - end - - inverse do - content " baz " - end - end - end - end - - it "parses an inverse ('else'-style) section" do - ast_for("{{#foo}} bar {{else}} baz {{/foo}}").should == root do - block do - mustache id("foo") - - program do - content " bar " - end - - inverse do - content " baz " - end - end - end - end - - it "parses empty blocks" do - ast_for("{{#foo}}{{/foo}}").should == root do - block do - mustache id("foo") - - program do - # empty program - end - end - end - end - - it "parses empty blocks with empty inverse section" do - ast_for("{{#foo}}{{^}}{{/foo}}").should == root do - block do - mustache id("foo") - - program do - # empty program - end - - inverse do - # empty inverse - end - end - end - end - - it "parses empty blocks with empty inverse ('else'-style) section" do - ast_for("{{#foo}}{{else}}{{/foo}}").should == root do - block do - mustache id("foo") - - program do - # empty program - end - - inverse do - # empty inverse - end - end - end - end - - it "parses non-empty blocks with empty inverse section" do - ast_for("{{#foo}} bar {{^}}{{/foo}}").should == root do - block do - mustache id("foo") - - program do - content " bar " - end - - inverse do - # empty inverse - end - end - end - end - - it "parses non-empty blocks with empty inverse ('else'-style) section" do - ast_for("{{#foo}} bar {{else}}{{/foo}}").should == root do - block do - mustache id("foo") - - program do - content " bar " - end - - inverse do - # empty inverse - end - end - end - end - - it "parses empty blocks with non-empty inverse section" do - ast_for("{{#foo}}{{^}} bar {{/foo}}").should == root do - block do - mustache id("foo") - - program do - # empty program - end - - inverse do - content " bar " - end - end - end - end - - it "parses empty blocks with non-empty inverse ('else'-style) section" do - ast_for("{{#foo}}{{else}} bar {{/foo}}").should == root do - block do - mustache id("foo") - - program do - # empty program - end - - inverse do - content " bar " - end - end - end - end - - it "parses a standalone inverse section" do - ast_for("{{^foo}}bar{{/foo}}").should == root do - block do - mustache id("foo") - - inverse do - content "bar" - end - end - end - end - - 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 - lambda { ast_for("hello\nmy\n{{foo}") }.should raise_error(V8::JSError, /Parse error on line 3/m) - lambda { ast_for("hello\n\nmy\n\n{{foo}") }.should raise_error(V8::JSError, /Parse error on line 5/m) - end - - it "knows how to report the correct line number in errors when the first character is a newline" do - lambda { ast_for("\n\nhello\n\nmy\n\n{{foo}") }.should raise_error(V8::JSError, /Parse error on line 7/m) - end - - context "externally compiled AST" do - it "can pass through an already-compiled AST" do - ast_for(@context.eval('new Handlebars.AST.ProgramNode([ new Handlebars.AST.ContentNode("Hello")]);')).should == root do - content "Hello" - end - end - end -end diff --git a/spec/partials.js b/spec/partials.js new file mode 100644 index 000000000..7ff9d3c25 --- /dev/null +++ b/spec/partials.js @@ -0,0 +1,126 @@ +/*global CompilerContext, shouldCompileTo, shouldCompileToWithPartials */ +describe('partials', function() { + it("basic partials", function() { + var string = "Dudes: {{#dudes}}{{> dude}}{{/dudes}}"; + var partial = "{{name}} ({{url}}) "; + var hash = {dudes: [{name: "Yehuda", url: "http://yehuda"}, {name: "Alan", url: "http://alan"}]}; + shouldCompileToWithPartials(string, [hash, {}, {dude: partial}], true, "Dudes: Yehuda (http://yehuda) Alan (http://alan) ", + "Basic partials output based on current context."); + }); + + it("partials with context", function() { + var string = "Dudes: {{>dude dudes}}"; + var partial = "{{#this}}{{name}} ({{url}}) {{/this}}"; + var hash = {dudes: [{name: "Yehuda", url: "http://yehuda"}, {name: "Alan", url: "http://alan"}]}; + shouldCompileToWithPartials(string, [hash, {}, {dude: partial}], true, "Dudes: Yehuda (http://yehuda) Alan (http://alan) ", + "Partials can be passed a context"); + }); + + it("partials with undefined context", function() { + var string = "Dudes: {{>dude dudes}}"; + var partial = "{{foo}} Empty"; + var hash = {}; + shouldCompileToWithPartials(string, [hash, {}, {dude: partial}], true, "Dudes: Empty"); + }); + + it("partial in a partial", function() { + var string = "Dudes: {{#dudes}}{{>dude}}{{/dudes}}"; + var dude = "{{name}} {{> url}} "; + var url = "{{url}}"; + var hash = {dudes: [{name: "Yehuda", url: "http://yehuda"}, {name: "Alan", url: "http://alan"}]}; + shouldCompileToWithPartials(string, [hash, {}, {dude: dude, url: url}], true, "Dudes: Yehuda http://yehuda Alan http://alan ", "Partials are rendered inside of other partials"); + }); + + it("rendering undefined partial throws an exception", function() { + (function() { + var template = CompilerContext.compile("{{> whatever}}"); + template(); + }).should.throw(Handlebars.Exception, 'The partial whatever could not be found'); + }); + + it("rendering template partial in vm mode throws an exception", function() { + (function() { + var template = CompilerContext.compile("{{> whatever}}"); + template(); + }).should.throw(Handlebars.Exception, 'The partial whatever could not be found'); + }); + + it("rendering function partial in vm mode", function() { + var string = "Dudes: {{#dudes}}{{> dude}}{{/dudes}}"; + var partial = function(context) { + 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."); + }); + + it("GH-14: a partial preceding a selector", function() { + var string = "Dudes: {{>dude}} {{another_dude}}"; + var dude = "{{name}}"; + var hash = {name:"Jeepers", another_dude:"Creepers"}; + shouldCompileToWithPartials(string, [hash, {}, {dude:dude}], true, "Dudes: Jeepers Creepers", "Regular selectors can follow a partial"); + }); + + it("Partials with slash paths", function() { + var string = "Dudes: {{> shared/dude}}"; + var dude = "{{name}}"; + var hash = {name:"Jeepers", another_dude:"Creepers"}; + shouldCompileToWithPartials(string, [hash, {}, {'shared/dude':dude}], true, "Dudes: Jeepers", "Partials can use literal paths"); + }); + + it("Partials with slash and point paths", function() { + var string = "Dudes: {{> shared/dude.thing}}"; + var dude = "{{name}}"; + var hash = {name:"Jeepers", another_dude:"Creepers"}; + shouldCompileToWithPartials(string, [hash, {}, {'shared/dude.thing':dude}], true, "Dudes: Jeepers", "Partials can use literal with points in paths"); + }); + + it("Global Partials", function() { + handlebarsEnv.registerPartial('global_test', '{{another_dude}}'); + + var string = "Dudes: {{> shared/dude}} {{> global_test}}"; + var dude = "{{name}}"; + var hash = {name:"Jeepers", another_dude:"Creepers"}; + shouldCompileToWithPartials(string, [hash, {}, {'shared/dude':dude}], true, "Dudes: Jeepers Creepers", "Partials can use globals or passed"); + }); + + it("Multiple partial registration", function() { + handlebarsEnv.registerPartial({ + 'shared/dude': '{{name}}', + global_test: '{{another_dude}}' + }); + + var string = "Dudes: {{> shared/dude}} {{> global_test}}"; + var hash = {name:"Jeepers", another_dude:"Creepers"}; + shouldCompileToWithPartials(string, [hash], true, "Dudes: Jeepers Creepers", "Partials can use globals or passed"); + }); + + it("Partials with integer path", function() { + var string = "Dudes: {{> 404}}"; + var dude = "{{name}}"; + var hash = {name:"Jeepers", another_dude:"Creepers"}; + shouldCompileToWithPartials(string, [hash, {}, {404:dude}], true, "Dudes: Jeepers", "Partials can use literal paths"); + }); + + it("Partials with complex path", function() { + var string = "Dudes: {{> 404/asdf?.bar}}"; + var dude = "{{name}}"; + var hash = {name:"Jeepers", another_dude:"Creepers"}; + shouldCompileToWithPartials(string, [hash, {}, {'404/asdf?.bar':dude}], true, "Dudes: Jeepers", "Partials can use literal paths"); + }); + + it("Partials with escaped", function() { + var string = "Dudes: {{> [+404/asdf?.bar]}}"; + var dude = "{{name}}"; + var hash = {name:"Jeepers", another_dude:"Creepers"}; + shouldCompileToWithPartials(string, [hash, {}, {'+404/asdf?.bar':dude}], true, "Dudes: Jeepers", "Partials can use literal paths"); + }); + + it("Partials with string", function() { + var string = "Dudes: {{> \"+404/asdf?.bar\"}}"; + var dude = "{{name}}"; + var hash = {name:"Jeepers", another_dude:"Creepers"}; + shouldCompileToWithPartials(string, [hash, {}, {'+404/asdf?.bar':dude}], true, "Dudes: Jeepers", "Partials can use literal paths"); + }); +}); diff --git a/spec/qunit_spec.js b/spec/qunit_spec.js deleted file mode 100644 index 5d52e444b..000000000 --- a/spec/qunit_spec.js +++ /dev/null @@ -1,1657 +0,0 @@ -var Handlebars; -if (!Handlebars) { - // Setup for Node package testing - Handlebars = require('../lib/handlebars'); - - var assert = require("assert"), - - equal = assert.equal, - equals = assert.equal, - ok = assert.ok; - - // Note that this doesn't have the same context separation as the rspec test. - // Both should be run for full acceptance of the two libary modes. - var CompilerContext = { - compile: function(template, options) { - var templateSpec = Handlebars.precompile(template, options); - return Handlebars.template(eval('(' + templateSpec + ')')); - }, - compileWithPartial: function(template, options) { - return Handlebars.compile(template, options); - } - }; -} else { - var _equal = equal; - equals = equal = function(a, b, msg) { - // Allow exec with missing message params - _equal(a, b, msg || ''); - }; -} - -suite("basic context"); - -function shouldCompileTo(string, hashOrArray, expected, message) { - shouldCompileToWithPartials(string, hashOrArray, false, expected, message); -} - -function shouldCompileToWithPartials(string, hashOrArray, partials, expected, message) { - var result = compileWithPartials(string, hashOrArray, partials); - equal(result, expected, "'" + expected + "' should === '" + result + "': " + message); -} - -function compileWithPartials(string, hashOrArray, partials) { - var template = CompilerContext[partials ? 'compileWithPartial' : 'compile'](string), ary; - if(Object.prototype.toString.call(hashOrArray) === "[object Array]") { - ary = []; - ary.push(hashOrArray[0]); - ary.push({ helpers: hashOrArray[1], partials: hashOrArray[2] }); - } else { - ary = [hashOrArray]; - } - - return template.apply(this, ary); -} - -function shouldThrow(fn, exception, message) { - var caught = false, - exType, exMessage; - - if (exception instanceof Array) { - exType = exception[0]; - exMessage = exception[1]; - } else if (typeof exception === 'string') { - exType = Error; - exMessage = exception; - } else { - exType = exception; - } - - try { - fn(); - } - catch (e) { - if (e instanceof exType) { - if (!exMessage || e.message === exMessage) { - caught = true; - } - } - } - - ok(caught, message || null); -} - -test("most basic", function() { - shouldCompileTo("{{foo}}", { foo: "foo" }, "foo"); -}); - -test("escaping", function() { - shouldCompileTo("\\{{foo}}", { foo: "food" }, "{{foo}}"); - shouldCompileTo("\\\\{{foo}}", { foo: "food" }, "\\food"); - shouldCompileTo("\\\\ {{foo}}", { foo: "food" }, "\\\\ food"); -}); - -test("compiling with a basic context", function() { - shouldCompileTo("Goodbye\n{{cruel}}\n{{world}}!", {cruel: "cruel", world: "world"}, "Goodbye\ncruel\nworld!", - "It works if all the required keys are provided"); -}); - -test("comments", function() { - shouldCompileTo("{{! Goodbye}}Goodbye\n{{cruel}}\n{{world}}!", - {cruel: "cruel", world: "world"}, "Goodbye\ncruel\nworld!", - "comments are ignored"); -}); - -test("boolean", function() { - var string = "{{#goodbye}}GOODBYE {{/goodbye}}cruel {{world}}!"; - shouldCompileTo(string, {goodbye: true, world: "world"}, "GOODBYE cruel world!", - "booleans show the contents when true"); - - shouldCompileTo(string, {goodbye: false, world: "world"}, "cruel world!", - "booleans do not show the contents when false"); -}); - -test("zeros", function() { - shouldCompileTo("num1: {{num1}}, num2: {{num2}}", {num1: 42, num2: 0}, - "num1: 42, num2: 0"); - shouldCompileTo("num: {{.}}", 0, "num: 0"); - shouldCompileTo("num: {{num1/num2}}", {num1: {num2: 0}}, "num: 0"); -}); - -test("newlines", function() { - shouldCompileTo("Alan's\nTest", {}, "Alan's\nTest"); - shouldCompileTo("Alan's\rTest", {}, "Alan's\rTest"); -}); - -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}}", {foo: '\\'}, "Awesome \\", "text is escaped so that it doesn't mess up backslashes"); - shouldCompileTo(' " " ', {}, ' " " ', "double quotes never produce invalid javascript"); -}); - -test("escaping expressions", function() { - shouldCompileTo("{{{awesome}}}", {awesome: "&\"\\<>"}, '&\"\\<>', - "expressions with 3 handlebars aren't escaped"); - - shouldCompileTo("{{&awesome}}", {awesome: "&\"\\<>"}, '&\"\\<>', - "expressions with {{& handlebars aren't escaped"); - - shouldCompileTo("{{awesome}}", {awesome: "&\"'`\\<>"}, '&"'`\\<>', - "by default expressions should be escaped"); - - shouldCompileTo("{{awesome}}", {awesome: "Escaped, looks like: <b>"}, 'Escaped, <b> looks like: &lt;b&gt;', - "escaping should properly handle amperstands"); -}); - -test("functions returning safestrings shouldn't be escaped", function() { - var hash = {awesome: function() { return new Handlebars.SafeString("&\"\\<>"); }}; - shouldCompileTo("{{awesome}}", hash, '&\"\\<>', - "functions returning safestrings aren't escaped"); -}); - -test("functions", function() { - shouldCompileTo("{{awesome}}", {awesome: function() { return "Awesome"; }}, "Awesome", - "functions are called and render their output"); - shouldCompileTo("{{awesome}}", {awesome: function() { return this.more; }, more: "More awesome"}, "More awesome", - "functions are bound to the context"); -}); - -test("functions with context argument", function() { - shouldCompileTo("{{awesome frank}}", - {awesome: function(context) { return context; }, - frank: "Frank"}, - "Frank", "functions are called with context arguments"); -}); - - -test("paths with hyphens", function() { - shouldCompileTo("{{foo-bar}}", {"foo-bar": "baz"}, "baz", "Paths can contain hyphens (-)"); - shouldCompileTo("{{foo.foo-bar}}", {foo: {"foo-bar": "baz"}}, "baz", "Paths can contain hyphens (-)"); - shouldCompileTo("{{foo/foo-bar}}", {foo: {"foo-bar": "baz"}}, "baz", "Paths can contain hyphens (-)"); -}); - -test("nested paths", function() { - shouldCompileTo("Goodbye {{alan/expression}} world!", {alan: {expression: "beautiful"}}, - "Goodbye beautiful world!", "Nested paths access nested objects"); -}); - -test("nested paths with empty string value", function() { - shouldCompileTo("Goodbye {{alan/expression}} world!", {alan: {expression: ""}}, - "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"); - shouldCompileTo("Goodbye {{[foo bar]/expression}} world!", {"foo bar": {expression: "beautiful"}}, - "Goodbye beautiful world!", "Literal paths can be used"); -}); - -test('literal references', function() { - shouldCompileTo("Goodbye {{[foo bar]}} world!", {"foo bar": "beautiful"}, - "Goodbye beautiful world!", "Literal paths can be used"); -}); - -test("that current context path ({{.}}) doesn't hit helpers", function() { - shouldCompileTo("test: {{.}}", [null, {helper: "awesome"}], "test: "); -}); - -test("complex but empty paths", function() { - shouldCompileTo("{{person/name}}", {person: {name: null}}, ""); - shouldCompileTo("{{person/name}}", {person: {}}, ""); -}); - -test("this keyword in paths", function() { - var string = "{{#goodbyes}}{{this}}{{/goodbyes}}"; - var hash = {goodbyes: ["goodbye", "Goodbye", "GOODBYE"]}; - shouldCompileTo(string, hash, "goodbyeGoodbyeGOODBYE", - "This keyword in paths evaluates to current context"); - - string = "{{#hellos}}{{this/text}}{{/hellos}}"; - hash = {hellos: [{text: "hello"}, {text: "Hello"}, {text: "HELLO"}]}; - shouldCompileTo(string, hash, "helloHelloHELLO", "This keyword evaluates in more complex paths"); -}); - -test("this keyword nested inside path", function() { - var string = "{{#hellos}}{{text/this/foo}}{{/hellos}}"; - shouldThrow(function() { - CompilerContext.compile(string); - }, Error, "Should throw exception"); -}); - -test("this keyword in helpers", function() { - var helpers = {foo: function(value) { - return 'bar ' + value; - }}; - var string = "{{#goodbyes}}{{foo this}}{{/goodbyes}}"; - var hash = {goodbyes: ["goodbye", "Goodbye", "GOODBYE"]}; - shouldCompileTo(string, [hash, helpers], "bar goodbyebar Goodbyebar GOODBYE", - "This keyword in paths evaluates to current context"); - - string = "{{#hellos}}{{foo this/text}}{{/hellos}}"; - hash = {hellos: [{text: "hello"}, {text: "Hello"}, {text: "HELLO"}]}; - shouldCompileTo(string, [hash, helpers], "bar hellobar Hellobar HELLO", "This keyword evaluates in more complex paths"); -}); - -test("this keyword nested inside helpers param", function() { - var string = "{{#hellos}}{{foo text/this/foo}}{{/hellos}}"; - shouldThrow(function() { - CompilerContext.compile(string); - }, Error, "Should throw exception"); -}); - -suite("inverted sections"); - -test("inverted sections with unset value", function() { - var string = "{{#goodbyes}}{{this}}{{/goodbyes}}{{^goodbyes}}Right On!{{/goodbyes}}"; - var hash = {}; - shouldCompileTo(string, hash, "Right On!", "Inverted section rendered when value isn't set."); -}); - -test("inverted section with false value", function() { - var string = "{{#goodbyes}}{{this}}{{/goodbyes}}{{^goodbyes}}Right On!{{/goodbyes}}"; - var hash = {goodbyes: false}; - shouldCompileTo(string, hash, "Right On!", "Inverted section rendered when value is false."); -}); - -test("inverted section with empty set", function() { - var string = "{{#goodbyes}}{{this}}{{/goodbyes}}{{^goodbyes}}Right On!{{/goodbyes}}"; - var hash = {goodbyes: []}; - shouldCompileTo(string, hash, "Right On!", "Inverted section rendered when value is empty set."); -}); - -suite("blocks"); - -test("array", function() { - var string = "{{#goodbyes}}{{text}}! {{/goodbyes}}cruel {{world}}!"; - var hash = {goodbyes: [{text: "goodbye"}, {text: "Goodbye"}, {text: "GOODBYE"}], world: "world"}; - shouldCompileTo(string, hash, "goodbye! Goodbye! GOODBYE! cruel world!", - "Arrays iterate over the contents when not empty"); - - shouldCompileTo(string, {goodbyes: [], world: "world"}, "cruel world!", - "Arrays ignore the contents when empty"); - -}); - -test("array with @index", function() { - var string = "{{#goodbyes}}{{@index}}. {{text}}! {{/goodbyes}}cruel {{world}}!"; - var hash = {goodbyes: [{text: "goodbye"}, {text: "Goodbye"}, {text: "GOODBYE"}], world: "world"}; - - var template = CompilerContext.compile(string); - var result = template(hash); - - equal(result, "0. goodbye! 1. Goodbye! 2. GOODBYE! cruel world!", "The @index variable is used"); -}); - -test("empty block", function() { - var string = "{{#goodbyes}}{{/goodbyes}}cruel {{world}}!"; - var hash = {goodbyes: [{text: "goodbye"}, {text: "Goodbye"}, {text: "GOODBYE"}], world: "world"}; - shouldCompileTo(string, hash, "cruel world!", - "Arrays iterate over the contents when not empty"); - - shouldCompileTo(string, {goodbyes: [], world: "world"}, "cruel world!", - "Arrays ignore the contents when empty"); -}); - -test("nested iteration", function() { - -}); - -test("block with complex lookup", function() { - var string = "{{#goodbyes}}{{text}} cruel {{../name}}! {{/goodbyes}}"; - var hash = {name: "Alan", goodbyes: [{text: "goodbye"}, {text: "Goodbye"}, {text: "GOODBYE"}]}; - - shouldCompileTo(string, hash, "goodbye cruel Alan! Goodbye cruel Alan! GOODBYE cruel Alan! ", - "Templates can access variables in contexts up the stack with relative path syntax"); -}); - -test("block with complex lookup using nested context", function() { - var string = "{{#goodbyes}}{{text}} cruel {{foo/../name}}! {{/goodbyes}}"; - - shouldThrow(function() { - CompilerContext.compile(string); - }, Error, "Should throw exception"); -}); - -test("helper with complex lookup$", function() { - var string = "{{#goodbyes}}{{{link ../prefix}}}{{/goodbyes}}"; - var hash = {prefix: "/root", goodbyes: [{text: "Goodbye", url: "goodbye"}]}; - var helpers = {link: function(prefix) { - return "" + this.text + ""; - }}; - shouldCompileTo(string, [hash, helpers], "Goodbye"); -}); - -test("helper block with complex lookup expression", function() { - var string = "{{#goodbyes}}{{../name}}{{/goodbyes}}"; - var hash = {name: "Alan"}; - var helpers = {goodbyes: function(options) { - var out = ""; - var byes = ["Goodbye", "goodbye", "GOODBYE"]; - for (var i = 0,j = byes.length; i < j; i++) { - out += byes[i] + " " + options.fn(this) + "! "; - } - return out; - }}; - 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 helpers = {link: function (prefix, options) { - return "" + options.fn(this) + ""; - }}; - shouldCompileToWithPartials(string, [hash, helpers], false, "Goodbye"); -}); - -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, options) { - return "" + options.fn(this) + ""; - }}; - shouldCompileToWithPartials(string, [hash, helpers], true, "Goodbye"); -}); - -test("block with deep nested complex lookup", function() { - var string = "{{#outer}}Goodbye {{#inner}}cruel {{../../omg}}{{/inner}}{{/outer}}"; - var hash = {omg: "OMG!", outer: [{ inner: [{ text: "goodbye" }] }] }; - - shouldCompileTo(string, hash, "Goodbye cruel OMG!"); -}); - -test("block helper", function() { - var string = "{{#goodbyes}}{{text}}! {{/goodbyes}}cruel {{world}}!"; - var template = CompilerContext.compile(string); - - var result = template({world: "world"}, { helpers: {goodbyes: function(options) { return options.fn({text: "GOODBYE"}); }}}); - equal(result, "GOODBYE! cruel world!", "Block helper executed"); -}); - -test("block helper staying in the same context", function() { - var string = "{{#form}}

{{name}}

{{/form}}"; - var template = CompilerContext.compile(string); - - var result = template({name: "Yehuda"}, {helpers: {form: function(options) { return "
" + options.fn(this) + "
"; } }}); - equal(result, "

Yehuda

", "Block helper executed with current context"); -}); - -test("block helper should have context in this", function() { - var source = "
    {{#people}}
  • {{#link}}{{name}}{{/link}}
  • {{/people}}
"; - var link = function(options) { - return '' + options.fn(this) + ''; - }; - var data = { "people": [ - { "name": "Alan", "id": 1 }, - { "name": "Yehuda", "id": 2 } - ]}; - - shouldCompileTo(source, [data, {link: link}], ""); -}); - -test("block helper for undefined value", function() { - shouldCompileTo("{{#empty}}shouldn't render{{/empty}}", {}, ""); -}); - -test("block helper passing a new context", function() { - var string = "{{#form yehuda}}

{{name}}

{{/form}}"; - var template = CompilerContext.compile(string); - - var result = template({yehuda: {name: "Yehuda"}}, { helpers: {form: function(context, options) { return "
" + options.fn(context) + "
"; }}}); - equal(result, "

Yehuda

", "Context variable resolved"); -}); - -test("block helper passing a complex path context", function() { - var string = "{{#form yehuda/cat}}

{{name}}

{{/form}}"; - var template = CompilerContext.compile(string); - - var result = template({yehuda: {name: "Yehuda", cat: {name: "Harold"}}}, { helpers: {form: function(context, options) { return "
" + options.fn(context) + "
"; }}}); - equal(result, "

Harold

", "Complex path variable resolved"); -}); - -test("nested block helpers", function() { - var string = "{{#form yehuda}}

{{name}}

{{#link}}Hello{{/link}}{{/form}}"; - var template = CompilerContext.compile(string); - - var result = template({ - yehuda: {name: "Yehuda" } - }, { - helpers: { - link: function(options) { return "" + options.fn(this) + ""; }, - form: function(context, options) { return "
" + options.fn(context) + "
"; } - } - }); - equal(result, "

Yehuda

Hello
", "Both blocks executed"); -}); - -test("block inverted sections", function() { - shouldCompileTo("{{#people}}{{name}}{{^}}{{none}}{{/people}}", {none: "No people"}, - "No people"); -}); - -test("block inverted sections with empty arrays", function() { - shouldCompileTo("{{#people}}{{name}}{{^}}{{none}}{{/people}}", {none: "No people", people: []}, - "No people"); -}); - -test("block helper inverted sections", function() { - var string = "{{#list people}}{{name}}{{^}}Nobody's here{{/list}}"; - var list = function(context, options) { - if (context.length > 0) { - var out = "
    "; - for(var i = 0,j=context.length; i < j; i++) { - out += "
  • "; - out += options.fn(context[i]); - out += "
  • "; - } - out += "
"; - return out; - } else { - return "

" + options.inverse(this) + "

"; - } - }; - - var hash = {people: [{name: "Alan"}, {name: "Yehuda"}]}; - var empty = {people: []}; - var rootMessage = { - people: [], - message: "Nobody's here" - }; - - var messageString = "{{#list people}}Hello{{^}}{{message}}{{/list}}"; - - // the meaning here may be kind of hard to catch, but list.not is always called, - // so we should see the output of both - shouldCompileTo(string, [hash, { list: list }], "
  • Alan
  • Yehuda
", "an inverse wrapper is passed in as a new context"); - shouldCompileTo(string, [empty, { list: list }], "

Nobody's here

", "an inverse wrapper can be optionally called"); - shouldCompileTo(messageString, [rootMessage, { list: list }], "

Nobody's here

", "the context of an inverse is the parent of the block"); -}); - -suite("helpers hash"); - -test("providing a helpers hash", function() { - shouldCompileTo("Goodbye {{cruel}} {{world}}!", [{cruel: "cruel"}, {world: function() { return "world"; }}], "Goodbye cruel world!", - "helpers hash is available"); - - shouldCompileTo("Goodbye {{#iter}}{{cruel}} {{world}}{{/iter}}!", [{iter: [{cruel: "cruel"}]}, {world: function() { return "world"; }}], - "Goodbye cruel world!", "helpers hash is available inside other blocks"); -}); - -test("in cases of conflict, helpers win", function() { - shouldCompileTo("{{{lookup}}}", [{lookup: 'Explicit'}, {lookup: function() { return 'helpers'; }}], "helpers", - "helpers hash has precedence escaped expansion"); - shouldCompileTo("{{lookup}}", [{lookup: 'Explicit'}, {lookup: function() { return 'helpers'; }}], "helpers", - "helpers hash has precedence simple expansion"); -}); - -test("the helpers hash is available is nested contexts", function() { - shouldCompileTo("{{#outer}}{{#inner}}{{helper}}{{/inner}}{{/outer}}", - [{'outer': {'inner': {'unused':[]}}}, {'helper': function() { return 'helper'; }}], "helper", - "helpers hash is available in nested contexts."); -}); - -test("the helper hash should augment the global hash", function() { - Handlebars.registerHelper('test_helper', function() { return 'found it!'; }); - - shouldCompileTo( - "{{test_helper}} {{#if cruel}}Goodbye {{cruel}} {{world}}!{{/if}}", [ - {cruel: "cruel"}, - {world: function() { return "world!"; }} - ], - "found it! Goodbye cruel world!!"); -}); - -test("Multiple global helper registration", function() { - var helpers = Handlebars.helpers; - try { - Handlebars.helpers = {}; - Handlebars.registerHelper({ - 'if': helpers['if'], - world: function() { return "world!"; }, - test_helper: function() { return 'found it!'; } - }); - - shouldCompileTo( - "{{test_helper}} {{#if cruel}}Goodbye {{cruel}} {{world}}!{{/if}}", - [{cruel: "cruel"}], - "found it! Goodbye cruel world!!"); - } finally { - if (helpers) { - Handlebars.helpers = helpers; - } - } -}); - -suite("partials"); - -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"}]}; - shouldCompileToWithPartials(string, [hash, {}, {dude: partial}], true, "Dudes: Yehuda (http://yehuda) Alan (http://alan) ", - "Basic partials output based on current context."); -}); - -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"}]}; - shouldCompileToWithPartials(string, [hash, {}, {dude: partial}], true, "Dudes: Yehuda (http://yehuda) Alan (http://alan) ", - "Partials can be passed a context"); -}); - -test("partial in a partial", function() { - var string = "Dudes: {{#dudes}}{{>dude}}{{/dudes}}"; - var dude = "{{name}} {{> url}} "; - var url = "{{url}}"; - var hash = {dudes: [{name: "Yehuda", url: "http://yehuda"}, {name: "Alan", url: "http://alan"}]}; - shouldCompileToWithPartials(string, [hash, {}, {dude: dude, url: url}], true, "Dudes: Yehuda http://yehuda Alan http://alan ", "Partials are rendered inside of other partials"); -}); - -test("rendering undefined partial throws an exception", function() { - shouldThrow(function() { - var template = CompilerContext.compile("{{> whatever}}"); - template(); - }, [Handlebars.Exception, 'The partial whatever could not be found'], "Should throw exception"); -}); - -test("rendering template partial in vm mode throws an exception", function() { - shouldThrow(function() { - var template = CompilerContext.compile("{{> whatever}}"); - template(); - }, [Handlebars.Exception, 'The partial whatever could not be found'], "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"}; - shouldCompileToWithPartials(string, [hash, {}, {dude:dude}], true, "Dudes: Jeepers Creepers", "Regular selectors can follow a partial"); -}); - -test("Partials with slash paths", function() { - var string = "Dudes: {{> shared/dude}}"; - var dude = "{{name}}"; - var hash = {name:"Jeepers", another_dude:"Creepers"}; - shouldCompileToWithPartials(string, [hash, {}, {'shared/dude':dude}], true, "Dudes: Jeepers", "Partials can use literal paths"); -}); - -test("Partials with slash and point paths", function() { - var string = "Dudes: {{> shared/dude.thing}}"; - var dude = "{{name}}"; - var hash = {name:"Jeepers", another_dude:"Creepers"}; - shouldCompileToWithPartials(string, [hash, {}, {'shared/dude.thing':dude}], true, "Dudes: Jeepers", "Partials can use literal with points in paths"); -}); - -test("Global Partials", function() { - Handlebars.registerPartial('global_test', '{{another_dude}}'); - - var string = "Dudes: {{> shared/dude}} {{> global_test}}"; - var dude = "{{name}}"; - var hash = {name:"Jeepers", another_dude:"Creepers"}; - shouldCompileToWithPartials(string, [hash, {}, {'shared/dude':dude}], true, "Dudes: Jeepers Creepers", "Partials can use globals or passed"); -}); - -test("Multiple partial registration", function() { - Handlebars.registerPartial({ - 'shared/dude': '{{name}}', - global_test: '{{another_dude}}' - }); - - var string = "Dudes: {{> shared/dude}} {{> global_test}}"; - var hash = {name:"Jeepers", another_dude:"Creepers"}; - shouldCompileToWithPartials(string, [hash], true, "Dudes: Jeepers Creepers", "Partials can use globals or passed"); -}); - -test("Partials with integer path", function() { - var string = "Dudes: {{> 404}}"; - var dude = "{{name}}"; - var hash = {name:"Jeepers", another_dude:"Creepers"}; - shouldCompileToWithPartials(string, [hash, {}, {404:dude}], true, "Dudes: Jeepers", "Partials can use literal paths"); -}); - -test("Partials with complex path", function() { - var string = "Dudes: {{> 404/asdf?.bar}}"; - var dude = "{{name}}"; - var hash = {name:"Jeepers", another_dude:"Creepers"}; - shouldCompileToWithPartials(string, [hash, {}, {'404/asdf?.bar':dude}], true, "Dudes: Jeepers", "Partials can use literal paths"); -}); - -test("Partials with escaped", function() { - var string = "Dudes: {{> [+404/asdf?.bar]}}"; - var dude = "{{name}}"; - var hash = {name:"Jeepers", another_dude:"Creepers"}; - shouldCompileToWithPartials(string, [hash, {}, {'+404/asdf?.bar':dude}], true, "Dudes: Jeepers", "Partials can use literal paths"); -}); - -test("Partials with string", function() { - var string = "Dudes: {{> \"+404/asdf?.bar\"}}"; - var dude = "{{name}}"; - var hash = {name:"Jeepers", another_dude:"Creepers"}; - shouldCompileToWithPartials(string, [hash, {}, {'+404/asdf?.bar':dude}], true, "Dudes: Jeepers", "Partials can use literal paths"); -}); - -suite("String literal parameters"); - -test("simple literals work", function() { - var string = 'Message: {{hello "world" 12 true false}}'; - var hash = {}; - var helpers = {hello: function(param, times, bool1, bool2) { - if(typeof times !== 'number') { times = "NaN"; } - if(typeof bool1 !== 'boolean') { bool1 = "NaB"; } - if(typeof bool2 !== 'boolean') { bool2 = "NaB"; } - return "Hello " + param + " " + times + " times: " + bool1 + " " + bool2; - }}; - shouldCompileTo(string, [hash, helpers], "Message: Hello world 12 times: true false", "template with a simple String literal"); -}); -test("negative number literals work", function() { - var string = 'Message: {{hello -12}}'; - var hash = {}; - var helpers = {hello: function(times) { - if(typeof times !== 'number') { times = "NaN"; } - return "Hello " + times + " times"; - }}; - shouldCompileTo(string, [hash, helpers], "Message: Hello -12 times", "template with a negative integer literal"); -}); - - -test("using a quote in the middle of a parameter raises an error", function() { - shouldThrow(function() { - var string = 'Message: {{hello wo"rld"}}'; - CompilerContext.compile(string); - }, Error, "should throw exception"); -}); - -test("escaping a String is possible", function(){ - var string = 'Message: {{{hello "\\"world\\""}}}'; - var hash = {}; - var helpers = {hello: function(param) { return "Hello " + param; }}; - shouldCompileTo(string, [hash, helpers], "Message: Hello \"world\"", "template with an escaped String literal"); -}); - -test("it works with ' marks", function() { - var string = 'Message: {{{hello "Alan\'s world"}}}'; - var hash = {}; - var helpers = {hello: function(param) { return "Hello " + param; }}; - shouldCompileTo(string, [hash, helpers], "Message: Hello Alan's world", "template with a ' mark"); -}); - -suite("multiple parameters"); - -test("simple multi-params work", function() { - var string = 'Message: {{goodbye cruel world}}'; - var hash = {cruel: "cruel", world: "world"}; - var helpers = {goodbye: function(cruel, world) { return "Goodbye " + cruel + " " + world; }}; - shouldCompileTo(string, [hash, helpers], "Message: Goodbye cruel world", "regular helpers with multiple params"); -}); - -test("block multi-params work", function() { - var string = 'Message: {{#goodbye cruel world}}{{greeting}} {{adj}} {{noun}}{{/goodbye}}'; - var hash = {cruel: "cruel", world: "world"}; - var helpers = {goodbye: function(cruel, world, options) { - return options.fn({greeting: "Goodbye", adj: cruel, noun: world}); - }}; - shouldCompileTo(string, [hash, helpers], "Message: Goodbye cruel world", "block helpers with multiple params"); -}); - -suite("safestring"); - -test("constructing a safestring from a string and checking its type", function() { - var safe = new Handlebars.SafeString("testing 1, 2, 3"); - ok(safe instanceof Handlebars.SafeString, "SafeString is an instance of Handlebars.SafeString"); - equal(safe, "testing 1, 2, 3", "SafeString is equivalent to its underlying string"); -}); - -test("it should not escape SafeString properties", function() { - var name = new Handlebars.SafeString("Sean O'Malley"); - - shouldCompileTo('{{name}}', [{ name: name }], "Sean O'Malley"); -}); - -suite("helperMissing"); - -test("if a context is not found, helperMissing is used", function() { - shouldThrow(function() { - var template = CompilerContext.compile("{{hello}} {{link_to world}}"); - template({}); - }, [Error, "Missing helper: 'link_to'"], "Should throw exception"); -}); - -test("if a context is not found, custom helperMissing is used", function() { - var string = "{{hello}} {{link_to world}}"; - var context = { hello: "Hello", world: "world" }; - - var helpers = { - helperMissing: function(helper, context) { - if(helper === "link_to") { - return new Handlebars.SafeString("" + context + ""); - } - } - }; - - shouldCompileTo(string, [context, helpers], "Hello world"); -}); - -suite("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); -}); -test("Functions are bound to the context in knownHelpers only mode", function() { - var template = CompilerContext.compile("{{foo}}", {knownHelpersOnly: true}); - var result = template({foo: function() { return this.bar; }, bar: 'bar'}); - equal(result, "bar", "'bar' should === '" + result); -}); -test("Unknown helper call in knownHelpers only mode should throw", function() { - shouldThrow(function() { - CompilerContext.compile("{{typeof hello}}", {knownHelpersOnly: true}); - }, Error, 'specified knownHelpersOnly'); -}); - -suite("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"); -}); -test("lambdas resolved by blockHelperMissing are bound to the context", function() { - var string = "{{#truthy}}yep{{/truthy}}"; - var boundData = { truthy: function() { return this.truthiness(); }, truthiness: function() { return false; } }; - shouldCompileTo(string, boundData, ""); -}); - -var teardown; -suite("built-in helpers", { - setup: function(){ teardown = null; }, - teardown: function(){ if (teardown) { teardown(); } } -}); - -test("with", function() { - var string = "{{#with person}}{{first}} {{last}}{{/with}}"; - shouldCompileTo(string, {person: {first: "Alan", last: "Johnson"}}, "Alan Johnson"); -}); -test("with with function argument", function() { - var string = "{{#with person}}{{first}} {{last}}{{/with}}"; - shouldCompileTo(string, {person: function() { return {first: "Alan", last: "Johnson"};}}, "Alan Johnson"); -}); - -test("if", function() { - var string = "{{#if goodbye}}GOODBYE {{/if}}cruel {{world}}!"; - shouldCompileTo(string, {goodbye: true, world: "world"}, "GOODBYE cruel world!", - "if with boolean argument shows the contents when true"); - shouldCompileTo(string, {goodbye: "dummy", world: "world"}, "GOODBYE cruel world!", - "if with string argument shows the contents"); - shouldCompileTo(string, {goodbye: false, world: "world"}, "cruel world!", - "if with boolean argument does not show the contents when false"); - shouldCompileTo(string, {world: "world"}, "cruel world!", - "if with undefined does not show the contents"); - shouldCompileTo(string, {goodbye: ['foo'], world: "world"}, "GOODBYE cruel world!", - "if with non-empty array shows the contents"); - shouldCompileTo(string, {goodbye: [], world: "world"}, "cruel world!", - "if with empty array does not show the contents"); -}); - -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() { - var string = "{{#each goodbyes}}{{text}}! {{/each}}cruel {{world}}!"; - var hash = {goodbyes: [{text: "goodbye"}, {text: "Goodbye"}, {text: "GOODBYE"}], world: "world"}; - shouldCompileTo(string, hash, "goodbye! Goodbye! GOODBYE! cruel world!", - "each with array argument iterates over the contents when not empty"); - shouldCompileTo(string, {goodbyes: [], world: "world"}, "cruel world!", - "each with array argument ignores the contents when empty"); -}); - -test("each with an object and @key", function() { - var string = "{{#each goodbyes}}{{@key}}. {{text}}! {{/each}}cruel {{world}}!"; - var hash = {goodbyes: {"#1": {text: "goodbye"}, 2: {text: "GOODBYE"}}, world: "world"}; - - // Object property iteration order is undefined according to ECMA spec, - // so we need to check both possible orders - // @see http://stackoverflow.com/questions/280713/elements-order-in-a-for-in-loop - var actual = compileWithPartials(string, hash); - var expected1 = "<b>#1</b>. goodbye! 2. GOODBYE! cruel world!"; - var expected2 = "2. GOODBYE! <b>#1</b>. goodbye! cruel world!"; - - ok(actual === expected1 || actual === expected2, "each with object argument iterates over the contents when not empty"); - shouldCompileTo(string, {goodbyes: [], world: "world"}, "cruel world!", - "each with object argument ignores the contents when empty"); -}); - -test("each with @index", function() { - var string = "{{#each goodbyes}}{{@index}}. {{text}}! {{/each}}cruel {{world}}!"; - var hash = {goodbyes: [{text: "goodbye"}, {text: "Goodbye"}, {text: "GOODBYE"}], world: "world"}; - - var template = CompilerContext.compile(string); - var result = template(hash); - - equal(result, "0. goodbye! 1. Goodbye! 2. GOODBYE! cruel world!", "The @index variable is used"); -}); - -test("each with function argument", function() { - var string = "{{#each goodbyes}}{{text}}! {{/each}}cruel {{world}}!"; - var hash = {goodbyes: function () { return [{text: "goodbye"}, {text: "Goodbye"}, {text: "GOODBYE"}];}, world: "world"}; - shouldCompileTo(string, hash, "goodbye! Goodbye! GOODBYE! cruel world!", - "each with array function argument iterates over the contents when not empty"); - shouldCompileTo(string, {goodbyes: [], world: "world"}, "cruel world!", - "each with array function argument ignores the contents when empty"); -}); - -test("data passed to helpers", function() { - var string = "{{#each letters}}{{this}}{{detectDataInsideEach}}{{/each}}"; - var hash = {letters: ['a', 'b', 'c']}; - - var template = CompilerContext.compile(string); - var result = template(hash, { - data: { - exclaim: '!' - } - }); - equal(result, 'a!b!c!', 'should output data'); -}); - -Handlebars.registerHelper('detectDataInsideEach', function(options) { - return options.data && options.data.exclaim; -}); - -test("log", function() { - var string = "{{log blah}}"; - var hash = { blah: "whee" }; - - var levelArg, logArg; - var originalLog = Handlebars.log; - Handlebars.log = function(level, arg){ levelArg = level, logArg = arg; }; - teardown = function(){ Handlebars.log = originalLog; }; - - shouldCompileTo(string, hash, "", "log should not display"); - equals(1, levelArg, "should call log with 1"); - 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 = CompilerContext.compile("{{hello}}", {data: true}); - - var helpers = { - hello: function(options) { - return options.data.adjective + " " + this.noun; - } - }; - - var result = template({noun: "cat"}, {helpers: helpers, data: {adjective: "happy"}}); - equals("happy cat", result, "Data output by helper"); -}); - -test("data can be looked up via @foo", function() { - var template = CompilerContext.compile("{{@hello}}"); - var result = template({}, { data: { hello: "hello" } }); - equals("hello", result, "@foo retrieves template data"); -}); - -var objectCreate = Handlebars.createFrame; - -test("deep @foo triggers automatic top-level data", function() { - var template = CompilerContext.compile('{{#let world="world"}}{{#if foo}}{{#if foo}}Hello {{@world}}{{/if}}{{/if}}{{/let}}'); - - var helpers = objectCreate(Handlebars.helpers); - - helpers.let = function(options) { - var frame = Handlebars.createFrame(options.data); - - for (var prop in options.hash) { - frame[prop] = options.hash[prop]; - } - return options.fn(this, { data: frame }); - }; - - var result = template({ foo: true }, { helpers: helpers }); - equals("Hello world", result, "Automatic data was triggered"); -}); - -test("parameter data can be looked up via @foo", function() { - var template = CompilerContext.compile("{{hello @world}}"); - var helpers = { - hello: function(noun) { - return "Hello " + noun; - } - }; - - var result = template({}, { helpers: helpers, data: { world: "world" } }); - equals("Hello world", result, "@foo as a parameter retrieves template data"); -}); - -test("hash values can be looked up via @foo", function() { - var template = CompilerContext.compile("{{hello noun=@world}}"); - var helpers = { - hello: function(options) { - return "Hello " + options.hash.noun; - } - }; - - var result = template({}, { helpers: helpers, data: { world: "world" } }); - equals("Hello world", result, "@foo as a parameter retrieves template data"); -}); - -test("nested parameter data can be looked up via @foo.bar", function() { - var template = CompilerContext.compile("{{hello @world.bar}}"); - var helpers = { - hello: function(noun) { - return "Hello " + noun; - } - }; - - var result = template({}, { helpers: helpers, data: { world: {bar: "world" } } }); - equals("Hello world", result, "@foo as a parameter retrieves template data"); -}); - -test("nested parameter data does not fail with @world.bar", function() { - var template = CompilerContext.compile("{{hello @world.bar}}"); - var helpers = { - hello: function(noun) { - return "Hello " + noun; - } - }; - - var result = template({}, { helpers: helpers, data: { foo: {bar: "world" } } }); - equals("Hello undefined", result, "@foo as a parameter retrieves template data"); -}); - -test("parameter data throws when using this scope references", function() { - var string = "{{#goodbyes}}{{text}} cruel {{@./name}}! {{/goodbyes}}"; - - shouldThrow(function() { - CompilerContext.compile(string); - }, Error, "Should throw exception"); -}); - -test("parameter data throws when using parent scope references", function() { - var string = "{{#goodbyes}}{{text}} cruel {{@../name}}! {{/goodbyes}}"; - - shouldThrow(function() { - CompilerContext.compile(string); - }, Error, "Should throw exception"); -}); - -test("parameter data throws when using complex scope references", function() { - var string = "{{#goodbyes}}{{text}} cruel {{@foo/../name}}! {{/goodbyes}}"; - - shouldThrow(function() { - CompilerContext.compile(string); - }, Error, "Should throw exception"); -}); - -test("data is inherited downstream", function() { - var template = CompilerContext.compile("{{#let foo=bar.baz}}{{@foo}}{{/let}}", { data: true }); - var helpers = { - let: function(options) { - for (var prop in options.hash) { - options.data[prop] = options.hash[prop]; - } - return options.fn(this); - } - }; - - var result = template({ bar: { baz: "hello world" } }, { helpers: helpers, data: {} }); - equals("hello world", result, "data variables are inherited downstream"); -}); - -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 = CompilerContext.compile("{{hello world}}", {data: true}); - - var helpers = { - hello: function(noun, options) { - return options.data.adjective + " " + noun + (this.exclaim ? "!" : ""); - } - }; - - var result = template({exclaim: true, world: "world"}, {helpers: helpers, data: {adjective: "happy"}}); - equals("happy world!", result, "Data output by helper"); -}); - -test("passing in data to a compiled function that expects data - works with block helpers", function() { - var template = CompilerContext.compile("{{#hello}}{{world}}{{/hello}}", {data: true}); - - var helpers = { - hello: function(options) { - return options.fn(this); - }, - world: function(options) { - return options.data.adjective + " world" + (this.exclaim ? "!" : ""); - } - }; - - 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 = CompilerContext.compile("{{#hello}}{{world ../zomg}}{{/hello}}", {data: true}); - - var helpers = { - hello: function(options) { - return options.fn({exclaim: "?"}); - }, - world: function(thing, options) { - return options.data.adjective + " " + thing + (this.exclaim || ""); - } - }; - - var result = template({exclaim: true, zomg: "world"}, {helpers: helpers, data: {adjective: "happy"}}); - equals("happy world?", result, "Data output by helper"); -}); - -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(options) { - return options.data.accessData + " " + options.fn({exclaim: "?"}); - }, - world: function(thing, options) { - return options.data.adjective + " " + thing + (this.exclaim || ""); - } - }; - - var result = template({exclaim: true, zomg: "world"}, {helpers: helpers, data: {adjective: "happy", accessData: "#win"}}); - equals("#win happy world?", result, "Data output by helper"); -}); - -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(options) { - return options.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(options) { - return options.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(); - }, - - cruel: function(world) { - return "cruel " + world.toUpperCase(); - } - }; - - var context = { - 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(options) { - return this.goodbye.toUpperCase() + options.fn(this); - }, - - cruel: function(world) { - return "cruel " + world.toUpperCase(); - } - }; - - var context = { - 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(); - }, - - cruel: function(world) { - return "cruel " + world.toUpperCase(); - }, - }; - - var context = { - 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(options) { - return this.goodbye.toUpperCase() + options.fn(this); - }, - - cruel: function(world) { - return "cruel " + world.toUpperCase(); - }, - }; - - var context = { - 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"); - - template = CompilerContext.compile('{{goodbye cruel="CRUEL" world="WORLD" print=false}}'); - 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 single quoted stings", function() { - var template = CompilerContext.compile("{{#goodbye cruel='CRUEL' times=12}}world{{/goodbye}}"); - - var helpers = { - goodbye: function(options) { - return "GOODBYE " + options.hash.cruel + " " + options.fn(this) + " " + options.hash.times + " TIMES"; - } - }; - - 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"); - - template = CompilerContext.compile('{{#goodbye cruel="CRUEL" print=false}}world{{/goodbye}}'); - 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) { - 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("in string mode, information about the types is passed along", function() { - var template = CompilerContext.compile('{{tomdale "need" dad.joke true false}}', { stringParams: true }); - - var helpers = { - tomdale: function(desire, noun, trueBool, falseBool, options) { - equal(options.types[0], 'STRING', "the string type is passed"); - equal(options.types[1], 'ID', "the expression type is passed"); - equal(options.types[2], 'BOOLEAN', "the expression type is passed"); - equal(desire, "need", "the string form is passed for strings"); - equal(noun, "dad.joke", "the string form is passed for expressions"); - equal(trueBool, true, "raw booleans are passed through"); - equal(falseBool, false, "raw booleans are passed through"); - return "Helper called"; - } - }; - - var result = template({}, { helpers: helpers }); - equal(result, "Helper called"); -}); - -test("in string mode, hash parameters get type information", function() { - var template = CompilerContext.compile('{{tomdale he.says desire="need" noun=dad.joke bool=true}}', { stringParams: true }); - - var helpers = { - tomdale: function(exclamation, options) { - equal(exclamation, "he.says"); - equal(options.types[0], "ID"); - - equal(options.hashTypes.desire, "STRING"); - equal(options.hashTypes.noun, "ID"); - equal(options.hashTypes.bool, "BOOLEAN"); - equal(options.hash.desire, "need"); - equal(options.hash.noun, "dad.joke"); - equal(options.hash.bool, true); - return "Helper called"; - } - }; - - var result = template({}, { helpers: helpers }); - equal(result, "Helper called"); -}); - -test("in string mode, hash parameters get context information", function() { - var template = CompilerContext.compile('{{#with dale}}{{tomdale he.says desire="need" noun=../dad/joke bool=true}}{{/with}}', { stringParams: true }); - - var context = {dale: {}}; - - var helpers = { - tomdale: function(exclamation, options) { - equal(exclamation, "he.says"); - equal(options.types[0], "ID"); - - equal(options.contexts.length, 1); - equal(options.hashContexts.noun, context); - equal(options.hash.desire, "need"); - equal(options.hash.noun, "dad.joke"); - equal(options.hash.bool, true); - return "Helper called"; - }, - "with": function(context, options) { - return options.fn(options.contexts[0][context]); - } - }; - - var result = template(context, { helpers: helpers }); - equal(result, "Helper called"); -}); - -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"); -}); - -suite("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 = "This is a slightly more complicated {{thing}}..\n{{! Just ignore this business. }}\nCheck this out:\n{{#hasThings}}\n
    \n{{#things}}\n
  • {{word}}
  • \n{{/things}}
.\n{{/hasThings}}\n{{^hasThings}}\n\nNothing to check out...\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 = "This is a slightly more complicated blah..\n\nCheck this out:\n\n
    \n\n
  • @fat
  • \n\n
  • @dhg
  • \n\n
  • @sayrer
  • \n
.\n\n"; - shouldCompileTo(string, data, output); -}); - -test("Passing falsy values to Handlebars.compile throws an error", function() { - shouldThrow(function() { - CompilerContext.compile(null); - }, "You must pass a string or Handlebars AST to Handlebars.precompile. You passed null"); -}); - -test("can pass through an already-compiled AST via compile/precompile", function() { - equal(Handlebars.compile(new Handlebars.AST.ProgramNode([ new Handlebars.AST.ContentNode("Hello")]))(), 'Hello') -}); - -test('GH-408: Multiple loops fail', function() { - var context = [ - { name: "John Doe", location: { city: "Chicago" } }, - { name: "Jane Doe", location: { city: "New York"} } - ]; - - var template = CompilerContext.compile('{{#.}}{{name}}{{/.}}{{#.}}{{name}}{{/.}}{{#.}}{{name}}{{/.}}'); - - var result = template(context); - equals(result, "John DoeJane DoeJohn DoeJane DoeJohn DoeJane Doe", 'It should output multiple times'); -}); - -test('GS-428: Nested if else rendering', function() { - var succeedingTemplate = '{{#inverse}} {{#blk}} Unexpected {{/blk}} {{else}} {{#blk}} Expected {{/blk}} {{/inverse}}'; - var failingTemplate = '{{#inverse}} {{#blk}} Unexpected {{/blk}} {{else}} {{#blk}} Expected {{/blk}} {{/inverse}}'; - - var helpers = { - blk: function(block) { return block.fn(''); }, - inverse: function(block) { return block.inverse(''); } - }; - - shouldCompileTo(succeedingTemplate, [{}, helpers], ' Expected '); - shouldCompileTo(failingTemplate, [{}, helpers], ' Expected '); -}); - -test('GH-458: Scoped this identifier', function() { - shouldCompileTo('{{./foo}}', {foo: 'bar'}, 'bar'); -}); - -test('GH-375: Unicode line terminators', function() { - shouldCompileTo('\u2028', {}, '\u2028'); -}); - -test('GH-534: Object prototype aliases', function() { - Object.prototype[0xD834] = true; - - shouldCompileTo('{{foo}}', { foo: 'bar' }, 'bar'); - - delete Object.prototype[0xD834]; -}); - -test('GH-437: Matching escaping', function() { - shouldThrow(function() { - CompilerContext.compile('{{{a}}'); - }, Error); - shouldThrow(function() { - CompilerContext.compile('{{a}}}'); - }, Error); -}); - -suite('Utils'); - -test('escapeExpression', function() { - equal(Handlebars.Utils.escapeExpression('foo<&"\'>'), 'foo<&"'>'); - equal(Handlebars.Utils.escapeExpression(new Handlebars.SafeString('foo<&"\'>')), 'foo<&"\'>'); - equal(Handlebars.Utils.escapeExpression(''), ''); - equal(Handlebars.Utils.escapeExpression(undefined), ''); - equal(Handlebars.Utils.escapeExpression(null), ''); - equal(Handlebars.Utils.escapeExpression(false), ''); - - equal(Handlebars.Utils.escapeExpression(0), '0'); - equal(Handlebars.Utils.escapeExpression({}), {}.toString()); - equal(Handlebars.Utils.escapeExpression([]), [].toString()); -}); - -test('isEmpty', function() { - equal(Handlebars.Utils.isEmpty(undefined), true); - equal(Handlebars.Utils.isEmpty(null), true); - equal(Handlebars.Utils.isEmpty(false), true); - equal(Handlebars.Utils.isEmpty(''), true); - equal(Handlebars.Utils.isEmpty([]), true); - - equal(Handlebars.Utils.isEmpty(0), false); - equal(Handlebars.Utils.isEmpty([1]), false); - equal(Handlebars.Utils.isEmpty('foo'), false); - equal(Handlebars.Utils.isEmpty({bar: 1}), false); -}); - -if (typeof(require) !== 'undefined') { - suite('Require'); - - test('Load .handlebars files with require()', function() { - var template = require("./example_1"); - assert.deepEqual(template, require("./example_1.handlebars")); - - var expected = 'foo\n'; - var result = template({foo: "foo"}); - - equal(result, expected); - }); - - test('Load .hbs files with require()', function() { - var template = require("./example_2"); - assert.deepEqual(template, require("./example_2.hbs")); - - var expected = 'Hello, World!\n'; - var result = template({name: "World"}); - - equal(result, expected); - }); -} diff --git a/spec/regressions.js b/spec/regressions.js new file mode 100644 index 000000000..44bde81b6 --- /dev/null +++ b/spec/regressions.js @@ -0,0 +1,119 @@ +/*global CompilerContext, shouldCompileTo */ +describe('Regressions', function() { + it("GH-94: Cannot read property of undefined", function() { + var data = {"books":[{"title":"The origin of species","author":{"name":"Charles Darwin"}},{"title":"Lazarillo de Tormes"}]}; + var string = "{{#books}}{{title}}{{author.name}}{{/books}}"; + shouldCompileTo(string, data, "The origin of speciesCharles DarwinLazarillo de Tormes", + "Renders without an undefined property error"); + }); + + it("GH-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"); + }); + + it("GH-158: Using array index twice, breaks the template", function() { + var string = "{{arr.[0]}}, {{arr.[1]}}"; + var data = { "arr": [1,2] }; + + shouldCompileTo(string, data, "1, 2", "it works as expected"); + }); + + it("bug reported by @fat where lambdas weren't being properly resolved", function() { + var string = "This is a slightly more complicated {{thing}}..\n{{! Just ignore this business. }}\nCheck this out:\n{{#hasThings}}\n
    \n{{#things}}\n
  • {{word}}
  • \n{{/things}}
.\n{{/hasThings}}\n{{^hasThings}}\n\nNothing to check out...\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 = "This is a slightly more complicated blah..\n\nCheck this out:\n\n
    \n\n
  • @fat
  • \n\n
  • @dhg
  • \n\n
  • @sayrer
  • \n
.\n\n"; + shouldCompileTo(string, data, output); + }); + + it('GH-408: Multiple loops fail', function() { + var context = [ + { name: "John Doe", location: { city: "Chicago" } }, + { name: "Jane Doe", location: { city: "New York"} } + ]; + + var template = CompilerContext.compile('{{#.}}{{name}}{{/.}}{{#.}}{{name}}{{/.}}{{#.}}{{name}}{{/.}}'); + + var result = template(context); + equals(result, "John DoeJane DoeJohn DoeJane DoeJohn DoeJane Doe", 'It should output multiple times'); + }); + + it('GS-428: Nested if else rendering', function() { + var succeedingTemplate = '{{#inverse}} {{#blk}} Unexpected {{/blk}} {{else}} {{#blk}} Expected {{/blk}} {{/inverse}}'; + var failingTemplate = '{{#inverse}} {{#blk}} Unexpected {{/blk}} {{else}} {{#blk}} Expected {{/blk}} {{/inverse}}'; + + var helpers = { + blk: function(block) { return block.fn(''); }, + inverse: function(block) { return block.inverse(''); } + }; + + shouldCompileTo(succeedingTemplate, [{}, helpers], ' Expected '); + shouldCompileTo(failingTemplate, [{}, helpers], ' Expected '); + }); + + it('GH-458: Scoped this identifier', function() { + shouldCompileTo('{{./foo}}', {foo: 'bar'}, 'bar'); + }); + + it('GH-375: Unicode line terminators', function() { + shouldCompileTo('\u2028', {}, '\u2028'); + }); + + it('GH-534: Object prototype aliases', function() { + Object.prototype[0xD834] = true; + + shouldCompileTo('{{foo}}', { foo: 'bar' }, 'bar'); + + delete Object.prototype[0xD834]; + }); + + it('GH-437: Matching escaping', function() { + (function() { + CompilerContext.compile('{{{a}}'); + }).should.throw(Error); + (function() { + CompilerContext.compile('{{a}}}'); + }).should.throw(Error); + }); + + it("Mustache man page", function() { + var string = "Hello {{name}}. You have just won ${{value}}!{{#in_ca}} Well, ${{taxed_value}}, after taxes.{{/in_ca}}"; + var data = { + "name": "Chris", + "value": 10000, + "taxed_value": 10000 - (10000 * 0.4), + "in_ca": true + }; + + shouldCompileTo(string, data, "Hello Chris. You have just won $10000! Well, $6000, after taxes.", "the hello world mustache example works"); + }); + + it("Passing falsy values to Handlebars.compile throws an error", function() { + (function() { + CompilerContext.compile(null); + }).should.throw("You must pass a string or Handlebars AST to Handlebars.precompile. You passed null"); + }); + + if (Handlebars.AST) { + it("can pass through an already-compiled AST via compile/precompile", function() { + equal(Handlebars.compile(new Handlebars.AST.ProgramNode([ new Handlebars.AST.ContentNode("Hello")]))(), 'Hello'); + }); + } +}); diff --git a/spec/require.js b/spec/require.js new file mode 100644 index 000000000..d750d7d3d --- /dev/null +++ b/spec/require.js @@ -0,0 +1,23 @@ +if (typeof(require) !== 'undefined' && require.extensions[".handlebars"]) { + describe('Require', function() { + it('Load .handlebars files with require()', function() { + var template = require("./artifacts/example_1"); + template.should.eql(require("./artifacts/example_1.handlebars")); + + var expected = 'foo\n'; + var result = template({foo: "foo"}); + + result.should.equal(expected); + }); + + it('Load .hbs files with require()', function() { + var template = require("./artifacts/example_2"); + template.should.eql(require("./artifacts/example_2.hbs")); + + var expected = 'Hello, World!\n'; + var result = template({name: "World"}); + + result.should.equal(expected); + }); + }); +} diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb deleted file mode 100644 index eb2f26afd..000000000 --- a/spec/spec_helper.rb +++ /dev/null @@ -1,132 +0,0 @@ -require "v8" - -# Monkey patches due to bugs in RubyRacer -class V8::JSError - def initialize(try, to) - @to = to - begin - super(initialize_unsafe(try)) - rescue Exception => e - # Original code does not make an Array here - @boundaries = [Boundary.new(:rbframes => e.backtrace)] - @value = e - super("BUG! please report. JSError#initialize failed!: #{e.message}") - end - end - - def parse_js_frames(try) - raw = @to.rb(try.StackTrace()) - if raw && !raw.empty? - raw.split("\n")[1..-1].tap do |frames| - # Original code uses strip!, and the frames are not guaranteed to be strippable - frames.each {|frame| frame.strip.chomp!(",")} - end - else - [] - end - end -end - -module Handlebars - module Spec - def self.js_backtrace(context) - begin - context.eval("throw") - rescue V8::JSError => e - return e.backtrace(:javascript) - end - end - - def self.remove_exports(string) - match = string.match(%r{\A(.*?)^// BEGIN\(BROWSER\)\n(.*)\n^// END\(BROWSER\)(.*?)\Z}m) - prelines = match ? match[1].count("\n") + 1 : 0 - ret = match ? match[2] : string - ("\n" * prelines) + ret - end - - def self.load_helpers(context) - context["exports"] = nil - - context["p"] = proc do |this, val| - p val if ENV["DEBUG_JS"] - end - - context["puts"] = proc do |this, val| - puts val if ENV["DEBUG_JS"] - end - - context["puts_node"] = proc do |this, val| - puts context["Handlebars"]["PrintVisitor"].new.accept(val) - puts - end - - context["puts_caller"] = proc do - puts "BACKTRACE:" - puts Handlebars::Spec.js_backtrace(context) - puts - end - end - - 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, 'dist/handlebars.js'); - - context["CompilerContext"] = {} - CompilerContext = context["CompilerContext"] - CompilerContext["compile"] = proc do |this, *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 |this, *args| - template, options = args[0], args[1] || nil - context["Handlebars"]["compile"].call(template, options); - end - end - - PARSER_CONTEXT = V8::Context.new - PARSER_CONTEXT.instance_eval do |context| - Handlebars::Spec.load_helpers(context); - - Handlebars::Spec.js_load(context, 'lib/handlebars/compiler/parser.js'); - end - - COMPILE_CONTEXT = V8::Context.new - COMPILE_CONTEXT.instance_eval do |context| - Handlebars::Spec.load_helpers(context); - - Handlebars::Spec.js_load(context, 'dist/handlebars.js'); - Handlebars::Spec.js_load(context, 'lib/handlebars/compiler/visitor.js'); - Handlebars::Spec.js_load(context, 'lib/handlebars/compiler/printer.js'); - - context["Handlebars"]["logger"]["level"] = ENV["DEBUG_JS"] ? context["Handlebars"]["logger"][ENV["DEBUG_JS"]] : 4 - - context["Handlebars"]["logger"]["log"] = proc do |this, level, str| - logger_level = context["Handlebars"]["logger"]["level"].to_i - - if logger_level <= level - puts str - end - end - end - end -end - - -require "test/unit/assertions" - -RSpec.configure do |config| - config.include Test::Unit::Assertions - - # 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 diff --git a/spec/string-params.js b/spec/string-params.js new file mode 100644 index 000000000..1ebb5838b --- /dev/null +++ b/spec/string-params.js @@ -0,0 +1,145 @@ +describe('string params mode', function() { + it("arguments to helpers can be retrieved from options hash in string form", function() { + var template = CompilerContext.compile('{{wycats is.a slave.driver}}', {stringParams: true}); + + var helpers = { + wycats: function(passiveVoice, noun) { + return "HELP ME MY BOSS " + passiveVoice + ' ' + noun; + } + }; + + var result = template({}, {helpers: helpers}); + + equals(result, "HELP ME MY BOSS is.a slave.driver", "String parameters output"); + }); + + it("when using block form, arguments to helpers can be retrieved from options hash in string form", function() { + 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"); + }); + + it("when inside a block in String mode, .. passes the appropriate context in the options hash", function() { + var template = CompilerContext.compile('{{#with dale}}{{tomdale ../need dad.joke}}{{/with}}', {stringParams: true}); + + var helpers = { + tomdale: function(desire, noun, options) { + return "STOP ME FROM READING HACKER NEWS I " + + options.contexts[0][desire] + " " + noun; + }, + + "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"); + }); + + it("information about the types is passed along", function() { + var template = CompilerContext.compile('{{tomdale "need" dad.joke true false}}', { stringParams: true }); + + var helpers = { + tomdale: function(desire, noun, trueBool, falseBool, options) { + equal(options.types[0], 'STRING', "the string type is passed"); + equal(options.types[1], 'ID', "the expression type is passed"); + equal(options.types[2], 'BOOLEAN', "the expression type is passed"); + equal(desire, "need", "the string form is passed for strings"); + equal(noun, "dad.joke", "the string form is passed for expressions"); + equal(trueBool, true, "raw booleans are passed through"); + equal(falseBool, false, "raw booleans are passed through"); + return "Helper called"; + } + }; + + var result = template({}, { helpers: helpers }); + equal(result, "Helper called"); + }); + + it("hash parameters get type information", function() { + var template = CompilerContext.compile('{{tomdale he.says desire="need" noun=dad.joke bool=true}}', { stringParams: true }); + + var helpers = { + tomdale: function(exclamation, options) { + equal(exclamation, "he.says"); + equal(options.types[0], "ID"); + + equal(options.hashTypes.desire, "STRING"); + equal(options.hashTypes.noun, "ID"); + equal(options.hashTypes.bool, "BOOLEAN"); + equal(options.hash.desire, "need"); + equal(options.hash.noun, "dad.joke"); + equal(options.hash.bool, true); + return "Helper called"; + } + }; + + var result = template({}, { helpers: helpers }); + equal(result, "Helper called"); + }); + + it("hash parameters get context information", function() { + var template = CompilerContext.compile('{{#with dale}}{{tomdale he.says desire="need" noun=../dad/joke bool=true}}{{/with}}', { stringParams: true }); + + var context = {dale: {}}; + + var helpers = { + tomdale: function(exclamation, options) { + equal(exclamation, "he.says"); + equal(options.types[0], "ID"); + + equal(options.contexts.length, 1); + equal(options.hashContexts.noun, context); + equal(options.hash.desire, "need"); + equal(options.hash.noun, "dad.joke"); + equal(options.hash.bool, true); + return "Helper called"; + }, + "with": function(context, options) { + return options.fn(options.contexts[0][context]); + } + }; + + var result = template(context, { helpers: helpers }); + equal(result, "Helper called"); + }); + + it("when inside a block in String mode, .. passes the appropriate context in the options hash to a block helper", function() { + 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"); + }); +}); diff --git a/spec/tokenizer.js b/spec/tokenizer.js new file mode 100644 index 000000000..de981e427 --- /dev/null +++ b/spec/tokenizer.js @@ -0,0 +1,358 @@ +var should = require('should'); + +should.Assertion.prototype.match_tokens = function(tokens) { + this.obj.forEach(function(value, index) { + value.name.should.equal(tokens[index]); + }); +}; +should.Assertion.prototype.be_token = function(name, text) { + this.obj.should.eql({name: name, text: text}); +}; + +describe('Tokenizer', function() { + if (!Handlebars.Parser) { + return; + } + + function tokenize(template) { + var parser = Handlebars.Parser, + lexer = parser.lexer; + + lexer.setInput(template); + var out = [], + token; + + while (token = lexer.lex()) { + var result = parser.terminals_[token] || token; + if (!result || result === 'EOF' || result === 'INVALID') { + break; + } + out.push({name: result, text: lexer.yytext}); + } + + return out; + } + + it('tokenizes a simple mustache as "OPEN ID CLOSE"', function() { + var result = tokenize("{{foo}}"); + result.should.match_tokens(['OPEN', 'ID', 'CLOSE']); + result[1].should.be_token("ID", "foo"); + }); + + it('supports unescaping with &', function() { + var result = tokenize("{{&bar}}"); + result.should.match_tokens(['OPEN', 'ID', 'CLOSE']); + + result[0].should.be_token("OPEN", "{{&"); + result[1].should.be_token("ID", "bar"); + }); + + it('supports unescaping with {{{', function() { + var result = tokenize("{{{bar}}}"); + result.should.match_tokens(['OPEN_UNESCAPED', 'ID', 'CLOSE_UNESCAPED']); + + result[1].should.be_token("ID", "bar"); + }); + + it('supports escaping delimiters', function() { + var result = tokenize("{{foo}} \\{{bar}} {{baz}}"); + result.should.match_tokens(['OPEN', 'ID', 'CLOSE', 'CONTENT', 'CONTENT', 'OPEN', 'ID', 'CLOSE']); + + result[3].should.be_token("CONTENT", " "); + result[4].should.be_token("CONTENT", "{{bar}} "); + }); + + it('supports escaping multiple delimiters', function() { + var result = tokenize("{{foo}} \\{{bar}} \\{{baz}}"); + result.should.match_tokens(['OPEN', 'ID', 'CLOSE', 'CONTENT', 'CONTENT', 'CONTENT']); + + result[3].should.be_token("CONTENT", " "); + result[4].should.be_token("CONTENT", "{{bar}} "); + result[5].should.be_token("CONTENT", "{{baz}}"); + }); + + it('supports escaping a triple stash', function() { + var result = tokenize("{{foo}} \\{{{bar}}} {{baz}}"); + result.should.match_tokens(['OPEN', 'ID', 'CLOSE', 'CONTENT', 'CONTENT', 'OPEN', 'ID', 'CLOSE']); + + result[4].should.be_token("CONTENT", "{{{bar}}} "); + }); + + it('supports escaping escape character', function() { + var result = tokenize("{{foo}} \\\\{{bar}} {{baz}}"); + result.should.match_tokens(['OPEN', 'ID', 'CLOSE', 'CONTENT', 'OPEN', 'ID', 'CLOSE', 'CONTENT', 'OPEN', 'ID', 'CLOSE']); + + result[3].should.be_token("CONTENT", " \\"); + result[5].should.be_token("ID", "bar"); + }); + + it('supports escaping multiple escape characters', function() { + var result = tokenize("{{foo}} \\\\{{bar}} \\\\{{baz}}"); + result.should.match_tokens(['OPEN', 'ID', 'CLOSE', 'CONTENT', 'OPEN', 'ID', 'CLOSE', 'CONTENT', 'OPEN', 'ID', 'CLOSE']); + + result[3].should.be_token("CONTENT", " \\"); + result[5].should.be_token("ID", "bar"); + result[7].should.be_token("CONTENT", " \\"); + result[9].should.be_token("ID", "baz"); + }); + + it('supports mixed escaped delimiters and escaped escape characters', function() { + var result = tokenize("{{foo}} \\\\{{bar}} \\{{baz}}"); + result.should.match_tokens(['OPEN', 'ID', 'CLOSE', 'CONTENT', 'OPEN', 'ID', 'CLOSE', 'CONTENT', 'CONTENT', 'CONTENT']); + + result[3].should.be_token("CONTENT", " \\"); + result[4].should.be_token("OPEN", "{{"); + result[5].should.be_token("ID", "bar"); + result[7].should.be_token("CONTENT", " "); + result[8].should.be_token("CONTENT", "{{baz}}"); + }); + + it('supports escaped escape character on a triple stash', function() { + var result = tokenize("{{foo}} \\\\{{{bar}}} {{baz}}"); + result.should.match_tokens(['OPEN', 'ID', 'CLOSE', 'CONTENT', 'OPEN_UNESCAPED', 'ID', 'CLOSE_UNESCAPED', 'CONTENT', 'OPEN', 'ID', 'CLOSE']); + + result[3].should.be_token("CONTENT", " \\"); + result[5].should.be_token("ID", "bar"); + }); + + it('tokenizes a simple path', function() { + var result = tokenize("{{foo/bar}}"); + result.should.match_tokens(['OPEN', 'ID', 'SEP', 'ID', 'CLOSE']); + }); + + it('allows dot notation', function() { + var result = tokenize("{{foo.bar}}"); + result.should.match_tokens(['OPEN', 'ID', 'SEP', 'ID', 'CLOSE']); + + tokenize("{{foo.bar.baz}}").should.match_tokens(['OPEN', 'ID', 'SEP', 'ID', 'SEP', 'ID', 'CLOSE']); + }); + + it('allows path literals with []', function() { + var result = tokenize("{{foo.[bar]}}"); + result.should.match_tokens(['OPEN', 'ID', 'SEP', 'ID', 'CLOSE']); + }); + + it('allows multiple path literals on a line with []', function() { + var result = tokenize("{{foo.[bar]}}{{foo.[baz]}}"); + result.should.match_tokens(['OPEN', 'ID', 'SEP', 'ID', 'CLOSE', 'OPEN', 'ID', 'SEP', 'ID', 'CLOSE']); + }); + + it('tokenizes {{.}} as OPEN ID CLOSE', function() { + var result = tokenize("{{.}}"); + result.should.match_tokens(['OPEN', 'ID', 'CLOSE']); + }); + + it('tokenizes a path as "OPEN (ID SEP)* ID CLOSE"', function() { + var result = tokenize("{{../foo/bar}}"); + result.should.match_tokens(['OPEN', 'ID', 'SEP', 'ID', 'SEP', 'ID', 'CLOSE']); + result[1].should.be_token("ID", ".."); + }); + + it('tokenizes a path with .. as a parent path', function() { + var result = tokenize("{{../foo.bar}}"); + result.should.match_tokens(['OPEN', 'ID', 'SEP', 'ID', 'SEP', 'ID', 'CLOSE']); + result[1].should.be_token("ID", ".."); + }); + + it('tokenizes a path with this/foo as OPEN ID SEP ID CLOSE', function() { + var result = tokenize("{{this/foo}}"); + result.should.match_tokens(['OPEN', 'ID', 'SEP', 'ID', 'CLOSE']); + result[1].should.be_token("ID", "this"); + result[3].should.be_token("ID", "foo"); + }); + + it('tokenizes a simple mustache with spaces as "OPEN ID CLOSE"', function() { + var result = tokenize("{{ foo }}"); + result.should.match_tokens(['OPEN', 'ID', 'CLOSE']); + result[1].should.be_token("ID", "foo"); + }); + + it('tokenizes a simple mustache with line breaks as "OPEN ID ID CLOSE"', function() { + var result = tokenize("{{ foo \n bar }}"); + result.should.match_tokens(['OPEN', 'ID', 'ID', 'CLOSE']); + result[1].should.be_token("ID", "foo"); + }); + + it('tokenizes raw content as "CONTENT"', function() { + var result = tokenize("foo {{ bar }} baz"); + result.should.match_tokens(['CONTENT', 'OPEN', 'ID', 'CLOSE', 'CONTENT']); + result[0].should.be_token("CONTENT", "foo "); + result[4].should.be_token("CONTENT", " baz"); + }); + + it('tokenizes a partial as "OPEN_PARTIAL ID CLOSE"', function() { + var result = tokenize("{{> foo}}"); + result.should.match_tokens(['OPEN_PARTIAL', 'ID', 'CLOSE']); + }); + + it('tokenizes a partial with context as "OPEN_PARTIAL ID ID CLOSE"', function() { + var result = tokenize("{{> foo bar }}"); + result.should.match_tokens(['OPEN_PARTIAL', 'ID', 'ID', 'CLOSE']); + }); + + it('tokenizes a partial without spaces as "OPEN_PARTIAL ID CLOSE"', function() { + var result = tokenize("{{>foo}}"); + result.should.match_tokens(['OPEN_PARTIAL', 'ID', 'CLOSE']); + }); + + it('tokenizes a partial space at the }); as "OPEN_PARTIAL ID CLOSE"', function() { + var result = tokenize("{{>foo }}"); + result.should.match_tokens(['OPEN_PARTIAL', 'ID', 'CLOSE']); + }); + + it('tokenizes a partial space at the }); as "OPEN_PARTIAL ID CLOSE"', function() { + var result = tokenize("{{>foo/bar.baz }}"); + result.should.match_tokens(['OPEN_PARTIAL', 'ID', 'SEP', 'ID', 'SEP', 'ID', 'CLOSE']); + }); + + it('tokenizes a comment as "COMMENT"', function() { + var result = tokenize("foo {{! this is a comment }} bar {{ baz }}"); + result.should.match_tokens(['CONTENT', 'COMMENT', 'CONTENT', 'OPEN', 'ID', 'CLOSE']); + result[1].should.be_token("COMMENT", " this is a comment "); + }); + + it('tokenizes a block comment as "COMMENT"', function() { + var result = tokenize("foo {{!-- this is a {{comment}} --}} bar {{ baz }}"); + result.should.match_tokens(['CONTENT', 'COMMENT', 'CONTENT', 'OPEN', 'ID', 'CLOSE']); + result[1].should.be_token("COMMENT", " this is a {{comment}} "); + }); + + it('tokenizes a block comment with whitespace as "COMMENT"', function() { + var result = tokenize("foo {{!-- this is a\n{{comment}}\n--}} bar {{ baz }}"); + result.should.match_tokens(['CONTENT', 'COMMENT', 'CONTENT', 'OPEN', 'ID', 'CLOSE']); + result[1].should.be_token("COMMENT", " this is a\n{{comment}}\n"); + }); + + it('tokenizes open and closing blocks as OPEN_BLOCK, ID, CLOSE ..., OPEN_ENDBLOCK ID CLOSE', function() { + var result = tokenize("{{#foo}}content{{/foo}}"); + result.should.match_tokens(['OPEN_BLOCK', 'ID', 'CLOSE', 'CONTENT', 'OPEN_ENDBLOCK', 'ID', 'CLOSE']); + }); + + it('tokenizes inverse sections as "OPEN_INVERSE CLOSE"', function() { + tokenize("{{^}}").should.match_tokens(['OPEN_INVERSE', 'CLOSE']); + tokenize("{{else}}").should.match_tokens(['OPEN_INVERSE', 'CLOSE']); + tokenize("{{ else }}").should.match_tokens(['OPEN_INVERSE', 'CLOSE']); + }); + + it('tokenizes inverse sections with ID as "OPEN_INVERSE ID CLOSE"', function() { + var result = tokenize("{{^foo}}"); + result.should.match_tokens(['OPEN_INVERSE', 'ID', 'CLOSE']); + result[1].should.be_token("ID", "foo"); + }); + + it('tokenizes inverse sections with ID and spaces as "OPEN_INVERSE ID CLOSE"', function() { + var result = tokenize("{{^ foo }}"); + result.should.match_tokens(['OPEN_INVERSE', 'ID', 'CLOSE']); + result[1].should.be_token("ID", "foo"); + }); + + it('tokenizes mustaches with params as "OPEN ID ID ID CLOSE"', function() { + var result = tokenize("{{ foo bar baz }}"); + result.should.match_tokens(['OPEN', 'ID', 'ID', 'ID', 'CLOSE']); + result[1].should.be_token("ID", "foo"); + result[2].should.be_token("ID", "bar"); + result[3].should.be_token("ID", "baz"); + }); + + it('tokenizes mustaches with String params as "OPEN ID ID STRING CLOSE"', function() { + var result = tokenize("{{ foo bar \"baz\" }}"); + result.should.match_tokens(['OPEN', 'ID', 'ID', 'STRING', 'CLOSE']); + result[3].should.be_token("STRING", "baz"); + }); + + it('tokenizes mustaches with String params using single quotes as "OPEN ID ID STRING CLOSE"', function() { + var result = tokenize("{{ foo bar \'baz\' }}"); + result.should.match_tokens(['OPEN', 'ID', 'ID', 'STRING', 'CLOSE']); + result[3].should.be_token("STRING", "baz"); + }); + + it('tokenizes String params with spaces inside as "STRING"', function() { + var result = tokenize("{{ foo bar \"baz bat\" }}"); + result.should.match_tokens(['OPEN', 'ID', 'ID', 'STRING', 'CLOSE']); + result[3].should.be_token("STRING", "baz bat"); + }); + + it('tokenizes String params with escapes quotes as STRING', function() { + var result = tokenize('{{ foo "bar\\"baz" }}'); + result.should.match_tokens(['OPEN', 'ID', 'STRING', 'CLOSE']); + result[2].should.be_token("STRING", 'bar"baz'); + }); + + it('tokenizes String params using single quotes with escapes quotes as STRING', function() { + var result = tokenize("{{ foo 'bar\\'baz' }}"); + result.should.match_tokens(['OPEN', 'ID', 'STRING', 'CLOSE']); + result[2].should.be_token("STRING", "bar'baz"); + }); + + it('tokenizes numbers', function() { + var result = tokenize('{{ foo 1 }}'); + result.should.match_tokens(['OPEN', 'ID', 'INTEGER', 'CLOSE']); + result[2].should.be_token("INTEGER", "1"); + + result = tokenize('{{ foo -1 }}'); + result.should.match_tokens(['OPEN', 'ID', 'INTEGER', 'CLOSE']); + result[2].should.be_token("INTEGER", "-1"); + }); + + it('tokenizes booleans', function() { + var result = tokenize('{{ foo true }}'); + result.should.match_tokens(['OPEN', 'ID', 'BOOLEAN', 'CLOSE']); + result[2].should.be_token("BOOLEAN", "true"); + + result = tokenize('{{ foo false }}'); + result.should.match_tokens(['OPEN', 'ID', 'BOOLEAN', 'CLOSE']); + result[2].should.be_token("BOOLEAN", "false"); + }); + + it('tokenizes hash arguments', function() { + var result = tokenize("{{ foo bar=baz }}"); + result.should.match_tokens(['OPEN', 'ID', 'ID', 'EQUALS', 'ID', 'CLOSE']); + + result = tokenize("{{ foo bar baz=bat }}"); + result.should.match_tokens(['OPEN', 'ID', 'ID', 'ID', 'EQUALS', 'ID', 'CLOSE']); + + result = tokenize("{{ foo bar baz=1 }}"); + result.should.match_tokens(['OPEN', 'ID', 'ID', 'ID', 'EQUALS', 'INTEGER', 'CLOSE']); + + result = tokenize("{{ foo bar baz=true }}"); + result.should.match_tokens(['OPEN', 'ID', 'ID', 'ID', 'EQUALS', 'BOOLEAN', 'CLOSE']); + + result = tokenize("{{ foo bar baz=false }}"); + result.should.match_tokens(['OPEN', 'ID', 'ID', 'ID', 'EQUALS', 'BOOLEAN', 'CLOSE']); + + result = tokenize("{{ foo bar\n baz=bat }}"); + result.should.match_tokens(['OPEN', 'ID', 'ID', 'ID', 'EQUALS', 'ID', 'CLOSE']); + + result = tokenize("{{ foo bar baz=\"bat\" }}"); + result.should.match_tokens(['OPEN', 'ID', 'ID', 'ID', 'EQUALS', 'STRING', 'CLOSE']); + + result = tokenize("{{ foo bar baz=\"bat\" bam=wot }}"); + result.should.match_tokens(['OPEN', 'ID', 'ID', 'ID', 'EQUALS', 'STRING', 'ID', 'EQUALS', 'ID', 'CLOSE']); + + result = tokenize("{{foo omg bar=baz bat=\"bam\"}}"); + result.should.match_tokens(['OPEN', 'ID', 'ID', 'ID', 'EQUALS', 'ID', 'ID', 'EQUALS', 'STRING', 'CLOSE']); + result[2].should.be_token("ID", "omg"); + }); + + it('tokenizes special @ identifiers', function() { + var result = tokenize("{{ @foo }}"); + result.should.match_tokens(['OPEN', 'DATA', 'ID', 'CLOSE']); + result[2].should.be_token("ID", "foo"); + + result = tokenize("{{ foo @bar }}"); + result.should.match_tokens(['OPEN', 'ID', 'DATA', 'ID', 'CLOSE']); + result[3].should.be_token("ID", "bar"); + + result = tokenize("{{ foo bar=@baz }}"); + result.should.match_tokens(['OPEN', 'ID', 'ID', 'EQUALS', 'DATA', 'ID', 'CLOSE']); + result[5].should.be_token("ID", "baz"); + }); + + it('does not time out in a mustache with a single } followed by EOF', function() { + tokenize("{{foo}").should.match_tokens(['OPEN', 'ID']); + }); + + it('does not time out in a mustache when invalid ID characters are used', function() { + tokenize("{{foo & }}").should.match_tokens(['OPEN', 'ID']); + }); +}); diff --git a/spec/tokenizer_spec.rb b/spec/tokenizer_spec.rb deleted file mode 100644 index 0a7c3f9db..000000000 --- a/spec/tokenizer_spec.rb +++ /dev/null @@ -1,322 +0,0 @@ -require "spec_helper" -require "timeout" - -describe "Tokenizer" do - let(:parser) { Handlebars::Spec::PARSER_CONTEXT["handlebars"] } - let(:lexer) { Handlebars::Spec::PARSER_CONTEXT["handlebars"]["lexer"] } - - Token = Struct.new(:name, :text) - - def tokenize(string) - lexer.setInput(string) - out = [] - - 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 - - out - end - - RSpec::Matchers.define :match_tokens do |tokens| - match do |result| - result.map(&:name).should == tokens - end - end - - RSpec::Matchers.define :be_token do |name, string| - match do |token| - token.name.should == name - token.text.should == string - end - end - - it "tokenizes a simple mustache 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 "supports unescaping with &" do - result = tokenize("{{&bar}}") - result.should match_tokens(%w(OPEN ID CLOSE)) - - result[0].should be_token("OPEN", "{{&") - result[1].should be_token("ID", "bar") - end - - it "supports unescaping with {{{" do - result = tokenize("{{{bar}}}") - result.should match_tokens(%w(OPEN_UNESCAPED ID CLOSE_UNESCAPED)) - - result[1].should be_token("ID", "bar") - 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 multiple delimiters" do - result = tokenize("{{foo}} \\{{bar}} \\{{baz}}") - result.should match_tokens(%w(OPEN ID CLOSE CONTENT CONTENT CONTENT)) - - result[3].should be_token("CONTENT", " ") - result[4].should be_token("CONTENT", "{{bar}} ") - result[5].should be_token("CONTENT", "{{baz}}") - 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)) - result[1].should be_token("ID", "this") - result[3].should be_token("ID", "foo") - end - - 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)) - result[0].should be_token("CONTENT", "foo ") - result[4].should be_token("CONTENT", " baz") - end - - it "tokenizes a partial as 'OPEN_PARTIAL ID CLOSE'" do - result = tokenize("{{> foo}}") - result.should match_tokens(%w(OPEN_PARTIAL ID CLOSE)) - end - - it "tokenizes a partial with context as 'OPEN_PARTIAL ID ID CLOSE'" do - result = tokenize("{{> foo bar }}") - result.should match_tokens(%w(OPEN_PARTIAL ID ID CLOSE)) - end - - it "tokenizes a partial without spaces as 'OPEN_PARTIAL ID CLOSE'" do - result = tokenize("{{>foo}}") - result.should match_tokens(%w(OPEN_PARTIAL ID CLOSE)) - end - - it "tokenizes a partial space at the end as 'OPEN_PARTIAL ID CLOSE'" do - result = tokenize("{{>foo }}") - result.should match_tokens(%w(OPEN_PARTIAL ID CLOSE)) - end - - it "tokenizes a partial space at the end as 'OPEN_PARTIAL ID CLOSE'" do - result = tokenize("{{>foo/bar.baz }}") - result.should match_tokens(%w(OPEN_PARTIAL ID SEP ID SEP ID CLOSE)) - end - - it "tokenizes a comment as 'COMMENT'" do - result = tokenize("foo {{! this is a comment }} bar {{ baz }}") - result.should match_tokens(%w(CONTENT COMMENT CONTENT OPEN ID CLOSE)) - result[1].should be_token("COMMENT", " this is a comment ") - end - - it "tokenizes a block comment as 'COMMENT'" do - result = tokenize("foo {{!-- this is a {{comment}} --}} bar {{ baz }}") - result.should match_tokens(%w(CONTENT COMMENT CONTENT OPEN ID CLOSE)) - result[1].should be_token("COMMENT", " this is a {{comment}} ") - end - - it "tokenizes a block comment with whitespace as 'COMMENT'" do - result = tokenize("foo {{!-- this is a\n{{comment}}\n--}} bar {{ baz }}") - result.should match_tokens(%w(CONTENT COMMENT CONTENT OPEN ID CLOSE)) - result[1].should be_token("COMMENT", " this is a\n{{comment}}\n") - end - - it "tokenizes open and closing blocks as 'OPEN_BLOCK ID CLOSE ... OPEN_ENDBLOCK ID CLOSE'" do - result = tokenize("{{#foo}}content{{/foo}}") - result.should match_tokens(%w(OPEN_BLOCK ID CLOSE CONTENT OPEN_ENDBLOCK ID CLOSE)) - end - - it "tokenizes inverse sections as 'OPEN_INVERSE CLOSE'" do - tokenize("{{^}}").should match_tokens(%w(OPEN_INVERSE CLOSE)) - tokenize("{{else}}").should match_tokens(%w(OPEN_INVERSE CLOSE)) - tokenize("{{ else }}").should match_tokens(%w(OPEN_INVERSE CLOSE)) - end - - it "tokenizes inverse sections with ID as 'OPEN_INVERSE ID CLOSE'" do - result = tokenize("{{^foo}}") - result.should match_tokens(%w(OPEN_INVERSE ID CLOSE)) - result[1].should be_token("ID", "foo") - end - - it "tokenizes inverse sections with ID and spaces as 'OPEN_INVERSE ID CLOSE'" do - result = tokenize("{{^ foo }}") - result.should match_tokens(%w(OPEN_INVERSE ID CLOSE)) - result[1].should be_token("ID", "foo") - end - - it "tokenizes mustaches with params as 'OPEN ID ID ID CLOSE'" do - result = tokenize("{{ foo bar baz }}") - result.should match_tokens(%w(OPEN ID ID ID CLOSE)) - result[1].should be_token("ID", "foo") - result[2].should be_token("ID", "bar") - result[3].should be_token("ID", "baz") - end - - it "tokenizes mustaches with String params as 'OPEN ID ID STRING CLOSE'" do - result = tokenize("{{ foo bar \"baz\" }}") - result.should match_tokens(%w(OPEN ID ID STRING CLOSE)) - result[3].should be_token("STRING", "baz") - end - - it "tokenizes mustaches with String params using single quotes as 'OPEN ID ID STRING CLOSE'" do - result = tokenize("{{ foo bar \'baz\' }}") - result.should match_tokens(%w(OPEN ID ID STRING CLOSE)) - result[3].should be_token("STRING", "baz") - end - - it "tokenizes String params with spaces inside as 'STRING'" do - result = tokenize("{{ foo bar \"baz bat\" }}") - result.should match_tokens(%w(OPEN ID ID STRING CLOSE)) - result[3].should be_token("STRING", "baz bat") - end - - it "tokenizes String params with escapes quotes as 'STRING'" do - result = tokenize(%|{{ foo "bar\\"baz" }}|) - result.should match_tokens(%w(OPEN ID STRING CLOSE)) - result[2].should be_token("STRING", %{bar"baz}) - end - - it "tokenizes String params using single quotes with escapes quotes as 'STRING'" do - result = tokenize(%|{{ foo 'bar\\'baz' }}|) - result.should match_tokens(%w(OPEN ID STRING CLOSE)) - 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") - - 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 "tokenizes special @ identifiers" do - result = tokenize("{{ @foo }}") - result.should match_tokens %w( OPEN DATA ID CLOSE ) - result[2].should be_token("ID", "foo") - - result = tokenize("{{ foo @bar }}") - result.should match_tokens %w( OPEN ID DATA ID CLOSE ) - result[3].should be_token("ID", "bar") - - result = tokenize("{{ foo bar=@baz }}") - result.should match_tokens %w( OPEN ID ID EQUALS DATA ID CLOSE ) - result[5].should be_token("ID", "baz") - 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 - - it "does not time out in a mustache when invalid ID characters are used" do - Timeout.timeout(1) { tokenize("{{foo & }}").should match_tokens(%w(OPEN ID)) } - end -end diff --git a/spec/utils.js b/spec/utils.js new file mode 100644 index 000000000..5eee69e06 --- /dev/null +++ b/spec/utils.js @@ -0,0 +1,57 @@ +/*global shouldCompileTo */ + +describe('utils', function() { + describe('#SafeString', function() { + it("constructing a safestring from a string and checking its type", function() { + var safe = new Handlebars.SafeString("testing 1, 2, 3"); + safe.should.be.instanceof(Handlebars.SafeString); + (safe == "testing 1, 2, 3").should.equal(true, "SafeString is equivalent to its underlying string"); + }); + + it("it should not escape SafeString properties", function() { + var name = new Handlebars.SafeString("Sean O'Malley"); + + shouldCompileTo('{{name}}', [{ name: name }], "Sean O'Malley"); + }); + }); + + describe('#escapeExpression', function() { + it('shouhld escape html', function() { + Handlebars.Utils.escapeExpression('foo<&"\'>').should.equal('foo<&"'>'); + }); + it('should not escape SafeString', function() { + var string = new Handlebars.SafeString('foo<&"\'>'); + Handlebars.Utils.escapeExpression(string).should.equal('foo<&"\'>'); + + }); + it('should handle falsy', function() { + Handlebars.Utils.escapeExpression('').should.equal(''); + Handlebars.Utils.escapeExpression(undefined).should.equal(''); + Handlebars.Utils.escapeExpression(null).should.equal(''); + Handlebars.Utils.escapeExpression(false).should.equal(''); + + Handlebars.Utils.escapeExpression(0).should.equal('0'); + }); + it('should handle empty objects', function() { + Handlebars.Utils.escapeExpression({}).should.equal({}.toString()); + Handlebars.Utils.escapeExpression([]).should.equal([].toString()); + }); + }); + + describe('#isEmpty', function() { + it('should not be empty', function() { + Handlebars.Utils.isEmpty(undefined).should.equal(true); + Handlebars.Utils.isEmpty(null).should.equal(true); + Handlebars.Utils.isEmpty(false).should.equal(true); + Handlebars.Utils.isEmpty('').should.equal(true); + Handlebars.Utils.isEmpty([]).should.equal(true); + }); + + it('should be empty', function() { + Handlebars.Utils.isEmpty(0).should.equal(false); + Handlebars.Utils.isEmpty([1]).should.equal(false); + Handlebars.Utils.isEmpty('foo').should.equal(false); + Handlebars.Utils.isEmpty({bar: 1}).should.equal(false); + }); + }); +}); diff --git a/spec/whitespace-control.js b/spec/whitespace-control.js new file mode 100644 index 000000000..2088ed8b7 --- /dev/null +++ b/spec/whitespace-control.js @@ -0,0 +1,62 @@ +describe('whitespace control', function() { + it('should strip whitespace around mustache calls', function() { + var hash = {foo: 'bar<'}; + + shouldCompileTo(' {{~foo~}} ', hash, 'bar<'); + shouldCompileTo(' {{~foo}} ', hash, 'bar< '); + shouldCompileTo(' {{foo~}} ', hash, ' bar<'); + + shouldCompileTo(' {{~&foo~}} ', hash, 'bar<'); + shouldCompileTo(' {{~{foo}~}} ', hash, 'bar<'); + }); + + describe('blocks', function() { + it('should strip whitespace around simple block calls', function() { + var hash = {foo: 'bar<'}; + + shouldCompileTo(' {{~#if foo~}} bar {{~/if~}} ', hash, 'bar'); + shouldCompileTo(' {{#if foo~}} bar {{/if~}} ', hash, ' bar '); + shouldCompileTo(' {{~#if foo}} bar {{~/if}} ', hash, ' bar '); + shouldCompileTo(' {{#if foo}} bar {{/if}} ', hash, ' bar '); + }); + it('should strip whitespace around inverse block calls', function() { + var hash = {}; + + shouldCompileTo(' {{~^if foo~}} bar {{~/if~}} ', hash, 'bar'); + shouldCompileTo(' {{^if foo~}} bar {{/if~}} ', hash, ' bar '); + shouldCompileTo(' {{~^if foo}} bar {{~/if}} ', hash, ' bar '); + shouldCompileTo(' {{^if foo}} bar {{/if}} ', hash, ' bar '); + }); + it('should strip whitespace around complex block calls', function() { + var hash = {foo: 'bar<'}; + + shouldCompileTo('{{#if foo~}} bar {{~^~}} baz {{~/if}}', hash, 'bar'); + shouldCompileTo('{{#if foo~}} bar {{^~}} baz {{/if}}', hash, 'bar '); + shouldCompileTo('{{#if foo}} bar {{~^~}} baz {{~/if}}', hash, ' bar'); + shouldCompileTo('{{#if foo}} bar {{^~}} baz {{/if}}', hash, ' bar '); + + shouldCompileTo('{{#if foo~}} bar {{~else~}} baz {{~/if}}', hash, 'bar'); + + hash = {}; + + shouldCompileTo('{{#if foo~}} bar {{~^~}} baz {{~/if}}', hash, 'baz'); + shouldCompileTo('{{#if foo}} bar {{~^~}} baz {{/if}}', hash, 'baz '); + shouldCompileTo('{{#if foo~}} bar {{~^}} baz {{~/if}}', hash, ' baz'); + shouldCompileTo('{{#if foo~}} bar {{~^}} baz {{/if}}', hash, ' baz '); + + shouldCompileTo('{{#if foo~}} bar {{~else~}} baz {{~/if}}', hash, 'baz'); + }); + }); + + it('should strip whitespace around partials', function() { + shouldCompileToWithPartials('foo {{~> dude~}} ', [{}, {}, {dude: 'bar'}], true, 'foobar'); + shouldCompileToWithPartials('foo {{> dude~}} ', [{}, {}, {dude: 'bar'}], true, 'foo bar'); + shouldCompileToWithPartials('foo {{> dude}} ', [{}, {}, {dude: 'bar'}], true, 'foo bar '); + }); + + it('should only strip whitespace once', function() { + var hash = {foo: 'bar'}; + + shouldCompileTo(' {{~foo~}} {{foo}} {{foo}} ', hash, 'barbar bar '); + }); +}); diff --git a/src/handlebars.l b/src/handlebars.l index aa76eabd3..ddb7fe9ca 100644 --- a/src/handlebars.l +++ b/src/handlebars.l @@ -1,49 +1,19 @@ %x mu emu com -%% +%{ -"\\\\"/("{{") { 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'; - } +function strip(start, end) { + return yytext = yytext.substr(start, yyleng-end); +} -[^\x00]+ { return 'CONTENT'; } +%} -[^\x00]{2,}?/("{{"|<>) { - if(yytext.slice(-1) !== "\\") this.popState(); - if(yytext.slice(-1) === "\\") yytext = yytext.substr(0,yyleng-1); - return 'CONTENT'; - } +LEFT_STRIP "~" +RIGHT_STRIP "~" -[\s\S]*?"--}}" { yytext = yytext.substr(0, yyleng-4); this.popState(); return 'COMMENT'; } - -"{{>" { return 'OPEN_PARTIAL'; } -"{{#" { return 'OPEN_BLOCK'; } -"{{/" { return 'OPEN_ENDBLOCK'; } -"{{^" { return 'OPEN_INVERSE'; } -"{{"\s*"else" { return 'OPEN_INVERSE'; } -"{{{" { return 'OPEN_UNESCAPED'; } -"{{&" { return 'OPEN'; } -"{{!--" { this.popState(); this.begin('com'); } -"{{!"[\s\S]*?"}}" { yytext = yytext.substr(3,yyleng-5); this.popState(); return 'COMMENT'; } -"{{" { return 'OPEN'; } - -"=" { return 'EQUALS'; } -"."/[}\/ ] { return 'ID'; } -".." { return 'ID'; } -[\/.] { return 'SEP'; } -\s+ { /*ignore whitespace*/ } -"}}}" { this.popState(); return 'CLOSE_UNESCAPED'; } -"}}" { this.popState(); return 'CLOSE'; } -'"'("\\"["]|[^"])*'"' { yytext = yytext.substr(1,yyleng-2).replace(/\\"/g,'"'); return 'STRING'; } -"'"("\\"[']|[^'])*"'" { yytext = yytext.substr(1,yyleng-2).replace(/\\'/g,"'"); return 'STRING'; } -"@" { return 'DATA'; } -"true"/[}\s] { return 'BOOLEAN'; } -"false"/[}\s] { return 'BOOLEAN'; } -\-?[0-9]+/[}\s] { return 'INTEGER'; } +LOOKAHEAD [=~}\s\/.] +LITERAL_LOOKAHEAD [~}\s] /* ID is the inverse of control characters. @@ -54,10 +24,61 @@ Control characters ranges: [\[-\^`] [, \, ], ^, `, Exceptions in range: _ [\{-~] {, |, }, ~ */ -[^\s!"#%-,\.\/;->@\[-\^`\{-~]+/[=}\s\/.] { return 'ID'; } +ID [^\s!"#%-,\.\/;->@\[-\^`\{-~]+/{LOOKAHEAD} + +%% + +[^\x00]*?/("{{") { + if(yytext.slice(-2) === "\\\\") { + strip(0,1); + this.begin("mu"); + } else if(yytext.slice(-1) === "\\") { + strip(0,1); + this.begin("emu"); + } else { + this.begin("mu"); + } + if(yytext) return 'CONTENT'; + } + +[^\x00]+ return 'CONTENT'; + +[^\x00]{2,}?/("{{"|<>) { + if(yytext.slice(-1) !== "\\") this.popState(); + if(yytext.slice(-1) === "\\") strip(0,1); + return 'CONTENT'; + } + +[\s\S]*?"--}}" strip(0,4); this.popState(); return 'COMMENT'; + +"{{"{LEFT_STRIP}?">" return 'OPEN_PARTIAL'; +"{{"{LEFT_STRIP}?"#" return 'OPEN_BLOCK'; +"{{"{LEFT_STRIP}?"/" return 'OPEN_ENDBLOCK'; +"{{"{LEFT_STRIP}?"^" return 'OPEN_INVERSE'; +"{{"{LEFT_STRIP}?\s*"else" return 'OPEN_INVERSE'; +"{{"{LEFT_STRIP}?"{" return 'OPEN_UNESCAPED'; +"{{"{LEFT_STRIP}?"&" return 'OPEN'; +"{{!--" this.popState(); this.begin('com'); +"{{!"[\s\S]*?"}}" strip(3,5); this.popState(); return 'COMMENT'; +"{{"{LEFT_STRIP}? return 'OPEN'; + +"=" return 'EQUALS'; +".." return 'ID'; +"."/{LOOKAHEAD} return 'ID'; +[\/.] return 'SEP'; +\s+ /*ignore whitespace*/ +"}"{RIGHT_STRIP}?"}}" this.popState(); return 'CLOSE_UNESCAPED'; +{RIGHT_STRIP}?"}}" this.popState(); return 'CLOSE'; +'"'("\\"["]|[^"])*'"' yytext = strip(1,2).replace(/\\"/g,'"'); return 'STRING'; +"'"("\\"[']|[^'])*"'" yytext = strip(1,2).replace(/\\'/g,"'"); return 'STRING'; +"@" return 'DATA'; +"true"/{LITERAL_LOOKAHEAD} return 'BOOLEAN'; +"false"/{LITERAL_LOOKAHEAD} return 'BOOLEAN'; +\-?[0-9]+/{LITERAL_LOOKAHEAD} return 'INTEGER'; -'['[^\]]*']' { yytext = yytext.substr(1, yyleng-2); return 'ID'; } -. { return 'INVALID'; } +{ID} return 'ID'; -<> { return 'EOF'; } +'['[^\]]*']' yytext = strip(1,2); return 'ID'; +. return 'INVALID'; +<> return 'EOF'; diff --git a/src/handlebars.yy b/src/handlebars.yy index 56b3b7040..0afd2cbbc 100644 --- a/src/handlebars.yy +++ b/src/handlebars.yy @@ -1,118 +1,112 @@ %start root +%ebnf + +%{ + +function stripFlags(open, close) { + return { + left: open[2] === '~', + right: close[0] === '~' || close[1] === '~' + }; +} + +%} + %% root - : program EOF { return $1; } + : statements EOF { return new yy.ProgramNode($1); } ; program - : simpleInverse statements { $$ = new yy.ProgramNode([], $2); } - | statements simpleInverse statements { $$ = new yy.ProgramNode($1, $3); } - | statements simpleInverse { $$ = new yy.ProgramNode($1, []); } - | statements { $$ = new yy.ProgramNode($1); } - | simpleInverse { $$ = new yy.ProgramNode([], []); } - | "" { $$ = new yy.ProgramNode([]); } + : simpleInverse statements -> new yy.ProgramNode([], $1, $2) + | statements simpleInverse statements -> new yy.ProgramNode($1, $2, $3) + | statements simpleInverse -> new yy.ProgramNode($1, $2, []) + | statements -> new yy.ProgramNode($1) + | simpleInverse -> new yy.ProgramNode([]) + | "" -> new yy.ProgramNode([]) ; statements - : statement { $$ = [$1]; } + : statement -> [$1] | statements statement { $1.push($2); $$ = $1; } ; statement - : openInverse program closeBlock { $$ = new yy.BlockNode($1, $2.inverse, $2, $3); } - | openBlock program closeBlock { $$ = new yy.BlockNode($1, $2, $2.inverse, $3); } - | mustache { $$ = $1; } - | partial { $$ = $1; } - | CONTENT { $$ = new yy.ContentNode($1); } - | COMMENT { $$ = new yy.CommentNode($1); } + : openInverse program closeBlock -> new yy.BlockNode($1, $2.inverse, $2, $3) + | openBlock program closeBlock -> new yy.BlockNode($1, $2, $2.inverse, $3) + | mustache -> $1 + | partial -> $1 + | CONTENT -> new yy.ContentNode($1) + | COMMENT -> new yy.CommentNode($1) ; openBlock - : OPEN_BLOCK inMustache CLOSE { $$ = new yy.MustacheNode($2[0], $2[1]); } + : OPEN_BLOCK inMustache CLOSE -> new yy.MustacheNode($2[0], $2[1], $1, stripFlags($1, $3)) ; openInverse - : OPEN_INVERSE inMustache CLOSE { $$ = new yy.MustacheNode($2[0], $2[1]); } + : OPEN_INVERSE inMustache CLOSE -> new yy.MustacheNode($2[0], $2[1], $1, stripFlags($1, $3)) ; closeBlock - : OPEN_ENDBLOCK path CLOSE { $$ = $2; } + : OPEN_ENDBLOCK path CLOSE -> {path: $2, strip: stripFlags($1, $3)} ; mustache - : OPEN inMustache CLOSE { - // Parsing out the '&' escape token at this level saves ~500 bytes after min due to the removal of one parser node. - $$ = new yy.MustacheNode($2[0], $2[1], $1[2] === '&'); - } - | OPEN_UNESCAPED inMustache CLOSE_UNESCAPED { $$ = new yy.MustacheNode($2[0], $2[1], true); } + // Parsing out the '&' escape token at AST level saves ~500 bytes after min due to the removal of one parser node. + // This also allows for handler unification as all mustache node instances can utilize the same handler + : OPEN inMustache CLOSE -> new yy.MustacheNode($2[0], $2[1], $1, stripFlags($1, $3)) + | OPEN_UNESCAPED inMustache CLOSE_UNESCAPED -> new yy.MustacheNode($2[0], $2[1], $1, stripFlags($1, $3)) ; partial - : OPEN_PARTIAL partialName CLOSE { $$ = new yy.PartialNode($2); } - | OPEN_PARTIAL partialName path CLOSE { $$ = new yy.PartialNode($2, $3); } + : OPEN_PARTIAL partialName path? CLOSE -> new yy.PartialNode($2, $3, stripFlags($1, $4)) ; simpleInverse - : OPEN_INVERSE CLOSE { } + : OPEN_INVERSE CLOSE -> stripFlags($1, $2) ; inMustache - : path params hash { $$ = [[$1].concat($2), $3]; } - | path params { $$ = [[$1].concat($2), null]; } - | path hash { $$ = [[$1], $2]; } - | path { $$ = [[$1], null]; } - | dataName { $$ = [[$1], null]; } - ; - -params - : params param { $1.push($2); $$ = $1; } - | param { $$ = [$1]; } + : path param* hash? -> [[$1].concat($2), $3] + | dataName -> [[$1], null] ; param - : path { $$ = $1; } - | STRING { $$ = new yy.StringNode($1); } - | INTEGER { $$ = new yy.IntegerNode($1); } - | BOOLEAN { $$ = new yy.BooleanNode($1); } - | dataName { $$ = $1; } + : path -> $1 + | STRING -> new yy.StringNode($1) + | INTEGER -> new yy.IntegerNode($1) + | BOOLEAN -> new yy.BooleanNode($1) + | dataName -> $1 ; hash - : hashSegments { $$ = new yy.HashNode($1); } - ; - -hashSegments - : hashSegments hashSegment { $1.push($2); $$ = $1; } - | hashSegment { $$ = [$1]; } + : hashSegment+ -> new yy.HashNode($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)]; } - | ID EQUALS dataName { $$ = [$1, $3]; } + : ID EQUALS param -> [$1, $3] ; partialName - : path { $$ = new yy.PartialNameNode($1); } - | STRING { $$ = new yy.PartialNameNode(new yy.StringNode($1)); } - | INTEGER { $$ = new yy.PartialNameNode(new yy.IntegerNode($1)); } + : path -> new yy.PartialNameNode($1) + | STRING -> new yy.PartialNameNode(new yy.StringNode($1)) + | INTEGER -> new yy.PartialNameNode(new yy.IntegerNode($1)) ; dataName - : DATA path { $$ = new yy.DataNode($2); } + : DATA path -> new yy.DataNode($2) ; path - : pathSegments { $$ = new yy.IdNode($1); } + : pathSegments -> new yy.IdNode($1) ; pathSegments : pathSegments SEP ID { $1.push({part: $3, separator: $2}); $$ = $1; } - | ID { $$ = [{part: $1}]; } + | ID -> [{part: $1}] ; diff --git a/src/parser-prefix.js b/src/parser-prefix.js deleted file mode 100644 index 42345d6f6..000000000 --- a/src/parser-prefix.js +++ /dev/null @@ -1 +0,0 @@ -// BEGIN(BROWSER) diff --git a/src/parser-suffix.js b/src/parser-suffix.js index fc8df1399..6e4aa20d6 100644 --- a/src/parser-suffix.js +++ b/src/parser-suffix.js @@ -1,4 +1 @@ - -// END(BROWSER) - -module.exports = handlebars; +export default handlebars; diff --git a/tasks/metrics.js b/tasks/metrics.js new file mode 100644 index 000000000..c4a202b15 --- /dev/null +++ b/tasks/metrics.js @@ -0,0 +1,63 @@ +var _ = require('underscore'), + async = require('async'), + git = require('./util/git'), + Keen = require('keen.io'), + metrics = require('../bench'); + +module.exports = function(grunt) { + grunt.registerTask('metrics', function() { + var done = this.async(), + execName = grunt.option('name'), + events = {}, + + projectId = process.env.KEEN_PROJECTID, + writeKey = process.env.KEEN_WRITEKEY, + keen; + + if (!execName && projectId && writeKey) { + keen = Keen.configure({ + projectId: projectId, + writeKey: writeKey + }); + } + + async.each(_.keys(metrics), function(name, complete) { + if (/^_/.test(name) || (execName && name !== execName)) { + return complete(); + } + + metrics[name](grunt, function(data) { + events[name] = data; + complete(); + }); + }, + function() { + if (!keen) { + return done(); + } + + emit(keen, events, function(err, res) { + if (err) { + throw err; + } + + grunt.log.writeln('Metrics recorded.'); + done(); + }); + }); + }); +}; +function emit(keen, collections, callback) { + git.commitInfo(function(err, info) { + _.each(collections, function(collection) { + _.each(collection, function(event) { + if (info.tagName) { + event.tag = info.tagName; + } + event.sha = info.head; + }); + }); + + keen.addEvents(collections, callback); + }); +} diff --git a/tasks/packager.js b/tasks/packager.js new file mode 100644 index 000000000..f01eef565 --- /dev/null +++ b/tasks/packager.js @@ -0,0 +1,13 @@ +module.exports = function(grunt) { + grunt.registerMultiTask('packager', 'Transpiles scripts written using ES6 to ES5.', function() { + // Execute in here to prevent traceur private var blowup + var Packager = require('es6-module-packager').default, + fs = require('fs'); + + var options = this.options(); + this.files.forEach(function(file) { + var packager = new Packager(file.src[0], {export: options.export}); + fs.writeFileSync(file.dest, packager.toLocals()); + }); + }); +}; diff --git a/tasks/parser.js b/tasks/parser.js new file mode 100644 index 000000000..b1c1c0f23 --- /dev/null +++ b/tasks/parser.js @@ -0,0 +1,23 @@ +var childProcess = require('child_process'); + +module.exports = function(grunt) { + grunt.registerTask('parser', 'Generate jison parser.', function() { + var done = this.async(); + + var child = childProcess.spawn('./node_modules/.bin/jison', ['-m', 'js', 'src/handlebars.yy', 'src/handlebars.l'], {stdio: 'inherit'}); + child.on('exit', function(code) { + if (code != 0) { + grunt.fatal('Jison failure: ' + code); + done(); + return; + } + + var src = ['handlebars.js', 'src/parser-suffix.js'].map(grunt.file.read).join(''); + grunt.file.delete('handlebars.js'); + + grunt.file.write('lib/handlebars/compiler/parser.js', src); + grunt.log.writeln('Parser "lib/handlebars/compiler/parser.js" created.'); + done(); + }); + }); +}; diff --git a/tasks/publish.js b/tasks/publish.js new file mode 100644 index 000000000..68b3157aa --- /dev/null +++ b/tasks/publish.js @@ -0,0 +1,83 @@ +var _ = require('underscore'), + async = require('async'), + AWS = require('aws-sdk'), + git = require('./util/git'), + semver = require('semver'); + +module.exports = function(grunt) { + grunt.registerTask('publish:latest', function() { + var done = this.async(); + + git.debug(function(remotes, branches) { + grunt.log.writeln('remotes: ' + remotes); + grunt.log.writeln('branches: ' + branches); + + git.commitInfo(function(err, info) { + grunt.log.writeln('tag: ' + info.tagName); + + if (info.isMaster) { + initSDK(); + + var files = ['-latest', '-' + info.head]; + if (info.tagName && semver.valid(info.tagName)) { + files.push('-' + info.tagName); + } + + publish(fileMap(files), done); + } else { + // Silently ignore for branches + done(); + } + }); + }); + }); + grunt.registerTask('publish:version', function() { + var done = this.async(); + initSDK(); + + git.commitInfo(function(err, info) { + if (!info.tagName) { + throw new Error('The current commit must be tagged'); + } + publish(fileMap(['-' + info.tagName]), done); + }); + }); + + function initSDK() { + var bucket = process.env.S3_BUCKET_NAME, + key = process.env.S3_ACCESS_KEY_ID, + secret = process.env.S3_SECRET_ACCESS_KEY; + + if (!bucket || !key || !secret) { + throw new Error('Missing S3 config values'); + } + + AWS.config.update({accessKeyId: key, secretAccessKey: secret}); + } + function publish(files, callback) { + var s3 = new AWS.S3(), + bucket = process.env.S3_BUCKET_NAME; + + async.forEach(_.keys(files), function(file, callback) { + var params = {Bucket: bucket, Key: file, Body: grunt.file.read(files[file])}; + s3.putObject(params, function(err, data) { + if (err) { + throw err; + } else { + grunt.log.writeln('Published ' + file + ' to build server.'); + callback(); + } + }); + }, + callback); + } + function fileMap(suffixes) { + var map = {}; + _.each(['handlebars.js', 'handlebars.min.js', 'handlebars.runtime.js', 'handlebars.runtime.min.js'], function(file) { + _.each(suffixes, function(suffix) { + map[file.replace(/\.js$/, suffix + '.js')] = 'dist/' + file; + }); + }); + return map; + } +}; diff --git a/tasks/util/git.js b/tasks/util/git.js new file mode 100644 index 000000000..dc57c91b6 --- /dev/null +++ b/tasks/util/git.js @@ -0,0 +1,100 @@ +var childProcess = require('child_process'); + +module.exports = { + debug: function(callback) { + childProcess.exec('git remote -v', {}, function(err, remotes) { + if (err) { + throw new Error('git.remote: ' + err.message); + } + + childProcess.exec('git branch -a', {}, function(err, branches) { + if (err) { + throw new Error('git.branch: ' + err.message); + } + + callback(remotes, branches); + }); + }); + }, + clean: function(callback) { + childProcess.exec('git diff-index --name-only HEAD --', {}, function(err, stdout) { + callback(undefined, !err && !stdout); + }); + }, + + commitInfo: function(callback) { + module.exports.head(function(err, headSha) { + module.exports.master(function(err, masterSha) { + module.exports.tagName(function(err, tagName) { + callback(undefined, { + head: headSha, + master: masterSha, + tagName: tagName, + isMaster: headSha === masterSha + }); + }); + }); + }); + }, + + head: function(callback) { + childProcess.exec('git rev-parse --short HEAD', {}, function(err, stdout) { + if (err) { + throw new Error('git.head: ' + err.message); + } + + callback(undefined, stdout.trim()); + }); + }, + master: function(callback) { + childProcess.exec('git rev-parse --short origin/master', {}, function(err, stdout) { + // This will error if master was not checked out but in this case we know we are not master + // so we can ignore. + if (err && !/Needed a single revision/.test(err.message)) { + throw new Error('git.master: ' + err.message); + } + + callback(undefined, stdout.trim()); + }); + }, + + add: function(path, callback) { + childProcess.exec('git add -f ' + path, {}, function(err, stdout) { + if (err) { + throw new Error('git.add: ' + err.message); + } + + callback(); + }); + }, + commit: function(name, callback) { + childProcess.exec('git commit --message=' + name, {}, function(err, stdout) { + if (err) { + throw new Error('git.commit: ' + err.message); + } + + callback(); + }); + }, + tag: function(name, callback) { + childProcess.exec('git tag -a --message=' + name + ' ' + name, {}, function(err, stdout, stderr) { + if (err) { + throw new Error('git.tag: ' + err.message); + throw err; + } + + callback(); + }); + }, + tagName: function(callback) { + childProcess.exec('git tag -l --points-at HEAD', {}, function(err, stdout) { + if (err) { + throw new Error('git.tagName: ' + err.message); + } + + var tags = stdout.trim().split(/\n/), + versionTags = tags.filter(function(tag) { return /^v/.test(tag); }); + callback(undefined, versionTags[0] || tags[0]); + }); + } +}; diff --git a/tasks/version.js b/tasks/version.js new file mode 100644 index 000000000..c622a22fd --- /dev/null +++ b/tasks/version.js @@ -0,0 +1,41 @@ +var async = require('async'), + git = require('./util/git'), + semver = require('semver'); + +module.exports = function(grunt) { + grunt.registerTask('version', 'Updates the current release version', function() { + var done = this.async(), + pkg = grunt.config('pkg'), + version = grunt.option('ver'); + + if (!semver.valid(version)) { + throw new Error('Must provide a version number (Ex: --ver=1.0.0):\n\t' + version + '\n\n'); + } + + pkg.version = version; + grunt.config('pkg', pkg); + + grunt.log.writeln('Updating to version ' + version); + + async.each([ + ['lib/handlebars/base.js', /var VERSION = "(.*)";/, 'var VERSION = "' + version + '";'], + ['components/bower.json', /"version":.*/, '"version": "' + version + '",'], + ['components/handlebars.js.nuspec', /.*<\/version>/, '' + version + ''] + ], + function(args, callback) { + replace.apply(undefined, args); + grunt.log.writeln(' - ' + args[0]); + git.add(args[0], callback); + }, + function() { + grunt.task.run(['default']); + done(); + }); + }); + + function replace(path, regex, replace) { + var content = grunt.file.read(path); + content = content.replace(regex, replace); + grunt.file.write(path, content); + } +};