diff options
-rw-r--r-- | .eslintrc | 2 | ||||
-rw-r--r-- | docs/compiler-api.md | 29 | ||||
-rw-r--r-- | lib/handlebars/base.js | 17 | ||||
-rw-r--r-- | lib/handlebars/compiler/code-gen.js | 3 | ||||
-rw-r--r-- | lib/handlebars/compiler/compiler.js | 13 | ||||
-rw-r--r-- | lib/handlebars/compiler/helpers.js | 11 | ||||
-rw-r--r-- | lib/handlebars/compiler/javascript-compiler.js | 71 | ||||
-rw-r--r-- | lib/handlebars/compiler/printer.js | 8 | ||||
-rw-r--r-- | lib/handlebars/compiler/visitor.js | 2 | ||||
-rw-r--r-- | lib/handlebars/compiler/whitespace-control.js | 2 | ||||
-rw-r--r-- | lib/handlebars/decorators.js | 6 | ||||
-rw-r--r-- | lib/handlebars/decorators/inline.js | 22 | ||||
-rw-r--r-- | lib/handlebars/runtime.js | 32 | ||||
-rw-r--r-- | spec/blocks.js | 178 | ||||
-rw-r--r-- | spec/parser.js | 14 | ||||
-rw-r--r-- | spec/partials.js | 39 | ||||
-rw-r--r-- | spec/runtime.js | 6 | ||||
-rw-r--r-- | spec/tokenizer.js | 9 | ||||
-rw-r--r-- | spec/visitor.js | 2 | ||||
-rw-r--r-- | src/handlebars.l | 4 | ||||
-rw-r--r-- | src/handlebars.yy | 2 |
21 files changed, 453 insertions, 19 deletions
@@ -45,7 +45,7 @@ "no-extra-boolean-cast": 2, "no-extra-parens": 0, "no-extra-semi": 2, - "no-func-assign": 2, + "no-func-assign": 0, // Stylistic... might consider disallowing in the future "no-inner-declarations": 0, diff --git a/docs/compiler-api.md b/docs/compiler-api.md index 81438d2..2938219 100644 --- a/docs/compiler-api.md +++ b/docs/compiler-api.md @@ -120,6 +120,33 @@ interface CommentStatement <: Statement { } ``` + +```java +interface Decorator <: Statement { + type: "Decorator"; + + path: PathExpression | Literal; + params: [ Expression ]; + hash: Hash; + + strip: StripFlags | null; +} + +interface DecoratorBlock <: Statement { + type: "DecoratorBlock"; + path: PathExpression | Literal; + params: [ Expression ]; + hash: Hash; + + program: Program | null; + + openStrip: StripFlags | null; + closeStrip: StripFlags | null; +} +``` + +Decorator paths only utilize the `path.original` value and as a consequence do not support depthed evaluation. + ### Expressions ```java @@ -249,7 +276,7 @@ The `Handlebars.JavaScriptCompiler` object has a number of methods that may be c - `parent` is the existing code in the path resolution - `name` is the current path component - - `type` is the type of name being evaluated. May be one of `context`, `data`, `helper`, or `partial`. + - `type` is the type of name being evaluated. May be one of `context`, `data`, `helper`, `decorator`, or `partial`. Note that this does not impact dynamic partials, which implementors need to be aware of. Overriding `VM.resolvePartial` may be required to support dynamic cases. diff --git a/lib/handlebars/base.js b/lib/handlebars/base.js index 41bb98d..e59f5e7 100644 --- a/lib/handlebars/base.js +++ b/lib/handlebars/base.js @@ -1,6 +1,7 @@ import {createFrame, extend, toString} from './utils'; import Exception from './exception'; import {registerDefaultHelpers} from './helpers'; +import {registerDefaultDecorators} from './decorators'; import logger from './logger'; export const VERSION = '3.0.1'; @@ -17,11 +18,13 @@ export const REVISION_CHANGES = { const objectType = '[object Object]'; -export function HandlebarsEnvironment(helpers, partials) { +export function HandlebarsEnvironment(helpers, partials, decorators) { this.helpers = helpers || {}; this.partials = partials || {}; + this.decorators = decorators || {}; registerDefaultHelpers(this); + registerDefaultDecorators(this); } HandlebarsEnvironment.prototype = { @@ -54,6 +57,18 @@ HandlebarsEnvironment.prototype = { }, unregisterPartial: function(name) { delete this.partials[name]; + }, + + registerDecorator: function(name, fn) { + if (toString.call(name) === objectType) { + if (fn) { throw new Exception('Arg not supported with multiple decorators'); } + extend(this.decorators, name); + } else { + this.decorators[name] = fn; + } + }, + unregisterDecorator: function(name) { + delete this.decorators[name]; } }; diff --git a/lib/handlebars/compiler/code-gen.js b/lib/handlebars/compiler/code-gen.js index 3af4f8c..6541fe8 100644 --- a/lib/handlebars/compiler/code-gen.js +++ b/lib/handlebars/compiler/code-gen.js @@ -69,6 +69,9 @@ function CodeGen(srcFile) { } CodeGen.prototype = { + isEmpty() { + return !this.source.length; + }, prepend: function(source, loc) { this.source.unshift(this.wrap(source, loc)); }, diff --git a/lib/handlebars/compiler/compiler.js b/lib/handlebars/compiler/compiler.js index a689e7d..64af5da 100644 --- a/lib/handlebars/compiler/compiler.js +++ b/lib/handlebars/compiler/compiler.js @@ -156,6 +156,15 @@ Compiler.prototype = { this.opcode('append'); }, + DecoratorBlock(decorator) { + let program = decorator.program && this.compileProgram(decorator.program); + let params = this.setupFullMustacheParams(decorator, program, undefined), + path = decorator.path; + + this.useDecorators = true; + this.opcode('registerDecorator', params.length, path.original); + }, + PartialStatement: function(partial) { this.usePartial = true; @@ -201,6 +210,10 @@ Compiler.prototype = { this.opcode('append'); } }, + Decorator(decorator) { + this.DecoratorBlock(decorator); + }, + ContentStatement: function(content) { if (content.value) { diff --git a/lib/handlebars/compiler/helpers.js b/lib/handlebars/compiler/helpers.js index 9c40f0d..e09a08d 100644 --- a/lib/handlebars/compiler/helpers.js +++ b/lib/handlebars/compiler/helpers.js @@ -84,8 +84,9 @@ export function prepareMustache(path, params, hash, open, strip, locInfo) { let escapeFlag = open.charAt(3) || open.charAt(2), escaped = escapeFlag !== '{' && escapeFlag !== '&'; + let decorator = (/\*/.test(open)); return { - type: 'MustacheStatement', + type: decorator ? 'Decorator' : 'MustacheStatement', path, params, hash, @@ -124,12 +125,18 @@ export function prepareBlock(openBlock, program, inverseAndProgram, close, inver validateClose(openBlock, close); } + let decorator = (/\*/.test(openBlock.open)); + program.blockParams = openBlock.blockParams; let inverse, inverseStrip; if (inverseAndProgram) { + if (decorator) { + throw new Exception('Unexpected inverse block on decorator', inverseAndProgram); + } + if (inverseAndProgram.chain) { inverseAndProgram.program.body[0].closeStrip = close.strip; } @@ -145,7 +152,7 @@ export function prepareBlock(openBlock, program, inverseAndProgram, close, inver } return { - type: 'BlockStatement', + type: decorator ? 'DecoratorBlock' : 'BlockStatement', path: openBlock.path, params: openBlock.params, hash: openBlock.hash, diff --git a/lib/handlebars/compiler/javascript-compiler.js b/lib/handlebars/compiler/javascript-compiler.js index b8fc976..ede0b5e 100644 --- a/lib/handlebars/compiler/javascript-compiler.js +++ b/lib/handlebars/compiler/javascript-compiler.js @@ -64,6 +64,7 @@ JavaScriptCompiler.prototype = { this.name = this.environment.name; this.isChild = !!context; this.context = context || { + decorators: [], programs: [], environments: [] }; @@ -81,7 +82,7 @@ JavaScriptCompiler.prototype = { this.compileChildren(environment, options); - this.useDepths = this.useDepths || environment.useDepths || this.options.compat; + this.useDepths = this.useDepths || environment.useDepths || environment.useDecorators || this.options.compat; this.useBlockParams = this.useBlockParams || environment.useBlockParams; let opcodes = environment.opcodes, @@ -107,16 +108,43 @@ JavaScriptCompiler.prototype = { throw new Exception('Compile completed with content left on stack'); } + if (!this.decorators.isEmpty()) { + this.useDecorators = true; + + this.decorators.prepend('var decorators = container.decorators;\n'); + this.decorators.push('return fn;'); + + if (asObject) { + this.decorators = Function.apply(this, ['fn', 'props', 'container', 'depth0', 'data', 'blockParams', 'depths', this.decorators.merge()]); + } else { + this.decorators.prepend('function(fn, props, container, depth0, data, blockParams, depths) {\n'); + this.decorators.push('}\n'); + this.decorators = this.decorators.merge(); + } + } else { + this.decorators = undefined; + } + let fn = this.createFunctionContext(asObject); if (!this.isChild) { let ret = { compiler: this.compilerInfo(), main: fn }; - let programs = this.context.programs; + + if (this.decorators) { + ret.main_d = this.decorators; // eslint-disable-line camelcase + ret.useDecorators = true; + } + + let {programs, decorators} = this.context; for (i = 0, l = programs.length; i < l; i++) { if (programs[i]) { ret[i] = programs[i]; + if (decorators[i]) { + ret[i + '_d'] = decorators[i]; + ret.useDecorators = true; + } } } @@ -163,6 +191,7 @@ JavaScriptCompiler.prototype = { // getContext opcode when it would be a noop this.lastContext = 0; this.source = new CodeGen(this.options.srcName); + this.decorators = new CodeGen(this.options.srcName); }, createFunctionContext: function(asObject) { @@ -561,6 +590,24 @@ JavaScriptCompiler.prototype = { } }, + // [registerDecorator] + // + // On stack, before: hash, program, params..., ... + // On stack, after: ... + // + // Pops off the decorator's parameters, invokes the decorator, + // and inserts the decorator into the decorators list. + registerDecorator(paramSize, name) { + let foundDecorator = this.nameLookup('decorators', name, 'decorator'), + options = this.setupHelperArgs(name, paramSize); + + this.decorators.push([ + 'fn = ', + this.decorators.functionCall(foundDecorator, '', ['fn', 'props', 'container', options]), + ' || fn;' + ]); + }, + // [invokeHelper] // // On stack, before: hash, inverse, program, params..., ... @@ -738,6 +785,7 @@ JavaScriptCompiler.prototype = { child.index = index; child.name = 'program' + index; this.context.programs[index] = compiler.compile(child, options, this.context, !this.precompile); + this.context.decorators[index] = compiler.decorators; this.context.environments[index] = child; this.useDepths = this.useDepths || compiler.useDepths; @@ -946,7 +994,16 @@ JavaScriptCompiler.prototype = { }, setupParams: function(helper, paramSize, params) { - let options = {}, contexts = [], types = [], ids = [], param; + let options = {}, + contexts = [], + types = [], + ids = [], + objectArgs = !params, + param; + + if (objectArgs) { + params = []; + } options.name = this.quotedString(helper); options.hash = this.popStack(); @@ -985,6 +1042,10 @@ JavaScriptCompiler.prototype = { } } + if (objectArgs) { + options.args = this.source.generateArray(params); + } + if (this.trackIds) { options.ids = this.source.generateArray(ids); } @@ -1009,9 +1070,11 @@ JavaScriptCompiler.prototype = { this.useRegister('options'); params.push('options'); return ['options=', options]; - } else { + } else if (params) { params.push(options); return ''; + } else { + return options; } } }; diff --git a/lib/handlebars/compiler/printer.js b/lib/handlebars/compiler/printer.js index cf7aa48..66e7c7d 100644 --- a/lib/handlebars/compiler/printer.js +++ b/lib/handlebars/compiler/printer.js @@ -48,11 +48,15 @@ PrintVisitor.prototype.Program = function(program) { PrintVisitor.prototype.MustacheStatement = function(mustache) { return this.pad('{{ ' + this.SubExpression(mustache) + ' }}'); }; +PrintVisitor.prototype.Decorator = function(mustache) { + return this.pad('{{ DIRECTIVE ' + this.SubExpression(mustache) + ' }}'); +}; -PrintVisitor.prototype.BlockStatement = function(block) { +PrintVisitor.prototype.BlockStatement = +PrintVisitor.prototype.DecoratorBlock = function(block) { let out = ''; - out += this.pad('BLOCK:'); + out += this.pad((block.type === 'DecoratorBlock' ? 'DIRECTIVE ' : '') + 'BLOCK:'); this.padding++; out += this.pad(this.SubExpression(block)); if (block.program) { diff --git a/lib/handlebars/compiler/visitor.js b/lib/handlebars/compiler/visitor.js index cc54c53..2c504d1 100644 --- a/lib/handlebars/compiler/visitor.js +++ b/lib/handlebars/compiler/visitor.js @@ -76,8 +76,10 @@ Visitor.prototype = { }, MustacheStatement: visitSubExpression, + Decorator: visitSubExpression, BlockStatement: visitBlock, + DecoratorBlock: visitBlock, PartialStatement: visitPartial, PartialBlockStatement: function(partial) { diff --git a/lib/handlebars/compiler/whitespace-control.js b/lib/handlebars/compiler/whitespace-control.js index 6c8a986..e11483c 100644 --- a/lib/handlebars/compiler/whitespace-control.js +++ b/lib/handlebars/compiler/whitespace-control.js @@ -63,6 +63,7 @@ WhitespaceControl.prototype.Program = function(program) { }; WhitespaceControl.prototype.BlockStatement = +WhitespaceControl.prototype.DecoratorBlock = WhitespaceControl.prototype.PartialBlockStatement = function(block) { this.accept(block.program); this.accept(block.inverse); @@ -124,6 +125,7 @@ WhitespaceControl.prototype.PartialBlockStatement = function(block) { return strip; }; +WhitespaceControl.prototype.Decorator = WhitespaceControl.prototype.MustacheStatement = function(mustache) { return mustache.strip; }; diff --git a/lib/handlebars/decorators.js b/lib/handlebars/decorators.js new file mode 100644 index 0000000..6f5a615 --- /dev/null +++ b/lib/handlebars/decorators.js @@ -0,0 +1,6 @@ +import registerInline from './decorators/inline'; + +export function registerDefaultDecorators(instance) { + registerInline(instance); +} + diff --git a/lib/handlebars/decorators/inline.js b/lib/handlebars/decorators/inline.js new file mode 100644 index 0000000..2142466 --- /dev/null +++ b/lib/handlebars/decorators/inline.js @@ -0,0 +1,22 @@ +import {extend} from '../utils'; + +export default function(instance) { + instance.registerDecorator('inline', function(fn, props, container, options) { + let ret = fn; + if (!props.partials) { + props.partials = {}; + ret = function(context, options) { + // Create a new partials stack frame prior to exec. + let original = container.partials; + container.partials = extend({}, original, props.partials); + let ret = fn(context, options); + container.partials = original; + return ret; + }; + } + + props.partials[options.args[0]] = options.fn; + + return ret; + }); +} diff --git a/lib/handlebars/runtime.js b/lib/handlebars/runtime.js index de42752..6b31a7b 100644 --- a/lib/handlebars/runtime.js +++ b/lib/handlebars/runtime.js @@ -29,6 +29,8 @@ export function template(templateSpec, env) { throw new Exception('Unknown template object: ' + typeof templateSpec); } + templateSpec.main.decorator = templateSpec.main_d; + // Note: Using env.VM references rather than local var references throughout this section to allow // for external users to override these as psuedo-supported APIs. env.VM.checkRevision(templateSpec.compiler); @@ -90,7 +92,9 @@ export function template(templateSpec, env) { invokePartial: invokePartialWrapper, fn: function(i) { - return templateSpec[i]; + let ret = templateSpec[i]; + ret.decorator = templateSpec[i + '_d']; + return ret; }, programs: [], @@ -142,7 +146,11 @@ export function template(templateSpec, env) { } } - return '' + templateSpec.main(container, context, container.helpers, container.partials, data, blockParams, depths); + function main(context/*, options*/) { + return '' + templateSpec.main(container, context, container.helpers, container.partials, data, blockParams, depths); + } + main = executeDecorators(templateSpec.main, main, container, options.depths || [], data, blockParams); + return main(context, options); } ret.isTop = true; @@ -153,9 +161,13 @@ export function template(templateSpec, env) { if (templateSpec.usePartial) { container.partials = container.merge(options.partials, env.partials); } + if (templateSpec.useDecorators) { + container.decorators = container.merge(options.decorators, env.decorators); + } } else { container.helpers = options.helpers; container.partials = options.partials; + container.decorators = options.decorators; } }; @@ -186,6 +198,9 @@ export function wrapProgram(container, i, fn, data, declaredBlockParams, blockPa blockParams && [options.blockParams].concat(blockParams), currentDepths); } + + prog = executeDecorators(fn, prog, container, depths, data, blockParams); + prog.program = i; prog.depth = depths ? depths.length : 0; prog.blockParams = declaredBlockParams || 0; @@ -216,6 +231,10 @@ export function invokePartial(partial, context, options) { let partialBlock; if (options.fn && options.fn !== noop) { partialBlock = options.data['partial-block'] = options.fn; + + if (partialBlock.partials) { + options.partials = Utils.extend({}, options.partials, partialBlock.partials); + } } if (partial === undefined && partialBlock) { @@ -238,3 +257,12 @@ function initData(context, data) { } return data; } + +function executeDecorators(fn, prog, container, depths, data, blockParams) { + if (fn.decorator) { + let props = {}; + prog = fn.decorator(prog, props, container, depths && depths[0], data, blockParams, depths); + Utils.extend(prog, props); + } + return prog; +} diff --git a/spec/blocks.js b/spec/blocks.js index 71c9045..2fbaee7 100644 --- a/spec/blocks.js +++ b/spec/blocks.js @@ -166,4 +166,182 @@ describe('blocks', function() { shouldCompileTo(string, [hash, undefined, undefined, true], 'Goodbye cruel '); }); }); + + describe('decorators', function() { + it('should apply mustache decorators', function() { + var helpers = { + helper: function(options) { + return options.fn.run; + } + }; + var decorators = { + decorator: function(fn) { + fn.run = 'success'; + return fn; + } + }; + shouldCompileTo( + '{{#helper}}{{*decorator}}{{/helper}}', + {hash: {}, helpers: helpers, decorators: decorators}, + 'success'); + }); + it('should apply allow undefined return', function() { + var helpers = { + helper: function(options) { + return options.fn() + options.fn.run; + } + }; + var decorators = { + decorator: function(fn) { + fn.run = 'cess'; + } + }; + shouldCompileTo( + '{{#helper}}{{*decorator}}suc{{/helper}}', + {hash: {}, helpers: helpers, decorators: decorators}, + 'success'); + }); + + it('should apply block decorators', function() { + var helpers = { + helper: function(options) { + return options.fn.run; + } + }; + var decorators = { + decorator: function(fn, props, container, options) { + fn.run = options.fn(); + return fn; + } + }; + shouldCompileTo( + '{{#helper}}{{#*decorator}}success{{/decorator}}{{/helper}}', + {hash: {}, helpers: helpers, decorators: decorators}, + 'success'); + }); + it('should support nested decorators', function() { + var helpers = { + helper: function(options) { + return options.fn.run; + } + }; + var decorators = { + decorator: function(fn, props, container, options) { + fn.run = options.fn.nested + options.fn(); + return fn; + }, + nested: function(fn, props, container, options) { + props.nested = options.fn(); + } + }; + shouldCompileTo( + '{{#helper}}{{#*decorator}}{{#*nested}}suc{{/nested}}cess{{/decorator}}{{/helper}}', + {hash: {}, helpers: helpers, decorators: decorators}, + 'success'); + }); + + it('should apply multiple decorators', function() { + var helpers = { + helper: function(options) { + return options.fn.run; + } + }; + var decorators = { + decorator: function(fn, props, container, options) { + fn.run = (fn.run || '') + options.fn(); + return fn; + } + }; + shouldCompileTo( + '{{#helper}}{{#*decorator}}suc{{/decorator}}{{#*decorator}}cess{{/decorator}}{{/helper}}', + {hash: {}, helpers: helpers, decorators: decorators}, + 'success'); + }); + + it('should access parent variables', function() { + var helpers = { + helper: function(options) { + return options.fn.run; + } + }; + var decorators = { + decorator: function(fn, props, container, options) { + fn.run = options.args; + return fn; + } + }; + shouldCompileTo( + '{{#helper}}{{*decorator foo}}{{/helper}}', + {hash: {'foo': 'success'}, helpers: helpers, decorators: decorators}, + 'success'); + }); + it('should work with root program', function() { + var run; + var decorators = { + decorator: function(fn, props, container, options) { + equals(options.args[0], 'success'); + run = true; + return fn; + } + }; + shouldCompileTo( + '{{*decorator "success"}}', + {hash: {'foo': 'success'}, decorators: decorators}, + ''); + equals(run, true); + }); + it('should fail when accessing variables from root', function() { + var run; + var decorators = { + decorator: function(fn, props, container, options) { + equals(options.args[0], undefined); + run = true; + return fn; + } + }; + shouldCompileTo( + '{{*decorator foo}}', + {hash: {'foo': 'fail'}, decorators: decorators}, + ''); + equals(run, true); + }); + + describe('registration', function() { + it('unregisters', function() { + handlebarsEnv.decorators = {}; + + handlebarsEnv.registerDecorator('foo', function() { + return 'fail'; + }); + + equals(!!handlebarsEnv.decorators.foo, true); + handlebarsEnv.unregisterDecorator('foo'); + equals(handlebarsEnv.decorators.foo, undefined); + }); + + it('allows multiple globals', function() { + handlebarsEnv.decorators = {}; + + handlebarsEnv.registerDecorator({ + foo: function() {}, + bar: function() {} + }); + + equals(!!handlebarsEnv.decorators.foo, true); + equals(!!handlebarsEnv.decorators.bar, true); + handlebarsEnv.unregisterDecorator('foo'); + handlebarsEnv.unregisterDecorator('bar'); + equals(handlebarsEnv.decorators.foo, undefined); + equals(handlebarsEnv.decorators.bar, undefined); + }); + it('fails with multiple and args', function() { + shouldThrow(function() { + handlebarsEnv.registerDecorator({ + world: function() { return 'world!'; }, + testHelper: function() { return 'found it!'; } + }, {}); + }, Error, 'Arg not supported with multiple decorators'); + }); + }); + }); }); diff --git a/spec/parser.js b/spec/parser.js index 5b60f93..3b7e3e4 100644 --- a/spec/parser.js +++ b/spec/parser.js @@ -247,6 +247,20 @@ describe('parser', function() { }); }); + describe('directives', function() { + it('should parse block directives', function() { + equals(astFor('{{#* foo}}{{/foo}}'), 'DIRECTIVE BLOCK:\n PATH:foo []\n PROGRAM:\n'); + }); + it('should parse directives', function() { + equals(astFor('{{* foo}}'), '{{ DIRECTIVE PATH:foo [] }}\n'); + }); + it('should fail if directives have inverse', function() { + shouldThrow(function() { + astFor('{{#* foo}}{{^}}{{/foo}}'); + }, Error, /Unexpected inverse/); + }); + }); + it('GH1024 - should track program location properly', function() { var p = Handlebars.parse('\n' + ' {{#if foo}}\n' diff --git a/spec/partials.js b/spec/partials.js index 314cca2..f3283ba 100644 --- a/spec/partials.js +++ b/spec/partials.js @@ -257,6 +257,45 @@ describe('partials', function() { }); }); + describe('inline partials', function() { + it('should define inline partials for template', function() { + shouldCompileTo('{{#*inline "myPartial"}}success{{/inline}}{{> myPartial}}', {}, 'success'); + }); + it('should overwrite multiple partials in the same template', function() { + shouldCompileTo('{{#*inline "myPartial"}}fail{{/inline}}{{#*inline "myPartial"}}success{{/inline}}{{> myPartial}}', {}, 'success'); + }); + it('should define inline partials for block', function() { + shouldCompileTo('{{#with .}}{{#*inline "myPartial"}}success{{/inline}}{{> myPartial}}{{/with}}', {}, 'success'); + shouldThrow(function() { + shouldCompileTo('{{#with .}}{{#*inline "myPartial"}}success{{/inline}}{{/with}}{{> myPartial}}', {}, 'success'); + }, Error, /myPartial could not/); + }); + it('should override global partials', function() { + shouldCompileTo('{{#*inline "myPartial"}}success{{/inline}}{{> myPartial}}', {hash: {}, partials: {myPartial: function() { return 'fail'; }}}, 'success'); + }); + it('should override template partials', function() { + shouldCompileTo('{{#*inline "myPartial"}}fail{{/inline}}{{#with .}}{{#*inline "myPartial"}}success{{/inline}}{{> myPartial}}{{/with}}', {}, 'success'); + }); + it('should override partials down the entire stack', function() { + shouldCompileTo('{{#with .}}{{#*inline "myPartial"}}success{{/inline}}{{#with .}}{{#with .}}{{> myPartial}}{{/with}}{{/with}}{{/with}}', {}, 'success'); + }); + + it('should define inline partials for partial call', function() { + shouldCompileToWithPartials( + '{{#*inline "myPartial"}}success{{/inline}}{{> dude}}', + [{}, {}, {dude: '{{> myPartial }}'}], + true, + 'success'); + }); + it('should define inline partials in partial block call', function() { + shouldCompileToWithPartials( + '{{#> dude}}{{#*inline "myPartial"}}success{{/inline}}{{/dude}}', + [{}, {}, {dude: '{{> myPartial }}'}], + true, + 'success'); + }); + }); + it('should pass compiler flags', function() { if (Handlebars.compile) { var env = Handlebars.create(); diff --git a/spec/runtime.js b/spec/runtime.js index 502a843..a4830ad 100644 --- a/spec/runtime.js +++ b/spec/runtime.js @@ -14,19 +14,19 @@ describe('runtime', function() { it('should throw on version mismatch', function() { shouldThrow(function() { Handlebars.template({ - main: true, + main: {}, compiler: [Handlebars.COMPILER_REVISION + 1] }); }, Error, /Template was precompiled with a newer version of Handlebars than the current runtime/); shouldThrow(function() { Handlebars.template({ - main: true, + main: {}, compiler: [Handlebars.COMPILER_REVISION - 1] }); }, Error, /Template was precompiled with an older version of Handlebars than the current runtime/); shouldThrow(function() { Handlebars.template({ - main: true + main: {} }); }, Error, /Template was precompiled with an older version of Handlebars than the current runtime/); }); diff --git a/spec/tokenizer.js b/spec/tokenizer.js index f170704..dc077ce 100644 --- a/spec/tokenizer.js +++ b/spec/tokenizer.js @@ -241,6 +241,15 @@ describe('Tokenizer', function() { shouldMatchTokens(result, ['OPEN_BLOCK', 'ID', 'CLOSE', 'CONTENT', 'OPEN_ENDBLOCK', 'ID', 'CLOSE']); }); + it('tokenizes directives', function() { + shouldMatchTokens( + tokenize('{{#*foo}}content{{/foo}}'), + ['OPEN_BLOCK', 'ID', 'CLOSE', 'CONTENT', 'OPEN_ENDBLOCK', 'ID', 'CLOSE']); + shouldMatchTokens( + tokenize('{{*foo}}'), + ['OPEN', 'ID', 'CLOSE']); + }); + it('tokenizes inverse sections as "INVERSE"', function() { shouldMatchTokens(tokenize('{{^}}'), ['INVERSE']); shouldMatchTokens(tokenize('{{else}}'), ['INVERSE']); diff --git a/spec/visitor.js b/spec/visitor.js index 3e2d523..d3fb795 100644 --- a/spec/visitor.js +++ b/spec/visitor.js @@ -9,6 +9,8 @@ describe('Visitor', function() { var visitor = new Handlebars.Visitor(); visitor.accept(Handlebars.parse('{{foo}}{{#foo (bar 1 "1" true undefined null) foo=@data}}{{!comment}}{{> bar }} {{/foo}}')); visitor.accept(Handlebars.parse('{{#> bar }} {{/bar}}')); + visitor.accept(Handlebars.parse('{{#* bar }} {{/bar}}')); + visitor.accept(Handlebars.parse('{{* bar }}')); }); it('should traverse to stubs', function() { diff --git a/src/handlebars.l b/src/handlebars.l index 39a7884..4c3c304 100644 --- a/src/handlebars.l +++ b/src/handlebars.l @@ -81,7 +81,7 @@ ID [^\s!"#%-,\.\/;->@\[-\^`\{-~]+/{LOOKAHEAD} } <mu>"{{"{LEFT_STRIP}?">" return 'OPEN_PARTIAL'; <mu>"{{"{LEFT_STRIP}?"#>" return 'OPEN_PARTIAL_BLOCK'; -<mu>"{{"{LEFT_STRIP}?"#" return 'OPEN_BLOCK'; +<mu>"{{"{LEFT_STRIP}?"#""*"? return 'OPEN_BLOCK'; <mu>"{{"{LEFT_STRIP}?"/" return 'OPEN_ENDBLOCK'; <mu>"{{"{LEFT_STRIP}?"^"\s*{RIGHT_STRIP}?"}}" this.popState(); return 'INVERSE'; <mu>"{{"{LEFT_STRIP}?\s*"else"\s*{RIGHT_STRIP}?"}}" this.popState(); return 'INVERSE'; @@ -98,7 +98,7 @@ ID [^\s!"#%-,\.\/;->@\[-\^`\{-~]+/{LOOKAHEAD} this.popState(); return 'COMMENT'; } -<mu>"{{"{LEFT_STRIP}? return 'OPEN'; +<mu>"{{"{LEFT_STRIP}?"*"? return 'OPEN'; <mu>"=" return 'EQUALS'; <mu>".." return 'ID'; diff --git a/src/handlebars.yy b/src/handlebars.yy index e94ab51..ce06498 100644 --- a/src/handlebars.yy +++ b/src/handlebars.yy @@ -52,7 +52,7 @@ block ; openBlock - : OPEN_BLOCK helperName param* hash? blockParams? CLOSE -> { path: $2, params: $3, hash: $4, blockParams: $5, strip: yy.stripFlags($1, $6) } + : OPEN_BLOCK helperName param* hash? blockParams? CLOSE -> { open: $1, path: $2, params: $3, hash: $4, blockParams: $5, strip: yy.stripFlags($1, $6) } ; openInverse |