├── spec ├── artifacts │ ├── bom.handlebars │ ├── empty.handlebars │ ├── example_1.handlebars │ └── example_2.hbs ├── expected │ └── empty.amd.js ├── env │ ├── node.js │ ├── browser.js │ ├── runner.js │ ├── runtime.js │ └── common.js ├── .eslintrc ├── require.js ├── source-map.js ├── spec.js ├── umd-runtime.html ├── utils.js ├── index.html ├── javascript-compiler.js ├── compiler.js ├── runtime.js ├── amd-runtime.html ├── umd.html ├── whitespace-control.js ├── amd.html ├── strict.js ├── visitor.js ├── string-params.js ├── regressions.js ├── subexpressions.js └── blocks.js ├── src ├── parser-prefix.js ├── parser-suffix.js ├── handlebars.yy └── handlebars.l ├── .istanbul.yml ├── .gitmodules ├── components ├── bower.json ├── component.json ├── lib │ └── handlebars │ │ └── source.rb ├── handlebars.js.nuspec ├── handlebars-source.gemspec └── composer.json ├── bench ├── templates │ ├── object-mustache.js │ ├── string.js │ ├── array-mustache.js │ ├── data.js │ ├── arguments.js │ ├── index.js │ ├── depth-1.js │ ├── object.js │ ├── complex.dust │ ├── array-each.js │ ├── complex.mustache │ ├── complex.handlebars │ ├── variables.js │ ├── subexpression.js │ ├── complex.eco │ ├── paths.js │ ├── depth-2.js │ ├── partial-recursion.js │ ├── partial.js │ └── complex.js ├── .eslintrc ├── index.js ├── precompile-size.js ├── util │ ├── template-runner.js │ └── benchwarmer.js ├── dist-size.js └── throughput.js ├── lib ├── handlebars │ ├── helpers │ │ ├── lookup.js │ │ ├── helper-missing.js │ │ ├── log.js │ │ ├── with.js │ │ ├── if.js │ │ ├── block-helper-missing.js │ │ └── each.js │ ├── safe-string.js │ ├── no-conflict.js │ ├── helpers.js │ ├── compiler │ │ ├── base.js │ │ ├── ast.js │ │ ├── visitor.js │ │ ├── printer.js │ │ ├── code-gen.js │ │ ├── helpers.js │ │ └── whitespace-control.js │ ├── exception.js │ ├── logger.js │ ├── base.js │ ├── utils.js │ └── runtime.js ├── index.js ├── handlebars.runtime.js └── handlebars.js ├── runtime.js ├── .gitignore ├── tasks ├── .eslintrc ├── parser.js ├── version.js ├── metrics.js ├── test.js ├── publish.js └── util │ └── git.js ├── .npmignore ├── LICENSE ├── .travis.yml ├── package.json ├── CONTRIBUTING.md ├── FAQ.md ├── bin └── handlebars ├── .eslintrc └── Gruntfile.js /spec/artifacts/bom.handlebars: -------------------------------------------------------------------------------- 1 | a -------------------------------------------------------------------------------- /spec/artifacts/empty.handlebars: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /spec/artifacts/example_1.handlebars: -------------------------------------------------------------------------------- 1 | {{foo}} 2 | -------------------------------------------------------------------------------- /spec/artifacts/example_2.hbs: -------------------------------------------------------------------------------- 1 | Hello, {{name}}! 2 | -------------------------------------------------------------------------------- /src/parser-prefix.js: -------------------------------------------------------------------------------- 1 | /* istanbul ignore next */ 2 | -------------------------------------------------------------------------------- /.istanbul.yml: -------------------------------------------------------------------------------- 1 | instrumentation: 2 | excludes: ['**/spec/**'] 3 | -------------------------------------------------------------------------------- /src/parser-suffix.js: -------------------------------------------------------------------------------- 1 | exports.__esModule = true; 2 | exports['default'] = handlebars; 3 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "spec/mustache"] 2 | path = spec/mustache 3 | url = git://github.com/mustache/spec.git 4 | -------------------------------------------------------------------------------- /components/bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "handlebars", 3 | "version": "3.0.3", 4 | "main": "handlebars.js", 5 | "license": "MIT", 6 | "dependencies": {} 7 | } 8 | -------------------------------------------------------------------------------- /bench/templates/object-mustache.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | context: { person: { name: 'Larry', age: 45 } }, 3 | handlebars: '{{#person}}{{name}}{{age}}{{/person}}' 4 | }; 5 | -------------------------------------------------------------------------------- /lib/handlebars/helpers/lookup.js: -------------------------------------------------------------------------------- 1 | export default function(instance) { 2 | instance.registerHelper('lookup', function(obj, field) { 3 | return obj && obj[field]; 4 | }); 5 | } 6 | -------------------------------------------------------------------------------- /bench/templates/string.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | context: {}, 3 | handlebars: 'Hello world', 4 | dust: 'Hello world', 5 | mustache: 'Hello world', 6 | eco: 'Hello world' 7 | }; 8 | -------------------------------------------------------------------------------- /runtime.js: -------------------------------------------------------------------------------- 1 | // Create a simple path alias to allow browserify to resolve 2 | // the runtime on a supported path. 3 | module.exports = require('./dist/cjs/handlebars.runtime')['default']; 4 | -------------------------------------------------------------------------------- /bench/templates/array-mustache.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | context: { names: [{name: 'Moe'}, {name: 'Larry'}, {name: 'Curly'}, {name: 'Shemp'}] }, 3 | handlebars: '{{#names}}{{name}}{{/names}}' 4 | }; 5 | -------------------------------------------------------------------------------- /bench/templates/data.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | context: { names: [{name: 'Moe'}, {name: 'Larry'}, {name: 'Curly'}, {name: 'Shemp'}] }, 3 | handlebars: '{{#each names}}{{@index}}{{name}}{{/each}}' 4 | }; 5 | -------------------------------------------------------------------------------- /components/component.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "handlebars", 3 | "repo": "components/handlebars.js", 4 | "version": "1.0.0", 5 | "main": "handlebars.js", 6 | "scripts": [ 7 | "handlebars.js" 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | vendor 2 | .rvmrc 3 | .DS_Store 4 | lib/handlebars/compiler/parser.js 5 | /dist/ 6 | /tmp/ 7 | /coverage/ 8 | node_modules 9 | *.sublime-project 10 | *.sublime-workspace 11 | npm-debug.log 12 | sauce_connect.log* 13 | -------------------------------------------------------------------------------- /bench/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "globals": { 3 | "require": true 4 | }, 5 | "rules": { 6 | // Disabling for tests, for now. 7 | "no-path-concat": 0, 8 | 9 | "no-var": 0, 10 | "no-shadow": 0, 11 | "handle-callback-err": 0, 12 | "no-console": 0 13 | } 14 | } -------------------------------------------------------------------------------- /bench/templates/arguments.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | helpers: { 3 | foo: function() { 4 | return ''; 5 | } 6 | }, 7 | context: { 8 | bar: true 9 | }, 10 | 11 | handlebars: '{{foo person "person" 1 true foo=bar foo="person" foo=1 foo=true}}' 12 | }; 13 | -------------------------------------------------------------------------------- /lib/handlebars/safe-string.js: -------------------------------------------------------------------------------- 1 | // Build out our basic SafeString type 2 | function SafeString(string) { 3 | this.string = string; 4 | } 5 | 6 | SafeString.prototype.toString = SafeString.prototype.toHTML = function() { 7 | return '' + this.string; 8 | }; 9 | 10 | export default SafeString; 11 | -------------------------------------------------------------------------------- /bench/templates/index.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'); 2 | 3 | var templates = fs.readdirSync(__dirname); 4 | templates.forEach(function(template) { 5 | if (template === 'index.js' || !(/(.*)\.js$/.test(template))) { 6 | return; 7 | } 8 | module.exports[RegExp.$1] = require('./' + RegExp.$1); 9 | }); 10 | -------------------------------------------------------------------------------- /bench/templates/depth-1.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | context: { names: [{name: 'Moe'}, {name: 'Larry'}, {name: 'Curly'}, {name: 'Shemp'}], foo: 'bar' }, 3 | handlebars: '{{#each names}}{{../foo}}{{/each}}', 4 | mustache: '{{#names}}{{foo}}{{/names}}', 5 | eco: '<% for item in @names: %><%= @foo %><% end %>' 6 | }; 7 | -------------------------------------------------------------------------------- /bench/templates/object.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | context: { person: { name: 'Larry', age: 45 } }, 3 | handlebars: '{{#with person}}{{name}}{{age}}{{/with}}', 4 | dust: '{#person}{name}{age}{/person}', 5 | eco: '<%= @person.name %><%= @person.age %>', 6 | mustache: '{{#person}}{{name}}{{age}}{{/person}}' 7 | }; 8 | -------------------------------------------------------------------------------- /components/lib/handlebars/source.rb: -------------------------------------------------------------------------------- 1 | module Handlebars 2 | module Source 3 | def self.bundled_path 4 | File.expand_path("../../../handlebars.js", __FILE__) 5 | end 6 | 7 | def self.runtime_bundled_path 8 | File.expand_path("../../../handlebars.runtime.js", __FILE__) 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /bench/templates/complex.dust: -------------------------------------------------------------------------------- 1 |

{header}

2 | {?items} 3 | 12 | {:else} 13 |

The list is empty.

14 | {/items} 15 | -------------------------------------------------------------------------------- /bench/templates/array-each.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | context: { names: [{name: 'Moe'}, {name: 'Larry'}, {name: 'Curly'}, {name: 'Shemp'}] }, 3 | handlebars: '{{#each names}}{{name}}{{/each}}', 4 | dust: '{#names}{name}{/names}', 5 | mustache: '{{#names}}{{name}}{{/names}}', 6 | eco: '<% for item in @names: %><%= item.name %><% end %>' 7 | }; 8 | -------------------------------------------------------------------------------- /bench/templates/complex.mustache: -------------------------------------------------------------------------------- 1 |

{{header}}

2 | {{#hasItems}} 3 | 13 | {{/hasItems}} 14 | -------------------------------------------------------------------------------- /bench/templates/complex.handlebars: -------------------------------------------------------------------------------- 1 |

{{header}}

2 | {{#if items}} 3 | 12 | {{^}} 13 |

The list is empty.

14 | {{/if}} 15 | -------------------------------------------------------------------------------- /bench/templates/variables.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | context: {name: 'Mick', count: 30}, 3 | handlebars: 'Hello {{name}}! You have {{count}} new messages.', 4 | dust: 'Hello {name}! You have {count} new messages.', 5 | mustache: 'Hello {{name}}! You have {{count}} new messages.', 6 | eco: 'Hello <%= @name %>! You have <%= @count %> new messages.' 7 | }; 8 | 9 | -------------------------------------------------------------------------------- /tasks/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "globals": { 3 | "require": true 4 | }, 5 | "rules": { 6 | // Disabling for tests, for now. 7 | "no-path-concat": 0, 8 | 9 | "no-var": 0, 10 | "no-shadow": 0, 11 | "handle-callback-err": 0, 12 | "no-console": 0, 13 | "no-process-env": 0, 14 | "dot-notation": [2, {"allowKeywords": true}] 15 | } 16 | } -------------------------------------------------------------------------------- /bench/templates/subexpression.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | helpers: { 3 | echo: function(value) { 4 | return 'foo ' + value; 5 | }, 6 | header: function() { 7 | return 'Colors'; 8 | } 9 | }, 10 | handlebars: '{{echo (header)}}', 11 | eco: '<%= @echo(@header()) %>' 12 | }; 13 | 14 | module.exports.context = module.exports.helpers; 15 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .gitignore 3 | .rvmrc 4 | .eslintrc 5 | .travis.yml 6 | .rspec 7 | Gemfile 8 | Gemfile.lock 9 | Rakefile 10 | Gruntfile.js 11 | *.gemspec 12 | *.nuspec 13 | *.log 14 | bench/* 15 | configurations/* 16 | components/* 17 | coverage/* 18 | dist/cdnjs/* 19 | dist/components/* 20 | spec/* 21 | src/* 22 | tasks/* 23 | tmp/* 24 | publish/* 25 | vendor/* 26 | -------------------------------------------------------------------------------- /bench/index.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'); 2 | 3 | var metrics = fs.readdirSync(__dirname); 4 | metrics.forEach(function(metric) { 5 | if (metric === 'index.js' || !(/(.*)\.js$/.test(metric))) { 6 | return; 7 | } 8 | 9 | var name = RegExp.$1; 10 | metric = require('./' + name); 11 | if (metric instanceof Function) { 12 | module.exports[name] = metric; 13 | } 14 | }); 15 | -------------------------------------------------------------------------------- /spec/expected/empty.amd.js: -------------------------------------------------------------------------------- 1 | define(['handlebars.runtime'], function(Handlebars) { 2 | Handlebars = Handlebars["default"]; var template = Handlebars.template, templates = Handlebars.templates = Handlebars.templates || {}; 3 | return templates['empty'] = template({"compiler":[6,">= 2.0.0-beta.1"],"main":function(container,depth0,helpers,partials,data) { 4 | return ""; 5 | },"useData":true}); 6 | }); 7 | -------------------------------------------------------------------------------- /bench/templates/complex.eco: -------------------------------------------------------------------------------- 1 |

<%= @header() %>

2 | <% if @items.length: %> 3 | 12 | <% else: %> 13 |

The list is empty.

14 | <% end %> 15 | -------------------------------------------------------------------------------- /lib/handlebars/no-conflict.js: -------------------------------------------------------------------------------- 1 | /*global window */ 2 | export default function(Handlebars) { 3 | /* istanbul ignore next */ 4 | let root = typeof global !== 'undefined' ? global : window, 5 | $Handlebars = root.Handlebars; 6 | /* istanbul ignore next */ 7 | Handlebars.noConflict = function() { 8 | if (root.Handlebars === Handlebars) { 9 | root.Handlebars = $Handlebars; 10 | } 11 | }; 12 | } 13 | -------------------------------------------------------------------------------- /bench/templates/paths.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | context: { person: { name: {bar: {baz: 'Larry'}}, age: 45 } }, 3 | handlebars: '{{person.name.bar.baz}}{{person.age}}{{person.foo}}{{animal.age}}', 4 | dust: '{person.name.bar.baz}{person.age}{person.foo}{animal.age}', 5 | eco: '<%= @person.name.bar.baz %><%= @person.age %><%= @person.foo %><% if @animal: %><%= @animal.age %><% end %>', 6 | mustache: '{{person.name.bar.baz}}{{person.age}}{{person.foo}}{{animal.age}}' 7 | }; 8 | -------------------------------------------------------------------------------- /bench/templates/depth-2.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | context: { names: [{bat: 'foo', name: ['Moe']}, {bat: 'foo', name: ['Larry']}, {bat: 'foo', name: ['Curly']}, {bat: 'foo', name: ['Shemp']}], foo: 'bar' }, 3 | handlebars: '{{#each names}}{{#each name}}{{../bat}}{{../../foo}}{{/each}}{{/each}}', 4 | mustache: '{{#names}}{{#name}}{{bat}}{{foo}}{{/name}}{{/names}}', 5 | eco: '<% for item in @names: %><% for child in item.name: %><%= item.bat %><%= @foo %><% end %><% end %>' 6 | }; 7 | -------------------------------------------------------------------------------- /bench/templates/partial-recursion.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | context: { name: '1', kids: [{ name: '1.1', kids: [{name: '1.1.1', kids: []}] }] }, 3 | partials: { 4 | mustache: { recursion: '{{name}}{{#kids}}{{>recursion}}{{/kids}}' }, 5 | handlebars: { recursion: '{{name}}{{#each kids}}{{>recursion}}{{/each}}' } 6 | }, 7 | handlebars: '{{name}}{{#each kids}}{{>recursion}}{{/each}}', 8 | dust: '{name}{#kids}{>recursion:./}{/kids}', 9 | mustache: '{{name}}{{#kids}}{{>recursion}}{{/kids}}' 10 | }; 11 | -------------------------------------------------------------------------------- /bench/templates/partial.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | context: { peeps: [{name: 'Moe', count: 15}, {name: 'Larry', count: 5}, {name: 'Curly', count: 1}] }, 3 | partials: { 4 | mustache: { variables: 'Hello {{name}}! You have {{count}} new messages.' }, 5 | handlebars: { variables: 'Hello {{name}}! You have {{count}} new messages.' } 6 | }, 7 | 8 | handlebars: '{{#each peeps}}{{>variables}}{{/each}}', 9 | dust: '{#peeps}{>variables/}{/peeps}', 10 | mustache: '{{#peeps}}{{>variables}}{{/peeps}}' 11 | }; 12 | -------------------------------------------------------------------------------- /lib/handlebars/helpers/helper-missing.js: -------------------------------------------------------------------------------- 1 | import Exception from '../exception'; 2 | 3 | export default function(instance) { 4 | instance.registerHelper('helperMissing', function(/* [args, ]options */) { 5 | if (arguments.length === 1) { 6 | // A missing field in a {{foo}} construct. 7 | return undefined; 8 | } else { 9 | // Someone is actually trying to call something, blow up. 10 | throw new Exception('Missing helper: "' + arguments[arguments.length - 1].name + '"'); 11 | } 12 | }); 13 | } 14 | -------------------------------------------------------------------------------- /lib/handlebars/helpers/log.js: -------------------------------------------------------------------------------- 1 | export default function(instance) { 2 | instance.registerHelper('log', function(/* message, options */) { 3 | let args = [undefined], 4 | options = arguments[arguments.length - 1]; 5 | for (let i = 0; i < arguments.length - 1; i++) { 6 | args.push(arguments[i]); 7 | } 8 | 9 | let level = 1; 10 | if (options.hash.level != null) { 11 | level = options.hash.level; 12 | } else if (options.data && options.data.level != null) { 13 | level = options.data.level; 14 | } 15 | args[0] = level; 16 | 17 | instance.log(... args); 18 | }); 19 | } 20 | -------------------------------------------------------------------------------- /lib/handlebars/helpers.js: -------------------------------------------------------------------------------- 1 | import registerBlockHelperMissing from './helpers/block-helper-missing'; 2 | import registerEach from './helpers/each'; 3 | import registerHelperMissing from './helpers/helper-missing'; 4 | import registerIf from './helpers/if'; 5 | import registerLog from './helpers/log'; 6 | import registerLookup from './helpers/lookup'; 7 | import registerWith from './helpers/with'; 8 | 9 | export function registerDefaultHelpers(instance) { 10 | registerBlockHelperMissing(instance); 11 | registerEach(instance); 12 | registerHelperMissing(instance); 13 | registerIf(instance); 14 | registerLog(instance); 15 | registerLookup(instance); 16 | registerWith(instance); 17 | } 18 | -------------------------------------------------------------------------------- /components/handlebars.js.nuspec: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | handlebars.js 5 | 3.0.3 6 | handlebars.js Authors 7 | https://github.com/wycats/handlebars.js/blob/master/LICENSE 8 | https://github.com/wycats/handlebars.js/ 9 | false 10 | Extension of the Mustache logicless template language 11 | 12 | handlebars mustache template html 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /spec/env/node.js: -------------------------------------------------------------------------------- 1 | require('./common'); 2 | 3 | global.Handlebars = require('../../lib'); 4 | 5 | global.CompilerContext = { 6 | compile: function(template, options) { 7 | var templateSpec = handlebarsEnv.precompile(template, options); 8 | return handlebarsEnv.template(safeEval(templateSpec)); 9 | }, 10 | compileWithPartial: function(template, options) { 11 | return handlebarsEnv.compile(template, options); 12 | } 13 | }; 14 | 15 | function safeEval(templateSpec) { 16 | /*eslint-disable no-eval, no-console */ 17 | try { 18 | return eval('(' + templateSpec + ')'); 19 | } catch (err) { 20 | console.error(templateSpec); 21 | throw err; 22 | } 23 | /*eslint-enable no-eval, no-console */ 24 | } 25 | -------------------------------------------------------------------------------- /bench/templates/complex.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'); 2 | 3 | module.exports = { 4 | context: { 5 | header: function() { 6 | return 'Colors'; 7 | }, 8 | hasItems: true, // To make things fairer in mustache land due to no `{{if}}` construct on arrays 9 | items: [ 10 | {name: 'red', current: true, url: '#Red'}, 11 | {name: 'green', current: false, url: '#Green'}, 12 | {name: 'blue', current: false, url: '#Blue'} 13 | ] 14 | }, 15 | 16 | handlebars: fs.readFileSync(__dirname + '/complex.handlebars').toString(), 17 | dust: fs.readFileSync(__dirname + '/complex.dust').toString(), 18 | eco: fs.readFileSync(__dirname + '/complex.eco').toString(), 19 | mustache: fs.readFileSync(__dirname + '/complex.mustache').toString() 20 | }; 21 | -------------------------------------------------------------------------------- /lib/handlebars/compiler/base.js: -------------------------------------------------------------------------------- 1 | import parser from './parser'; 2 | import WhitespaceControl from './whitespace-control'; 3 | import * as Helpers from './helpers'; 4 | import { extend } from '../utils'; 5 | 6 | export { parser }; 7 | 8 | let yy = {}; 9 | extend(yy, Helpers); 10 | 11 | export function parse(input, options) { 12 | // Just return if an already-compiled AST was passed in. 13 | if (input.type === 'Program') { return input; } 14 | 15 | parser.yy = yy; 16 | 17 | // Altering the shared object here, but this is ok as parser is a sync operation 18 | yy.locInfo = function(locInfo) { 19 | return new yy.SourceLocation(options && options.srcName, locInfo); 20 | }; 21 | 22 | let strip = new WhitespaceControl(options); 23 | return strip.accept(parser.parse(input)); 24 | } 25 | -------------------------------------------------------------------------------- /spec/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "globals": { 3 | "CompilerContext": true, 4 | "Handlebars": true, 5 | "handlebarsEnv": true, 6 | 7 | "shouldCompileTo": true, 8 | "shouldCompileToWithPartials": true, 9 | "shouldThrow": true, 10 | "compileWithPartials": true, 11 | 12 | "console": true, 13 | "require": true, 14 | "suite": true, 15 | "equal": true, 16 | "equals": true, 17 | "test": true, 18 | "testBoth": true, 19 | "raises": true, 20 | "deepEqual": true, 21 | "start": true, 22 | "stop": true, 23 | "ok": true, 24 | "strictEqual": true, 25 | "define": true 26 | }, 27 | "env": { 28 | "mocha": true 29 | }, 30 | "rules": { 31 | // Disabling for tests, for now. 32 | "no-path-concat": 0, 33 | 34 | "no-var": 0 35 | } 36 | } -------------------------------------------------------------------------------- /components/handlebars-source.gemspec: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | require 'json' 3 | 4 | package = JSON.parse(File.read('bower.json')) 5 | 6 | Gem::Specification.new do |gem| 7 | gem.name = "handlebars-source" 8 | gem.authors = ["Yehuda Katz"] 9 | gem.email = ["wycats@gmail.com"] 10 | gem.date = Time.now.strftime("%Y-%m-%d") 11 | gem.description = %q{Handlebars.js source code wrapper for (pre)compilation gems.} 12 | gem.summary = %q{Handlebars.js source code wrapper} 13 | gem.homepage = "https://github.com/wycats/handlebars.js/" 14 | gem.version = package["version"].sub "-", "." 15 | gem.license = "MIT" 16 | 17 | gem.files = [ 18 | 'handlebars.js', 19 | 'handlebars.runtime.js', 20 | 'lib/handlebars/source.rb' 21 | ] 22 | end 23 | -------------------------------------------------------------------------------- /spec/require.js: -------------------------------------------------------------------------------- 1 | if (typeof require !== 'undefined' && require.extensions['.handlebars']) { 2 | describe('Require', function() { 3 | it('Load .handlebars files with require()', function() { 4 | var template = require('./artifacts/example_1'); 5 | equal(template, require('./artifacts/example_1.handlebars')); 6 | 7 | var expected = 'foo\n'; 8 | var result = template({foo: 'foo'}); 9 | 10 | equal(result, expected); 11 | }); 12 | 13 | it('Load .hbs files with require()', function() { 14 | var template = require('./artifacts/example_2'); 15 | equal(template, require('./artifacts/example_2.hbs')); 16 | 17 | var expected = 'Hello, World!\n'; 18 | var result = template({name: 'World'}); 19 | 20 | equal(result, expected); 21 | }); 22 | }); 23 | } 24 | -------------------------------------------------------------------------------- /bench/precompile-size.js: -------------------------------------------------------------------------------- 1 | var _ = require('underscore'), 2 | templates = require('./templates'); 3 | 4 | module.exports = function(grunt, callback) { 5 | // Deferring to here in case we have a build for parser, etc as part of this grunt exec 6 | var Handlebars = require('../lib'); 7 | 8 | var templateSizes = {}; 9 | _.each(templates, function(info, template) { 10 | var src = info.handlebars, 11 | compiled = Handlebars.precompile(src, {}), 12 | knownHelpers = Handlebars.precompile(src, {knownHelpersOnly: true, knownHelpers: info.helpers}); 13 | 14 | templateSizes[template] = compiled.length; 15 | templateSizes['knownOnly_' + template] = knownHelpers.length; 16 | }); 17 | grunt.log.writeln('Precompiled sizes: ' + JSON.stringify(templateSizes, undefined, 2)); 18 | callback([templateSizes]); 19 | }; 20 | -------------------------------------------------------------------------------- /lib/handlebars/helpers/with.js: -------------------------------------------------------------------------------- 1 | import {appendContextPath, blockParams, createFrame, isEmpty, isFunction} from '../utils'; 2 | 3 | export default function(instance) { 4 | instance.registerHelper('with', function(context, options) { 5 | if (isFunction(context)) { context = context.call(this); } 6 | 7 | let fn = options.fn; 8 | 9 | if (!isEmpty(context)) { 10 | let data = options.data; 11 | if (options.data && options.ids) { 12 | data = createFrame(options.data); 13 | data.contextPath = appendContextPath(options.data.contextPath, options.ids[0]); 14 | } 15 | 16 | return fn(context, { 17 | data: data, 18 | blockParams: blockParams([context], [data && data.contextPath]) 19 | }); 20 | } else { 21 | return options.inverse(this); 22 | } 23 | }); 24 | } 25 | -------------------------------------------------------------------------------- /bench/util/template-runner.js: -------------------------------------------------------------------------------- 1 | var _ = require('underscore'), 2 | BenchWarmer = require('./benchwarmer'), 3 | templates = require('../templates'); 4 | 5 | module.exports = function(grunt, makeSuite, callback) { 6 | var warmer = new BenchWarmer(); 7 | 8 | var handlebarsOnly = grunt.option('handlebars-only'), 9 | grep = grunt.option('grep'); 10 | if (grep) { 11 | grep = new RegExp(grep); 12 | } 13 | 14 | _.each(templates, function(template, name) { 15 | if (!template.handlebars || (grep && !grep.test(name))) { 16 | return; 17 | } 18 | 19 | warmer.suite(name, function(bench) { 20 | makeSuite(bench, name, template, handlebarsOnly); 21 | }); 22 | }); 23 | 24 | warmer.bench(function() { 25 | if (callback) { 26 | callback(warmer.times, warmer.scaled); 27 | } 28 | }); 29 | }; 30 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | // USAGE: 2 | // var handlebars = require('handlebars'); 3 | /* eslint-disable no-var */ 4 | 5 | // var local = handlebars.create(); 6 | 7 | var handlebars = require('../dist/cjs/handlebars')['default']; 8 | 9 | var printer = require('../dist/cjs/handlebars/compiler/printer'); 10 | handlebars.PrintVisitor = printer.PrintVisitor; 11 | handlebars.print = printer.print; 12 | 13 | module.exports = handlebars; 14 | 15 | // Publish a Node.js require() handler for .handlebars and .hbs files 16 | function extension(module, filename) { 17 | var fs = require('fs'); 18 | var templateString = fs.readFileSync(filename, 'utf8'); 19 | module.exports = handlebars.compile(templateString); 20 | } 21 | /* istanbul ignore else */ 22 | if (typeof require !== 'undefined' && require.extensions) { 23 | require.extensions['.handlebars'] = extension; 24 | require.extensions['.hbs'] = extension; 25 | } 26 | -------------------------------------------------------------------------------- /spec/env/browser.js: -------------------------------------------------------------------------------- 1 | require('./common'); 2 | 3 | var fs = require('fs'), 4 | vm = require('vm'); 5 | 6 | global.Handlebars = 'no-conflict'; 7 | vm.runInThisContext(fs.readFileSync(__dirname + '/../../dist/handlebars.js'), 'dist/handlebars.js'); 8 | 9 | global.CompilerContext = { 10 | browser: true, 11 | 12 | compile: function(template, options) { 13 | var templateSpec = handlebarsEnv.precompile(template, options); 14 | return handlebarsEnv.template(safeEval(templateSpec)); 15 | }, 16 | compileWithPartial: function(template, options) { 17 | return handlebarsEnv.compile(template, options); 18 | } 19 | }; 20 | 21 | function safeEval(templateSpec) { 22 | /*eslint-disable no-eval, no-console */ 23 | try { 24 | return eval('(' + templateSpec + ')'); 25 | } catch (err) { 26 | console.error(templateSpec); 27 | throw err; 28 | } 29 | /*eslint-enable no-eval, no-console */ 30 | } 31 | -------------------------------------------------------------------------------- /lib/handlebars/helpers/if.js: -------------------------------------------------------------------------------- 1 | import {isEmpty, isFunction} from '../utils'; 2 | 3 | export default function(instance) { 4 | instance.registerHelper('if', function(conditional, options) { 5 | if (isFunction(conditional)) { conditional = conditional.call(this); } 6 | 7 | // Default behavior is to render the positive path if the value is truthy and not empty. 8 | // The `includeZero` option may be set to treat the condtional as purely not empty based on the 9 | // behavior of isEmpty. Effectively this determines if 0 is handled by the positive path or negative. 10 | if ((!options.hash.includeZero && !conditional) || isEmpty(conditional)) { 11 | return options.inverse(this); 12 | } else { 13 | return options.fn(this); 14 | } 15 | }); 16 | 17 | instance.registerHelper('unless', function(conditional, options) { 18 | return instance.helpers['if'].call(this, conditional, {fn: options.inverse, inverse: options.fn, hash: options.hash}); 19 | }); 20 | } 21 | -------------------------------------------------------------------------------- /lib/handlebars/exception.js: -------------------------------------------------------------------------------- 1 | 2 | const errorProps = ['description', 'fileName', 'lineNumber', 'message', 'name', 'number', 'stack']; 3 | 4 | function Exception(message, node) { 5 | let loc = node && node.loc, 6 | line, 7 | column; 8 | if (loc) { 9 | line = loc.start.line; 10 | column = loc.start.column; 11 | 12 | message += ' - ' + line + ':' + column; 13 | } 14 | 15 | let tmp = Error.prototype.constructor.call(this, message); 16 | 17 | // Unfortunately errors are not enumerable in Chrome (at least), so `for prop in tmp` doesn't work. 18 | for (let idx = 0; idx < errorProps.length; idx++) { 19 | this[errorProps[idx]] = tmp[errorProps[idx]]; 20 | } 21 | 22 | /* istanbul ignore else */ 23 | if (Error.captureStackTrace) { 24 | Error.captureStackTrace(this, Exception); 25 | } 26 | 27 | if (loc) { 28 | this.lineNumber = line; 29 | this.column = column; 30 | } 31 | } 32 | 33 | Exception.prototype = new Error(); 34 | 35 | export default Exception; 36 | -------------------------------------------------------------------------------- /tasks/parser.js: -------------------------------------------------------------------------------- 1 | var childProcess = require('child_process'); 2 | 3 | module.exports = function(grunt) { 4 | grunt.registerTask('parser', 'Generate jison parser.', function() { 5 | var done = this.async(); 6 | 7 | var cmd = './node_modules/.bin/jison'; 8 | 9 | if (process.platform === 'win32') { 10 | cmd = 'node_modules\\.bin\\jison.cmd'; 11 | } 12 | 13 | var child = childProcess.spawn(cmd, ['-m', 'js', 'src/handlebars.yy', 'src/handlebars.l'], {stdio: 'inherit'}); 14 | child.on('exit', function(code) { 15 | if (code != 0) { 16 | grunt.fatal('Jison failure: ' + code); 17 | done(); 18 | return; 19 | } 20 | 21 | var src = ['src/parser-prefix.js', 'handlebars.js', 'src/parser-suffix.js'].map(grunt.file.read).join(''); 22 | grunt.file.delete('handlebars.js'); 23 | 24 | grunt.file.write('lib/handlebars/compiler/parser.js', src); 25 | grunt.log.writeln('Parser "lib/handlebars/compiler/parser.js" created.'); 26 | done(); 27 | }); 28 | }); 29 | }; 30 | -------------------------------------------------------------------------------- /lib/handlebars/logger.js: -------------------------------------------------------------------------------- 1 | let logger = { 2 | methodMap: ['debug', 'info', 'warn', 'error'], 3 | level: 'info', 4 | 5 | // Maps a given level value to the `methodMap` indexes above. 6 | lookupLevel: function(level) { 7 | if (typeof level === 'string') { 8 | let levelMap = logger.methodMap.indexOf(level.toLowerCase()); 9 | if (levelMap >= 0) { 10 | level = levelMap; 11 | } else { 12 | level = parseInt(level, 10); 13 | } 14 | } 15 | 16 | return level; 17 | }, 18 | 19 | // Can be overridden in the host environment 20 | log: function(level, ...message) { 21 | level = logger.lookupLevel(level); 22 | 23 | if (typeof console !== 'undefined' && logger.lookupLevel(logger.level) <= level) { 24 | let method = logger.methodMap[level]; 25 | if (!console[method]) { // eslint-disable-line no-console 26 | method = 'log'; 27 | } 28 | console[method](...message); // eslint-disable-line no-console 29 | } 30 | } 31 | }; 32 | 33 | export default logger; 34 | -------------------------------------------------------------------------------- /components/composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "components/handlebars.js", 3 | "description": "Handlebars.js and Mustache are both logicless templating languages that keep the view and the code separated like we all know they should be.", 4 | "homepage": "http://handlebarsjs.com", 5 | "license": "MIT", 6 | "type": "component", 7 | "keywords": [ 8 | "handlebars", 9 | "mustache", 10 | "html" 11 | ], 12 | "authors": [ 13 | { 14 | "name": "Chris Wanstrath", 15 | "homepage": "http://chriswanstrath.com" 16 | } 17 | ], 18 | "require": { 19 | "robloach/component-installer": "*" 20 | }, 21 | "extra": { 22 | "component": { 23 | "name": "handlebars", 24 | "scripts": [ 25 | "handlebars.js" 26 | ], 27 | "files": [ 28 | "handlebars.runtime.js" 29 | ], 30 | "shim": { 31 | "exports": "Handlebars" 32 | } 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /lib/handlebars/helpers/block-helper-missing.js: -------------------------------------------------------------------------------- 1 | import {appendContextPath, createFrame, isArray} from '../utils'; 2 | 3 | export default function(instance) { 4 | instance.registerHelper('blockHelperMissing', function(context, options) { 5 | let inverse = options.inverse, 6 | fn = options.fn; 7 | 8 | if (context === true) { 9 | return fn(this); 10 | } else if (context === false || context == null) { 11 | return inverse(this); 12 | } else if (isArray(context)) { 13 | if (context.length > 0) { 14 | if (options.ids) { 15 | options.ids = [options.name]; 16 | } 17 | 18 | return instance.helpers.each(context, options); 19 | } else { 20 | return inverse(this); 21 | } 22 | } else { 23 | if (options.data && options.ids) { 24 | let data = createFrame(options.data); 25 | data.contextPath = appendContextPath(options.data.contextPath, options.name); 26 | options = {data: data}; 27 | } 28 | 29 | return fn(context, options); 30 | } 31 | }); 32 | } 33 | -------------------------------------------------------------------------------- /lib/handlebars/compiler/ast.js: -------------------------------------------------------------------------------- 1 | let AST = { 2 | // Public API used to evaluate derived attributes regarding AST nodes 3 | helpers: { 4 | // a mustache is definitely a helper if: 5 | // * it is an eligible helper, and 6 | // * it has at least one parameter or hash segment 7 | helperExpression: function(node) { 8 | return (node.type === 'SubExpression') 9 | || ((node.type === 'MustacheStatement' || node.type === 'BlockStatement') 10 | && !!((node.params && node.params.length) || node.hash)); 11 | }, 12 | 13 | scopedId: function(path) { 14 | return (/^\.|this\b/).test(path.original); 15 | }, 16 | 17 | // an ID is simple if it only has one part, and that part is not 18 | // `..` or `this`. 19 | simpleId: function(path) { 20 | return path.parts.length === 1 && !AST.helpers.scopedId(path) && !path.depth; 21 | } 22 | } 23 | }; 24 | 25 | 26 | // Must be exported as an object rather than the root of the module as the jison lexer 27 | // must modify the object to operate properly. 28 | export default AST; 29 | -------------------------------------------------------------------------------- /bench/dist-size.js: -------------------------------------------------------------------------------- 1 | var async = require('async'), 2 | fs = require('fs'), 3 | zlib = require('zlib'); 4 | 5 | module.exports = function(grunt, callback) { 6 | var distFiles = fs.readdirSync('dist'), 7 | distSizes = {}; 8 | 9 | async.each(distFiles, function(file, callback) { 10 | var content; 11 | try { 12 | content = fs.readFileSync('dist/' + file); 13 | } catch (err) { 14 | if (err.code === 'EISDIR') { 15 | callback(); 16 | return; 17 | } else { 18 | throw err; 19 | } 20 | } 21 | 22 | file = file.replace(/\.js/, '').replace(/\./g, '_'); 23 | distSizes[file] = content.length; 24 | 25 | zlib.gzip(content, function(err, data) { 26 | if (err) { 27 | throw err; 28 | } 29 | 30 | distSizes[file + '_gz'] = data.length; 31 | callback(); 32 | }); 33 | }, 34 | function() { 35 | grunt.log.writeln('Distribution sizes: ' + JSON.stringify(distSizes, undefined, 2)); 36 | callback([distSizes]); 37 | }); 38 | }; 39 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (C) 2011-2015 by Yehuda Katz 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /lib/handlebars.runtime.js: -------------------------------------------------------------------------------- 1 | import * as base from './handlebars/base'; 2 | 3 | // Each of these augment the Handlebars object. No need to setup here. 4 | // (This is done to easily share code between commonjs and browse envs) 5 | import SafeString from './handlebars/safe-string'; 6 | import Exception from './handlebars/exception'; 7 | import * as Utils from './handlebars/utils'; 8 | import * as runtime from './handlebars/runtime'; 9 | 10 | import noConflict from './handlebars/no-conflict'; 11 | 12 | // For compatibility and usage outside of module systems, make the Handlebars object a namespace 13 | function create() { 14 | let hb = new base.HandlebarsEnvironment(); 15 | 16 | Utils.extend(hb, base); 17 | hb.SafeString = SafeString; 18 | hb.Exception = Exception; 19 | hb.Utils = Utils; 20 | hb.escapeExpression = Utils.escapeExpression; 21 | 22 | hb.VM = runtime; 23 | hb.template = function(spec) { 24 | return runtime.template(spec, hb); 25 | }; 26 | 27 | return hb; 28 | } 29 | 30 | let inst = create(); 31 | inst.create = create; 32 | 33 | noConflict(inst); 34 | 35 | inst['default'] = inst; 36 | 37 | export default inst; 38 | -------------------------------------------------------------------------------- /lib/handlebars.js: -------------------------------------------------------------------------------- 1 | import runtime from './handlebars.runtime'; 2 | 3 | // Compiler imports 4 | import AST from './handlebars/compiler/ast'; 5 | import { parser as Parser, parse } from './handlebars/compiler/base'; 6 | import { Compiler, compile, precompile } from './handlebars/compiler/compiler'; 7 | import JavaScriptCompiler from './handlebars/compiler/javascript-compiler'; 8 | import Visitor from './handlebars/compiler/visitor'; 9 | 10 | import noConflict from './handlebars/no-conflict'; 11 | 12 | let _create = runtime.create; 13 | function create() { 14 | let hb = _create(); 15 | 16 | hb.compile = function(input, options) { 17 | return compile(input, options, hb); 18 | }; 19 | hb.precompile = function(input, options) { 20 | return precompile(input, options, hb); 21 | }; 22 | 23 | hb.AST = AST; 24 | hb.Compiler = Compiler; 25 | hb.JavaScriptCompiler = JavaScriptCompiler; 26 | hb.Parser = Parser; 27 | hb.parse = parse; 28 | 29 | return hb; 30 | } 31 | 32 | let inst = create(); 33 | inst.create = create; 34 | 35 | noConflict(inst); 36 | 37 | inst.Visitor = Visitor; 38 | 39 | inst['default'] = inst; 40 | 41 | export default inst; 42 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | before_install: 3 | - npm install -g grunt-cli 4 | script: 5 | - grunt --stack travis 6 | email: 7 | on_failure: change 8 | on_success: never 9 | env: 10 | global: 11 | - S3_BUCKET_NAME=builds.handlebarsjs.com 12 | - secure: ckyEe5dzjdFDjmZ6wIrhGm0CFBEnKq8c1dYptfgVV/Q5/nJFGzu8T0yTjouS/ERxzdT2H327/63VCxhFnLCRHrsh4rlW/rCy4XI3O/0TeMLgFPa4TXkO8359qZ4CB44TBb3NsJyQXNMYdJpPLTCVTMpuiqqkFFOr+6OeggR7ufA= 13 | - secure: Nm4AgSfsgNB21kgKrF9Tl7qVZU8YYREhouQunFracTcZZh2NZ2XH5aHuSiXCj88B13Cr/jGbJKsZ4T3QS3wWYtz6lkyVOx3H3iI+TMtqhD9RM3a7A4O+4vVN8IioB2YjhEu0OKjwgX5gp+0uF+pLEi7Hpj6fupD3AbbL5uYcKg8= 14 | matrix: 15 | include: 16 | - node_js: '0.10' 17 | env: 18 | - PUBLISH=true 19 | - secure: pLTzghtVll9yGKJI0AaB0uI8GypfWxLTaIB0ZL8//yN3nAEIKMhf/RRilYTsn/rKj2NUa7vt2edYILi3lttOUlCBOwTc9amiRms1W8Lwr/3IdWPeBLvLuH1zNJRm2lBAwU4LBSqaOwhGaxOQr6KHTnWudhNhgOucxpZfvfI/dFw= 20 | - secure: yERYCf7AwL11D9uMtacly/THGV8BlzsMmrt+iQVvGA3GaY6QMmfYqf6P6cCH98sH5etd1Y+1e6YrPeMjqI6lyRllT7FptoyOdHulazQe86VQN4sc0EpqMlH088kB7gGjTut9Z+X9ViooT5XEh9WA5jXEI9pXhQJNoIHkWPuwGuY= 21 | cache: 22 | directories: 23 | - node_modules 24 | 25 | git: 26 | depth: 100 27 | -------------------------------------------------------------------------------- /spec/env/runner.js: -------------------------------------------------------------------------------- 1 | /*eslint-disable no-console */ 2 | var fs = require('fs'), 3 | Mocha = require('mocha'), 4 | path = require('path'); 5 | 6 | var errors = 0, 7 | testDir = path.dirname(__dirname), 8 | grep = process.argv[2]; 9 | 10 | var files = [ testDir + '/basic.js' ]; 11 | 12 | var files = fs.readdirSync(testDir) 13 | .filter(function(name) { return (/.*\.js$/).test(name); }) 14 | .map(function(name) { return testDir + '/' + name; }); 15 | 16 | run('./runtime', function() { 17 | run('./browser', function() { 18 | run('./node', function() { 19 | /*eslint-disable no-process-exit */ 20 | process.exit(errors); 21 | /*eslint-enable no-process-exit */ 22 | }); 23 | }); 24 | }); 25 | 26 | 27 | function run(env, callback) { 28 | var mocha = new Mocha(); 29 | mocha.ui('bdd'); 30 | mocha.files = files.slice(); 31 | if (grep) { 32 | mocha.grep(grep); 33 | } 34 | 35 | files.forEach(function(name) { 36 | delete require.cache[name]; 37 | }); 38 | 39 | console.log('Running env: ' + env); 40 | require(env); 41 | mocha.run(function(errorCount) { 42 | errors += errorCount; 43 | callback(); 44 | }); 45 | } 46 | -------------------------------------------------------------------------------- /tasks/version.js: -------------------------------------------------------------------------------- 1 | var async = require('async'), 2 | git = require('./util/git'), 3 | semver = require('semver'); 4 | 5 | module.exports = function(grunt) { 6 | grunt.registerTask('version', 'Updates the current release version', function() { 7 | var done = this.async(), 8 | pkg = grunt.config('pkg'), 9 | version = grunt.option('ver'); 10 | 11 | if (!semver.valid(version)) { 12 | throw new Error('Must provide a version number (Ex: --ver=1.0.0):\n\t' + version + '\n\n'); 13 | } 14 | 15 | pkg.version = version; 16 | grunt.config('pkg', pkg); 17 | 18 | grunt.log.writeln('Updating to version ' + version); 19 | 20 | async.each([ 21 | ['lib/handlebars/base.js', (/const VERSION = ['"](.*)['"];/), 'const VERSION = \'' + version + '\';'], 22 | ['components/bower.json', (/"version":.*/), '"version": "' + version + '",'], 23 | ['components/handlebars.js.nuspec', (/.*<\/version>/), '' + version + ''] 24 | ], 25 | function(args, callback) { 26 | replace.apply(undefined, args); 27 | grunt.log.writeln(' - ' + args[0]); 28 | git.add(args[0], callback); 29 | }, 30 | function() { 31 | grunt.task.run(['default']); 32 | done(); 33 | }); 34 | }); 35 | 36 | function replace(path, regex, value) { 37 | var content = grunt.file.read(path); 38 | content = content.replace(regex, value); 39 | grunt.file.write(path, content); 40 | } 41 | }; 42 | -------------------------------------------------------------------------------- /spec/source-map.js: -------------------------------------------------------------------------------- 1 | try { 2 | if (typeof define !== 'function' || !define.amd) { 3 | var SourceMap = require('source-map'), 4 | SourceMapConsumer = SourceMap.SourceMapConsumer; 5 | } 6 | } catch (err) { 7 | /* NOP for in browser */ 8 | } 9 | 10 | describe('source-map', function() { 11 | if (!Handlebars.precompile || !SourceMap) { 12 | return; 13 | } 14 | 15 | it('should safely include source map info', function() { 16 | var template = Handlebars.precompile('{{hello}}', {destName: 'dest.js', srcName: 'src.hbs'}); 17 | 18 | equal(!!template.code, true); 19 | equal(!!template.map, !CompilerContext.browser); 20 | }); 21 | it('should map source properly', function() { 22 | var templateSource = ' b{{hello}} \n {{bar}}a {{#block arg hash=(subex 1 subval)}}{{/block}}', 23 | template = Handlebars.precompile(templateSource, {destName: 'dest.js', srcName: 'src.hbs'}); 24 | 25 | if (template.map) { 26 | var consumer = new SourceMapConsumer(template.map), 27 | lines = template.code.split('\n'), 28 | srcLines = templateSource.split('\n'), 29 | 30 | generated = grepLine('" b"', lines), 31 | source = grepLine(' b', srcLines); 32 | 33 | var mapped = consumer.originalPositionFor(generated); 34 | equal(mapped.line, source.line); 35 | equal(mapped.column, source.column); 36 | } 37 | }); 38 | }); 39 | 40 | function grepLine(token, lines) { 41 | for (var i = 0; i < lines.length; i++) { 42 | var column = lines[i].indexOf(token); 43 | if (column >= 0) { 44 | return { 45 | line: i + 1, 46 | column: column 47 | }; 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /tasks/metrics.js: -------------------------------------------------------------------------------- 1 | var _ = require('underscore'), 2 | async = require('async'), 3 | git = require('./util/git'), 4 | Keen = require('keen.io'), 5 | metrics = require('../bench'); 6 | 7 | module.exports = function(grunt) { 8 | grunt.registerTask('metrics', function() { 9 | var done = this.async(), 10 | execName = grunt.option('name'), 11 | events = {}, 12 | 13 | projectId = process.env.KEEN_PROJECTID, 14 | writeKey = process.env.KEEN_WRITEKEY, 15 | keen; 16 | 17 | if (!execName && projectId && writeKey) { 18 | keen = Keen.configure({ 19 | projectId: projectId, 20 | writeKey: writeKey 21 | }); 22 | } 23 | 24 | async.each(_.keys(metrics), function(name, complete) { 25 | if (/^_/.test(name) || (execName && name !== execName)) { 26 | return complete(); 27 | } 28 | 29 | metrics[name](grunt, function(data) { 30 | events[name] = data; 31 | complete(); 32 | }); 33 | }, 34 | function() { 35 | if (!keen) { 36 | return done(); 37 | } 38 | 39 | emit(keen, events, function(err) { 40 | if (err) { 41 | throw err; 42 | } 43 | 44 | grunt.log.writeln('Metrics recorded.'); 45 | done(); 46 | }); 47 | }); 48 | }); 49 | }; 50 | function emit(keen, collections, callback) { 51 | git.commitInfo(function(err, info) { 52 | _.each(collections, function(collection) { 53 | _.each(collection, function(event) { 54 | if (info.tagName) { 55 | event.tag = info.tagName; 56 | } 57 | event.sha = info.head; 58 | }); 59 | }); 60 | 61 | keen.addEvents(collections, callback); 62 | }); 63 | } 64 | -------------------------------------------------------------------------------- /lib/handlebars/base.js: -------------------------------------------------------------------------------- 1 | import {createFrame, extend, toString} from './utils'; 2 | import Exception from './exception'; 3 | import {registerDefaultHelpers} from './helpers'; 4 | import logger from './logger'; 5 | 6 | export const VERSION = '3.0.1'; 7 | export const COMPILER_REVISION = 6; 8 | 9 | export const REVISION_CHANGES = { 10 | 1: '<= 1.0.rc.2', // 1.0.rc.2 is actually rev2 but doesn't report it 11 | 2: '== 1.0.0-rc.3', 12 | 3: '== 1.0.0-rc.4', 13 | 4: '== 1.x.x', 14 | 5: '== 2.0.0-alpha.x', 15 | 6: '>= 2.0.0-beta.1' 16 | }; 17 | 18 | const objectType = '[object Object]'; 19 | 20 | export function HandlebarsEnvironment(helpers, partials) { 21 | this.helpers = helpers || {}; 22 | this.partials = partials || {}; 23 | 24 | registerDefaultHelpers(this); 25 | } 26 | 27 | HandlebarsEnvironment.prototype = { 28 | constructor: HandlebarsEnvironment, 29 | 30 | logger: logger, 31 | log: logger.log, 32 | 33 | registerHelper: function(name, fn) { 34 | if (toString.call(name) === objectType) { 35 | if (fn) { throw new Exception('Arg not supported with multiple helpers'); } 36 | extend(this.helpers, name); 37 | } else { 38 | this.helpers[name] = fn; 39 | } 40 | }, 41 | unregisterHelper: function(name) { 42 | delete this.helpers[name]; 43 | }, 44 | 45 | registerPartial: function(name, partial) { 46 | if (toString.call(name) === objectType) { 47 | extend(this.partials, name); 48 | } else { 49 | if (typeof partial === 'undefined') { 50 | throw new Exception('Attempting to register a partial as undefined'); 51 | } 52 | this.partials[name] = partial; 53 | } 54 | }, 55 | unregisterPartial: function(name) { 56 | delete this.partials[name]; 57 | } 58 | }; 59 | 60 | export let log = logger.log; 61 | 62 | export {createFrame, logger}; 63 | -------------------------------------------------------------------------------- /spec/spec.js: -------------------------------------------------------------------------------- 1 | describe('spec', function() { 2 | // NOP Under non-node environments 3 | if (typeof process === 'undefined') { 4 | return; 5 | } 6 | 7 | var _ = require('underscore'), 8 | fs = require('fs'); 9 | 10 | var specDir = __dirname + '/mustache/specs/'; 11 | var specs = _.filter(fs.readdirSync(specDir), function(name) { 12 | return (/.*\.json$/).test(name); 13 | }); 14 | 15 | _.each(specs, function(name) { 16 | var spec = require(specDir + name); 17 | _.each(spec.tests, function(test) { 18 | // Our lambda implementation knowingly deviates from the optional Mustace lambda spec 19 | // We also do not support alternative delimeters 20 | if (name === '~lambdas.json' 21 | 22 | // We also choose to throw if paritals are not found 23 | || (name === 'partials.json' && test.name === 'Failed Lookup') 24 | 25 | // We nest the entire response from partials, not just the literals 26 | || (name === 'partials.json' && test.name === 'Standalone Indentation') 27 | 28 | || (/\{\{\=/).test(test.template) 29 | || _.any(test.partials, function(partial) { return (/\{\{\=/).test(partial); })) { 30 | it.skip(name + ' - ' + test.name); 31 | return; 32 | } 33 | 34 | var data = _.clone(test.data); 35 | if (data.lambda) { 36 | // Blergh 37 | /*eslint-disable no-eval */ 38 | data.lambda = eval('(' + data.lambda.js + ')'); 39 | /*eslint-enable no-eval */ 40 | } 41 | it(name + ' - ' + test.name, function() { 42 | if (test.partials) { 43 | shouldCompileToWithPartials(test.template, [data, {}, test.partials, true], true, test.expected, test.desc + ' "' + test.template + '"'); 44 | } else { 45 | shouldCompileTo(test.template, [data, {}, {}, true], test.expected, test.desc + ' "' + test.template + '"'); 46 | } 47 | }); 48 | }); 49 | }); 50 | }); 51 | -------------------------------------------------------------------------------- /tasks/test.js: -------------------------------------------------------------------------------- 1 | var childProcess = require('child_process'), 2 | fs = require('fs'); 3 | 4 | module.exports = function(grunt) { 5 | grunt.registerTask('test:bin', function() { 6 | var done = this.async(); 7 | 8 | childProcess.exec('./bin/handlebars -a spec/artifacts/empty.handlebars', function(err, stdout) { 9 | if (err) { 10 | throw err; 11 | } 12 | 13 | var expected = fs.readFileSync('./spec/expected/empty.amd.js'); 14 | if (stdout.toString() !== expected.toString()) { 15 | throw new Error('Expected binary output differed:\n\n"' + stdout + '"\n\n"' + expected + '"'); 16 | } 17 | 18 | done(); 19 | }); 20 | }); 21 | grunt.registerTask('test:mocha', function() { 22 | var done = this.async(); 23 | 24 | var runner = childProcess.fork('./spec/env/runner', [], {stdio: 'inherit'}); 25 | runner.on('close', function(code) { 26 | if (code != 0) { 27 | grunt.fatal(code + ' tests failed'); 28 | } 29 | done(); 30 | }); 31 | }); 32 | grunt.registerTask('test:cov', function() { 33 | var done = this.async(); 34 | 35 | var runner = childProcess.fork('node_modules/.bin/istanbul', ['cover', '--', './spec/env/runner.js'], {stdio: 'inherit'}); 36 | runner.on('close', function(code) { 37 | if (code != 0) { 38 | grunt.fatal(code + ' tests failed'); 39 | } 40 | done(); 41 | }); 42 | }); 43 | 44 | grunt.registerTask('test:check-cov', function() { 45 | var done = this.async(); 46 | 47 | var runner = childProcess.fork('node_modules/.bin/istanbul', ['check-coverage', '--statements', '100', '--functions', '100', '--branches', '100', '--lines 100'], {stdio: 'inherit'}); 48 | runner.on('close', function(code) { 49 | if (code != 0) { 50 | grunt.fatal('Coverage check failed: ' + code); 51 | } 52 | done(); 53 | }); 54 | }); 55 | grunt.registerTask('test', ['test:bin', 'test:cov', 'test:check-cov']); 56 | }; 57 | -------------------------------------------------------------------------------- /spec/env/runtime.js: -------------------------------------------------------------------------------- 1 | require('./common'); 2 | 3 | var fs = require('fs'), 4 | vm = require('vm'); 5 | 6 | global.Handlebars = 'no-conflict'; 7 | vm.runInThisContext(fs.readFileSync(__dirname + '/../../dist/handlebars.runtime.js'), 'dist/handlebars.runtime.js'); 8 | 9 | var parse = require('../../dist/cjs/handlebars/compiler/base').parse; 10 | var compiler = require('../../dist/cjs/handlebars/compiler/compiler'); 11 | var JavaScriptCompiler = require('../../dist/cjs/handlebars/compiler/javascript-compiler'); 12 | 13 | global.CompilerContext = { 14 | browser: true, 15 | 16 | compile: function(template, options) { 17 | // Hack the compiler on to the environment for these specific tests 18 | handlebarsEnv.precompile = function(precompileTemplate, precompileOptions) { 19 | return compiler.precompile(precompileTemplate, precompileOptions, handlebarsEnv); 20 | }; 21 | handlebarsEnv.parse = parse; 22 | handlebarsEnv.Compiler = compiler.Compiler; 23 | handlebarsEnv.JavaScriptCompiler = JavaScriptCompiler; 24 | 25 | var templateSpec = handlebarsEnv.precompile(template, options); 26 | return handlebarsEnv.template(safeEval(templateSpec)); 27 | }, 28 | compileWithPartial: function(template, options) { 29 | // Hack the compiler on to the environment for these specific tests 30 | handlebarsEnv.compile = function(compileTemplate, compileOptions) { 31 | return compiler.compile(compileTemplate, compileOptions, handlebarsEnv); 32 | }; 33 | handlebarsEnv.parse = parse; 34 | handlebarsEnv.Compiler = compiler.Compiler; 35 | handlebarsEnv.JavaScriptCompiler = JavaScriptCompiler; 36 | 37 | return handlebarsEnv.compile(template, options); 38 | } 39 | }; 40 | 41 | function safeEval(templateSpec) { 42 | /*eslint-disable no-eval, no-console */ 43 | try { 44 | return eval('(' + templateSpec + ')'); 45 | } catch (err) { 46 | console.error(templateSpec); 47 | throw err; 48 | } 49 | /*eslint-enable no-eval, no-console */ 50 | } 51 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "handlebars", 3 | "barename": "handlebars", 4 | "version": "3.0.3", 5 | "description": "Handlebars provides the power necessary to let you build semantic templates effectively with no frustration", 6 | "homepage": "http://www.handlebarsjs.com/", 7 | "keywords": [ 8 | "handlebars", 9 | "mustache", 10 | "template", 11 | "html" 12 | ], 13 | "repository": { 14 | "type": "git", 15 | "url": "https://github.com/wycats/handlebars.js.git" 16 | }, 17 | "author": "Yehuda Katz", 18 | "license": "MIT", 19 | "readmeFilename": "README.md", 20 | "engines": { 21 | "node": ">=0.4.7" 22 | }, 23 | "dependencies": { 24 | "async": "^1.4.0", 25 | "optimist": "^0.6.1", 26 | "source-map": "^0.1.40" 27 | }, 28 | "optionalDependencies": { 29 | "uglify-js": "~2.3" 30 | }, 31 | "devDependencies": { 32 | "async": "^0.9.0", 33 | "aws-sdk": "~1.5.0", 34 | "babel-loader": "^5.0.0", 35 | "babel-runtime": "^5.1.10", 36 | "benchmark": "~1.0", 37 | "dustjs-linkedin": "^2.0.2", 38 | "eco": "~1.1.0-rc-3", 39 | "grunt": "~0.4.1", 40 | "grunt-babel": "^5.0.0", 41 | "grunt-cli": "~0.1.10", 42 | "grunt-contrib-clean": "0.x", 43 | "grunt-contrib-concat": "0.x", 44 | "grunt-contrib-connect": "0.x", 45 | "grunt-contrib-copy": "0.x", 46 | "grunt-contrib-requirejs": "0.x", 47 | "grunt-contrib-uglify": "0.x", 48 | "grunt-contrib-watch": "0.x", 49 | "grunt-eslint": "^11.0.0", 50 | "grunt-saucelabs": "8.x", 51 | "grunt-webpack": "^1.0.8", 52 | "istanbul": "^0.3.0", 53 | "jison": "~0.3.0", 54 | "keen.io": "0.0.3", 55 | "mocha": "~1.20.0", 56 | "mock-stdin": "^0.3.0", 57 | "mustache": "0.x", 58 | "semver": "^4.0.0", 59 | "underscore": "^1.5.1" 60 | }, 61 | "main": "lib/index.js", 62 | "bin": { 63 | "handlebars": "bin/handlebars" 64 | }, 65 | "scripts": { 66 | "test": "grunt" 67 | }, 68 | "jspm": { 69 | "main": "handlebars", 70 | "directories": { 71 | "lib": "dist/amd" 72 | }, 73 | "buildConfig": { 74 | "minify": true 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /lib/handlebars/helpers/each.js: -------------------------------------------------------------------------------- 1 | import {appendContextPath, blockParams, createFrame, isArray, isFunction} from '../utils'; 2 | import Exception from '../exception'; 3 | 4 | export default function(instance) { 5 | instance.registerHelper('each', function(context, options) { 6 | if (!options) { 7 | throw new Exception('Must pass iterator to #each'); 8 | } 9 | 10 | let fn = options.fn, 11 | inverse = options.inverse, 12 | i = 0, 13 | ret = '', 14 | data, 15 | contextPath; 16 | 17 | if (options.data && options.ids) { 18 | contextPath = appendContextPath(options.data.contextPath, options.ids[0]) + '.'; 19 | } 20 | 21 | if (isFunction(context)) { context = context.call(this); } 22 | 23 | if (options.data) { 24 | data = createFrame(options.data); 25 | } 26 | 27 | function execIteration(field, index, last) { 28 | // Don't iterate over undefined values since we can't execute blocks against them 29 | // in non-strict (js) mode. 30 | if (context[field] == null) { 31 | return; 32 | } 33 | 34 | if (data) { 35 | data.key = field; 36 | data.index = index; 37 | data.first = index === 0; 38 | data.last = !!last; 39 | 40 | if (contextPath) { 41 | data.contextPath = contextPath + field; 42 | } 43 | } 44 | 45 | ret = ret + fn(context[field], { 46 | data: data, 47 | blockParams: blockParams([context[field], field], [contextPath + field, null]) 48 | }); 49 | } 50 | 51 | if (context && typeof context === 'object') { 52 | if (isArray(context)) { 53 | for (let j = context.length; i < j; i++) { 54 | execIteration(i, i, i === context.length - 1); 55 | } 56 | } else { 57 | let priorKey; 58 | 59 | for (let key in context) { 60 | if (context.hasOwnProperty(key)) { 61 | // We're running the iterations one step out of sync so we can detect 62 | // the last iteration without have to scan the object twice and create 63 | // an itermediate keys array. 64 | if (priorKey !== undefined) { 65 | execIteration(priorKey, i - 1); 66 | } 67 | priorKey = key; 68 | i++; 69 | } 70 | } 71 | if (priorKey) { 72 | execIteration(priorKey, i - 1, true); 73 | } 74 | } 75 | } 76 | 77 | if (i === 0) { 78 | ret = inverse(this); 79 | } 80 | 81 | return ret; 82 | }); 83 | } 84 | -------------------------------------------------------------------------------- /spec/env/common.js: -------------------------------------------------------------------------------- 1 | var AssertError; 2 | if (Error.captureStackTrace) { 3 | AssertError = function AssertError(message, caller) { 4 | Error.prototype.constructor.call(this, message); 5 | this.message = message; 6 | 7 | if (Error.captureStackTrace) { 8 | Error.captureStackTrace(this, caller || AssertError); 9 | } 10 | }; 11 | 12 | AssertError.prototype = new Error(); 13 | } else { 14 | AssertError = Error; 15 | } 16 | 17 | global.shouldCompileTo = function(string, hashOrArray, expected, message) { 18 | shouldCompileToWithPartials(string, hashOrArray, false, expected, message); 19 | }; 20 | 21 | global.shouldCompileToWithPartials = function shouldCompileToWithPartials(string, hashOrArray, partials, expected, message) { 22 | var result = compileWithPartials(string, hashOrArray, partials); 23 | if (result !== expected) { 24 | throw new AssertError("'" + result + "' should === '" + expected + "': " + message, shouldCompileToWithPartials); 25 | } 26 | }; 27 | 28 | global.compileWithPartials = function(string, hashOrArray, partials) { 29 | var template, 30 | ary, 31 | options; 32 | if (hashOrArray && hashOrArray.hash) { 33 | ary = [hashOrArray.hash, hashOrArray]; 34 | delete hashOrArray.hash; 35 | } else if (Object.prototype.toString.call(hashOrArray) === '[object Array]') { 36 | ary = []; 37 | ary.push(hashOrArray[0]); 38 | ary.push({ helpers: hashOrArray[1], partials: hashOrArray[2] }); 39 | options = typeof hashOrArray[3] === 'object' ? hashOrArray[3] : {compat: hashOrArray[3]}; 40 | if (hashOrArray[4] != null) { 41 | options.data = !!hashOrArray[4]; 42 | ary[1].data = hashOrArray[4]; 43 | } 44 | } else { 45 | ary = [hashOrArray]; 46 | } 47 | 48 | template = CompilerContext[partials ? 'compileWithPartial' : 'compile'](string, options); 49 | return template.apply(this, ary); 50 | }; 51 | 52 | 53 | global.equals = global.equal = function equals(a, b, msg) { 54 | if (a !== b) { 55 | throw new AssertError("'" + a + "' should === '" + b + "'" + (msg ? ': ' + msg : ''), equals); 56 | } 57 | }; 58 | 59 | global.shouldThrow = function(callback, type, msg) { 60 | var failed; 61 | try { 62 | callback(); 63 | failed = true; 64 | } catch (err) { 65 | if (type && !(err instanceof type)) { 66 | throw new AssertError('Type failure: ' + err); 67 | } 68 | if (msg && !(msg.test ? msg.test(err.message) : msg === err.message)) { 69 | equal(msg, err.message); 70 | } 71 | } 72 | if (failed) { 73 | throw new AssertError('It failed to throw', shouldThrow); 74 | } 75 | }; 76 | -------------------------------------------------------------------------------- /tasks/publish.js: -------------------------------------------------------------------------------- 1 | var _ = require('underscore'), 2 | async = require('async'), 3 | AWS = require('aws-sdk'), 4 | git = require('./util/git'), 5 | semver = require('semver'); 6 | 7 | module.exports = function(grunt) { 8 | grunt.registerTask('publish:latest', function() { 9 | var done = this.async(); 10 | 11 | git.debug(function(remotes, branches) { 12 | grunt.log.writeln('remotes: ' + remotes); 13 | grunt.log.writeln('branches: ' + branches); 14 | 15 | git.commitInfo(function(err, info) { 16 | grunt.log.writeln('tag: ' + info.tagName); 17 | 18 | if (info.isMaster) { 19 | initSDK(); 20 | 21 | var files = ['-latest', '-' + info.head]; 22 | if (info.tagName && semver.valid(info.tagName)) { 23 | files.push('-' + info.tagName); 24 | } 25 | 26 | publish(fileMap(files), done); 27 | } else { 28 | // Silently ignore for branches 29 | done(); 30 | } 31 | }); 32 | }); 33 | }); 34 | grunt.registerTask('publish:version', function() { 35 | var done = this.async(); 36 | initSDK(); 37 | 38 | git.commitInfo(function(err, info) { 39 | if (!info.tagName) { 40 | throw new Error('The current commit must be tagged'); 41 | } 42 | publish(fileMap(['-' + info.tagName]), done); 43 | }); 44 | }); 45 | 46 | function initSDK() { 47 | var bucket = process.env.S3_BUCKET_NAME, 48 | key = process.env.S3_ACCESS_KEY_ID, 49 | secret = process.env.S3_SECRET_ACCESS_KEY; 50 | 51 | if (!bucket || !key || !secret) { 52 | throw new Error('Missing S3 config values'); 53 | } 54 | 55 | AWS.config.update({accessKeyId: key, secretAccessKey: secret}); 56 | } 57 | function publish(files, callback) { 58 | var s3 = new AWS.S3(), 59 | bucket = process.env.S3_BUCKET_NAME; 60 | 61 | async.forEach(_.keys(files), function(file, callback) { 62 | var params = {Bucket: bucket, Key: file, Body: grunt.file.read(files[file])}; 63 | s3.putObject(params, function(err) { 64 | if (err) { 65 | throw err; 66 | } else { 67 | grunt.log.writeln('Published ' + file + ' to build server.'); 68 | callback(); 69 | } 70 | }); 71 | }, 72 | callback); 73 | } 74 | function fileMap(suffixes) { 75 | var map = {}; 76 | _.each(['handlebars.js', 'handlebars.min.js', 'handlebars.runtime.js', 'handlebars.runtime.min.js'], function(file) { 77 | _.each(suffixes, function(suffix) { 78 | map[file.replace(/\.js$/, suffix + '.js')] = 'dist/' + file; 79 | }); 80 | }); 81 | return map; 82 | } 83 | }; 84 | -------------------------------------------------------------------------------- /spec/umd-runtime.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Mocha 4 | 5 | 6 | 7 | 8 | 14 | 15 | 21 | 22 | 25 | 26 | 27 | 28 | 29 | 30 | 37 | 85 | 86 | 87 |
88 | 89 | 90 | -------------------------------------------------------------------------------- /spec/utils.js: -------------------------------------------------------------------------------- 1 | describe('utils', function() { 2 | describe('#SafeString', function() { 3 | it('constructing a safestring from a string and checking its type', function() { 4 | var safe = new Handlebars.SafeString('testing 1, 2, 3'); 5 | if (!(safe instanceof Handlebars.SafeString)) { 6 | throw new Error('Must be instance of SafeString'); 7 | } 8 | equals(safe.toString(), 'testing 1, 2, 3', 'SafeString is equivalent to its underlying string'); 9 | }); 10 | 11 | it('it should not escape SafeString properties', function() { 12 | var name = new Handlebars.SafeString('Sean O'Malley'); 13 | 14 | shouldCompileTo('{{name}}', [{name: name}], 'Sean O'Malley'); 15 | }); 16 | }); 17 | 18 | describe('#escapeExpression', function() { 19 | it('shouhld escape html', function() { 20 | equals(Handlebars.Utils.escapeExpression('foo<&"\'>'), 'foo<&"'>'); 21 | }); 22 | it('should not escape SafeString', function() { 23 | var string = new Handlebars.SafeString('foo<&"\'>'); 24 | equals(Handlebars.Utils.escapeExpression(string), 'foo<&"\'>'); 25 | 26 | var obj = { 27 | toHTML: function() { 28 | return 'foo<&"\'>'; 29 | } 30 | }; 31 | equals(Handlebars.Utils.escapeExpression(obj), 'foo<&"\'>'); 32 | }); 33 | it('should handle falsy', function() { 34 | equals(Handlebars.Utils.escapeExpression(''), ''); 35 | equals(Handlebars.Utils.escapeExpression(undefined), ''); 36 | equals(Handlebars.Utils.escapeExpression(null), ''); 37 | 38 | equals(Handlebars.Utils.escapeExpression(false), 'false'); 39 | equals(Handlebars.Utils.escapeExpression(0), '0'); 40 | }); 41 | it('should handle empty objects', function() { 42 | equals(Handlebars.Utils.escapeExpression({}), {}.toString()); 43 | equals(Handlebars.Utils.escapeExpression([]), [].toString()); 44 | }); 45 | }); 46 | 47 | describe('#isEmpty', function() { 48 | it('should not be empty', function() { 49 | equals(Handlebars.Utils.isEmpty(undefined), true); 50 | equals(Handlebars.Utils.isEmpty(null), true); 51 | equals(Handlebars.Utils.isEmpty(false), true); 52 | equals(Handlebars.Utils.isEmpty(''), true); 53 | equals(Handlebars.Utils.isEmpty([]), true); 54 | }); 55 | 56 | it('should be empty', function() { 57 | equals(Handlebars.Utils.isEmpty(0), false); 58 | equals(Handlebars.Utils.isEmpty([1]), false); 59 | equals(Handlebars.Utils.isEmpty('foo'), false); 60 | equals(Handlebars.Utils.isEmpty({bar: 1}), false); 61 | }); 62 | }); 63 | 64 | describe('#extend', function() { 65 | it('should ignore prototype values', function() { 66 | function A() { 67 | this.a = 1; 68 | } 69 | A.prototype.b = 4; 70 | 71 | var b = {b: 2}; 72 | 73 | Handlebars.Utils.extend(b, new A()); 74 | 75 | equals(b.a, 1); 76 | equals(b.b, 2); 77 | }); 78 | }); 79 | }); 80 | -------------------------------------------------------------------------------- /spec/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Mocha 4 | 5 | 6 | 7 | 8 | 14 | 15 | 21 | 22 | 25 | 26 | 27 | 28 | 29 | 51 | 52 | 91 | 92 | 93 |
94 | 95 | 96 | -------------------------------------------------------------------------------- /lib/handlebars/utils.js: -------------------------------------------------------------------------------- 1 | const escape = { 2 | '&': '&', 3 | '<': '<', 4 | '>': '>', 5 | '"': '"', 6 | "'": ''', 7 | '`': '`' 8 | }; 9 | 10 | const badChars = /[&<>"'`]/g, 11 | possible = /[&<>"'`]/; 12 | 13 | function escapeChar(chr) { 14 | return escape[chr]; 15 | } 16 | 17 | export function extend(obj /* , ...source */) { 18 | for (let i = 1; i < arguments.length; i++) { 19 | for (let key in arguments[i]) { 20 | if (Object.prototype.hasOwnProperty.call(arguments[i], key)) { 21 | obj[key] = arguments[i][key]; 22 | } 23 | } 24 | } 25 | 26 | return obj; 27 | } 28 | 29 | export let toString = Object.prototype.toString; 30 | 31 | // Sourced from lodash 32 | // https://github.com/bestiejs/lodash/blob/master/LICENSE.txt 33 | /*eslint-disable func-style */ 34 | let isFunction = function(value) { 35 | return typeof value === 'function'; 36 | }; 37 | // fallback for older versions of Chrome and Safari 38 | /* istanbul ignore next */ 39 | if (isFunction(/x/)) { 40 | isFunction = function(value) { 41 | return typeof value === 'function' && toString.call(value) === '[object Function]'; 42 | }; 43 | } 44 | export {isFunction}; 45 | /*eslint-enable func-style */ 46 | 47 | /* istanbul ignore next */ 48 | export const isArray = Array.isArray || function(value) { 49 | return (value && typeof value === 'object') ? toString.call(value) === '[object Array]' : false; 50 | }; 51 | 52 | // Older IE versions do not directly support indexOf so we must implement our own, sadly. 53 | export function indexOf(array, value) { 54 | for (let i = 0, len = array.length; i < len; i++) { 55 | if (array[i] === value) { 56 | return i; 57 | } 58 | } 59 | return -1; 60 | } 61 | 62 | 63 | export function escapeExpression(string) { 64 | if (typeof string !== 'string') { 65 | // don't escape SafeStrings, since they're already safe 66 | if (string && string.toHTML) { 67 | return string.toHTML(); 68 | } else if (string == null) { 69 | return ''; 70 | } else if (!string) { 71 | return string + ''; 72 | } 73 | 74 | // Force a string conversion as this will be done by the append regardless and 75 | // the regex test will do this transparently behind the scenes, causing issues if 76 | // an object's to string has escaped characters in it. 77 | string = '' + string; 78 | } 79 | 80 | if (!possible.test(string)) { return string; } 81 | return string.replace(badChars, escapeChar); 82 | } 83 | 84 | export function isEmpty(value) { 85 | if (!value && value !== 0) { 86 | return true; 87 | } else if (isArray(value) && value.length === 0) { 88 | return true; 89 | } else { 90 | return false; 91 | } 92 | } 93 | 94 | export function createFrame(object) { 95 | let frame = extend({}, object); 96 | frame._parent = object; 97 | return frame; 98 | } 99 | 100 | export function blockParams(params, ids) { 101 | params.path = ids; 102 | return params; 103 | } 104 | 105 | export function appendContextPath(contextPath, id) { 106 | return (contextPath ? contextPath + '.' : '') + id; 107 | } 108 | -------------------------------------------------------------------------------- /spec/javascript-compiler.js: -------------------------------------------------------------------------------- 1 | describe('javascript-compiler api', function() { 2 | if (!Handlebars.JavaScriptCompiler) { 3 | return; 4 | } 5 | 6 | describe('#nameLookup', function() { 7 | var $superName; 8 | beforeEach(function() { 9 | $superName = handlebarsEnv.JavaScriptCompiler.prototype.nameLookup; 10 | }); 11 | afterEach(function() { 12 | handlebarsEnv.JavaScriptCompiler.prototype.nameLookup = $superName; 13 | }); 14 | 15 | it('should allow override', function() { 16 | handlebarsEnv.JavaScriptCompiler.prototype.nameLookup = function(parent, name) { 17 | return parent + '.bar_' + name; 18 | }; 19 | /*eslint-disable camelcase */ 20 | shouldCompileTo('{{foo}}', { bar_foo: 'food' }, 'food'); 21 | /*eslint-enable camelcase */ 22 | }); 23 | 24 | // Tests nameLookup dot vs. bracket behavior. Bracket is required in certain cases 25 | // to avoid errors in older browsers. 26 | it('should handle reserved words', function() { 27 | shouldCompileTo('{{foo}} {{~null~}}', { foo: 'food' }, 'food'); 28 | }); 29 | }); 30 | describe('#compilerInfo', function() { 31 | var $superCheck, $superInfo; 32 | beforeEach(function() { 33 | $superCheck = handlebarsEnv.VM.checkRevision; 34 | $superInfo = handlebarsEnv.JavaScriptCompiler.prototype.compilerInfo; 35 | }); 36 | afterEach(function() { 37 | handlebarsEnv.VM.checkRevision = $superCheck; 38 | handlebarsEnv.JavaScriptCompiler.prototype.compilerInfo = $superInfo; 39 | }); 40 | it('should allow compilerInfo override', function() { 41 | handlebarsEnv.JavaScriptCompiler.prototype.compilerInfo = function() { 42 | return 'crazy'; 43 | }; 44 | handlebarsEnv.VM.checkRevision = function(compilerInfo) { 45 | if (compilerInfo !== 'crazy') { 46 | throw new Error('It didn\'t work'); 47 | } 48 | }; 49 | shouldCompileTo('{{foo}} ', { foo: 'food' }, 'food '); 50 | }); 51 | }); 52 | describe('buffer', function() { 53 | var $superAppend, $superCreate; 54 | beforeEach(function() { 55 | handlebarsEnv.JavaScriptCompiler.prototype.forceBuffer = true; 56 | $superAppend = handlebarsEnv.JavaScriptCompiler.prototype.appendToBuffer; 57 | $superCreate = handlebarsEnv.JavaScriptCompiler.prototype.initializeBuffer; 58 | }); 59 | afterEach(function() { 60 | handlebarsEnv.JavaScriptCompiler.prototype.forceBuffer = false; 61 | handlebarsEnv.JavaScriptCompiler.prototype.appendToBuffer = $superAppend; 62 | handlebarsEnv.JavaScriptCompiler.prototype.initializeBuffer = $superCreate; 63 | }); 64 | 65 | it('should allow init buffer override', function() { 66 | handlebarsEnv.JavaScriptCompiler.prototype.initializeBuffer = function() { 67 | return this.quotedString('foo_'); 68 | }; 69 | shouldCompileTo('{{foo}} ', { foo: 'food' }, 'foo_food '); 70 | }); 71 | it('should allow append buffer override', function() { 72 | handlebarsEnv.JavaScriptCompiler.prototype.appendToBuffer = function(string) { 73 | return $superAppend.call(this, [string, ' + "_foo"']); 74 | }; 75 | shouldCompileTo('{{foo}}', { foo: 'food' }, 'food_foo'); 76 | }); 77 | }); 78 | }); 79 | -------------------------------------------------------------------------------- /spec/compiler.js: -------------------------------------------------------------------------------- 1 | describe('compiler', function() { 2 | if (!Handlebars.compile) { 3 | return; 4 | } 5 | 6 | describe('#equals', function() { 7 | function compile(string) { 8 | var ast = Handlebars.parse(string); 9 | return new Handlebars.Compiler().compile(ast, {}); 10 | } 11 | 12 | it('should treat as equal', function() { 13 | equal(compile('foo').equals(compile('foo')), true); 14 | equal(compile('{{foo}}').equals(compile('{{foo}}')), true); 15 | equal(compile('{{foo.bar}}').equals(compile('{{foo.bar}}')), true); 16 | equal(compile('{{foo.bar baz "foo" true false bat=1}}').equals(compile('{{foo.bar baz "foo" true false bat=1}}')), true); 17 | equal(compile('{{foo.bar (baz bat=1)}}').equals(compile('{{foo.bar (baz bat=1)}}')), true); 18 | equal(compile('{{#foo}} {{/foo}}').equals(compile('{{#foo}} {{/foo}}')), true); 19 | }); 20 | it('should treat as not equal', function() { 21 | equal(compile('foo').equals(compile('bar')), false); 22 | equal(compile('{{foo}}').equals(compile('{{bar}}')), false); 23 | equal(compile('{{foo.bar}}').equals(compile('{{bar.bar}}')), false); 24 | equal(compile('{{foo.bar baz bat=1}}').equals(compile('{{foo.bar bar bat=1}}')), false); 25 | equal(compile('{{foo.bar (baz bat=1)}}').equals(compile('{{foo.bar (bar bat=1)}}')), false); 26 | equal(compile('{{#foo}} {{/foo}}').equals(compile('{{#bar}} {{/bar}}')), false); 27 | equal(compile('{{#foo}} {{/foo}}').equals(compile('{{#foo}} {{foo}}{{/foo}}')), false); 28 | }); 29 | }); 30 | 31 | describe('#compile', function() { 32 | it('should fail with invalid input', function() { 33 | shouldThrow(function() { 34 | Handlebars.compile(null); 35 | }, Error, 'You must pass a string or Handlebars AST to Handlebars.compile. You passed null'); 36 | shouldThrow(function() { 37 | Handlebars.compile({}); 38 | }, Error, 'You must pass a string or Handlebars AST to Handlebars.compile. You passed [object Object]'); 39 | }); 40 | 41 | it('can utilize AST instance', function() { 42 | equal(Handlebars.compile({ 43 | type: 'Program', 44 | body: [ {type: 'ContentStatement', value: 'Hello'}] 45 | })(), 'Hello'); 46 | }); 47 | 48 | it('can pass through an empty string', function() { 49 | equal(Handlebars.compile('')(), ''); 50 | }); 51 | }); 52 | 53 | describe('#precompile', function() { 54 | it('should fail with invalid input', function() { 55 | shouldThrow(function() { 56 | Handlebars.precompile(null); 57 | }, Error, 'You must pass a string or Handlebars AST to Handlebars.precompile. You passed null'); 58 | shouldThrow(function() { 59 | Handlebars.precompile({}); 60 | }, Error, 'You must pass a string or Handlebars AST to Handlebars.precompile. You passed [object Object]'); 61 | }); 62 | 63 | it('can utilize AST instance', function() { 64 | equal(/return "Hello"/.test(Handlebars.precompile({ 65 | type: 'Program', 66 | body: [ {type: 'ContentStatement', value: 'Hello'}] 67 | })), true); 68 | }); 69 | 70 | it('can pass through an empty string', function() { 71 | equal(/return ""/.test(Handlebars.precompile('')), true); 72 | }); 73 | }); 74 | }); 75 | -------------------------------------------------------------------------------- /spec/runtime.js: -------------------------------------------------------------------------------- 1 | describe('runtime', function() { 2 | describe('#template', function() { 3 | it('should throw on invalid templates', function() { 4 | shouldThrow(function() { 5 | Handlebars.template({}); 6 | }, Error, 'Unknown template object: object'); 7 | shouldThrow(function() { 8 | Handlebars.template(); 9 | }, Error, 'Unknown template object: undefined'); 10 | shouldThrow(function() { 11 | Handlebars.template(''); 12 | }, Error, 'Unknown template object: string'); 13 | }); 14 | it('should throw on version mismatch', function() { 15 | shouldThrow(function() { 16 | Handlebars.template({ 17 | main: true, 18 | compiler: [Handlebars.COMPILER_REVISION + 1] 19 | }); 20 | }, Error, /Template was precompiled with a newer version of Handlebars than the current runtime/); 21 | shouldThrow(function() { 22 | Handlebars.template({ 23 | main: true, 24 | compiler: [Handlebars.COMPILER_REVISION - 1] 25 | }); 26 | }, Error, /Template was precompiled with an older version of Handlebars than the current runtime/); 27 | shouldThrow(function() { 28 | Handlebars.template({ 29 | main: true 30 | }); 31 | }, Error, /Template was precompiled with an older version of Handlebars than the current runtime/); 32 | }); 33 | }); 34 | 35 | describe('#child', function() { 36 | if (!Handlebars.compile) { 37 | return; 38 | } 39 | 40 | it('should throw for depthed methods without depths', function() { 41 | shouldThrow(function() { 42 | var template = Handlebars.compile('{{#foo}}{{../bar}}{{/foo}}'); 43 | // Calling twice to hit the non-compiled case. 44 | template._setup({}); 45 | template._setup({}); 46 | template._child(1); 47 | }, Error, 'must pass parent depths'); 48 | }); 49 | 50 | it('should throw for block param methods without params', function() { 51 | shouldThrow(function() { 52 | var template = Handlebars.compile('{{#foo as |foo|}}{{foo}}{{/foo}}'); 53 | // Calling twice to hit the non-compiled case. 54 | template._setup({}); 55 | template._setup({}); 56 | template._child(1); 57 | }, Error, 'must pass block params'); 58 | }); 59 | it('should expose child template', function() { 60 | var template = Handlebars.compile('{{#foo}}bar{{/foo}}'); 61 | // Calling twice to hit the non-compiled case. 62 | equal(template._child(1)(), 'bar'); 63 | equal(template._child(1)(), 'bar'); 64 | }); 65 | it('should render depthed content', function() { 66 | var template = Handlebars.compile('{{#foo}}{{../bar}}{{/foo}}'); 67 | // Calling twice to hit the non-compiled case. 68 | equal(template._child(1, undefined, [], [{bar: 'baz'}])(), 'baz'); 69 | }); 70 | }); 71 | 72 | describe('#noConflict', function() { 73 | if (!CompilerContext.browser) { 74 | return; 75 | } 76 | 77 | it('should reset on no conflict', function() { 78 | var reset = Handlebars; 79 | Handlebars.noConflict(); 80 | equal(Handlebars, 'no-conflict'); 81 | 82 | Handlebars = 'really, none'; 83 | reset.noConflict(); 84 | equal(Handlebars, 'really, none'); 85 | 86 | Handlebars = reset; 87 | }); 88 | }); 89 | }); 90 | -------------------------------------------------------------------------------- /tasks/util/git.js: -------------------------------------------------------------------------------- 1 | var childProcess = require('child_process'); 2 | 3 | module.exports = { 4 | debug: function(callback) { 5 | childProcess.exec('git remote -v', {}, function(err, remotes) { 6 | if (err) { 7 | throw new Error('git.remote: ' + err.message); 8 | } 9 | 10 | childProcess.exec('git branch -a', {}, function(err, branches) { 11 | if (err) { 12 | throw new Error('git.branch: ' + err.message); 13 | } 14 | 15 | callback(remotes, branches); 16 | }); 17 | }); 18 | }, 19 | clean: function(callback) { 20 | childProcess.exec('git diff-index --name-only HEAD --', {}, function(err, stdout) { 21 | callback(undefined, !err && !stdout); 22 | }); 23 | }, 24 | 25 | commitInfo: function(callback) { 26 | module.exports.head(function(err, headSha) { 27 | module.exports.master(function(err, masterSha) { 28 | module.exports.tagName(function(err, tagName) { 29 | callback(undefined, { 30 | head: headSha, 31 | master: masterSha, 32 | tagName: tagName, 33 | isMaster: headSha === masterSha 34 | }); 35 | }); 36 | }); 37 | }); 38 | }, 39 | 40 | head: function(callback) { 41 | childProcess.exec('git rev-parse --short HEAD', {}, function(err, stdout) { 42 | if (err) { 43 | throw new Error('git.head: ' + err.message); 44 | } 45 | 46 | callback(undefined, stdout.trim()); 47 | }); 48 | }, 49 | master: function(callback) { 50 | childProcess.exec('git rev-parse --short origin/master', {}, function(err, stdout) { 51 | // This will error if master was not checked out but in this case we know we are not master 52 | // so we can ignore. 53 | if (err && !(/Needed a single revision/.test(err.message))) { 54 | throw new Error('git.master: ' + err.message); 55 | } 56 | 57 | callback(undefined, stdout.trim()); 58 | }); 59 | }, 60 | 61 | add: function(path, callback) { 62 | childProcess.exec('git add -f ' + path, {}, function(err) { 63 | if (err) { 64 | throw new Error('git.add: ' + err.message); 65 | } 66 | 67 | callback(); 68 | }); 69 | }, 70 | commit: function(name, callback) { 71 | childProcess.exec('git commit --message=' + name, {}, function(err) { 72 | if (err) { 73 | throw new Error('git.commit: ' + err.message); 74 | } 75 | 76 | callback(); 77 | }); 78 | }, 79 | tag: function(name, callback) { 80 | childProcess.exec('git tag -a --message=' + name + ' ' + name, {}, function(err) { 81 | if (err) { 82 | throw new Error('git.tag: ' + err.message); 83 | } 84 | 85 | callback(); 86 | }); 87 | }, 88 | tagName: function(callback) { 89 | childProcess.exec('git describe --tags', {}, function(err, stdout) { 90 | if (err) { 91 | throw new Error('git.tagName: ' + err.message); 92 | } 93 | 94 | var tags = stdout.trim().split(/\n/); 95 | tags = tags.filter(function(info) { 96 | info = info.split('-'); 97 | return info.length == 1; 98 | }); 99 | 100 | var versionTags = tags.filter(function(info) { 101 | return (/^v/.test(info[0])); 102 | }); 103 | 104 | callback(undefined, versionTags[0] || tags[0]); 105 | }); 106 | } 107 | }; 108 | -------------------------------------------------------------------------------- /lib/handlebars/compiler/visitor.js: -------------------------------------------------------------------------------- 1 | import Exception from '../exception'; 2 | 3 | function Visitor() { 4 | this.parents = []; 5 | } 6 | 7 | Visitor.prototype = { 8 | constructor: Visitor, 9 | mutating: false, 10 | 11 | // Visits a given value. If mutating, will replace the value if necessary. 12 | acceptKey: function(node, name) { 13 | let value = this.accept(node[name]); 14 | if (this.mutating) { 15 | // Hacky sanity check: 16 | if (value && typeof value.type !== 'string') { 17 | throw new Exception('Unexpected node type "' + value.type + '" found when accepting ' + name + ' on ' + node.type); 18 | } 19 | node[name] = value; 20 | } 21 | }, 22 | 23 | // Performs an accept operation with added sanity check to ensure 24 | // required keys are not removed. 25 | acceptRequired: function(node, name) { 26 | this.acceptKey(node, name); 27 | 28 | if (!node[name]) { 29 | throw new Exception(node.type + ' requires ' + name); 30 | } 31 | }, 32 | 33 | // Traverses a given array. If mutating, empty respnses will be removed 34 | // for child elements. 35 | acceptArray: function(array) { 36 | for (let i = 0, l = array.length; i < l; i++) { 37 | this.acceptKey(array, i); 38 | 39 | if (!array[i]) { 40 | array.splice(i, 1); 41 | i--; 42 | l--; 43 | } 44 | } 45 | }, 46 | 47 | accept: function(object) { 48 | if (!object) { 49 | return; 50 | } 51 | 52 | if (this.current) { 53 | this.parents.unshift(this.current); 54 | } 55 | this.current = object; 56 | 57 | let ret = this[object.type](object); 58 | 59 | this.current = this.parents.shift(); 60 | 61 | if (!this.mutating || ret) { 62 | return ret; 63 | } else if (ret !== false) { 64 | return object; 65 | } 66 | }, 67 | 68 | Program: function(program) { 69 | this.acceptArray(program.body); 70 | }, 71 | 72 | MustacheStatement: function(mustache) { 73 | this.acceptRequired(mustache, 'path'); 74 | this.acceptArray(mustache.params); 75 | this.acceptKey(mustache, 'hash'); 76 | }, 77 | 78 | BlockStatement: function(block) { 79 | this.acceptRequired(block, 'path'); 80 | this.acceptArray(block.params); 81 | this.acceptKey(block, 'hash'); 82 | 83 | this.acceptKey(block, 'program'); 84 | this.acceptKey(block, 'inverse'); 85 | }, 86 | 87 | PartialStatement: function(partial) { 88 | this.acceptRequired(partial, 'name'); 89 | this.acceptArray(partial.params); 90 | this.acceptKey(partial, 'hash'); 91 | }, 92 | 93 | ContentStatement: function(/* content */) {}, 94 | CommentStatement: function(/* comment */) {}, 95 | 96 | SubExpression: function(sexpr) { 97 | this.acceptRequired(sexpr, 'path'); 98 | this.acceptArray(sexpr.params); 99 | this.acceptKey(sexpr, 'hash'); 100 | }, 101 | 102 | PathExpression: function(/* path */) {}, 103 | 104 | StringLiteral: function(/* string */) {}, 105 | NumberLiteral: function(/* number */) {}, 106 | BooleanLiteral: function(/* bool */) {}, 107 | UndefinedLiteral: function(/* literal */) {}, 108 | NullLiteral: function(/* literal */) {}, 109 | 110 | Hash: function(hash) { 111 | this.acceptArray(hash.pairs); 112 | }, 113 | HashPair: function(pair) { 114 | this.acceptRequired(pair, 'value'); 115 | } 116 | }; 117 | 118 | export default Visitor; 119 | -------------------------------------------------------------------------------- /spec/amd-runtime.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Mocha 4 | 5 | 6 | 7 | 8 | 14 | 15 | 21 | 22 | 25 | 26 | 27 | 28 | 29 | 30 | 42 | 101 | 102 | 103 |
104 | 105 | 106 | -------------------------------------------------------------------------------- /spec/umd.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Mocha 4 | 5 | 6 | 7 | 8 | 14 | 15 | 21 | 22 | 25 | 26 | 27 | 28 | 29 | 30 | 59 | 105 | 106 | 107 |
108 | 109 | 110 | -------------------------------------------------------------------------------- /spec/whitespace-control.js: -------------------------------------------------------------------------------- 1 | describe('whitespace control', function() { 2 | it('should strip whitespace around mustache calls', function() { 3 | var hash = {foo: 'bar<'}; 4 | 5 | shouldCompileTo(' {{~foo~}} ', hash, 'bar<'); 6 | shouldCompileTo(' {{~foo}} ', hash, 'bar< '); 7 | shouldCompileTo(' {{foo~}} ', hash, ' bar<'); 8 | 9 | shouldCompileTo(' {{~&foo~}} ', hash, 'bar<'); 10 | shouldCompileTo(' {{~{foo}~}} ', hash, 'bar<'); 11 | 12 | shouldCompileTo('1\n{{foo~}} \n\n 23\n{{bar}}4', {}, '1\n23\n4'); 13 | }); 14 | 15 | describe('blocks', function() { 16 | it('should strip whitespace around simple block calls', function() { 17 | var hash = {foo: 'bar<'}; 18 | 19 | shouldCompileTo(' {{~#if foo~}} bar {{~/if~}} ', hash, 'bar'); 20 | shouldCompileTo(' {{#if foo~}} bar {{/if~}} ', hash, ' bar '); 21 | shouldCompileTo(' {{~#if foo}} bar {{~/if}} ', hash, ' bar '); 22 | shouldCompileTo(' {{#if foo}} bar {{/if}} ', hash, ' bar '); 23 | 24 | shouldCompileTo(' \n\n{{~#if foo~}} \n\nbar \n\n{{~/if~}}\n\n ', hash, 'bar'); 25 | shouldCompileTo(' a\n\n{{~#if foo~}} \n\nbar \n\n{{~/if~}}\n\na ', hash, ' abara '); 26 | }); 27 | it('should strip whitespace around inverse block calls', function() { 28 | var hash = {}; 29 | 30 | shouldCompileTo(' {{~^if foo~}} bar {{~/if~}} ', hash, 'bar'); 31 | shouldCompileTo(' {{^if foo~}} bar {{/if~}} ', hash, ' bar '); 32 | shouldCompileTo(' {{~^if foo}} bar {{~/if}} ', hash, ' bar '); 33 | shouldCompileTo(' {{^if foo}} bar {{/if}} ', hash, ' bar '); 34 | 35 | shouldCompileTo(' \n\n{{~^if foo~}} \n\nbar \n\n{{~/if~}}\n\n ', hash, 'bar'); 36 | }); 37 | it('should strip whitespace around complex block calls', function() { 38 | var hash = {foo: 'bar<'}; 39 | 40 | shouldCompileTo('{{#if foo~}} bar {{~^~}} baz {{~/if}}', hash, 'bar'); 41 | shouldCompileTo('{{#if foo~}} bar {{^~}} baz {{/if}}', hash, 'bar '); 42 | shouldCompileTo('{{#if foo}} bar {{~^~}} baz {{~/if}}', hash, ' bar'); 43 | shouldCompileTo('{{#if foo}} bar {{^~}} baz {{/if}}', hash, ' bar '); 44 | 45 | shouldCompileTo('{{#if foo~}} bar {{~else~}} baz {{~/if}}', hash, 'bar'); 46 | 47 | shouldCompileTo('\n\n{{~#if foo~}} \n\nbar \n\n{{~^~}} \n\nbaz \n\n{{~/if~}}\n\n', hash, 'bar'); 48 | shouldCompileTo('\n\n{{~#if foo~}} \n\n{{{foo}}} \n\n{{~^~}} \n\nbaz \n\n{{~/if~}}\n\n', hash, 'bar<'); 49 | 50 | hash = {}; 51 | 52 | shouldCompileTo('{{#if foo~}} bar {{~^~}} baz {{~/if}}', hash, 'baz'); 53 | shouldCompileTo('{{#if foo}} bar {{~^~}} baz {{/if}}', hash, 'baz '); 54 | shouldCompileTo('{{#if foo~}} bar {{~^}} baz {{~/if}}', hash, ' baz'); 55 | shouldCompileTo('{{#if foo~}} bar {{~^}} baz {{/if}}', hash, ' baz '); 56 | 57 | shouldCompileTo('{{#if foo~}} bar {{~else~}} baz {{~/if}}', hash, 'baz'); 58 | 59 | shouldCompileTo('\n\n{{~#if foo~}} \n\nbar \n\n{{~^~}} \n\nbaz \n\n{{~/if~}}\n\n', hash, 'baz'); 60 | }); 61 | }); 62 | 63 | it('should strip whitespace around partials', function() { 64 | shouldCompileToWithPartials('foo {{~> dude~}} ', [{}, {}, {dude: 'bar'}], true, 'foobar'); 65 | shouldCompileToWithPartials('foo {{> dude~}} ', [{}, {}, {dude: 'bar'}], true, 'foo bar'); 66 | shouldCompileToWithPartials('foo {{> dude}} ', [{}, {}, {dude: 'bar'}], true, 'foo bar '); 67 | 68 | shouldCompileToWithPartials('foo\n {{~> dude}} ', [{}, {}, {dude: 'bar'}], true, 'foobar'); 69 | shouldCompileToWithPartials('foo\n {{> dude}} ', [{}, {}, {dude: 'bar'}], true, 'foo\n bar'); 70 | }); 71 | 72 | it('should only strip whitespace once', function() { 73 | var hash = {foo: 'bar'}; 74 | 75 | shouldCompileTo(' {{~foo~}} {{foo}} {{foo}} ', hash, 'barbar bar '); 76 | }); 77 | }); 78 | -------------------------------------------------------------------------------- /bench/throughput.js: -------------------------------------------------------------------------------- 1 | var _ = require('underscore'), 2 | runner = require('./util/template-runner'), 3 | 4 | eco, dust, Handlebars, Mustache, eco; 5 | 6 | try { 7 | dust = require('dustjs-linkedin'); 8 | } catch (err) { /* NOP */ } 9 | 10 | try { 11 | Mustache = require('mustache'); 12 | } catch (err) { /* NOP */ } 13 | 14 | try { 15 | eco = require('eco'); 16 | } catch (err) { /* NOP */ } 17 | 18 | function error() { 19 | throw new Error('EWOT'); 20 | } 21 | 22 | function makeSuite(bench, name, template, handlebarsOnly) { 23 | // Create aliases to minimize any impact from having to walk up the closure tree. 24 | var templateName = name, 25 | 26 | context = template.context, 27 | partials = template.partials, 28 | 29 | handlebarsOut, 30 | compatOut, 31 | dustOut, 32 | ecoOut, 33 | mustacheOut; 34 | 35 | var handlebar = Handlebars.compile(template.handlebars, {data: false}), 36 | compat = Handlebars.compile(template.handlebars, {data: false, compat: true}), 37 | options = {helpers: template.helpers}; 38 | _.each(template.partials && template.partials.handlebars, function(partial, partialName) { 39 | Handlebars.registerPartial(partialName, Handlebars.compile(partial, {data: false})); 40 | }); 41 | 42 | handlebarsOut = handlebar(context, options); 43 | bench('handlebars', function() { 44 | handlebar(context, options); 45 | }); 46 | 47 | compatOut = compat(context, options); 48 | bench('compat', function() { 49 | compat(context, options); 50 | }); 51 | 52 | if (handlebarsOnly) { 53 | return; 54 | } 55 | 56 | if (dust) { 57 | if (template.dust) { 58 | dustOut = false; 59 | dust.loadSource(dust.compile(template.dust, templateName)); 60 | 61 | dust.render(templateName, context, function(err, out) { dustOut = out; }); 62 | 63 | bench('dust', function() { 64 | dust.render(templateName, context, function() {}); 65 | }); 66 | } else { 67 | bench('dust', error); 68 | } 69 | } 70 | 71 | if (eco) { 72 | if (template.eco) { 73 | var ecoTemplate = eco.compile(template.eco); 74 | 75 | ecoOut = ecoTemplate(context); 76 | 77 | bench('eco', function() { 78 | ecoTemplate(context); 79 | }); 80 | } else { 81 | bench('eco', error); 82 | } 83 | } 84 | 85 | if (Mustache) { 86 | var mustacheSource = template.mustache, 87 | mustachePartials = partials && partials.mustache; 88 | 89 | if (mustacheSource) { 90 | mustacheOut = Mustache.to_html(mustacheSource, context, mustachePartials); 91 | 92 | bench('mustache', function() { 93 | Mustache.to_html(mustacheSource, context, mustachePartials); 94 | }); 95 | } else { 96 | bench('mustache', error); 97 | } 98 | } 99 | 100 | // Hack around whitespace until we have whitespace control 101 | handlebarsOut = handlebarsOut.replace(/\s/g, ''); 102 | function compare(b, lang) { 103 | if (b == null) { 104 | return; 105 | } 106 | 107 | b = b.replace(/\s/g, ''); 108 | 109 | if (handlebarsOut !== b) { 110 | throw new Error('Template output mismatch: ' + name 111 | + '\n\nHandlebars: ' + handlebarsOut 112 | + '\n\n' + lang + ': ' + b); 113 | } 114 | } 115 | 116 | compare(compatOut, 'compat'); 117 | compare(dustOut, 'dust'); 118 | compare(ecoOut, 'eco'); 119 | compare(mustacheOut, 'mustache'); 120 | } 121 | 122 | module.exports = function(grunt, callback) { 123 | // Deferring load incase we are being run inline with the grunt build 124 | Handlebars = require('../lib'); 125 | 126 | console.log('Execution Throughput'); 127 | runner(grunt, makeSuite, function(times, scaled) { 128 | callback(scaled); 129 | }); 130 | }; 131 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to Contribute 2 | 3 | ## Reporting Issues 4 | 5 | Please see our [FAQ](https://github.com/wycats/handlebars.js/blob/master/FAQ.md) for common issues that people run into. 6 | 7 | Should you run into other issues with the project, please don't hesitate to let us know by filing an [issue][issue]! In general we are going to ask for an example of the problem failing, which can be as simple as a jsfiddle/jsbin/etc. We've put together a jsfiddle [template][jsfiddle] to ease this. (We will keep this link up to date as new releases occur, so feel free to check back here) 8 | 9 | Pull requests containing only failing thats demonstrating the issue are welcomed and this also helps ensure that your issue won't regress in the future once it's fixed. 10 | 11 | Documentation issues on the handlebarsjs.com site should be reported on [handlebars-site](https://github.com/wycats/handlebars-site). 12 | 13 | ## Pull Requests 14 | 15 | We also accept [pull requests][pull-request]! 16 | 17 | Generally we like to see pull requests that 18 | - Maintain the existing code style 19 | - Are focused on a single change (i.e. avoid large refactoring or style adjustments in untouched code if not the primary goal of the pull request) 20 | - Have [good commit messages](http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html) 21 | - Have tests 22 | - Don't significantly decrease the current code coverage (see coverage/lcov-report/index.html) 23 | 24 | ## Building 25 | 26 | To build Handlebars.js you'll need a few things installed. 27 | 28 | * Node.js 29 | * [Grunt](http://gruntjs.com/getting-started) 30 | 31 | Before building, you need to make sure that the Git submodule `spec/mustache` is included (i.e. the directory `spec/mustache` should not be empty). To include it, if using Git version 1.6.5 or newer, use `git clone --recursive` rather than `git clone`. Or, if you already cloned without `--recursive`, use `git submodule update --init`. 32 | 33 | Project dependencies may be installed via `npm install`. 34 | 35 | To build Handlebars.js from scratch, you'll want to run `grunt` 36 | in the root of the project. That will build Handlebars and output the 37 | results to the dist/ folder. To re-run tests, run `grunt test` or `npm test`. 38 | You can also run our set of benchmarks with `grunt bench`. 39 | 40 | The `grunt dev` implements watching for tests and allows for in browser testing at `http://localhost:9999/spec/`. 41 | 42 | If you notice any problems, please report them to the GitHub issue tracker at 43 | [http://github.com/wycats/handlebars.js/issues](http://github.com/wycats/handlebars.js/issues). 44 | 45 | ## Ember testing 46 | 47 | The current ember distribution should be tested as part of the handlebars release process. This requires building the `handlebars-source` gem locally and then executing the ember test script. 48 | 49 | ```sh 50 | npm link 51 | grunt build release 52 | cp dist/*.js $emberRepoDir/bower_components/handlebars/ 53 | 54 | cd $emberRepoDir 55 | npm link handlebars 56 | npm test 57 | ``` 58 | 59 | ## Releasing 60 | 61 | Handlebars utilizes the [release yeoman generator][generator-release] to perform most release tasks. 62 | 63 | A full release may be completed with the following: 64 | 65 | ``` 66 | yo release 67 | npm publish 68 | yo release:publish components handlebars.js dist/components/ 69 | 70 | cd dist/components/ 71 | gem build handlebars-source.gemspec 72 | gem push handlebars-source-*.gem 73 | ``` 74 | 75 | After this point the handlebars site needs to be updated to point to the new version numbers. The jsfiddle link should be updated to point to the most recent distribution for all instances in our documentation. 76 | 77 | [generator-release]: https://github.com/walmartlabs/generator-release 78 | [pull-request]: https://github.com/wycats/handlebars.js/pull/new/master 79 | [issue]: https://github.com/wycats/handlebars.js/issues/new 80 | [jsfiddle]: http://jsfiddle.net/9D88g/46/ 81 | -------------------------------------------------------------------------------- /FAQ.md: -------------------------------------------------------------------------------- 1 | # Frequently Asked Questions 2 | 3 | 1. How can I file a bug report: 4 | 5 | See our guidelines on [reporting issues](https://github.com/wycats/handlebars.js/blob/master/CONTRIBUTING.md#reporting-issues). 6 | 7 | 1. Why isn't my Mustache template working? 8 | 9 | Handlebars deviates from Mustache slightly on a few behaviors. These variations are documented in our [readme](https://github.com/wycats/handlebars.js#differences-between-handlebarsjs-and-mustache). 10 | 11 | 1. Why is it slower when compiling? 12 | 13 | The Handlebars compiler must parse the template and construct a JavaScript program which can then be run. Under some environments such as older mobile devices this can have a performance impact which can be avoided by precompiling. Generally it's recommended that precompilation and the runtime library be used on all clients. 14 | 15 | 1. Why doesn't this work with Content Security Policy restrictions? 16 | 17 | When not using the precompiler, Handlebars generates a dynamic function for each template which can cause issues with pages that have enabled Content Policy. It's recommended that templates are precompiled or the `unsafe-eval` policy is enabled for sites that must generate dynamic templates at runtime. 18 | 19 | 1. How can I include script tags in my template? 20 | 21 | If loading the template via an inlined ` 28 | ``` 29 | 30 | It's generally recommended that templates are served through external, precompiled, files, which do not suffer from this issue. 31 | 32 | 1. Why are my precompiled scripts throwing exceptions? 33 | 34 | When using the precompiler, it's important that a supporting version of the Handlebars runtime be loaded on the target page. In version 1.x there were rudimentary checks to compare the version but these did not always work. This is fixed under 2.x but the version checking does not work between these two versions. If you see unexpected errors such as `undefined is not a function` or similar, please verify that the same version is being used for both the precompiler and the client. This can be checked via: 35 | 36 | ```sh 37 | handlebars --version 38 | ``` 39 | If using the integrated precompiler and 40 | 41 | ```javascript 42 | console.log(Handlebars.VERSION); 43 | ``` 44 | On the client side. 45 | 46 | We include the built client libraries in the npm package for those who want to be certain that they are using the same client libraries as the compiler. 47 | 48 | Should these match, please file an issue with us, per our [issue filing guidelines](https://github.com/wycats/handlebars.js/blob/master/CONTRIBUTING.md#reporting-issues). 49 | 50 | 1. Why doesn't IE like the `default` name in the AMD module? 51 | 52 | Some browsers such as particular versions of IE treat `default` as a reserved word in JavaScript source files. To safely use this you need to reference this via the `Handlebars['default']` lookup method. This is an unfortunate side effect of the shims necessary to backport the Handlebars ES6 code to all current browsers. 53 | 54 | 1. How do I load the runtime library when using AMD? 55 | 56 | There are two options for loading under AMD environments. The first is to use the `handlebars.runtime.amd.js` file. This may require a [path mapping](https://github.com/wycats/handlebars.js/blob/master/spec/amd-runtime.html#L31) as well as access via the `default` field. 57 | 58 | The other option is to load the `handlebars.runtime.js` UMD build, which might not require path configuration and exposes the library as both the module root and the `default` field for compatibility. 59 | 60 | If not using ES6 transpilers or accessing submodules in the build the former option should be sufficient for most use cases. 61 | -------------------------------------------------------------------------------- /bin/handlebars: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | var optimist = require('optimist') 4 | .usage('Precompile handlebar templates.\nUsage: $0 [template|directory]...', { 5 | 'f': { 6 | 'type': 'string', 7 | 'description': 'Output File', 8 | 'alias': 'output' 9 | }, 10 | 'map': { 11 | 'type': 'string', 12 | 'description': 'Source Map File' 13 | }, 14 | 'a': { 15 | 'type': 'boolean', 16 | 'description': 'Exports amd style (require.js)', 17 | 'alias': 'amd' 18 | }, 19 | 'c': { 20 | 'type': 'string', 21 | 'description': 'Exports CommonJS style, path to Handlebars module', 22 | 'alias': 'commonjs', 23 | 'default': null 24 | }, 25 | 'h': { 26 | 'type': 'string', 27 | 'description': 'Path to handlebar.js (only valid for amd-style)', 28 | 'alias': 'handlebarPath', 29 | 'default': '' 30 | }, 31 | 'k': { 32 | 'type': 'string', 33 | 'description': 'Known helpers', 34 | 'alias': 'known' 35 | }, 36 | 'o': { 37 | 'type': 'boolean', 38 | 'description': 'Known helpers only', 39 | 'alias': 'knownOnly' 40 | }, 41 | 'm': { 42 | 'type': 'boolean', 43 | 'description': 'Minimize output', 44 | 'alias': 'min' 45 | }, 46 | 'n': { 47 | 'type': 'string', 48 | 'description': 'Template namespace', 49 | 'alias': 'namespace', 50 | 'default': 'Handlebars.templates' 51 | }, 52 | 's': { 53 | 'type': 'boolean', 54 | 'description': 'Output template function only.', 55 | 'alias': 'simple' 56 | }, 57 | 'N': { 58 | 'type': 'string', 59 | 'description': 'Name of passed string templates. Optional if running in a simple mode. Required when operating on multiple templates.', 60 | 'alias': 'name' 61 | }, 62 | 'i': { 63 | 'type': 'string', 64 | 'description': 'Generates a template from the passed CLI argument.\n"-" is treated as a special value and causes stdin to be read for the template value.', 65 | 'alias': 'string' 66 | }, 67 | 'r': { 68 | 'type': 'string', 69 | 'description': 'Template root. Base value that will be stripped from template names.', 70 | 'alias': 'root' 71 | }, 72 | 'p': { 73 | 'type': 'boolean', 74 | 'description': 'Compiling a partial template', 75 | 'alias': 'partial' 76 | }, 77 | 'd': { 78 | 'type': 'boolean', 79 | 'description': 'Include data when compiling', 80 | 'alias': 'data' 81 | }, 82 | 'e': { 83 | 'type': 'string', 84 | 'description': 'Template extension.', 85 | 'alias': 'extension', 86 | 'default': 'handlebars' 87 | }, 88 | 'b': { 89 | 'type': 'boolean', 90 | 'description': 'Removes the BOM (Byte Order Mark) from the beginning of the templates.', 91 | 'alias': 'bom' 92 | }, 93 | 'v': { 94 | 'type': 'boolean', 95 | 'description': 'Prints the current compiler version', 96 | 'alias': 'version' 97 | }, 98 | 99 | 'help': { 100 | 'type': 'boolean', 101 | 'description': 'Outputs this message' 102 | } 103 | }) 104 | 105 | .wrap(120) 106 | .check(function(argv) { 107 | if (argv.version) { 108 | return; 109 | } 110 | }); 111 | 112 | 113 | var argv = optimist.argv; 114 | argv.files = argv._; 115 | delete argv._; 116 | 117 | var Precompiler = require('../dist/cjs/precompiler'); 118 | Precompiler.loadTemplates(argv, function(err, opts) { 119 | if (err) { 120 | throw err; 121 | } 122 | 123 | if (opts.help || (!opts.templates.length && !opts.version)) { 124 | optimist.showHelp(); 125 | } else { 126 | Precompiler.cli(opts); 127 | } 128 | }); 129 | -------------------------------------------------------------------------------- /spec/amd.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Mocha 5 | 6 | 7 | 8 | 9 | 15 | 16 | 22 | 23 | 26 | 27 | 28 | 29 | 30 | 31 | 65 | 121 | 122 | 123 |
124 | 125 | 126 | -------------------------------------------------------------------------------- /lib/handlebars/compiler/printer.js: -------------------------------------------------------------------------------- 1 | /*eslint-disable new-cap */ 2 | import Visitor from './visitor'; 3 | 4 | export function print(ast) { 5 | return new PrintVisitor().accept(ast); 6 | } 7 | 8 | export function PrintVisitor() { 9 | this.padding = 0; 10 | } 11 | 12 | PrintVisitor.prototype = new Visitor(); 13 | 14 | PrintVisitor.prototype.pad = function(string) { 15 | let out = ''; 16 | 17 | for (let i = 0, l = this.padding; i < l; i++) { 18 | out += ' '; 19 | } 20 | 21 | out += string + '\n'; 22 | return out; 23 | }; 24 | 25 | PrintVisitor.prototype.Program = function(program) { 26 | let out = '', 27 | body = program.body, 28 | i, l; 29 | 30 | if (program.blockParams) { 31 | let blockParams = 'BLOCK PARAMS: ['; 32 | for (i = 0, l = program.blockParams.length; i < l; i++) { 33 | blockParams += ' ' + program.blockParams[i]; 34 | } 35 | blockParams += ' ]'; 36 | out += this.pad(blockParams); 37 | } 38 | 39 | for (i = 0, l = body.length; i < l; i++) { 40 | out += this.accept(body[i]); 41 | } 42 | 43 | this.padding--; 44 | 45 | return out; 46 | }; 47 | 48 | PrintVisitor.prototype.MustacheStatement = function(mustache) { 49 | return this.pad('{{ ' + this.SubExpression(mustache) + ' }}'); 50 | }; 51 | 52 | PrintVisitor.prototype.BlockStatement = function(block) { 53 | let out = ''; 54 | 55 | out += this.pad('BLOCK:'); 56 | this.padding++; 57 | out += this.pad(this.SubExpression(block)); 58 | if (block.program) { 59 | out += this.pad('PROGRAM:'); 60 | this.padding++; 61 | out += this.accept(block.program); 62 | this.padding--; 63 | } 64 | if (block.inverse) { 65 | if (block.program) { this.padding++; } 66 | out += this.pad('{{^}}'); 67 | this.padding++; 68 | out += this.accept(block.inverse); 69 | this.padding--; 70 | if (block.program) { this.padding--; } 71 | } 72 | this.padding--; 73 | 74 | return out; 75 | }; 76 | 77 | PrintVisitor.prototype.PartialStatement = function(partial) { 78 | let content = 'PARTIAL:' + partial.name.original; 79 | if (partial.params[0]) { 80 | content += ' ' + this.accept(partial.params[0]); 81 | } 82 | if (partial.hash) { 83 | content += ' ' + this.accept(partial.hash); 84 | } 85 | return this.pad('{{> ' + content + ' }}'); 86 | }; 87 | 88 | PrintVisitor.prototype.ContentStatement = function(content) { 89 | return this.pad("CONTENT[ '" + content.value + "' ]"); 90 | }; 91 | 92 | PrintVisitor.prototype.CommentStatement = function(comment) { 93 | return this.pad("{{! '" + comment.value + "' }}"); 94 | }; 95 | 96 | PrintVisitor.prototype.SubExpression = function(sexpr) { 97 | let params = sexpr.params, 98 | paramStrings = [], 99 | hash; 100 | 101 | for (let i = 0, l = params.length; i < l; i++) { 102 | paramStrings.push(this.accept(params[i])); 103 | } 104 | 105 | params = '[' + paramStrings.join(', ') + ']'; 106 | 107 | hash = sexpr.hash ? ' ' + this.accept(sexpr.hash) : ''; 108 | 109 | return this.accept(sexpr.path) + ' ' + params + hash; 110 | }; 111 | 112 | PrintVisitor.prototype.PathExpression = function(id) { 113 | let path = id.parts.join('/'); 114 | return (id.data ? '@' : '') + 'PATH:' + path; 115 | }; 116 | 117 | 118 | PrintVisitor.prototype.StringLiteral = function(string) { 119 | return '"' + string.value + '"'; 120 | }; 121 | 122 | PrintVisitor.prototype.NumberLiteral = function(number) { 123 | return 'NUMBER{' + number.value + '}'; 124 | }; 125 | 126 | PrintVisitor.prototype.BooleanLiteral = function(bool) { 127 | return 'BOOLEAN{' + bool.value + '}'; 128 | }; 129 | 130 | PrintVisitor.prototype.UndefinedLiteral = function() { 131 | return 'UNDEFINED'; 132 | }; 133 | 134 | PrintVisitor.prototype.NullLiteral = function() { 135 | return 'NULL'; 136 | }; 137 | 138 | PrintVisitor.prototype.Hash = function(hash) { 139 | let pairs = hash.pairs, 140 | joinedPairs = []; 141 | 142 | for (let i = 0, l = pairs.length; i < l; i++) { 143 | joinedPairs.push(this.accept(pairs[i])); 144 | } 145 | 146 | return 'HASH{' + joinedPairs.join(', ') + '}'; 147 | }; 148 | PrintVisitor.prototype.HashPair = function(pair) { 149 | return pair.key + '=' + this.accept(pair.value); 150 | }; 151 | /*eslint-enable new-cap */ 152 | -------------------------------------------------------------------------------- /lib/handlebars/compiler/code-gen.js: -------------------------------------------------------------------------------- 1 | /*global define */ 2 | import {isArray} from '../utils'; 3 | 4 | let SourceNode; 5 | 6 | try { 7 | /* istanbul ignore next */ 8 | if (typeof define !== 'function' || !define.amd) { 9 | // We don't support this in AMD environments. For these environments, we asusme that 10 | // they are running on the browser and thus have no need for the source-map library. 11 | let SourceMap = require('source-map'); 12 | SourceNode = SourceMap.SourceNode; 13 | } 14 | } catch (err) { 15 | /* NOP */ 16 | } 17 | 18 | /* istanbul ignore if: tested but not covered in istanbul due to dist build */ 19 | if (!SourceNode) { 20 | SourceNode = function(line, column, srcFile, chunks) { 21 | this.src = ''; 22 | if (chunks) { 23 | this.add(chunks); 24 | } 25 | }; 26 | /* istanbul ignore next */ 27 | SourceNode.prototype = { 28 | add: function(chunks) { 29 | if (isArray(chunks)) { 30 | chunks = chunks.join(''); 31 | } 32 | this.src += chunks; 33 | }, 34 | prepend: function(chunks) { 35 | if (isArray(chunks)) { 36 | chunks = chunks.join(''); 37 | } 38 | this.src = chunks + this.src; 39 | }, 40 | toStringWithSourceMap: function() { 41 | return {code: this.toString()}; 42 | }, 43 | toString: function() { 44 | return this.src; 45 | } 46 | }; 47 | } 48 | 49 | 50 | function castChunk(chunk, codeGen, loc) { 51 | if (isArray(chunk)) { 52 | let ret = []; 53 | 54 | for (let i = 0, len = chunk.length; i < len; i++) { 55 | ret.push(codeGen.wrap(chunk[i], loc)); 56 | } 57 | return ret; 58 | } else if (typeof chunk === 'boolean' || typeof chunk === 'number') { 59 | // Handle primitives that the SourceNode will throw up on 60 | return chunk + ''; 61 | } 62 | return chunk; 63 | } 64 | 65 | 66 | function CodeGen(srcFile) { 67 | this.srcFile = srcFile; 68 | this.source = []; 69 | } 70 | 71 | CodeGen.prototype = { 72 | prepend: function(source, loc) { 73 | this.source.unshift(this.wrap(source, loc)); 74 | }, 75 | push: function(source, loc) { 76 | this.source.push(this.wrap(source, loc)); 77 | }, 78 | 79 | merge: function() { 80 | let source = this.empty(); 81 | this.each(function(line) { 82 | source.add([' ', line, '\n']); 83 | }); 84 | return source; 85 | }, 86 | 87 | each: function(iter) { 88 | for (let i = 0, len = this.source.length; i < len; i++) { 89 | iter(this.source[i]); 90 | } 91 | }, 92 | 93 | empty: function() { 94 | let loc = this.currentLocation || {start: {}}; 95 | return new SourceNode(loc.start.line, loc.start.column, this.srcFile); 96 | }, 97 | wrap: function(chunk, loc = this.currentLocation || {start: {}}) { 98 | if (chunk instanceof SourceNode) { 99 | return chunk; 100 | } 101 | 102 | chunk = castChunk(chunk, this, loc); 103 | 104 | return new SourceNode(loc.start.line, loc.start.column, this.srcFile, chunk); 105 | }, 106 | 107 | functionCall: function(fn, type, params) { 108 | params = this.generateList(params); 109 | return this.wrap([fn, type ? '.' + type + '(' : '(', params, ')']); 110 | }, 111 | 112 | quotedString: function(str) { 113 | return '"' + (str + '') 114 | .replace(/\\/g, '\\\\') 115 | .replace(/"/g, '\\"') 116 | .replace(/\n/g, '\\n') 117 | .replace(/\r/g, '\\r') 118 | .replace(/\u2028/g, '\\u2028') // Per Ecma-262 7.3 + 7.8.4 119 | .replace(/\u2029/g, '\\u2029') + '"'; 120 | }, 121 | 122 | objectLiteral: function(obj) { 123 | let pairs = []; 124 | 125 | for (let key in obj) { 126 | if (obj.hasOwnProperty(key)) { 127 | let value = castChunk(obj[key], this); 128 | if (value !== 'undefined') { 129 | pairs.push([this.quotedString(key), ':', value]); 130 | } 131 | } 132 | } 133 | 134 | let ret = this.generateList(pairs); 135 | ret.prepend('{'); 136 | ret.add('}'); 137 | return ret; 138 | }, 139 | 140 | 141 | generateList: function(entries) { 142 | let ret = this.empty(); 143 | 144 | for (let i = 0, len = entries.length; i < len; i++) { 145 | if (i) { 146 | ret.add(','); 147 | } 148 | 149 | ret.add(castChunk(entries[i], this)); 150 | } 151 | 152 | return ret; 153 | }, 154 | 155 | generateArray: function(entries) { 156 | let ret = this.generateList(entries); 157 | ret.prepend('['); 158 | ret.add(']'); 159 | 160 | return ret; 161 | } 162 | }; 163 | 164 | export default CodeGen; 165 | 166 | -------------------------------------------------------------------------------- /src/handlebars.yy: -------------------------------------------------------------------------------- 1 | %start root 2 | 3 | %ebnf 4 | 5 | %% 6 | 7 | root 8 | : program EOF { return $1; } 9 | ; 10 | 11 | program 12 | : statement* -> yy.prepareProgram($1) 13 | ; 14 | 15 | statement 16 | : mustache -> $1 17 | | block -> $1 18 | | rawBlock -> $1 19 | | partial -> $1 20 | | content -> $1 21 | | COMMENT { 22 | $$ = { 23 | type: 'CommentStatement', 24 | value: yy.stripComment($1), 25 | strip: yy.stripFlags($1, $1), 26 | loc: yy.locInfo(@$) 27 | }; 28 | }; 29 | 30 | content 31 | : CONTENT { 32 | $$ = { 33 | type: 'ContentStatement', 34 | original: $1, 35 | value: $1, 36 | loc: yy.locInfo(@$) 37 | }; 38 | }; 39 | 40 | rawBlock 41 | : openRawBlock content+ END_RAW_BLOCK -> yy.prepareRawBlock($1, $2, $3, @$) 42 | ; 43 | 44 | openRawBlock 45 | : OPEN_RAW_BLOCK helperName param* hash? CLOSE_RAW_BLOCK -> { path: $2, params: $3, hash: $4 } 46 | ; 47 | 48 | block 49 | : openBlock program inverseChain? closeBlock -> yy.prepareBlock($1, $2, $3, $4, false, @$) 50 | | openInverse program inverseAndProgram? closeBlock -> yy.prepareBlock($1, $2, $3, $4, true, @$) 51 | ; 52 | 53 | openBlock 54 | : OPEN_BLOCK helperName param* hash? blockParams? CLOSE -> { path: $2, params: $3, hash: $4, blockParams: $5, strip: yy.stripFlags($1, $6) } 55 | ; 56 | 57 | openInverse 58 | : OPEN_INVERSE helperName param* hash? blockParams? CLOSE -> { path: $2, params: $3, hash: $4, blockParams: $5, strip: yy.stripFlags($1, $6) } 59 | ; 60 | 61 | openInverseChain 62 | : OPEN_INVERSE_CHAIN helperName param* hash? blockParams? CLOSE -> { path: $2, params: $3, hash: $4, blockParams: $5, strip: yy.stripFlags($1, $6) } 63 | ; 64 | 65 | inverseAndProgram 66 | : INVERSE program -> { strip: yy.stripFlags($1, $1), program: $2 } 67 | ; 68 | 69 | inverseChain 70 | : openInverseChain program inverseChain? { 71 | var inverse = yy.prepareBlock($1, $2, $3, $3, false, @$), 72 | program = yy.prepareProgram([inverse], $2.loc); 73 | program.chained = true; 74 | 75 | $$ = { strip: $1.strip, program: program, chain: true }; 76 | } 77 | | inverseAndProgram -> $1 78 | ; 79 | 80 | closeBlock 81 | : OPEN_ENDBLOCK helperName CLOSE -> {path: $2, strip: yy.stripFlags($1, $3)} 82 | ; 83 | 84 | mustache 85 | // Parsing out the '&' escape token at AST level saves ~500 bytes after min due to the removal of one parser node. 86 | // This also allows for handler unification as all mustache node instances can utilize the same handler 87 | : OPEN helperName param* hash? CLOSE -> yy.prepareMustache($2, $3, $4, $1, yy.stripFlags($1, $5), @$) 88 | | OPEN_UNESCAPED helperName param* hash? CLOSE_UNESCAPED -> yy.prepareMustache($2, $3, $4, $1, yy.stripFlags($1, $5), @$) 89 | ; 90 | 91 | partial 92 | : OPEN_PARTIAL partialName param* hash? CLOSE { 93 | $$ = { 94 | type: 'PartialStatement', 95 | name: $2, 96 | params: $3, 97 | hash: $4, 98 | indent: '', 99 | strip: yy.stripFlags($1, $5), 100 | loc: yy.locInfo(@$) 101 | }; 102 | } 103 | ; 104 | 105 | param 106 | : helperName -> $1 107 | | sexpr -> $1 108 | ; 109 | 110 | sexpr 111 | : OPEN_SEXPR helperName param* hash? CLOSE_SEXPR { 112 | $$ = { 113 | type: 'SubExpression', 114 | path: $2, 115 | params: $3, 116 | hash: $4, 117 | loc: yy.locInfo(@$) 118 | }; 119 | }; 120 | 121 | hash 122 | : hashSegment+ -> {type: 'Hash', pairs: $1, loc: yy.locInfo(@$)} 123 | ; 124 | 125 | hashSegment 126 | : ID EQUALS param -> {type: 'HashPair', key: yy.id($1), value: $3, loc: yy.locInfo(@$)} 127 | ; 128 | 129 | blockParams 130 | : OPEN_BLOCK_PARAMS ID+ CLOSE_BLOCK_PARAMS -> yy.id($2) 131 | ; 132 | 133 | helperName 134 | : path -> $1 135 | | dataName -> $1 136 | | STRING -> {type: 'StringLiteral', value: $1, original: $1, loc: yy.locInfo(@$)} 137 | | NUMBER -> {type: 'NumberLiteral', value: Number($1), original: Number($1), loc: yy.locInfo(@$)} 138 | | BOOLEAN -> {type: 'BooleanLiteral', value: $1 === 'true', original: $1 === 'true', loc: yy.locInfo(@$)} 139 | | UNDEFINED -> {type: 'UndefinedLiteral', original: undefined, value: undefined, loc: yy.locInfo(@$)} 140 | | NULL -> {type: 'NullLiteral', original: null, value: null, loc: yy.locInfo(@$)} 141 | ; 142 | 143 | partialName 144 | : helperName -> $1 145 | | sexpr -> $1 146 | ; 147 | 148 | dataName 149 | : DATA pathSegments -> yy.preparePath(true, $2, @$) 150 | ; 151 | 152 | path 153 | : pathSegments -> yy.preparePath(false, $1, @$) 154 | ; 155 | 156 | pathSegments 157 | : pathSegments SEP ID { $1.push({part: yy.id($3), original: $3, separator: $2}); $$ = $1; } 158 | | ID -> [{part: yy.id($1), original: $1}] 159 | ; 160 | -------------------------------------------------------------------------------- /lib/handlebars/compiler/helpers.js: -------------------------------------------------------------------------------- 1 | import Exception from '../exception'; 2 | 3 | export function SourceLocation(source, locInfo) { 4 | this.source = source; 5 | this.start = { 6 | line: locInfo.first_line, 7 | column: locInfo.first_column 8 | }; 9 | this.end = { 10 | line: locInfo.last_line, 11 | column: locInfo.last_column 12 | }; 13 | } 14 | 15 | export function id(token) { 16 | if (/^\[.*\]$/.test(token)) { 17 | return token.substr(1, token.length - 2); 18 | } else { 19 | return token; 20 | } 21 | } 22 | 23 | export function stripFlags(open, close) { 24 | return { 25 | open: open.charAt(2) === '~', 26 | close: close.charAt(close.length - 3) === '~' 27 | }; 28 | } 29 | 30 | export function stripComment(comment) { 31 | return comment.replace(/^\{\{~?\!-?-?/, '') 32 | .replace(/-?-?~?\}\}$/, ''); 33 | } 34 | 35 | export function preparePath(data, parts, loc) { 36 | loc = this.locInfo(loc); 37 | 38 | let original = data ? '@' : '', 39 | dig = [], 40 | depth = 0, 41 | depthString = ''; 42 | 43 | for (let i = 0, l = parts.length; i < l; i++) { 44 | let part = parts[i].part, 45 | // If we have [] syntax then we do not treat path references as operators, 46 | // i.e. foo.[this] resolves to approximately context.foo['this'] 47 | isLiteral = parts[i].original !== part; 48 | original += (parts[i].separator || '') + part; 49 | 50 | if (!isLiteral && (part === '..' || part === '.' || part === 'this')) { 51 | if (dig.length > 0) { 52 | throw new Exception('Invalid path: ' + original, {loc}); 53 | } else if (part === '..') { 54 | depth++; 55 | depthString += '../'; 56 | } 57 | } else { 58 | dig.push(part); 59 | } 60 | } 61 | 62 | return { 63 | type: 'PathExpression', 64 | data, 65 | depth, 66 | parts: dig, 67 | original, 68 | loc 69 | }; 70 | } 71 | 72 | export function prepareMustache(path, params, hash, open, strip, locInfo) { 73 | // Must use charAt to support IE pre-10 74 | let escapeFlag = open.charAt(3) || open.charAt(2), 75 | escaped = escapeFlag !== '{' && escapeFlag !== '&'; 76 | 77 | return { 78 | type: 'MustacheStatement', 79 | path, 80 | params, 81 | hash, 82 | escaped, 83 | strip, 84 | loc: this.locInfo(locInfo) 85 | }; 86 | } 87 | 88 | export function prepareRawBlock(openRawBlock, contents, close, locInfo) { 89 | if (openRawBlock.path.original !== close) { 90 | let errorNode = {loc: openRawBlock.path.loc}; 91 | 92 | throw new Exception(openRawBlock.path.original + " doesn't match " + close, errorNode); 93 | } 94 | 95 | locInfo = this.locInfo(locInfo); 96 | let program = { 97 | type: 'Program', 98 | body: contents, 99 | strip: {}, 100 | loc: locInfo 101 | }; 102 | 103 | return { 104 | type: 'BlockStatement', 105 | path: openRawBlock.path, 106 | params: openRawBlock.params, 107 | hash: openRawBlock.hash, 108 | program, 109 | openStrip: {}, 110 | inverseStrip: {}, 111 | closeStrip: {}, 112 | loc: locInfo 113 | }; 114 | } 115 | 116 | export function prepareBlock(openBlock, program, inverseAndProgram, close, inverted, locInfo) { 117 | // When we are chaining inverse calls, we will not have a close path 118 | if (close && close.path && openBlock.path.original !== close.path.original) { 119 | let errorNode = {loc: openBlock.path.loc}; 120 | 121 | throw new Exception(openBlock.path.original + ' doesn\'t match ' + close.path.original, errorNode); 122 | } 123 | 124 | program.blockParams = openBlock.blockParams; 125 | 126 | let inverse, 127 | inverseStrip; 128 | 129 | if (inverseAndProgram) { 130 | if (inverseAndProgram.chain) { 131 | inverseAndProgram.program.body[0].closeStrip = close.strip; 132 | } 133 | 134 | inverseStrip = inverseAndProgram.strip; 135 | inverse = inverseAndProgram.program; 136 | } 137 | 138 | if (inverted) { 139 | inverted = inverse; 140 | inverse = program; 141 | program = inverted; 142 | } 143 | 144 | return { 145 | type: 'BlockStatement', 146 | path: openBlock.path, 147 | params: openBlock.params, 148 | hash: openBlock.hash, 149 | program, 150 | inverse, 151 | openStrip: openBlock.strip, 152 | inverseStrip, 153 | closeStrip: close && close.strip, 154 | loc: this.locInfo(locInfo) 155 | }; 156 | } 157 | 158 | export function prepareProgram(statements, loc) { 159 | if (!loc && statements.length) { 160 | const firstLoc = statements[0].loc, 161 | lastLoc = statements[statements.length - 1].loc; 162 | 163 | /* istanbul ignore else */ 164 | if (firstLoc && lastLoc) { 165 | loc = { 166 | source: firstLoc.source, 167 | start: { 168 | line: firstLoc.start.line, 169 | column: firstLoc.start.column 170 | }, 171 | end: { 172 | line: lastLoc.end.line, 173 | column: lastLoc.end.column 174 | } 175 | }; 176 | } 177 | } 178 | 179 | return { 180 | type: 'Program', 181 | body: statements, 182 | strip: {}, 183 | loc: loc 184 | }; 185 | } 186 | 187 | 188 | -------------------------------------------------------------------------------- /spec/strict.js: -------------------------------------------------------------------------------- 1 | var Exception = Handlebars.Exception; 2 | 3 | describe('strict', function() { 4 | describe('strict mode', function() { 5 | it('should error on missing property lookup', function() { 6 | shouldThrow(function() { 7 | var template = CompilerContext.compile('{{hello}}', {strict: true}); 8 | 9 | template({}); 10 | }, Exception, /"hello" not defined in/); 11 | }); 12 | it('should error on missing child', function() { 13 | var template = CompilerContext.compile('{{hello.bar}}', {strict: true}); 14 | equals(template({hello: {bar: 'foo'}}), 'foo'); 15 | 16 | shouldThrow(function() { 17 | template({hello: {}}); 18 | }, Exception, /"bar" not defined in/); 19 | }); 20 | it('should handle explicit undefined', function() { 21 | var template = CompilerContext.compile('{{hello.bar}}', {strict: true}); 22 | 23 | equals(template({hello: {bar: undefined}}), ''); 24 | }); 25 | it('should error on missing property lookup in known helpers mode', function() { 26 | shouldThrow(function() { 27 | var template = CompilerContext.compile('{{hello}}', {strict: true, knownHelpersOnly: true}); 28 | 29 | template({}); 30 | }, Exception, /"hello" not defined in/); 31 | }); 32 | it('should error on missing context', function() { 33 | shouldThrow(function() { 34 | var template = CompilerContext.compile('{{hello}}', {strict: true}); 35 | 36 | template(); 37 | }, Error); 38 | }); 39 | 40 | it('should error on missing data lookup', function() { 41 | var template = CompilerContext.compile('{{@hello}}', {strict: true}); 42 | equals(template(undefined, {data: {hello: 'foo'}}), 'foo'); 43 | 44 | shouldThrow(function() { 45 | template(); 46 | }, Error); 47 | }); 48 | 49 | it('should not run helperMissing for helper calls', function() { 50 | shouldThrow(function() { 51 | var template = CompilerContext.compile('{{hello foo}}', {strict: true}); 52 | 53 | template({foo: true}); 54 | }, Exception, /"hello" not defined in/); 55 | 56 | shouldThrow(function() { 57 | var template = CompilerContext.compile('{{#hello foo}}{{/hello}}', {strict: true}); 58 | 59 | template({foo: true}); 60 | }, Exception, /"hello" not defined in/); 61 | }); 62 | it('should throw on ambiguous blocks', function() { 63 | shouldThrow(function() { 64 | var template = CompilerContext.compile('{{#hello}}{{/hello}}', {strict: true}); 65 | 66 | template({}); 67 | }, Exception, /"hello" not defined in/); 68 | 69 | shouldThrow(function() { 70 | var template = CompilerContext.compile('{{^hello}}{{/hello}}', {strict: true}); 71 | 72 | template({}); 73 | }, Exception, /"hello" not defined in/); 74 | 75 | shouldThrow(function() { 76 | var template = CompilerContext.compile('{{#hello.bar}}{{/hello.bar}}', {strict: true}); 77 | 78 | template({hello: {}}); 79 | }, Exception, /"bar" not defined in/); 80 | }); 81 | 82 | it('should allow undefined parameters when passed to helpers', function() { 83 | var template = CompilerContext.compile('{{#unless foo}}success{{/unless}}', {strict: true}); 84 | equals(template({}), 'success'); 85 | }); 86 | 87 | it('should allow undefined hash when passed to helpers', function() { 88 | var template = CompilerContext.compile('{{helper value=@foo}}', {strict: true}); 89 | var helpers = { 90 | helper: function(options) { 91 | equals('value' in options.hash, true); 92 | equals(options.hash.value, undefined); 93 | return 'success'; 94 | } 95 | }; 96 | equals(template({}, {helpers: helpers}), 'success'); 97 | }); 98 | }); 99 | 100 | describe('assume objects', function() { 101 | it('should ignore missing property', function() { 102 | var template = CompilerContext.compile('{{hello}}', {assumeObjects: true}); 103 | 104 | equal(template({}), ''); 105 | }); 106 | it('should ignore missing child', function() { 107 | var template = CompilerContext.compile('{{hello.bar}}', {assumeObjects: true}); 108 | 109 | equal(template({hello: {}}), ''); 110 | }); 111 | it('should error on missing object', function() { 112 | shouldThrow(function() { 113 | var template = CompilerContext.compile('{{hello.bar}}', {assumeObjects: true}); 114 | 115 | template({}); 116 | }, Error); 117 | }); 118 | it('should error on missing context', function() { 119 | shouldThrow(function() { 120 | var template = CompilerContext.compile('{{hello}}', {assumeObjects: true}); 121 | 122 | template(); 123 | }, Error); 124 | }); 125 | 126 | it('should error on missing data lookup', function() { 127 | shouldThrow(function() { 128 | var template = CompilerContext.compile('{{@hello.bar}}', {assumeObjects: true}); 129 | 130 | template(); 131 | }, Error); 132 | }); 133 | 134 | it('should execute blockHelperMissing', function() { 135 | var template = CompilerContext.compile('{{^hello}}foo{{/hello}}', {assumeObjects: true}); 136 | 137 | equals(template({}), 'foo'); 138 | }); 139 | }); 140 | }); 141 | -------------------------------------------------------------------------------- /spec/visitor.js: -------------------------------------------------------------------------------- 1 | describe('Visitor', function() { 2 | if (!Handlebars.Visitor || !Handlebars.print) { 3 | return; 4 | } 5 | 6 | it('should provide coverage', function() { 7 | // Simply run the thing and make sure it does not fail and that all of the 8 | // stub methods are executed 9 | var visitor = new Handlebars.Visitor(); 10 | visitor.accept(Handlebars.parse('{{foo}}{{#foo (bar 1 "1" true undefined null) foo=@data}}{{!comment}}{{> bar }} {{/foo}}')); 11 | }); 12 | 13 | it('should traverse to stubs', function() { 14 | var visitor = new Handlebars.Visitor(); 15 | 16 | visitor.StringLiteral = function(string) { 17 | equal(string.value, '2'); 18 | }; 19 | visitor.NumberLiteral = function(number) { 20 | equal(number.value, 1); 21 | }; 22 | visitor.BooleanLiteral = function(bool) { 23 | equal(bool.value, true); 24 | 25 | equal(this.parents.length, 3); 26 | equal(this.parents[0].type, 'SubExpression'); 27 | equal(this.parents[1].type, 'BlockStatement'); 28 | equal(this.parents[2].type, 'Program'); 29 | }; 30 | visitor.PathExpression = function(id) { 31 | equal(/(foo\.)?bar$/.test(id.original), true); 32 | }; 33 | visitor.ContentStatement = function(content) { 34 | equal(content.value, ' '); 35 | }; 36 | visitor.CommentStatement = function(comment) { 37 | equal(comment.value, 'comment'); 38 | }; 39 | 40 | visitor.accept(Handlebars.parse('{{#foo.bar (foo.bar 1 "2" true) foo=@foo.bar}}{{!comment}}{{> bar }} {{/foo.bar}}')); 41 | }); 42 | 43 | it('should return undefined'); 44 | 45 | describe('mutating', function() { 46 | describe('fields', function() { 47 | it('should replace value', function() { 48 | var visitor = new Handlebars.Visitor(); 49 | 50 | visitor.mutating = true; 51 | visitor.StringLiteral = function(string) { 52 | return {type: 'NumberLiteral', value: 42, loc: string.loc}; 53 | }; 54 | 55 | var ast = Handlebars.parse('{{foo foo="foo"}}'); 56 | visitor.accept(ast); 57 | equals(Handlebars.print(ast), '{{ PATH:foo [] HASH{foo=NUMBER{42}} }}\n'); 58 | }); 59 | it('should treat undefined resonse as identity', function() { 60 | var visitor = new Handlebars.Visitor(); 61 | visitor.mutating = true; 62 | 63 | var ast = Handlebars.parse('{{foo foo=42}}'); 64 | visitor.accept(ast); 65 | equals(Handlebars.print(ast), '{{ PATH:foo [] HASH{foo=NUMBER{42}} }}\n'); 66 | }); 67 | it('should remove false responses', function() { 68 | var visitor = new Handlebars.Visitor(); 69 | 70 | visitor.mutating = true; 71 | visitor.Hash = function() { 72 | return false; 73 | }; 74 | 75 | var ast = Handlebars.parse('{{foo foo=42}}'); 76 | visitor.accept(ast); 77 | equals(Handlebars.print(ast), '{{ PATH:foo [] }}\n'); 78 | }); 79 | it('should throw when removing required values', function() { 80 | shouldThrow(function() { 81 | var visitor = new Handlebars.Visitor(); 82 | 83 | visitor.mutating = true; 84 | visitor.PathExpression = function() { 85 | return false; 86 | }; 87 | 88 | var ast = Handlebars.parse('{{foo 42}}'); 89 | visitor.accept(ast); 90 | }, Handlebars.Exception, 'MustacheStatement requires path'); 91 | }); 92 | it('should throw when returning non-node responses', function() { 93 | shouldThrow(function() { 94 | var visitor = new Handlebars.Visitor(); 95 | 96 | visitor.mutating = true; 97 | visitor.PathExpression = function() { 98 | return {}; 99 | }; 100 | 101 | var ast = Handlebars.parse('{{foo 42}}'); 102 | visitor.accept(ast); 103 | }, Handlebars.Exception, 'Unexpected node type "undefined" found when accepting path on MustacheStatement'); 104 | }); 105 | }); 106 | describe('arrays', function() { 107 | it('should replace value', function() { 108 | var visitor = new Handlebars.Visitor(); 109 | 110 | visitor.mutating = true; 111 | visitor.StringLiteral = function(string) { 112 | return {type: 'NumberLiteral', value: 42, loc: string.locInfo}; 113 | }; 114 | 115 | var ast = Handlebars.parse('{{foo "foo"}}'); 116 | visitor.accept(ast); 117 | equals(Handlebars.print(ast), '{{ PATH:foo [NUMBER{42}] }}\n'); 118 | }); 119 | it('should treat undefined resonse as identity', function() { 120 | var visitor = new Handlebars.Visitor(); 121 | visitor.mutating = true; 122 | 123 | var ast = Handlebars.parse('{{foo 42}}'); 124 | visitor.accept(ast); 125 | equals(Handlebars.print(ast), '{{ PATH:foo [NUMBER{42}] }}\n'); 126 | }); 127 | it('should remove false responses', function() { 128 | var visitor = new Handlebars.Visitor(); 129 | 130 | visitor.mutating = true; 131 | visitor.NumberLiteral = function() { 132 | return false; 133 | }; 134 | 135 | var ast = Handlebars.parse('{{foo 42}}'); 136 | visitor.accept(ast); 137 | equals(Handlebars.print(ast), '{{ PATH:foo [] }}\n'); 138 | }); 139 | }); 140 | }); 141 | }); 142 | -------------------------------------------------------------------------------- /src/handlebars.l: -------------------------------------------------------------------------------- 1 | 2 | %x mu emu com raw 3 | 4 | %{ 5 | 6 | function strip(start, end) { 7 | return yytext = yytext.substr(start, yyleng-end); 8 | } 9 | 10 | %} 11 | 12 | LEFT_STRIP "~" 13 | RIGHT_STRIP "~" 14 | 15 | LOOKAHEAD [=~}\s\/.)|] 16 | LITERAL_LOOKAHEAD [~}\s)] 17 | 18 | /* 19 | ID is the inverse of control characters. 20 | Control characters ranges: 21 | [\s] Whitespace 22 | [!"#%-,\./] !, ", #, %, &, ', (, ), *, +, ,, ., /, Exceptions in range: $, - 23 | [;->@] ;, <, =, >, @, Exceptions in range: :, ? 24 | [\[-\^`] [, \, ], ^, `, Exceptions in range: _ 25 | [\{-~] {, |, }, ~ 26 | */ 27 | ID [^\s!"#%-,\.\/;->@\[-\^`\{-~]+/{LOOKAHEAD} 28 | 29 | %% 30 | 31 | [^\x00]*?/("{{") { 32 | if(yytext.slice(-2) === "\\\\") { 33 | strip(0,1); 34 | this.begin("mu"); 35 | } else if(yytext.slice(-1) === "\\") { 36 | strip(0,1); 37 | this.begin("emu"); 38 | } else { 39 | this.begin("mu"); 40 | } 41 | if(yytext) return 'CONTENT'; 42 | } 43 | 44 | [^\x00]+ return 'CONTENT'; 45 | 46 | // marks CONTENT up to the next mustache or escaped mustache 47 | [^\x00]{2,}?/("{{"|"\\{{"|"\\\\{{"|<>) { 48 | this.popState(); 49 | return 'CONTENT'; 50 | } 51 | 52 | // nested raw block will create stacked 'raw' condition 53 | "{{{{"/[^/] this.begin('raw'); return 'CONTENT'; 54 | "{{{{/"[^\s!"#%-,\.\/;->@\[-\^`\{-~]+/[=}\s\/.]"}}}}" { 55 | this.popState(); 56 | // Should be using `this.topState()` below, but it currently 57 | // returns the second top instead of the first top. Opened an 58 | // issue about it at https://github.com/zaach/jison/issues/291 59 | if (this.conditionStack[this.conditionStack.length-1] === 'raw') { 60 | return 'CONTENT'; 61 | } else { 62 | yytext = yytext.substr(5, yyleng-9); 63 | return 'END_RAW_BLOCK'; 64 | } 65 | } 66 | [^\x00]*?/("{{{{") { return 'CONTENT'; } 67 | 68 | [\s\S]*?"--"{RIGHT_STRIP}?"}}" { 69 | this.popState(); 70 | return 'COMMENT'; 71 | } 72 | 73 | "(" return 'OPEN_SEXPR'; 74 | ")" return 'CLOSE_SEXPR'; 75 | 76 | "{{{{" { return 'OPEN_RAW_BLOCK'; } 77 | "}}}}" { 78 | this.popState(); 79 | this.begin('raw'); 80 | return 'CLOSE_RAW_BLOCK'; 81 | } 82 | "{{"{LEFT_STRIP}?">" return 'OPEN_PARTIAL'; 83 | "{{"{LEFT_STRIP}?"#" return 'OPEN_BLOCK'; 84 | "{{"{LEFT_STRIP}?"/" return 'OPEN_ENDBLOCK'; 85 | "{{"{LEFT_STRIP}?"^"\s*{RIGHT_STRIP}?"}}" this.popState(); return 'INVERSE'; 86 | "{{"{LEFT_STRIP}?\s*"else"\s*{RIGHT_STRIP}?"}}" this.popState(); return 'INVERSE'; 87 | "{{"{LEFT_STRIP}?"^" return 'OPEN_INVERSE'; 88 | "{{"{LEFT_STRIP}?\s*"else" return 'OPEN_INVERSE_CHAIN'; 89 | "{{"{LEFT_STRIP}?"{" return 'OPEN_UNESCAPED'; 90 | "{{"{LEFT_STRIP}?"&" return 'OPEN'; 91 | "{{"{LEFT_STRIP}?"!--" { 92 | this.unput(yytext); 93 | this.popState(); 94 | this.begin('com'); 95 | } 96 | "{{"{LEFT_STRIP}?"!"[\s\S]*?"}}" { 97 | this.popState(); 98 | return 'COMMENT'; 99 | } 100 | "{{"{LEFT_STRIP}? return 'OPEN'; 101 | 102 | "=" return 'EQUALS'; 103 | ".." return 'ID'; 104 | "."/{LOOKAHEAD} return 'ID'; 105 | [\/.] return 'SEP'; 106 | \s+ // ignore whitespace 107 | "}"{RIGHT_STRIP}?"}}" this.popState(); return 'CLOSE_UNESCAPED'; 108 | {RIGHT_STRIP}?"}}" this.popState(); return 'CLOSE'; 109 | '"'("\\"["]|[^"])*'"' yytext = strip(1,2).replace(/\\"/g,'"'); return 'STRING'; 110 | "'"("\\"[']|[^'])*"'" yytext = strip(1,2).replace(/\\'/g,"'"); return 'STRING'; 111 | "@" return 'DATA'; 112 | "true"/{LITERAL_LOOKAHEAD} return 'BOOLEAN'; 113 | "false"/{LITERAL_LOOKAHEAD} return 'BOOLEAN'; 114 | "undefined"/{LITERAL_LOOKAHEAD} return 'UNDEFINED'; 115 | "null"/{LITERAL_LOOKAHEAD} return 'NULL'; 116 | \-?[0-9]+(?:\.[0-9]+)?/{LITERAL_LOOKAHEAD} return 'NUMBER'; 117 | "as"\s+"|" return 'OPEN_BLOCK_PARAMS'; 118 | "|" return 'CLOSE_BLOCK_PARAMS'; 119 | 120 | {ID} return 'ID'; 121 | 122 | '['[^\]]*']' return 'ID'; 123 | . return 'INVALID'; 124 | 125 | <> return 'EOF'; 126 | -------------------------------------------------------------------------------- /bench/util/benchwarmer.js: -------------------------------------------------------------------------------- 1 | var _ = require('underscore'), 2 | Benchmark = require('benchmark'); 3 | 4 | function BenchWarmer() { 5 | this.benchmarks = []; 6 | this.currentBenches = []; 7 | this.names = []; 8 | this.times = {}; 9 | this.minimum = Infinity; 10 | this.maximum = -Infinity; 11 | this.errors = {}; 12 | } 13 | 14 | var print = require('sys').print; 15 | 16 | BenchWarmer.prototype = { 17 | winners: function(benches) { 18 | return Benchmark.filter(benches, 'fastest'); 19 | }, 20 | suite: function(suite, fn) { 21 | this.suiteName = suite; 22 | this.times[suite] = {}; 23 | this.first = true; 24 | 25 | var self = this; 26 | 27 | fn(function(name, benchFn) { 28 | self.push(name, benchFn); 29 | }); 30 | }, 31 | push: function(name, fn) { 32 | if (this.names.indexOf(name) == -1) { 33 | this.names.push(name); 34 | } 35 | 36 | var first = this.first, suiteName = this.suiteName, self = this; 37 | this.first = false; 38 | 39 | var bench = new Benchmark(fn, { 40 | name: this.suiteName + ': ' + name, 41 | onComplete: function() { 42 | if (first) { self.startLine(suiteName); } 43 | self.writeBench(bench); 44 | self.currentBenches.push(bench); 45 | }, onError: function() { 46 | self.errors[this.name] = this; 47 | } 48 | }); 49 | bench.suiteName = this.suiteName; 50 | bench.benchName = name; 51 | 52 | this.benchmarks.push(bench); 53 | }, 54 | 55 | bench: function(callback) { 56 | var self = this; 57 | 58 | this.printHeader('ops/msec', true); 59 | 60 | Benchmark.invoke(this.benchmarks, { 61 | name: 'run', 62 | onComplete: function() { 63 | self.scaleTimes(); 64 | 65 | self.startLine(''); 66 | 67 | print('\n'); 68 | self.printHeader('scaled'); 69 | _.each(self.scaled, function(value, name) { 70 | self.startLine(name); 71 | 72 | _.each(self.names, function(lang) { 73 | self.writeValue(value[lang] || ''); 74 | }); 75 | }); 76 | print('\n'); 77 | 78 | var errors = false, prop, bench; 79 | for (prop in self.errors) { 80 | if (self.errors.hasOwnProperty(prop) 81 | && self.errors[prop].error.message !== 'EWOT') { 82 | errors = true; 83 | break; 84 | } 85 | } 86 | 87 | if (errors) { 88 | print('\n\nErrors:\n'); 89 | for (prop in self.errors) { 90 | if (self.errors.hasOwnProperty(prop) 91 | && self.errors[prop].error.message !== 'EWOT') { 92 | bench = self.errors[prop]; 93 | print('\n' + bench.name + ':\n'); 94 | print(bench.error.message); 95 | if (bench.error.stack) { 96 | print(bench.error.stack.join('\n')); 97 | } 98 | print('\n'); 99 | } 100 | } 101 | } 102 | 103 | callback(); 104 | } 105 | }); 106 | 107 | print('\n'); 108 | }, 109 | 110 | scaleTimes: function() { 111 | var scaled = this.scaled = {}; 112 | _.each(this.times, function(times, name) { 113 | var output = scaled[name] = {}; 114 | 115 | _.each(times, function(time, lang) { 116 | output[lang] = ((time - this.minimum) / (this.maximum - this.minimum) * 100).toFixed(2); 117 | }, this); 118 | }, this); 119 | }, 120 | 121 | printHeader: function(title, winners) { 122 | var benchSize = 0, names = this.names, i, l; 123 | 124 | for (i = 0, l = names.length; i < l; i++) { 125 | var name = names[i]; 126 | 127 | if (benchSize < name.length) { benchSize = name.length; } 128 | } 129 | 130 | this.nameSize = benchSize + 2; 131 | this.benchSize = 20; 132 | var horSize = 0; 133 | 134 | this.startLine(title); 135 | horSize = horSize + this.benchSize; 136 | for (i = 0, l = names.length; i < l; i++) { 137 | this.writeValue(names[i]); 138 | horSize = horSize + this.benchSize; 139 | } 140 | 141 | if (winners) { 142 | print('WINNER(S)'); 143 | horSize = horSize + 'WINNER(S)'.length; 144 | } 145 | 146 | print('\n' + new Array(horSize + 1).join('-')); 147 | }, 148 | 149 | startLine: function(name) { 150 | var winners = Benchmark.map(this.winners(this.currentBenches), function(bench) { 151 | return bench.name.split(': ')[1]; 152 | }); 153 | 154 | this.currentBenches = []; 155 | 156 | print(winners.join(', ')); 157 | print('\n'); 158 | 159 | if (name) { 160 | this.writeValue(name); 161 | } 162 | }, 163 | writeBench: function(bench) { 164 | var out; 165 | 166 | if (!bench.error) { 167 | var count = bench.hz, 168 | moe = count * bench.stats.rme / 100, 169 | minimum, 170 | maximum; 171 | 172 | count = Math.round(count / 1000); 173 | moe = Math.round(moe / 1000); 174 | minimum = count - moe; 175 | maximum = count + moe; 176 | 177 | out = count + ' ±' + moe + ' (' + bench.cycles + ')'; 178 | 179 | this.times[bench.suiteName][bench.benchName] = count; 180 | this.minimum = Math.min(this.minimum, minimum); 181 | this.maximum = Math.max(this.maximum, maximum); 182 | } else if (bench.error.message === 'EWOT') { 183 | out = 'NA'; 184 | } else { 185 | out = 'E'; 186 | } 187 | this.writeValue(out); 188 | }, 189 | 190 | writeValue: function(out) { 191 | var padding = this.benchSize - out.length + 1; 192 | out = out + new Array(padding).join(' '); 193 | print(out); 194 | } 195 | }; 196 | 197 | module.exports = BenchWarmer; 198 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "globals": { 3 | "self": false 4 | }, 5 | "env": { 6 | "node": true 7 | }, 8 | "ecmaFeatures": { 9 | // Enabling features that can be implemented without polyfills. Want to avoid polyfills at this time. 10 | "arrowFunctions": true, 11 | "blockBindings": true, 12 | "defaultParams": true, 13 | "destructuring": true, 14 | "modules": true, 15 | "objectLiteralComputedProperties": true, 16 | "objectLiteralDuplicateProperties": true, 17 | "objectLiteralShorthandMethods": true, 18 | "objectLiteralShorthandProperties": true, 19 | "restParams": true, 20 | "spread": true, 21 | "templateStrings": true 22 | }, 23 | "rules": { 24 | // Possible Errors // 25 | //-----------------// 26 | 27 | "comma-dangle": [2, "never"], 28 | "no-cond-assign": [2, "except-parens"], 29 | 30 | // Allow for debugging 31 | "no-console": 1, 32 | 33 | "no-constant-condition": 2, 34 | "no-control-regex": 2, 35 | 36 | // Allow for debugging 37 | "no-debugger": 1, 38 | 39 | "no-dupe-args": 2, 40 | "no-dupe-keys": 2, 41 | "no-duplicate-case": 2, 42 | "no-empty": 2, 43 | "no-empty-class": 2, 44 | "no-ex-assign": 2, 45 | "no-extra-boolean-cast": 2, 46 | "no-extra-parens": 0, 47 | "no-extra-semi": 2, 48 | "no-func-assign": 2, 49 | 50 | // Stylistic... might consider disallowing in the future 51 | "no-inner-declarations": 0, 52 | 53 | "no-invalid-regexp": 2, 54 | "no-irregular-whitespace": 2, 55 | "no-negated-in-lhs": 2, 56 | "no-obj-calls": 2, 57 | "no-regex-spaces": 2, 58 | "no-reserved-keys": 2, // Important for IE 59 | "no-sparse-arrays": 0, 60 | 61 | // Optimizer and coverage will handle/highlight this and can be useful for debugging 62 | "no-unreachable": 1, 63 | 64 | "use-isnan": 2, 65 | "valid-jsdoc": 0, 66 | "valid-typeof": 2, 67 | 68 | 69 | // Best Practices // 70 | //----------------// 71 | "block-scoped-var": 0, 72 | "complexity": 0, 73 | "consistent-return": 0, 74 | "curly": 2, 75 | "default-case": 1, 76 | "dot-notation": [2, {"allowKeywords": false}], 77 | "eqeqeq": 0, 78 | "guard-for-in": 1, 79 | "no-alert": 2, 80 | "no-caller": 2, 81 | "no-div-regex": 1, 82 | "no-else-return": 0, 83 | "no-empty-label": 2, 84 | "no-eq-null": 0, 85 | "no-eval": 2, 86 | "no-extend-native": 2, 87 | "no-extra-bind": 2, 88 | "no-fallthrough": 2, 89 | "no-floating-decimal": 2, 90 | "no-implied-eval": 2, 91 | "no-iterator": 2, 92 | "no-labels": 2, 93 | "no-lone-blocks": 2, 94 | "no-loop-func": 2, 95 | "no-multi-spaces": 2, 96 | "no-multi-str": 1, 97 | "no-native-reassign": 2, 98 | "no-new": 2, 99 | "no-new-func": 2, 100 | "no-new-wrappers": 2, 101 | "no-octal": 2, 102 | "no-octal-escape": 2, 103 | "no-param-reassign": 0, 104 | "no-process-env": 2, 105 | "no-proto": 2, 106 | "no-redeclare": 2, 107 | "no-return-assign": 2, 108 | "no-script-url": 2, 109 | "no-self-compare": 2, 110 | "no-sequences": 2, 111 | "no-throw-literal": 2, 112 | "no-unused-expressions": 2, 113 | "no-void": 0, 114 | "no-warning-comments": 1, 115 | "no-with": 2, 116 | "radix": 2, 117 | "vars-on-top": 0, 118 | "wrap-iife": 2, 119 | "yoda": 0, 120 | 121 | 122 | // Strict // 123 | //--------// 124 | "strict": 0, 125 | 126 | 127 | // Variables // 128 | //-----------// 129 | "no-catch-shadow": 2, 130 | "no-delete-var": 2, 131 | "no-label-var": 2, 132 | "no-shadow": 0, 133 | "no-shadow-restricted-names": 2, 134 | "no-undef": 2, 135 | "no-undef-init": 2, 136 | "no-undefined": 0, 137 | "no-unused-vars": [2, {"vars": "all", "args": "after-used"}], 138 | "no-use-before-define": [2, "nofunc"], 139 | 140 | 141 | // Node.js // 142 | //---------// 143 | // Others left to environment defaults 144 | "no-mixed-requires": 0, 145 | 146 | 147 | // Stylistic // 148 | //-----------// 149 | "indent": 0, 150 | "brace-style": [2, "1tbs", {"allowSingleLine": true}], 151 | "camelcase": 2, 152 | "comma-spacing": [2, {"before": false, "after": true}], 153 | "comma-style": [2, "last"], 154 | "consistent-this": [1, "self"], 155 | "eol-last": 2, 156 | "func-names": 0, 157 | "func-style": [2, "declaration"], 158 | "key-spacing": [2, { 159 | "beforeColon": false, 160 | "afterColon": true 161 | }], 162 | "max-nested-callbacks": 0, 163 | "new-cap": 2, 164 | "new-parens": 2, 165 | "newline-after-var": 0, 166 | "no-array-constructor": 2, 167 | "no-continue": 0, 168 | "no-inline-comments": 0, 169 | "no-lonely-if": 2, 170 | "no-mixed-spaces-and-tabs": 2, 171 | "no-multiple-empty-lines": 0, 172 | "no-nested-ternary": 1, 173 | "no-new-object": 2, 174 | "no-spaced-func": 2, 175 | "no-ternary": 0, 176 | "no-trailing-spaces": 2, 177 | "no-underscore-dangle": 0, 178 | "no-wrap-func": 2, 179 | "one-var": 0, 180 | "operator-assignment": 0, 181 | "padded-blocks": 0, 182 | "quote-props": 0, 183 | "quotes": [2, "single", "avoid-escape"], 184 | "semi": 2, 185 | "semi-spacing": [2, {"before": false, "after": true}], 186 | "sort-vars": 0, 187 | "space-after-keywords": [2, "always"], 188 | "space-before-blocks": [2, "always"], 189 | "space-before-function-paren": [2, {"anonymous": "never", "named": "never"}], 190 | "space-in-brackets": 0, 191 | "space-in-parens": [2, "never"], 192 | "space-infix-ops": 2, 193 | "space-return-throw-case": 2, 194 | "space-unary-ops": 2, 195 | "spaced-line-comment": 2, 196 | "wrap-regex": 1, 197 | 198 | "no-var": 1 199 | } 200 | } -------------------------------------------------------------------------------- /spec/string-params.js: -------------------------------------------------------------------------------- 1 | describe('string params mode', function() { 2 | it('arguments to helpers can be retrieved from options hash in string form', function() { 3 | var template = CompilerContext.compile('{{wycats is.a slave.driver}}', {stringParams: true}); 4 | 5 | var helpers = { 6 | wycats: function(passiveVoice, noun) { 7 | return 'HELP ME MY BOSS ' + passiveVoice + ' ' + noun; 8 | } 9 | }; 10 | 11 | var result = template({}, {helpers: helpers}); 12 | 13 | equals(result, 'HELP ME MY BOSS is.a slave.driver', 'String parameters output'); 14 | }); 15 | 16 | it('when using block form, arguments to helpers can be retrieved from options hash in string form', function() { 17 | var template = CompilerContext.compile('{{#wycats is.a slave.driver}}help :({{/wycats}}', {stringParams: true}); 18 | 19 | var helpers = { 20 | wycats: function(passiveVoice, noun, options) { 21 | return 'HELP ME MY BOSS ' + passiveVoice + ' ' + 22 | noun + ': ' + options.fn(this); 23 | } 24 | }; 25 | 26 | var result = template({}, {helpers: helpers}); 27 | 28 | equals(result, 'HELP ME MY BOSS is.a slave.driver: help :(', 'String parameters output'); 29 | }); 30 | 31 | it('when inside a block in String mode, .. passes the appropriate context in the options hash', function() { 32 | var template = CompilerContext.compile('{{#with dale}}{{tomdale ../need dad.joke}}{{/with}}', {stringParams: true}); 33 | 34 | var helpers = { 35 | tomdale: function(desire, noun, options) { 36 | return 'STOP ME FROM READING HACKER NEWS I ' + 37 | options.contexts[0][desire] + ' ' + noun; 38 | }, 39 | 40 | 'with': function(context, options) { 41 | return options.fn(options.contexts[0][context]); 42 | } 43 | }; 44 | 45 | var result = template({ 46 | dale: {}, 47 | 48 | need: 'need-a' 49 | }, {helpers: helpers}); 50 | 51 | equals(result, 'STOP ME FROM READING HACKER NEWS I need-a dad.joke', 'Proper context variable output'); 52 | }); 53 | 54 | it('information about the types is passed along', function() { 55 | var template = CompilerContext.compile("{{tomdale 'need' dad.joke true false}}", { stringParams: true }); 56 | 57 | var helpers = { 58 | tomdale: function(desire, noun, trueBool, falseBool, options) { 59 | equal(options.types[0], 'StringLiteral', 'the string type is passed'); 60 | equal(options.types[1], 'PathExpression', 'the expression type is passed'); 61 | equal(options.types[2], 'BooleanLiteral', 'the expression type is passed'); 62 | equal(desire, 'need', 'the string form is passed for strings'); 63 | equal(noun, 'dad.joke', 'the string form is passed for expressions'); 64 | equal(trueBool, true, 'raw booleans are passed through'); 65 | equal(falseBool, false, 'raw booleans are passed through'); 66 | return 'Helper called'; 67 | } 68 | }; 69 | 70 | var result = template({}, { helpers: helpers }); 71 | equal(result, 'Helper called'); 72 | }); 73 | 74 | it('hash parameters get type information', function() { 75 | var template = CompilerContext.compile("{{tomdale he.says desire='need' noun=dad.joke bool=true}}", { stringParams: true }); 76 | 77 | var helpers = { 78 | tomdale: function(exclamation, options) { 79 | equal(exclamation, 'he.says'); 80 | equal(options.types[0], 'PathExpression'); 81 | 82 | equal(options.hashTypes.desire, 'StringLiteral'); 83 | equal(options.hashTypes.noun, 'PathExpression'); 84 | equal(options.hashTypes.bool, 'BooleanLiteral'); 85 | equal(options.hash.desire, 'need'); 86 | equal(options.hash.noun, 'dad.joke'); 87 | equal(options.hash.bool, true); 88 | return 'Helper called'; 89 | } 90 | }; 91 | 92 | var result = template({}, { helpers: helpers }); 93 | equal(result, 'Helper called'); 94 | }); 95 | 96 | it('hash parameters get context information', function() { 97 | var template = CompilerContext.compile("{{#with dale}}{{tomdale he.says desire='need' noun=../dad/joke bool=true}}{{/with}}", { stringParams: true }); 98 | 99 | var context = {dale: {}}; 100 | 101 | var helpers = { 102 | tomdale: function(exclamation, options) { 103 | equal(exclamation, 'he.says'); 104 | equal(options.types[0], 'PathExpression'); 105 | 106 | equal(options.contexts.length, 1); 107 | equal(options.hashContexts.noun, context); 108 | equal(options.hash.desire, 'need'); 109 | equal(options.hash.noun, 'dad.joke'); 110 | equal(options.hash.bool, true); 111 | return 'Helper called'; 112 | }, 113 | 'with': function(withContext, options) { 114 | return options.fn(options.contexts[0][withContext]); 115 | } 116 | }; 117 | 118 | var result = template(context, { helpers: helpers }); 119 | equal(result, 'Helper called'); 120 | }); 121 | 122 | it('when inside a block in String mode, .. passes the appropriate context in the options hash to a block helper', function() { 123 | var template = CompilerContext.compile('{{#with dale}}{{#tomdale ../need dad.joke}}wot{{/tomdale}}{{/with}}', {stringParams: true}); 124 | 125 | var helpers = { 126 | tomdale: function(desire, noun, options) { 127 | return 'STOP ME FROM READING HACKER NEWS I ' + 128 | options.contexts[0][desire] + ' ' + noun + ' ' + 129 | options.fn(this); 130 | }, 131 | 132 | 'with': function(context, options) { 133 | return options.fn(options.contexts[0][context]); 134 | } 135 | }; 136 | 137 | var result = template({ 138 | dale: {}, 139 | 140 | need: 'need-a' 141 | }, {helpers: helpers}); 142 | 143 | equals(result, 'STOP ME FROM READING HACKER NEWS I need-a dad.joke wot', 'Proper context variable output'); 144 | }); 145 | 146 | it('with nested block ambiguous', function() { 147 | var template = CompilerContext.compile('{{#with content}}{{#view}}{{firstName}} {{lastName}}{{/view}}{{/with}}', {stringParams: true}); 148 | 149 | var helpers = { 150 | 'with': function() { 151 | return 'WITH'; 152 | }, 153 | view: function() { 154 | return 'VIEW'; 155 | } 156 | }; 157 | 158 | var result = template({}, {helpers: helpers}); 159 | equals(result, 'WITH'); 160 | }); 161 | 162 | it('should handle DATA', function() { 163 | var template = CompilerContext.compile('{{foo @bar}}', { stringParams: true }); 164 | 165 | var helpers = { 166 | foo: function(bar, options) { 167 | equal(bar, '@bar'); 168 | equal(options.types[0], 'PathExpression'); 169 | return 'Foo!'; 170 | } 171 | }; 172 | 173 | var result = template({}, { helpers: helpers }); 174 | equal(result, 'Foo!'); 175 | }); 176 | }); 177 | -------------------------------------------------------------------------------- /lib/handlebars/compiler/whitespace-control.js: -------------------------------------------------------------------------------- 1 | import Visitor from './visitor'; 2 | 3 | function WhitespaceControl(options = {}) { 4 | this.options = options; 5 | } 6 | WhitespaceControl.prototype = new Visitor(); 7 | 8 | WhitespaceControl.prototype.Program = function(program) { 9 | const doStandalone = !this.options.ignoreStandalone; 10 | 11 | let isRoot = !this.isRootSeen; 12 | this.isRootSeen = true; 13 | 14 | let body = program.body; 15 | for (let i = 0, l = body.length; i < l; i++) { 16 | let current = body[i], 17 | strip = this.accept(current); 18 | 19 | if (!strip) { 20 | continue; 21 | } 22 | 23 | let _isPrevWhitespace = isPrevWhitespace(body, i, isRoot), 24 | _isNextWhitespace = isNextWhitespace(body, i, isRoot), 25 | 26 | openStandalone = strip.openStandalone && _isPrevWhitespace, 27 | closeStandalone = strip.closeStandalone && _isNextWhitespace, 28 | inlineStandalone = strip.inlineStandalone && _isPrevWhitespace && _isNextWhitespace; 29 | 30 | if (strip.close) { 31 | omitRight(body, i, true); 32 | } 33 | if (strip.open) { 34 | omitLeft(body, i, true); 35 | } 36 | 37 | if (doStandalone && inlineStandalone) { 38 | omitRight(body, i); 39 | 40 | if (omitLeft(body, i)) { 41 | // If we are on a standalone node, save the indent info for partials 42 | if (current.type === 'PartialStatement') { 43 | // Pull out the whitespace from the final line 44 | current.indent = (/([ \t]+$)/).exec(body[i - 1].original)[1]; 45 | } 46 | } 47 | } 48 | if (doStandalone && openStandalone) { 49 | omitRight((current.program || current.inverse).body); 50 | 51 | // Strip out the previous content node if it's whitespace only 52 | omitLeft(body, i); 53 | } 54 | if (doStandalone && closeStandalone) { 55 | // Always strip the next node 56 | omitRight(body, i); 57 | 58 | omitLeft((current.inverse || current.program).body); 59 | } 60 | } 61 | 62 | return program; 63 | }; 64 | WhitespaceControl.prototype.BlockStatement = function(block) { 65 | this.accept(block.program); 66 | this.accept(block.inverse); 67 | 68 | // Find the inverse program that is involed with whitespace stripping. 69 | let program = block.program || block.inverse, 70 | inverse = block.program && block.inverse, 71 | firstInverse = inverse, 72 | lastInverse = inverse; 73 | 74 | if (inverse && inverse.chained) { 75 | firstInverse = inverse.body[0].program; 76 | 77 | // Walk the inverse chain to find the last inverse that is actually in the chain. 78 | while (lastInverse.chained) { 79 | lastInverse = lastInverse.body[lastInverse.body.length - 1].program; 80 | } 81 | } 82 | 83 | let strip = { 84 | open: block.openStrip.open, 85 | close: block.closeStrip.close, 86 | 87 | // Determine the standalone candiacy. Basically flag our content as being possibly standalone 88 | // so our parent can determine if we actually are standalone 89 | openStandalone: isNextWhitespace(program.body), 90 | closeStandalone: isPrevWhitespace((firstInverse || program).body) 91 | }; 92 | 93 | if (block.openStrip.close) { 94 | omitRight(program.body, null, true); 95 | } 96 | 97 | if (inverse) { 98 | let inverseStrip = block.inverseStrip; 99 | 100 | if (inverseStrip.open) { 101 | omitLeft(program.body, null, true); 102 | } 103 | 104 | if (inverseStrip.close) { 105 | omitRight(firstInverse.body, null, true); 106 | } 107 | if (block.closeStrip.open) { 108 | omitLeft(lastInverse.body, null, true); 109 | } 110 | 111 | // Find standalone else statments 112 | if (!this.options.ignoreStandalone 113 | && isPrevWhitespace(program.body) 114 | && isNextWhitespace(firstInverse.body)) { 115 | omitLeft(program.body); 116 | omitRight(firstInverse.body); 117 | } 118 | } else if (block.closeStrip.open) { 119 | omitLeft(program.body, null, true); 120 | } 121 | 122 | return strip; 123 | }; 124 | 125 | WhitespaceControl.prototype.MustacheStatement = function(mustache) { 126 | return mustache.strip; 127 | }; 128 | 129 | WhitespaceControl.prototype.PartialStatement = 130 | WhitespaceControl.prototype.CommentStatement = function(node) { 131 | /* istanbul ignore next */ 132 | let strip = node.strip || {}; 133 | return { 134 | inlineStandalone: true, 135 | open: strip.open, 136 | close: strip.close 137 | }; 138 | }; 139 | 140 | 141 | function isPrevWhitespace(body, i, isRoot) { 142 | if (i === undefined) { 143 | i = body.length; 144 | } 145 | 146 | // Nodes that end with newlines are considered whitespace (but are special 147 | // cased for strip operations) 148 | let prev = body[i - 1], 149 | sibling = body[i - 2]; 150 | if (!prev) { 151 | return isRoot; 152 | } 153 | 154 | if (prev.type === 'ContentStatement') { 155 | return (sibling || !isRoot ? (/\r?\n\s*?$/) : (/(^|\r?\n)\s*?$/)).test(prev.original); 156 | } 157 | } 158 | function isNextWhitespace(body, i, isRoot) { 159 | if (i === undefined) { 160 | i = -1; 161 | } 162 | 163 | let next = body[i + 1], 164 | sibling = body[i + 2]; 165 | if (!next) { 166 | return isRoot; 167 | } 168 | 169 | if (next.type === 'ContentStatement') { 170 | return (sibling || !isRoot ? (/^\s*?\r?\n/) : (/^\s*?(\r?\n|$)/)).test(next.original); 171 | } 172 | } 173 | 174 | // Marks the node to the right of the position as omitted. 175 | // I.e. {{foo}}' ' will mark the ' ' node as omitted. 176 | // 177 | // If i is undefined, then the first child will be marked as such. 178 | // 179 | // If mulitple is truthy then all whitespace will be stripped out until non-whitespace 180 | // content is met. 181 | function omitRight(body, i, multiple) { 182 | let current = body[i == null ? 0 : i + 1]; 183 | if (!current || current.type !== 'ContentStatement' || (!multiple && current.rightStripped)) { 184 | return; 185 | } 186 | 187 | let original = current.value; 188 | current.value = current.value.replace(multiple ? (/^\s+/) : (/^[ \t]*\r?\n?/), ''); 189 | current.rightStripped = current.value !== original; 190 | } 191 | 192 | // Marks the node to the left of the position as omitted. 193 | // I.e. ' '{{foo}} will mark the ' ' node as omitted. 194 | // 195 | // If i is undefined then the last child will be marked as such. 196 | // 197 | // If mulitple is truthy then all whitespace will be stripped out until non-whitespace 198 | // content is met. 199 | function omitLeft(body, i, multiple) { 200 | let current = body[i == null ? body.length - 1 : i - 1]; 201 | if (!current || current.type !== 'ContentStatement' || (!multiple && current.leftStripped)) { 202 | return; 203 | } 204 | 205 | // We omit the last node if it's whitespace only and not preceeded by a non-content node. 206 | let original = current.value; 207 | current.value = current.value.replace(multiple ? (/\s+$/) : (/[ \t]+$/), ''); 208 | current.leftStripped = current.value !== original; 209 | return current.leftStripped; 210 | } 211 | 212 | export default WhitespaceControl; 213 | -------------------------------------------------------------------------------- /spec/regressions.js: -------------------------------------------------------------------------------- 1 | describe('Regressions', function() { 2 | it('GH-94: Cannot read property of undefined', function() { 3 | var data = { 4 | 'books': [{ 5 | 'title': 'The origin of species', 6 | 'author': { 7 | 'name': 'Charles Darwin' 8 | } 9 | }, { 10 | 'title': 'Lazarillo de Tormes' 11 | }] 12 | }; 13 | var string = '{{#books}}{{title}}{{author.name}}{{/books}}'; 14 | shouldCompileTo(string, data, 'The origin of speciesCharles DarwinLazarillo de Tormes', 15 | 'Renders without an undefined property error'); 16 | }); 17 | 18 | it("GH-150: Inverted sections print when they shouldn't", function() { 19 | var string = '{{^set}}not set{{/set}} :: {{#set}}set{{/set}}'; 20 | 21 | shouldCompileTo(string, {}, 'not set :: ', "inverted sections run when property isn't present in context"); 22 | shouldCompileTo(string, {set: undefined}, 'not set :: ', 'inverted sections run when property is undefined'); 23 | shouldCompileTo(string, {set: false}, 'not set :: ', 'inverted sections run when property is false'); 24 | shouldCompileTo(string, {set: true}, ' :: set', "inverted sections don't run when property is true"); 25 | }); 26 | 27 | it('GH-158: Using array index twice, breaks the template', function() { 28 | var string = '{{arr.[0]}}, {{arr.[1]}}'; 29 | var data = { 'arr': [1, 2] }; 30 | 31 | shouldCompileTo(string, data, '1, 2', 'it works as expected'); 32 | }); 33 | 34 | it("bug reported by @fat where lambdas weren't being properly resolved", function() { 35 | var string = 'This is a slightly more complicated {{thing}}..\n' 36 | + '{{! Just ignore this business. }}\n' 37 | + 'Check this out:\n' 38 | + '{{#hasThings}}\n' 39 | + '
    \n' 40 | + '{{#things}}\n' 41 | + '
  • {{word}}
  • \n' 42 | + '{{/things}}
.\n' 43 | + '{{/hasThings}}\n' 44 | + '{{^hasThings}}\n' 45 | + '\n' 46 | + 'Nothing to check out...\n' 47 | + '{{/hasThings}}'; 48 | var data = { 49 | thing: function() { 50 | return 'blah'; 51 | }, 52 | things: [ 53 | {className: 'one', word: '@fat'}, 54 | {className: 'two', word: '@dhg'}, 55 | {className: 'three', word: '@sayrer'} 56 | ], 57 | hasThings: function() { 58 | return true; 59 | } 60 | }; 61 | 62 | var output = 'This is a slightly more complicated blah..\n' 63 | + 'Check this out:\n' 64 | + '
    \n' 65 | + '
  • @fat
  • \n' 66 | + '
  • @dhg
  • \n' 67 | + '
  • @sayrer
  • \n' 68 | + '
.\n'; 69 | shouldCompileTo(string, data, output); 70 | }); 71 | 72 | it('GH-408: Multiple loops fail', function() { 73 | var context = [ 74 | { name: 'John Doe', location: { city: 'Chicago' } }, 75 | { name: 'Jane Doe', location: { city: 'New York'} } 76 | ]; 77 | 78 | var template = CompilerContext.compile('{{#.}}{{name}}{{/.}}{{#.}}{{name}}{{/.}}{{#.}}{{name}}{{/.}}'); 79 | 80 | var result = template(context); 81 | equals(result, 'John DoeJane DoeJohn DoeJane DoeJohn DoeJane Doe', 'It should output multiple times'); 82 | }); 83 | 84 | it('GS-428: Nested if else rendering', function() { 85 | var succeedingTemplate = '{{#inverse}} {{#blk}} Unexpected {{/blk}} {{else}} {{#blk}} Expected {{/blk}} {{/inverse}}'; 86 | var failingTemplate = '{{#inverse}} {{#blk}} Unexpected {{/blk}} {{else}} {{#blk}} Expected {{/blk}} {{/inverse}}'; 87 | 88 | var helpers = { 89 | blk: function(block) { return block.fn(''); }, 90 | inverse: function(block) { return block.inverse(''); } 91 | }; 92 | 93 | shouldCompileTo(succeedingTemplate, [{}, helpers], ' Expected '); 94 | shouldCompileTo(failingTemplate, [{}, helpers], ' Expected '); 95 | }); 96 | 97 | it('GH-458: Scoped this identifier', function() { 98 | shouldCompileTo('{{./foo}}', {foo: 'bar'}, 'bar'); 99 | }); 100 | 101 | it('GH-375: Unicode line terminators', function() { 102 | shouldCompileTo('\u2028', {}, '\u2028'); 103 | }); 104 | 105 | it('GH-534: Object prototype aliases', function() { 106 | /*eslint-disable no-extend-native */ 107 | Object.prototype[0xD834] = true; 108 | 109 | shouldCompileTo('{{foo}}', { foo: 'bar' }, 'bar'); 110 | 111 | delete Object.prototype[0xD834]; 112 | /*eslint-enable no-extend-native */ 113 | }); 114 | 115 | it('GH-437: Matching escaping', function() { 116 | shouldThrow(function() { 117 | CompilerContext.compile('{{{a}}'); 118 | }, Error); 119 | shouldThrow(function() { 120 | CompilerContext.compile('{{a}}}'); 121 | }, Error); 122 | }); 123 | 124 | it('GH-676: Using array in escaping mustache fails', function() { 125 | var string = '{{arr}}'; 126 | var data = { 'arr': [1, 2] }; 127 | 128 | shouldCompileTo(string, data, data.arr.toString(), 'it works as expected'); 129 | }); 130 | 131 | it('Mustache man page', function() { 132 | var string = 'Hello {{name}}. You have just won ${{value}}!{{#in_ca}} Well, ${{taxed_value}}, after taxes.{{/in_ca}}'; 133 | var data = { 134 | 'name': 'Chris', 135 | 'value': 10000, 136 | 'taxed_value': 10000 - (10000 * 0.4), 137 | 'in_ca': true 138 | }; 139 | 140 | shouldCompileTo(string, data, 'Hello Chris. You have just won $10000! Well, $6000, after taxes.', 'the hello world mustache example works'); 141 | }); 142 | 143 | it('GH-731: zero context rendering', function() { 144 | shouldCompileTo('{{#foo}} This is {{bar}} ~ {{/foo}}', {foo: 0, bar: 'OK'}, ' This is ~ '); 145 | }); 146 | 147 | it('GH-820: zero pathed rendering', function() { 148 | shouldCompileTo('{{foo.bar}}', {foo: 0}, ''); 149 | }); 150 | 151 | it('GH-837: undefined values for helpers', function() { 152 | var helpers = { 153 | str: function(value) { return value + ''; } 154 | }; 155 | 156 | shouldCompileTo('{{str bar.baz}}', [{}, helpers], 'undefined'); 157 | }); 158 | 159 | it('GH-926: Depths and de-dupe', function() { 160 | var context = { 161 | name: 'foo', 162 | data: [ 163 | 1 164 | ], 165 | notData: [ 166 | 1 167 | ] 168 | }; 169 | 170 | var template = CompilerContext.compile('{{#if dater}}{{#each data}}{{../name}}{{/each}}{{else}}{{#each notData}}{{../name}}{{/each}}{{/if}}'); 171 | 172 | var result = template(context); 173 | equals(result, 'foo'); 174 | }); 175 | 176 | it('GH-1021: Each empty string key', function() { 177 | var data = { 178 | '': 'foo', 179 | 'name': 'Chris', 180 | 'value': 10000 181 | }; 182 | 183 | shouldCompileTo('{{#each data}}Key: {{@key}}\n{{/each}}', {data: data}, 'Key: \nKey: name\nKey: value\n'); 184 | }); 185 | 186 | it('GH-1054: Should handle simple safe string responses', function() { 187 | var root = '{{#wrap}}{{>partial}}{{/wrap}}'; 188 | var partials = { 189 | partial: '{{#wrap}}{{/wrap}}' 190 | }; 191 | var helpers = { 192 | wrap: function(options) { 193 | return new Handlebars.SafeString(options.fn()); 194 | } 195 | }; 196 | 197 | shouldCompileToWithPartials(root, [{}, helpers, partials], true, ''); 198 | }); 199 | 200 | it('GH-1065: Sparse arrays', function() { 201 | var array = []; 202 | array[1] = 'foo'; 203 | array[3] = 'bar'; 204 | shouldCompileTo('{{#each array}}{{@index}}{{.}}{{/each}}', {array: array}, '1foo3bar'); 205 | }); 206 | }); 207 | -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | /*eslint-disable no-process-env */ 2 | module.exports = function(grunt) { 3 | 4 | grunt.initConfig({ 5 | pkg: grunt.file.readJSON('package.json'), 6 | 7 | eslint: { 8 | options: { 9 | }, 10 | files: [ 11 | '*.js', 12 | 'bench/**/*.js', 13 | 'tasks/**/*.js', 14 | 'lib/**/!(*.min|parser).js', 15 | 'spec/**/!(*.amd|json2|require).js' 16 | ] 17 | }, 18 | 19 | clean: ['tmp', 'dist', 'lib/handlebars/compiler/parser.js'], 20 | 21 | copy: { 22 | dist: { 23 | options: { 24 | processContent: function(content) { 25 | return grunt.template.process('/*!\n\n <%= pkg.name %> v<%= pkg.version %>\n\n<%= grunt.file.read("LICENSE") %>\n@license\n*/\n') 26 | + content; 27 | } 28 | }, 29 | files: [ 30 | {expand: true, cwd: 'dist/', src: ['*.js'], dest: 'dist/'} 31 | ] 32 | }, 33 | cdnjs: { 34 | files: [ 35 | {expand: true, cwd: 'dist/', src: ['*.js'], dest: 'dist/cdnjs'} 36 | ] 37 | }, 38 | components: { 39 | files: [ 40 | {expand: true, cwd: 'components/', src: ['**'], dest: 'dist/components'}, 41 | {expand: true, cwd: 'dist/', src: ['*.js'], dest: 'dist/components'} 42 | ] 43 | } 44 | }, 45 | 46 | babel: { 47 | options: { 48 | sourceMaps: 'inline', 49 | loose: ['es6.modules'], 50 | auxiliaryCommentBefore: 'istanbul ignore next' 51 | }, 52 | amd: { 53 | options: { 54 | modules: 'amd' 55 | }, 56 | files: [{ 57 | expand: true, 58 | cwd: 'lib/', 59 | src: '**/!(index).js', 60 | dest: 'dist/amd/' 61 | }] 62 | }, 63 | 64 | cjs: { 65 | options: { 66 | modules: 'common' 67 | }, 68 | files: [{ 69 | cwd: 'lib/', 70 | expand: true, 71 | src: '**/!(index).js', 72 | dest: 'dist/cjs/' 73 | }] 74 | } 75 | }, 76 | webpack: { 77 | options: { 78 | context: __dirname, 79 | module: { 80 | loaders: [ 81 | // the optional 'runtime' transformer tells babel to require the runtime instead of inlining it. 82 | { test: /\.jsx?$/, exclude: /node_modules/, loader: 'babel-loader?optional=runtime&loose=es6.modules&auxiliaryCommentBefore=istanbul%20ignore%20next' } 83 | ] 84 | }, 85 | output: { 86 | path: 'dist/', 87 | library: 'Handlebars', 88 | libraryTarget: 'umd' 89 | } 90 | }, 91 | handlebars: { 92 | entry: './lib/handlebars.js', 93 | output: { 94 | filename: 'handlebars.js' 95 | } 96 | }, 97 | runtime: { 98 | entry: './lib/handlebars.runtime.js', 99 | output: { 100 | filename: 'handlebars.runtime.js' 101 | } 102 | } 103 | }, 104 | 105 | requirejs: { 106 | options: { 107 | optimize: 'none', 108 | baseUrl: 'dist/amd/' 109 | }, 110 | dist: { 111 | options: { 112 | name: 'handlebars', 113 | out: 'dist/handlebars.amd.js' 114 | } 115 | }, 116 | runtime: { 117 | options: { 118 | name: 'handlebars.runtime', 119 | out: 'dist/handlebars.runtime.amd.js' 120 | } 121 | } 122 | }, 123 | 124 | uglify: { 125 | options: { 126 | mangle: true, 127 | compress: true, 128 | preserveComments: 'some' 129 | }, 130 | dist: { 131 | files: [{ 132 | cwd: 'dist/', 133 | expand: true, 134 | src: ['handlebars*.js', '!*.min.js'], 135 | dest: 'dist/', 136 | rename: function(dest, src) { 137 | return dest + src.replace(/\.js$/, '.min.js'); 138 | } 139 | }] 140 | } 141 | }, 142 | 143 | concat: { 144 | tests: { 145 | src: ['spec/!(require).js'], 146 | dest: 'tmp/tests.js' 147 | } 148 | }, 149 | 150 | connect: { 151 | server: { 152 | options: { 153 | base: '.', 154 | hostname: '*', 155 | port: 9999 156 | } 157 | } 158 | }, 159 | 'saucelabs-mocha': { 160 | all: { 161 | options: { 162 | build: process.env.TRAVIS_JOB_ID, 163 | urls: ['http://localhost:9999/spec/?headless=true', 'http://localhost:9999/spec/amd.html?headless=true'], 164 | detailedError: true, 165 | concurrency: 4, 166 | browsers: [ 167 | {browserName: 'chrome'}, 168 | {browserName: 'firefox', platform: 'Linux'}, 169 | {browserName: 'safari', version: 7, platform: 'OS X 10.9'}, 170 | {browserName: 'safari', version: 6, platform: 'OS X 10.8'}, 171 | {browserName: 'internet explorer', version: 11, platform: 'Windows 8.1'}, 172 | {browserName: 'internet explorer', version: 10, platform: 'Windows 8'}, 173 | {browserName: 'internet explorer', version: 9, platform: 'Windows 7'} 174 | ] 175 | } 176 | }, 177 | sanity: { 178 | options: { 179 | build: process.env.TRAVIS_JOB_ID, 180 | urls: ['http://localhost:9999/spec/umd.html?headless=true', 'http://localhost:9999/spec/amd-runtime.html?headless=true', 'http://localhost:9999/spec/umd-runtime.html?headless=true'], 181 | detailedError: true, 182 | concurrency: 2, 183 | browsers: [ 184 | {browserName: 'chrome'} 185 | ] 186 | } 187 | } 188 | }, 189 | 190 | watch: { 191 | scripts: { 192 | options: { 193 | atBegin: true 194 | }, 195 | 196 | files: ['src/*', 'lib/**/*.js', 'spec/**/*.js'], 197 | tasks: ['build', 'amd', 'tests', 'test'] 198 | } 199 | } 200 | }); 201 | 202 | // Build a new version of the library 203 | this.registerTask('build', 'Builds a distributable version of the current project', [ 204 | 'eslint', 205 | 'parser', 206 | 'node', 207 | 'globals']); 208 | 209 | this.registerTask('amd', ['babel:amd', 'requirejs']); 210 | this.registerTask('node', ['babel:cjs']); 211 | this.registerTask('globals', ['webpack']); 212 | this.registerTask('tests', ['concat:tests']); 213 | 214 | this.registerTask('release', 'Build final packages', ['eslint', 'amd', 'uglify', 'copy:dist', 'copy:components', 'copy:cdnjs']); 215 | 216 | // Load tasks from npm 217 | grunt.loadNpmTasks('grunt-contrib-clean'); 218 | grunt.loadNpmTasks('grunt-contrib-concat'); 219 | grunt.loadNpmTasks('grunt-contrib-connect'); 220 | grunt.loadNpmTasks('grunt-contrib-copy'); 221 | grunt.loadNpmTasks('grunt-contrib-requirejs'); 222 | grunt.loadNpmTasks('grunt-contrib-uglify'); 223 | grunt.loadNpmTasks('grunt-contrib-watch'); 224 | grunt.loadNpmTasks('grunt-babel'); 225 | grunt.loadNpmTasks('grunt-eslint'); 226 | grunt.loadNpmTasks('grunt-saucelabs'); 227 | grunt.loadNpmTasks('grunt-webpack'); 228 | 229 | grunt.task.loadTasks('tasks'); 230 | 231 | grunt.registerTask('bench', ['metrics']); 232 | grunt.registerTask('sauce', process.env.SAUCE_USERNAME ? ['tests', 'connect', 'saucelabs-mocha'] : []); 233 | 234 | grunt.registerTask('travis', process.env.PUBLISH ? ['default', 'sauce', 'metrics', 'publish:latest'] : ['default']); 235 | 236 | grunt.registerTask('dev', ['clean', 'connect', 'watch']); 237 | grunt.registerTask('default', ['clean', 'build', 'test', 'release']); 238 | }; 239 | -------------------------------------------------------------------------------- /spec/subexpressions.js: -------------------------------------------------------------------------------- 1 | describe('subexpressions', function() { 2 | it('arg-less helper', function() { 3 | var string = '{{foo (bar)}}!'; 4 | var context = {}; 5 | var helpers = { 6 | foo: function(val) { 7 | return val + val; 8 | }, 9 | bar: function() { 10 | return 'LOL'; 11 | } 12 | }; 13 | shouldCompileTo(string, [context, helpers], 'LOLLOL!'); 14 | }); 15 | 16 | it('helper w args', function() { 17 | var string = '{{blog (equal a b)}}'; 18 | 19 | var context = { bar: 'LOL' }; 20 | var helpers = { 21 | blog: function(val) { 22 | return 'val is ' + val; 23 | }, 24 | equal: function(x, y) { 25 | return x === y; 26 | } 27 | }; 28 | shouldCompileTo(string, [context, helpers], 'val is true'); 29 | }); 30 | 31 | it('mixed paths and helpers', function() { 32 | var string = '{{blog baz.bat (equal a b) baz.bar}}'; 33 | 34 | var context = { bar: 'LOL', baz: {bat: 'foo!', bar: 'bar!'} }; 35 | var helpers = { 36 | blog: function(val, that, theOther) { 37 | return 'val is ' + val + ', ' + that + ' and ' + theOther; 38 | }, 39 | equal: function(x, y) { 40 | return x === y; 41 | } 42 | }; 43 | shouldCompileTo(string, [context, helpers], 'val is foo!, true and bar!'); 44 | }); 45 | 46 | it('supports much nesting', function() { 47 | var string = '{{blog (equal (equal true true) true)}}'; 48 | 49 | var context = { bar: 'LOL' }; 50 | var helpers = { 51 | blog: function(val) { 52 | return 'val is ' + val; 53 | }, 54 | equal: function(x, y) { 55 | return x === y; 56 | } 57 | }; 58 | shouldCompileTo(string, [context, helpers], 'val is true'); 59 | }); 60 | 61 | it('GH-800 : Complex subexpressions', function() { 62 | var context = {a: 'a', b: 'b', c: {c: 'c'}, d: 'd', e: {e: 'e'}}; 63 | var helpers = { 64 | dash: function(a, b) { 65 | return a + '-' + b; 66 | }, 67 | concat: function(a, b) { 68 | return a + b; 69 | } 70 | }; 71 | 72 | shouldCompileTo("{{dash 'abc' (concat a b)}}", [context, helpers], 'abc-ab'); 73 | shouldCompileTo('{{dash d (concat a b)}}', [context, helpers], 'd-ab'); 74 | shouldCompileTo('{{dash c.c (concat a b)}}', [context, helpers], 'c-ab'); 75 | shouldCompileTo('{{dash (concat a b) c.c}}', [context, helpers], 'ab-c'); 76 | shouldCompileTo('{{dash (concat a e.e) c.c}}', [context, helpers], 'ae-c'); 77 | }); 78 | 79 | it('provides each nested helper invocation its own options hash', function() { 80 | var string = '{{equal (equal true true) true}}'; 81 | 82 | var lastOptions = null; 83 | var helpers = { 84 | equal: function(x, y, options) { 85 | if (!options || options === lastOptions) { 86 | throw new Error('options hash was reused'); 87 | } 88 | lastOptions = options; 89 | return x === y; 90 | } 91 | }; 92 | shouldCompileTo(string, [{}, helpers], 'true'); 93 | }); 94 | 95 | it('with hashes', function() { 96 | var string = "{{blog (equal (equal true true) true fun='yes')}}"; 97 | 98 | var context = { bar: 'LOL' }; 99 | var helpers = { 100 | blog: function(val) { 101 | return 'val is ' + val; 102 | }, 103 | equal: function(x, y) { 104 | return x === y; 105 | } 106 | }; 107 | shouldCompileTo(string, [context, helpers], 'val is true'); 108 | }); 109 | 110 | it('as hashes', function() { 111 | var string = "{{blog fun=(equal (blog fun=1) 'val is 1')}}"; 112 | 113 | var helpers = { 114 | blog: function(options) { 115 | return 'val is ' + options.hash.fun; 116 | }, 117 | equal: function(x, y) { 118 | return x === y; 119 | } 120 | }; 121 | shouldCompileTo(string, [{}, helpers], 'val is true'); 122 | }); 123 | 124 | it('multiple subexpressions in a hash', function() { 125 | var string = '{{input aria-label=(t "Name") placeholder=(t "Example User")}}'; 126 | 127 | var helpers = { 128 | input: function(options) { 129 | var hash = options.hash; 130 | var ariaLabel = Handlebars.Utils.escapeExpression(hash['aria-label']); 131 | var placeholder = Handlebars.Utils.escapeExpression(hash.placeholder); 132 | return new Handlebars.SafeString(''); 133 | }, 134 | t: function(defaultString) { 135 | return new Handlebars.SafeString(defaultString); 136 | } 137 | }; 138 | shouldCompileTo(string, [{}, helpers], ''); 139 | }); 140 | 141 | it('multiple subexpressions in a hash with context', function() { 142 | var string = '{{input aria-label=(t item.field) placeholder=(t item.placeholder)}}'; 143 | 144 | var context = { 145 | item: { 146 | field: 'Name', 147 | placeholder: 'Example User' 148 | } 149 | }; 150 | 151 | var helpers = { 152 | input: function(options) { 153 | var hash = options.hash; 154 | var ariaLabel = Handlebars.Utils.escapeExpression(hash['aria-label']); 155 | var placeholder = Handlebars.Utils.escapeExpression(hash.placeholder); 156 | return new Handlebars.SafeString(''); 157 | }, 158 | t: function(defaultString) { 159 | return new Handlebars.SafeString(defaultString); 160 | } 161 | }; 162 | shouldCompileTo(string, [context, helpers], ''); 163 | }); 164 | 165 | it('in string params mode,', function() { 166 | var template = CompilerContext.compile('{{snog (blorg foo x=y) yeah a=b}}', {stringParams: true}); 167 | 168 | var helpers = { 169 | snog: function(a, b, options) { 170 | equals(a, 'foo'); 171 | equals(options.types.length, 2, 'string params for outer helper processed correctly'); 172 | equals(options.types[0], 'SubExpression', 'string params for outer helper processed correctly'); 173 | equals(options.types[1], 'PathExpression', 'string params for outer helper processed correctly'); 174 | return a + b; 175 | }, 176 | 177 | blorg: function(a, options) { 178 | equals(options.types.length, 1, 'string params for inner helper processed correctly'); 179 | equals(options.types[0], 'PathExpression', 'string params for inner helper processed correctly'); 180 | return a; 181 | } 182 | }; 183 | 184 | var result = template({ 185 | foo: {}, 186 | yeah: {} 187 | }, {helpers: helpers}); 188 | 189 | equals(result, 'fooyeah'); 190 | }); 191 | 192 | it('as hashes in string params mode', function() { 193 | var template = CompilerContext.compile('{{blog fun=(bork)}}', {stringParams: true}); 194 | 195 | var helpers = { 196 | blog: function(options) { 197 | equals(options.hashTypes.fun, 'SubExpression'); 198 | return 'val is ' + options.hash.fun; 199 | }, 200 | bork: function() { 201 | return 'BORK'; 202 | } 203 | }; 204 | 205 | var result = template({}, {helpers: helpers}); 206 | equals(result, 'val is BORK'); 207 | }); 208 | 209 | it('subexpression functions on the context', function() { 210 | var string = '{{foo (bar)}}!'; 211 | var context = { 212 | bar: function() { 213 | return 'LOL'; 214 | } 215 | }; 216 | var helpers = { 217 | foo: function(val) { 218 | return val + val; 219 | } 220 | }; 221 | shouldCompileTo(string, [context, helpers], 'LOLLOL!'); 222 | }); 223 | 224 | it("subexpressions can't just be property lookups", function() { 225 | var string = '{{foo (bar)}}!'; 226 | var context = { 227 | bar: 'LOL' 228 | }; 229 | var helpers = { 230 | foo: function(val) { 231 | return val + val; 232 | } 233 | }; 234 | shouldThrow(function() { 235 | shouldCompileTo(string, [context, helpers], 'LOLLOL!'); 236 | }); 237 | }); 238 | }); 239 | -------------------------------------------------------------------------------- /lib/handlebars/runtime.js: -------------------------------------------------------------------------------- 1 | import * as Utils from './utils'; 2 | import Exception from './exception'; 3 | import { COMPILER_REVISION, REVISION_CHANGES, createFrame } from './base'; 4 | 5 | export function checkRevision(compilerInfo) { 6 | const compilerRevision = compilerInfo && compilerInfo[0] || 1, 7 | currentRevision = COMPILER_REVISION; 8 | 9 | if (compilerRevision !== currentRevision) { 10 | if (compilerRevision < currentRevision) { 11 | const runtimeVersions = REVISION_CHANGES[currentRevision], 12 | compilerVersions = REVISION_CHANGES[compilerRevision]; 13 | throw new Exception('Template was precompiled with an older version of Handlebars than the current runtime. ' + 14 | 'Please update your precompiler to a newer version (' + runtimeVersions + ') or downgrade your runtime to an older version (' + compilerVersions + ').'); 15 | } else { 16 | // Use the embedded version info since the runtime doesn't know about this revision yet 17 | throw new Exception('Template was precompiled with a newer version of Handlebars than the current runtime. ' + 18 | 'Please update your runtime to a newer version (' + compilerInfo[1] + ').'); 19 | } 20 | } 21 | } 22 | 23 | export function template(templateSpec, env) { 24 | /* istanbul ignore next */ 25 | if (!env) { 26 | throw new Exception('No environment passed to template'); 27 | } 28 | if (!templateSpec || !templateSpec.main) { 29 | throw new Exception('Unknown template object: ' + typeof templateSpec); 30 | } 31 | 32 | // Note: Using env.VM references rather than local var references throughout this section to allow 33 | // for external users to override these as psuedo-supported APIs. 34 | env.VM.checkRevision(templateSpec.compiler); 35 | 36 | function invokePartialWrapper(partial, context, options) { 37 | if (options.hash) { 38 | context = Utils.extend({}, context, options.hash); 39 | if (options.ids) { 40 | options.ids[0] = true; 41 | } 42 | } 43 | 44 | partial = env.VM.resolvePartial.call(this, partial, context, options); 45 | let result = env.VM.invokePartial.call(this, partial, context, options); 46 | 47 | if (result == null && env.compile) { 48 | options.partials[options.name] = env.compile(partial, templateSpec.compilerOptions, env); 49 | result = options.partials[options.name](context, options); 50 | } 51 | if (result != null) { 52 | if (options.indent) { 53 | let lines = result.split('\n'); 54 | for (let i = 0, l = lines.length; i < l; i++) { 55 | if (!lines[i] && i + 1 === l) { 56 | break; 57 | } 58 | 59 | lines[i] = options.indent + lines[i]; 60 | } 61 | result = lines.join('\n'); 62 | } 63 | return result; 64 | } else { 65 | throw new Exception('The partial ' + options.name + ' could not be compiled when running in runtime-only mode'); 66 | } 67 | } 68 | 69 | // Just add water 70 | let container = { 71 | strict: function(obj, name) { 72 | if (!(name in obj)) { 73 | throw new Exception('"' + name + '" not defined in ' + obj); 74 | } 75 | return obj[name]; 76 | }, 77 | lookup: function(depths, name) { 78 | const len = depths.length; 79 | for (let i = 0; i < len; i++) { 80 | if (depths[i] && depths[i][name] != null) { 81 | return depths[i][name]; 82 | } 83 | } 84 | }, 85 | lambda: function(current, context) { 86 | return typeof current === 'function' ? current.call(context) : current; 87 | }, 88 | 89 | escapeExpression: Utils.escapeExpression, 90 | invokePartial: invokePartialWrapper, 91 | 92 | fn: function(i) { 93 | return templateSpec[i]; 94 | }, 95 | 96 | programs: [], 97 | program: function(i, data, declaredBlockParams, blockParams, depths) { 98 | let programWrapper = this.programs[i], 99 | fn = this.fn(i); 100 | if (data || depths || blockParams || declaredBlockParams) { 101 | programWrapper = wrapProgram(this, i, fn, data, declaredBlockParams, blockParams, depths); 102 | } else if (!programWrapper) { 103 | programWrapper = this.programs[i] = wrapProgram(this, i, fn); 104 | } 105 | return programWrapper; 106 | }, 107 | 108 | data: function(value, depth) { 109 | while (value && depth--) { 110 | value = value._parent; 111 | } 112 | return value; 113 | }, 114 | merge: function(param, common) { 115 | let obj = param || common; 116 | 117 | if (param && common && (param !== common)) { 118 | obj = Utils.extend({}, common, param); 119 | } 120 | 121 | return obj; 122 | }, 123 | 124 | noop: env.VM.noop, 125 | compilerInfo: templateSpec.compiler 126 | }; 127 | 128 | function ret(context, options = {}) { 129 | let data = options.data; 130 | 131 | ret._setup(options); 132 | if (!options.partial && templateSpec.useData) { 133 | data = initData(context, data); 134 | } 135 | let depths, 136 | blockParams = templateSpec.useBlockParams ? [] : undefined; 137 | if (templateSpec.useDepths) { 138 | if (options.depths) { 139 | depths = context !== options.depths[0] ? [context].concat(options.depths) : options.depths; 140 | } else { 141 | depths = [context]; 142 | } 143 | } 144 | 145 | return '' + templateSpec.main(container, context, container.helpers, container.partials, data, blockParams, depths); 146 | } 147 | ret.isTop = true; 148 | 149 | ret._setup = function(options) { 150 | if (!options.partial) { 151 | container.helpers = container.merge(options.helpers, env.helpers); 152 | 153 | if (templateSpec.usePartial) { 154 | container.partials = container.merge(options.partials, env.partials); 155 | } 156 | } else { 157 | container.helpers = options.helpers; 158 | container.partials = options.partials; 159 | } 160 | }; 161 | 162 | ret._child = function(i, data, blockParams, depths) { 163 | if (templateSpec.useBlockParams && !blockParams) { 164 | throw new Exception('must pass block params'); 165 | } 166 | if (templateSpec.useDepths && !depths) { 167 | throw new Exception('must pass parent depths'); 168 | } 169 | 170 | return wrapProgram(container, i, templateSpec[i], data, 0, blockParams, depths); 171 | }; 172 | return ret; 173 | } 174 | 175 | export function wrapProgram(container, i, fn, data, declaredBlockParams, blockParams, depths) { 176 | function prog(context, options = {}) { 177 | let currentDepths = depths; 178 | if (depths && context !== depths[0]) { 179 | currentDepths = [context].concat(depths); 180 | } 181 | 182 | return fn(container, 183 | context, 184 | container.helpers, container.partials, 185 | options.data || data, 186 | blockParams && [options.blockParams].concat(blockParams), 187 | currentDepths); 188 | } 189 | prog.program = i; 190 | prog.depth = depths ? depths.length : 0; 191 | prog.blockParams = declaredBlockParams || 0; 192 | return prog; 193 | } 194 | 195 | export function resolvePartial(partial, context, options) { 196 | if (!partial) { 197 | partial = options.partials[options.name]; 198 | } else if (!partial.call && !options.name) { 199 | // This is a dynamic partial that returned a string 200 | options.name = partial; 201 | partial = options.partials[partial]; 202 | } 203 | return partial; 204 | } 205 | 206 | export function invokePartial(partial, context, options) { 207 | options.partial = true; 208 | if (options.ids) { 209 | options.data.contextPath = options.ids[0] || options.data.contextPath; 210 | } 211 | 212 | if (partial === undefined) { 213 | throw new Exception('The partial ' + options.name + ' could not be found'); 214 | } else if (partial instanceof Function) { 215 | return partial(context, options); 216 | } 217 | } 218 | 219 | export function noop() { return ''; } 220 | 221 | function initData(context, data) { 222 | if (!data || !('root' in data)) { 223 | data = data ? createFrame(data) : {}; 224 | data.root = context; 225 | } 226 | return data; 227 | } 228 | -------------------------------------------------------------------------------- /spec/blocks.js: -------------------------------------------------------------------------------- 1 | describe('blocks', function() { 2 | it('array', function() { 3 | var string = '{{#goodbyes}}{{text}}! {{/goodbyes}}cruel {{world}}!'; 4 | var hash = {goodbyes: [{text: 'goodbye'}, {text: 'Goodbye'}, {text: 'GOODBYE'}], world: 'world'}; 5 | shouldCompileTo(string, hash, 'goodbye! Goodbye! GOODBYE! cruel world!', 6 | 'Arrays iterate over the contents when not empty'); 7 | 8 | shouldCompileTo(string, {goodbyes: [], world: 'world'}, 'cruel world!', 9 | 'Arrays ignore the contents when empty'); 10 | }); 11 | 12 | it('array without data', function() { 13 | var string = '{{#goodbyes}}{{text}}{{/goodbyes}} {{#goodbyes}}{{text}}{{/goodbyes}}'; 14 | var hash = {goodbyes: [{text: 'goodbye'}, {text: 'Goodbye'}, {text: 'GOODBYE'}], world: 'world'}; 15 | shouldCompileTo(string, [hash,,, false], 'goodbyeGoodbyeGOODBYE goodbyeGoodbyeGOODBYE'); 16 | }); 17 | 18 | it('array with @index', function() { 19 | var string = '{{#goodbyes}}{{@index}}. {{text}}! {{/goodbyes}}cruel {{world}}!'; 20 | var hash = {goodbyes: [{text: 'goodbye'}, {text: 'Goodbye'}, {text: 'GOODBYE'}], world: 'world'}; 21 | 22 | var template = CompilerContext.compile(string); 23 | var result = template(hash); 24 | 25 | equal(result, '0. goodbye! 1. Goodbye! 2. GOODBYE! cruel world!', 'The @index variable is used'); 26 | }); 27 | 28 | it('empty block', function() { 29 | var string = '{{#goodbyes}}{{/goodbyes}}cruel {{world}}!'; 30 | var hash = {goodbyes: [{text: 'goodbye'}, {text: 'Goodbye'}, {text: 'GOODBYE'}], world: 'world'}; 31 | shouldCompileTo(string, hash, 'cruel world!', 32 | 'Arrays iterate over the contents when not empty'); 33 | 34 | shouldCompileTo(string, {goodbyes: [], world: 'world'}, 'cruel world!', 35 | 'Arrays ignore the contents when empty'); 36 | }); 37 | 38 | it('block with complex lookup', function() { 39 | var string = '{{#goodbyes}}{{text}} cruel {{../name}}! {{/goodbyes}}'; 40 | var hash = {name: 'Alan', goodbyes: [{text: 'goodbye'}, {text: 'Goodbye'}, {text: 'GOODBYE'}]}; 41 | 42 | shouldCompileTo(string, hash, 'goodbye cruel Alan! Goodbye cruel Alan! GOODBYE cruel Alan! ', 43 | 'Templates can access variables in contexts up the stack with relative path syntax'); 44 | }); 45 | 46 | it('multiple blocks with complex lookup', function() { 47 | var string = '{{#goodbyes}}{{../name}}{{../name}}{{/goodbyes}}'; 48 | var hash = {name: 'Alan', goodbyes: [{text: 'goodbye'}, {text: 'Goodbye'}, {text: 'GOODBYE'}]}; 49 | 50 | shouldCompileTo(string, hash, 'AlanAlanAlanAlanAlanAlan'); 51 | }); 52 | 53 | it('block with complex lookup using nested context', function() { 54 | var string = '{{#goodbyes}}{{text}} cruel {{foo/../name}}! {{/goodbyes}}'; 55 | 56 | shouldThrow(function() { 57 | CompilerContext.compile(string); 58 | }, Error); 59 | }); 60 | 61 | it('block with deep nested complex lookup', function() { 62 | var string = '{{#outer}}Goodbye {{#inner}}cruel {{../sibling}} {{../../omg}}{{/inner}}{{/outer}}'; 63 | var hash = {omg: 'OMG!', outer: [{ sibling: 'sad', inner: [{ text: 'goodbye' }] }] }; 64 | 65 | shouldCompileTo(string, hash, 'Goodbye cruel sad OMG!'); 66 | }); 67 | 68 | it('works with cached blocks', function() { 69 | var template = CompilerContext.compile('{{#each person}}{{#with .}}{{first}} {{last}}{{/with}}{{/each}}', {data: false}); 70 | 71 | var result = template({person: [{first: 'Alan', last: 'Johnson'}, {first: 'Alan', last: 'Johnson'}]}); 72 | equals(result, 'Alan JohnsonAlan Johnson'); 73 | }); 74 | 75 | describe('inverted sections', function() { 76 | it('inverted sections with unset value', function() { 77 | var string = '{{#goodbyes}}{{this}}{{/goodbyes}}{{^goodbyes}}Right On!{{/goodbyes}}'; 78 | var hash = {}; 79 | shouldCompileTo(string, hash, 'Right On!', "Inverted section rendered when value isn't set."); 80 | }); 81 | 82 | it('inverted section with false value', function() { 83 | var string = '{{#goodbyes}}{{this}}{{/goodbyes}}{{^goodbyes}}Right On!{{/goodbyes}}'; 84 | var hash = {goodbyes: false}; 85 | shouldCompileTo(string, hash, 'Right On!', 'Inverted section rendered when value is false.'); 86 | }); 87 | 88 | it('inverted section with empty set', function() { 89 | var string = '{{#goodbyes}}{{this}}{{/goodbyes}}{{^goodbyes}}Right On!{{/goodbyes}}'; 90 | var hash = {goodbyes: []}; 91 | shouldCompileTo(string, hash, 'Right On!', 'Inverted section rendered when value is empty set.'); 92 | }); 93 | 94 | it('block inverted sections', function() { 95 | shouldCompileTo('{{#people}}{{name}}{{^}}{{none}}{{/people}}', {none: 'No people'}, 96 | 'No people'); 97 | }); 98 | it('chained inverted sections', function() { 99 | shouldCompileTo('{{#people}}{{name}}{{else if none}}{{none}}{{/people}}', {none: 'No people'}, 100 | 'No people'); 101 | shouldCompileTo('{{#people}}{{name}}{{else if nothere}}fail{{else unless nothere}}{{none}}{{/people}}', {none: 'No people'}, 102 | 'No people'); 103 | shouldCompileTo('{{#people}}{{name}}{{else if none}}{{none}}{{else}}fail{{/people}}', {none: 'No people'}, 104 | 'No people'); 105 | }); 106 | it('chained inverted sections with mismatch', function() { 107 | shouldThrow(function() { 108 | shouldCompileTo('{{#people}}{{name}}{{else if none}}{{none}}{{/if}}', {none: 'No people'}, 109 | 'No people'); 110 | }, Error); 111 | }); 112 | 113 | it('block inverted sections with empty arrays', function() { 114 | shouldCompileTo('{{#people}}{{name}}{{^}}{{none}}{{/people}}', {none: 'No people', people: []}, 115 | 'No people'); 116 | }); 117 | }); 118 | 119 | describe('standalone sections', function() { 120 | it('block standalone else sections', function() { 121 | shouldCompileTo('{{#people}}\n{{name}}\n{{^}}\n{{none}}\n{{/people}}\n', {none: 'No people'}, 122 | 'No people\n'); 123 | shouldCompileTo('{{#none}}\n{{.}}\n{{^}}\n{{none}}\n{{/none}}\n', {none: 'No people'}, 124 | 'No people\n'); 125 | shouldCompileTo('{{#people}}\n{{name}}\n{{^}}\n{{none}}\n{{/people}}\n', {none: 'No people'}, 126 | 'No people\n'); 127 | }); 128 | it('block standalone else sections can be disabled', function() { 129 | shouldCompileTo( 130 | '{{#people}}\n{{name}}\n{{^}}\n{{none}}\n{{/people}}\n', 131 | [{none: 'No people'}, {}, {}, {ignoreStandalone: true}], 132 | '\nNo people\n\n'); 133 | shouldCompileTo( 134 | '{{#none}}\n{{.}}\n{{^}}\nFail\n{{/none}}\n', 135 | [{none: 'No people'}, {}, {}, {ignoreStandalone: true}], 136 | '\nNo people\n\n'); 137 | }); 138 | it('block standalone chained else sections', function() { 139 | shouldCompileTo('{{#people}}\n{{name}}\n{{else if none}}\n{{none}}\n{{/people}}\n', {none: 'No people'}, 140 | 'No people\n'); 141 | shouldCompileTo('{{#people}}\n{{name}}\n{{else if none}}\n{{none}}\n{{^}}\n{{/people}}\n', {none: 'No people'}, 142 | 'No people\n'); 143 | }); 144 | it('should handle nesting', function() { 145 | shouldCompileTo('{{#data}}\n{{#if true}}\n{{.}}\n{{/if}}\n{{/data}}\nOK.', {data: [1, 3, 5]}, '1\n3\n5\nOK.'); 146 | }); 147 | }); 148 | 149 | describe('compat mode', function() { 150 | it('block with deep recursive lookup lookup', function() { 151 | var string = '{{#outer}}Goodbye {{#inner}}cruel {{omg}}{{/inner}}{{/outer}}'; 152 | var hash = {omg: 'OMG!', outer: [{ inner: [{ text: 'goodbye' }] }] }; 153 | 154 | shouldCompileTo(string, [hash, undefined, undefined, true], 'Goodbye cruel OMG!'); 155 | }); 156 | it('block with deep recursive pathed lookup', function() { 157 | var string = '{{#outer}}Goodbye {{#inner}}cruel {{omg.yes}}{{/inner}}{{/outer}}'; 158 | var hash = {omg: {yes: 'OMG!'}, outer: [{ inner: [{ yes: 'no', text: 'goodbye' }] }] }; 159 | 160 | shouldCompileTo(string, [hash, undefined, undefined, true], 'Goodbye cruel OMG!'); 161 | }); 162 | it('block with missed recursive lookup', function() { 163 | var string = '{{#outer}}Goodbye {{#inner}}cruel {{omg.yes}}{{/inner}}{{/outer}}'; 164 | var hash = {omg: {no: 'OMG!'}, outer: [{ inner: [{ yes: 'no', text: 'goodbye' }] }] }; 165 | 166 | shouldCompileTo(string, [hash, undefined, undefined, true], 'Goodbye cruel '); 167 | }); 168 | }); 169 | }); 170 | --------------------------------------------------------------------------------