diff --git a/docs/compiler-api.md b/docs/compiler-api.md index 98ca8943c..74af67246 100644 --- a/docs/compiler-api.md +++ b/docs/compiler-api.md @@ -204,6 +204,12 @@ var scanner = new ImportScanner(); scanner.accept(ast); ``` +The current node's ancestors will be maintained in the `parents` array, with the most recent parent listed first. + +The visitor may also be configured to operate in mutation mode by setting the `mutation` field to true. When in this mode, handler methods may return any valid AST node and it will replace the one they are currently operating on. Returning `false` will remove the given value (if valid) and returning `undefined` will leave the node in tact. This return structure only apply to mutation mode and non-mutation mode visitors are free to return whatever values they wish. + +Implementors that may need to support mutation mode are encouraged to utilize the `acceptKey`, `acceptRequired` and `acceptArray` helpers which provide the conditional overwrite behavior as well as implement sanity checks where pertinent. + ## JavaScript Compiler The `Handlebars.JavaScriptCompiler` object has a number of methods that may be customized to alter the output of the compiler: diff --git a/lib/handlebars/compiler/visitor.js b/lib/handlebars/compiler/visitor.js index c2480adc5..3fb37fbfc 100644 --- a/lib/handlebars/compiler/visitor.js +++ b/lib/handlebars/compiler/visitor.js @@ -1,66 +1,109 @@ -/*jshint unused: false */ -function Visitor() {} +import Exception from "../exception"; +import AST from "./ast"; + +function Visitor() { + this.parents = []; +} Visitor.prototype = { constructor: Visitor, + mutating: false, - accept: function(object) { - return object && this[object.type](object); + // Visits a given value. If mutating, will replace the value if necessary. + acceptKey: function(node, name) { + var value = this.accept(node[name]); + if (this.mutating) { + // Hacky sanity check: + if (value && (!value.type || !AST[value.type])) { + throw new Exception('Unexpected node type "' + value.type + '" found when accepting ' + name + ' on ' + node.type); + } + node[name] = value; + } }, - Program: function(program) { - var body = program.body, - i, l; + // Performs an accept operation with added sanity check to ensure + // required keys are not removed. + acceptRequired: function(node, name) { + this.acceptKey(node, name); - for(i=0, l=body.length; i bar }} {{/foo.bar}}')); }); + + it('should return undefined'); + + describe('mutating', function() { + describe('fields', function() { + it('should replace value', function() { + var visitor = new Handlebars.Visitor(); + + visitor.mutating = true; + visitor.StringLiteral = function(string) { + return new Handlebars.AST.NumberLiteral(42, string.locInfo); + }; + + var ast = Handlebars.parse('{{foo foo="foo"}}'); + visitor.accept(ast); + equals(Handlebars.print(ast), '{{ PATH:foo [] HASH{foo=NUMBER{42}} }}\n'); + }); + it('should treat undefined resonse as identity', function() { + var visitor = new Handlebars.Visitor(); + visitor.mutating = true; + + var ast = Handlebars.parse('{{foo foo=42}}'); + visitor.accept(ast); + equals(Handlebars.print(ast), '{{ PATH:foo [] HASH{foo=NUMBER{42}} }}\n'); + }); + it('should remove false responses', function() { + var visitor = new Handlebars.Visitor(); + + visitor.mutating = true; + visitor.Hash = function() { + return false; + }; + + var ast = Handlebars.parse('{{foo foo=42}}'); + visitor.accept(ast); + equals(Handlebars.print(ast), '{{ PATH:foo [] }}\n'); + }); + it('should throw when removing required values', function() { + shouldThrow(function() { + var visitor = new Handlebars.Visitor(); + + visitor.mutating = true; + visitor.SubExpression = function() { + return false; + }; + + var ast = Handlebars.parse('{{foo 42}}'); + visitor.accept(ast); + }, Handlebars.Exception, 'MustacheStatement requires sexpr'); + }); + it('should throw when returning non-node responses', function() { + shouldThrow(function() { + var visitor = new Handlebars.Visitor(); + + visitor.mutating = true; + visitor.SubExpression = function() { + return {}; + }; + + var ast = Handlebars.parse('{{foo 42}}'); + visitor.accept(ast); + }, Handlebars.Exception, 'Unexpected node type "undefined" found when accepting sexpr on MustacheStatement'); + }); + }); + describe('arrays', function() { + it('should replace value', function() { + var visitor = new Handlebars.Visitor(); + + visitor.mutating = true; + visitor.StringLiteral = function(string) { + return new Handlebars.AST.NumberLiteral(42, string.locInfo); + }; + + var ast = Handlebars.parse('{{foo "foo"}}'); + visitor.accept(ast); + equals(Handlebars.print(ast), '{{ PATH:foo [NUMBER{42}] }}\n'); + }); + it('should treat undefined resonse as identity', function() { + var visitor = new Handlebars.Visitor(); + visitor.mutating = true; + + var ast = Handlebars.parse('{{foo 42}}'); + visitor.accept(ast); + equals(Handlebars.print(ast), '{{ PATH:foo [NUMBER{42}] }}\n'); + }); + it('should remove false responses', function() { + var visitor = new Handlebars.Visitor(); + + visitor.mutating = true; + visitor.NumberLiteral = function() { + return false; + }; + + var ast = Handlebars.parse('{{foo 42}}'); + visitor.accept(ast); + equals(Handlebars.print(ast), '{{ PATH:foo [] }}\n'); + }); + }); + }); });