From 61dd721ca2a9055036d0f350970d033ca5227411 Mon Sep 17 00:00:00 2001 From: kpdecker Date: Wed, 26 Nov 2014 09:01:03 -0600 Subject: [PATCH 01/15] Update AST location info to match SpiderMonkey Part of #889 --- lib/handlebars/compiler/ast.js | 36 ++++++-------- lib/handlebars/compiler/base.js | 7 ++- lib/handlebars/compiler/code-gen.js | 8 ++-- lib/handlebars/compiler/compiler.js | 10 ++-- lib/handlebars/compiler/helpers.js | 22 +++++---- .../compiler/javascript-compiler.js | 2 +- lib/handlebars/exception.js | 17 ++++--- spec/ast.js | 39 ++++++++------- src/handlebars.yy | 48 +++++++++---------- 9 files changed, 95 insertions(+), 94 deletions(-) diff --git a/lib/handlebars/compiler/ast.js b/lib/handlebars/compiler/ast.js index 0bc70e93a..363c25209 100644 --- a/lib/handlebars/compiler/ast.js +++ b/lib/handlebars/compiler/ast.js @@ -1,16 +1,8 @@ import Exception from "../exception"; -function LocationInfo(locInfo) { - locInfo = locInfo || {}; - this.firstLine = locInfo.first_line; - this.firstColumn = locInfo.first_column; - this.lastColumn = locInfo.last_column; - this.lastLine = locInfo.last_line; -} - var AST = { ProgramNode: function(statements, blockParams, strip, locInfo) { - LocationInfo.call(this, locInfo); + this.loc = locInfo; this.type = "program"; this.statements = statements; this.blockParams = blockParams; @@ -18,7 +10,7 @@ var AST = { }, MustacheNode: function(rawParams, hash, open, strip, locInfo) { - LocationInfo.call(this, locInfo); + this.loc = locInfo; this.type = "mustache"; this.strip = strip; @@ -47,7 +39,7 @@ var AST = { }, SexprNode: function(rawParams, hash, locInfo) { - LocationInfo.call(this, locInfo); + this.loc = locInfo; this.type = "sexpr"; this.hash = hash; @@ -70,7 +62,7 @@ var AST = { }, PartialNode: function(partialName, context, hash, strip, locInfo) { - LocationInfo.call(this, locInfo); + this.loc = locInfo; this.type = "partial"; this.partialName = partialName; this.context = context; @@ -81,7 +73,7 @@ var AST = { }, BlockNode: function(sexpr, program, inverse, strip, locInfo) { - LocationInfo.call(this, locInfo); + this.loc = locInfo; this.type = 'block'; this.sexpr = sexpr; @@ -95,19 +87,19 @@ var AST = { }, ContentNode: function(string, locInfo) { - LocationInfo.call(this, locInfo); + this.loc = locInfo; this.type = "content"; this.original = this.string = string; }, HashNode: function(pairs, locInfo) { - LocationInfo.call(this, locInfo); + this.loc = locInfo; this.type = "hash"; this.pairs = pairs; }, IdNode: function(parts, locInfo) { - LocationInfo.call(this, locInfo); + this.loc = locInfo; this.type = "ID"; var original = "", @@ -147,13 +139,13 @@ var AST = { }, PartialNameNode: function(name, locInfo) { - LocationInfo.call(this, locInfo); + this.loc = locInfo; this.type = "PARTIAL_NAME"; this.name = name.original; }, DataNode: function(id, locInfo) { - LocationInfo.call(this, locInfo); + this.loc = locInfo; this.type = "DATA"; this.id = id; this.stringModeValue = id.stringModeValue; @@ -161,7 +153,7 @@ var AST = { }, StringNode: function(string, locInfo) { - LocationInfo.call(this, locInfo); + this.loc = locInfo; this.type = "STRING"; this.original = this.string = @@ -169,7 +161,7 @@ var AST = { }, NumberNode: function(number, locInfo) { - LocationInfo.call(this, locInfo); + this.loc = locInfo; this.type = "NUMBER"; this.original = this.number = number; @@ -177,14 +169,14 @@ var AST = { }, BooleanNode: function(bool, locInfo) { - LocationInfo.call(this, locInfo); + this.loc = locInfo; this.type = "BOOLEAN"; this.bool = bool; this.stringModeValue = bool === "true"; }, CommentNode: function(comment, strip, locInfo) { - LocationInfo.call(this, locInfo); + this.loc = locInfo; this.type = "comment"; this.comment = comment; diff --git a/lib/handlebars/compiler/base.js b/lib/handlebars/compiler/base.js index 13784637e..df6b92887 100644 --- a/lib/handlebars/compiler/base.js +++ b/lib/handlebars/compiler/base.js @@ -8,11 +8,16 @@ export { parser }; var yy = {}; extend(yy, Helpers, AST); -export function parse(input) { +export function parse(input, options) { // Just return if an already-compile AST was passed in. if (input.constructor === AST.ProgramNode) { return input; } parser.yy = yy; + // Altering the shared object here, but this is ok as parser is a sync operation + yy.locInfo = function(locInfo) { + return new yy.SourceLocation(options && options.srcName, locInfo); + }; + return parser.parse(input); } diff --git a/lib/handlebars/compiler/code-gen.js b/lib/handlebars/compiler/code-gen.js index 7d1b4ca2e..30f0735e1 100644 --- a/lib/handlebars/compiler/code-gen.js +++ b/lib/handlebars/compiler/code-gen.js @@ -79,18 +79,18 @@ CodeGen.prototype = { }, empty: function(loc) { - loc = loc || this.currentLocation || {}; - return new SourceNode(loc.firstLine, loc.firstColumn, this.srcFile); + loc = loc || this.currentLocation || {start:{}}; + return new SourceNode(loc.start.line, loc.start.column, this.srcFile); }, wrap: function(chunk, loc) { if (chunk instanceof SourceNode) { return chunk; } - loc = loc || this.currentLocation || {}; + loc = loc || this.currentLocation || {start:{}}; chunk = castChunk(chunk, this, loc); - return new SourceNode(loc.firstLine, loc.firstColumn, this.srcFile, chunk); + return new SourceNode(loc.start.line, loc.start.column, this.srcFile, chunk); }, functionCall: function(fn, type, params) { diff --git a/lib/handlebars/compiler/compiler.js b/lib/handlebars/compiler/compiler.js index 1e5d07afa..d80bfac11 100644 --- a/lib/handlebars/compiler/compiler.js +++ b/lib/handlebars/compiler/compiler.js @@ -298,11 +298,7 @@ Compiler.prototype = { // HELPERS opcode: function(name, node) { - var loc = { - firstLine: node.firstLine, firstColumn: node.firstColumn, - lastLine: node.lastLine, lastColumn: node.lastColumn - }; - this.opcodes.push({ opcode: name, args: slice.call(arguments, 2), loc: loc }); + this.opcodes.push({ opcode: name, args: slice.call(arguments, 2), loc: node.loc }); }, addDepth: function(depth) { @@ -393,7 +389,7 @@ export function precompile(input, options, env) { options.useDepths = true; } - var ast = env.parse(input); + var ast = env.parse(input, options); var environment = new env.Compiler().compile(ast, options); return new env.JavaScriptCompiler().compile(environment, options); } @@ -415,7 +411,7 @@ export function compile(input, options, env) { var compiled; function compileInput() { - var ast = env.parse(input); + var ast = env.parse(input, options); var environment = new env.Compiler().compile(ast, options); var templateSpec = new env.JavaScriptCompiler().compile(environment, options, undefined, true); return env.template(templateSpec); diff --git a/lib/handlebars/compiler/helpers.js b/lib/handlebars/compiler/helpers.js index 50a3c53a3..3a3ece661 100644 --- a/lib/handlebars/compiler/helpers.js +++ b/lib/handlebars/compiler/helpers.js @@ -1,5 +1,17 @@ import Exception from "../exception"; +export function SourceLocation(source, locInfo) { + this.source = source; + this.start = { + line: locInfo.first_line, + column: locInfo.first_column + }; + this.end = { + line: locInfo.last_line, + column: locInfo.last_column + }; +} + export function stripFlags(open, close) { return { left: open.charAt(2) === '~', @@ -15,10 +27,7 @@ export function stripComment(comment) { export function prepareRawBlock(openRawBlock, content, close, locInfo) { /*jshint -W040 */ if (openRawBlock.sexpr.id.original !== close) { - var errorNode = { - firstLine: openRawBlock.sexpr.firstLine, - firstColumn: openRawBlock.sexpr.firstColumn - }; + var errorNode = {loc: openRawBlock.sexpr.loc}; throw new Exception(openRawBlock.sexpr.id.original + " doesn't match " + close, errorNode); } @@ -32,10 +41,7 @@ export function prepareBlock(openBlock, program, inverseAndProgram, close, inver /*jshint -W040 */ // When we are chaining inverse calls, we will not have a close path if (close && close.path && openBlock.sexpr.id.original !== close.path.original) { - var errorNode = { - firstLine: openBlock.sexpr.firstLine, - firstColumn: openBlock.sexpr.firstColumn - }; + var errorNode = {loc: openBlock.sexpr.loc}; throw new Exception(openBlock.sexpr.id.original + ' doesn\'t match ' + close.path.original, errorNode); } diff --git a/lib/handlebars/compiler/javascript-compiler.js b/lib/handlebars/compiler/javascript-compiler.js index ba84a3ef7..639aa4118 100644 --- a/lib/handlebars/compiler/javascript-compiler.js +++ b/lib/handlebars/compiler/javascript-compiler.js @@ -136,7 +136,7 @@ JavaScriptCompiler.prototype = { if (!asObject) { ret.compiler = JSON.stringify(ret.compiler); - this.source.currentLocation = {firstLine: 1, firstColumn: 0}; + this.source.currentLocation = {start: {line: 1, column: 0}}; ret = this.objectLiteral(ret); if (options.srcName) { diff --git a/lib/handlebars/exception.js b/lib/handlebars/exception.js index 8c5c2f669..3fde1c14f 100644 --- a/lib/handlebars/exception.js +++ b/lib/handlebars/exception.js @@ -2,11 +2,14 @@ var errorProps = ['description', 'fileName', 'lineNumber', 'message', 'name', 'number', 'stack']; function Exception(message, node) { - var line; - if (node && node.firstLine) { - line = node.firstLine; - - message += ' - ' + line + ':' + node.firstColumn; + var loc = node && node.loc, + line, + column; + if (loc) { + line = loc.start.line; + column = loc.start.column; + + message += ' - ' + line + ':' + column; } var tmp = Error.prototype.constructor.call(this, message); @@ -16,9 +19,9 @@ function Exception(message, node) { this[errorProps[idx]] = tmp[errorProps[idx]]; } - if (line) { + if (loc) { this.lineNumber = line; - this.column = node.firstColumn; + this.column = column; } } diff --git a/spec/ast.js b/spec/ast.js index ef2ef68f5..83b24b999 100644 --- a/spec/ast.js +++ b/spec/ast.js @@ -5,22 +5,21 @@ describe('ast', function() { } var LOCATION_INFO = { - last_line: 0, - first_line: 0, - first_column: 0, - last_column: 0 + start: { + line: 1, + column: 1 + }, + end: { + line: 1, + column: 1 + } }; function testLocationInfoStorage(node){ - var properties = [ 'firstLine', 'lastLine', 'firstColumn', 'lastColumn' ], - property, - propertiesLen = properties.length, - i; - - for (i = 0; i < propertiesLen; i++){ - property = properties[0]; - equals(node[property], 0); - } + equals(node.loc.start.line, 1); + equals(node.loc.start.column, 1); + equals(node.loc.end.line, 1); + equals(node.loc.end.column, 1); } describe('MustacheNode', function() { @@ -94,21 +93,21 @@ describe('ast', function() { {part: 'foo'}, {part: '..'}, {part: 'bar'} - ], {first_line: 1, first_column: 1}); + ], {start: {line: 1, column: 1}}); }, Handlebars.Exception, "Invalid path: foo.. - 1:1"); shouldThrow(function() { new handlebarsEnv.AST.IdNode([ {part: 'foo'}, {part: '.'}, {part: 'bar'} - ], {first_line: 1, first_column: 1}); + ], {start: {line: 1, column: 1}}); }, Handlebars.Exception, "Invalid path: foo. - 1:1"); shouldThrow(function() { new handlebarsEnv.AST.IdNode([ {part: 'foo'}, {part: 'this'}, {part: 'bar'} - ], {first_line: 1, first_column: 1}); + ], {start: {line: 1, column: 1}}); }, Handlebars.Exception, "Invalid path: foothis - 1:1"); }); @@ -201,10 +200,10 @@ describe('ast', function() { var ast, statements; function testColumns(node, firstLine, lastLine, firstColumn, lastColumn){ - equals(node.firstLine, firstLine); - equals(node.lastLine, lastLine); - equals(node.firstColumn, firstColumn); - equals(node.lastColumn, lastColumn); + equals(node.loc.start.line, firstLine); + equals(node.loc.start.column, firstColumn); + equals(node.loc.end.line, lastLine); + equals(node.loc.end.column, lastColumn); } ast = Handlebars.parse("line 1 {{line1Token}}\n line 2 {{line2token}}\n line 3 {{#blockHelperOnLine3}}\nline 4{{line4token}}\n" + diff --git a/src/handlebars.yy b/src/handlebars.yy index 775d5ca0f..f5c69ff6f 100644 --- a/src/handlebars.yy +++ b/src/handlebars.yy @@ -9,7 +9,7 @@ root ; program - : statement* -> new yy.ProgramNode(yy.prepareProgram($1), null, {}, @$) + : statement* -> new yy.ProgramNode(yy.prepareProgram($1), null, {}, yy.locInfo(@$)) ; statement @@ -18,15 +18,15 @@ statement | rawBlock -> $1 | partial -> $1 | content -> $1 - | COMMENT -> new yy.CommentNode(yy.stripComment($1), yy.stripFlags($1, $1), @$) + | COMMENT -> new yy.CommentNode(yy.stripComment($1), yy.stripFlags($1, $1), yy.locInfo(@$)) ; content - : CONTENT -> new yy.ContentNode($1, @$) + : CONTENT -> new yy.ContentNode($1, yy.locInfo(@$)) ; rawBlock - : openRawBlock content END_RAW_BLOCK -> yy.prepareRawBlock($1, $2, $3, @$) + : openRawBlock content END_RAW_BLOCK -> yy.prepareRawBlock($1, $2, $3, yy.locInfo(@$)) ; openRawBlock @@ -34,8 +34,8 @@ openRawBlock ; block - : openBlock program inverseChain? closeBlock -> yy.prepareBlock($1, $2, $3, $4, false, @$) - | openInverse program inverseAndProgram? closeBlock -> yy.prepareBlock($1, $2, $3, $4, true, @$) + : openBlock program inverseChain? closeBlock -> yy.prepareBlock($1, $2, $3, $4, false, yy.locInfo(@$)) + | openInverse program inverseAndProgram? closeBlock -> yy.prepareBlock($1, $2, $3, $4, true, yy.locInfo(@$)) ; openBlock @@ -47,7 +47,7 @@ openInverse ; openInverseChain - : OPEN_INVERSE_CHAIN sexpr CLOSE -> new yy.MustacheNode($2, null, $1, yy.stripFlags($1, $3), @$) + : OPEN_INVERSE_CHAIN sexpr CLOSE -> new yy.MustacheNode($2, null, $1, yy.stripFlags($1, $3), yy.locInfo(@$)) ; inverseAndProgram @@ -56,8 +56,8 @@ inverseAndProgram inverseChain : openInverseChain program inverseChain? { - var inverse = yy.prepareBlock($1, $2, $3, $3, false, @$), - program = new yy.ProgramNode(yy.prepareProgram([inverse]), null, {}, @$); + var inverse = yy.prepareBlock($1, $2, $3, $3, false, yy.locInfo(@$)), + program = new yy.ProgramNode(yy.prepareProgram([inverse]), null, {}, yy.locInfo(@$)); program.inverse = inverse; @@ -73,31 +73,31 @@ closeBlock mustache // Parsing out the '&' escape token at AST level saves ~500 bytes after min due to the removal of one parser node. // This also allows for handler unification as all mustache node instances can utilize the same handler - : OPEN sexpr CLOSE -> new yy.MustacheNode($2, null, $1, yy.stripFlags($1, $3), @$) - | OPEN_UNESCAPED sexpr CLOSE_UNESCAPED -> new yy.MustacheNode($2, null, $1, yy.stripFlags($1, $3), @$) + : OPEN sexpr CLOSE -> new yy.MustacheNode($2, null, $1, yy.stripFlags($1, $3), yy.locInfo(@$)) + | OPEN_UNESCAPED sexpr CLOSE_UNESCAPED -> new yy.MustacheNode($2, null, $1, yy.stripFlags($1, $3), yy.locInfo(@$)) ; partial - : OPEN_PARTIAL partialName param hash? CLOSE -> new yy.PartialNode($2, $3, $4, yy.stripFlags($1, $5), @$) - | OPEN_PARTIAL partialName hash? CLOSE -> new yy.PartialNode($2, undefined, $3, yy.stripFlags($1, $4), @$) + : OPEN_PARTIAL partialName param hash? CLOSE -> new yy.PartialNode($2, $3, $4, yy.stripFlags($1, $5), yy.locInfo(@$)) + | OPEN_PARTIAL partialName hash? CLOSE -> new yy.PartialNode($2, undefined, $3, yy.stripFlags($1, $4), yy.locInfo(@$)) ; sexpr - : path param* hash? -> new yy.SexprNode([$1].concat($2), $3, @$) - | dataName -> new yy.SexprNode([$1], null, @$) + : path param* hash? -> new yy.SexprNode([$1].concat($2), $3, yy.locInfo(@$)) + | dataName -> new yy.SexprNode([$1], null, yy.locInfo(@$)) ; param : path -> $1 - | STRING -> new yy.StringNode($1, @$) - | NUMBER -> new yy.NumberNode($1, @$) - | BOOLEAN -> new yy.BooleanNode($1, @$) + | STRING -> new yy.StringNode($1, yy.locInfo(@$)) + | NUMBER -> new yy.NumberNode($1, yy.locInfo(@$)) + | BOOLEAN -> new yy.BooleanNode($1, yy.locInfo(@$)) | dataName -> $1 | OPEN_SEXPR sexpr CLOSE_SEXPR {$2.isHelper = true; $$ = $2;} ; hash - : hashSegment+ -> new yy.HashNode($1, @$) + : hashSegment+ -> new yy.HashNode($1, yy.locInfo(@$)) ; hashSegment @@ -109,17 +109,17 @@ blockParams ; partialName - : path -> new yy.PartialNameNode($1, @$) - | STRING -> new yy.PartialNameNode(new yy.StringNode($1, @$), @$) - | NUMBER -> new yy.PartialNameNode(new yy.NumberNode($1, @$)) + : path -> new yy.PartialNameNode($1, yy.locInfo(@$)) + | STRING -> new yy.PartialNameNode(new yy.StringNode($1, yy.locInfo(@$)), yy.locInfo(@$)) + | NUMBER -> new yy.PartialNameNode(new yy.NumberNode($1, yy.locInfo(@$))) ; dataName - : DATA path -> new yy.DataNode($2, @$) + : DATA path -> new yy.DataNode($2, yy.locInfo(@$)) ; path - : pathSegments -> new yy.IdNode($1, @$) + : pathSegments -> new yy.IdNode($1, yy.locInfo(@$)) ; pathSegments From df421f1e4b43773c61bc7c2c3cd06e0ff9ec4905 Mon Sep 17 00:00:00 2001 From: kpdecker Date: Wed, 26 Nov 2014 09:18:33 -0600 Subject: [PATCH 02/15] Update ProgramNode to better match SpiderMonkey --- lib/handlebars/compiler/ast.js | 5 +- lib/handlebars/compiler/base.js | 2 +- lib/handlebars/compiler/compiler.js | 14 ++-- lib/handlebars/compiler/helpers.js | 78 +++++++++---------- lib/handlebars/compiler/printer.js | 16 ++-- lib/handlebars/compiler/visitor.js | 8 +- spec/ast.js | 112 ++++++++++++++-------------- src/handlebars.yy | 2 +- 8 files changed, 119 insertions(+), 118 deletions(-) diff --git a/lib/handlebars/compiler/ast.js b/lib/handlebars/compiler/ast.js index 363c25209..a0ed42e28 100644 --- a/lib/handlebars/compiler/ast.js +++ b/lib/handlebars/compiler/ast.js @@ -3,8 +3,9 @@ import Exception from "../exception"; var AST = { ProgramNode: function(statements, blockParams, strip, locInfo) { this.loc = locInfo; - this.type = "program"; - this.statements = statements; + this.type = 'Program'; + this.body = statements; + this.blockParams = blockParams; this.strip = strip; }, diff --git a/lib/handlebars/compiler/base.js b/lib/handlebars/compiler/base.js index df6b92887..786c37ebd 100644 --- a/lib/handlebars/compiler/base.js +++ b/lib/handlebars/compiler/base.js @@ -10,7 +10,7 @@ extend(yy, Helpers, AST); export function parse(input, options) { // Just return if an already-compile AST was passed in. - if (input.constructor === AST.ProgramNode) { return input; } + if (input.type === 'Program') { return input; } parser.yy = yy; diff --git a/lib/handlebars/compiler/compiler.js b/lib/handlebars/compiler/compiler.js index d80bfac11..683e3e50b 100644 --- a/lib/handlebars/compiler/compiler.js +++ b/lib/handlebars/compiler/compiler.js @@ -74,12 +74,12 @@ Compiler.prototype = { return this[node.type](node); }, - program: function(program) { - var statements = program.statements; - - for(var i=0, l=statements.length; i partial }} '); - equals(ast.statements[1].string, ''); + equals(ast.body[1].string, ''); }); it('marks indented partial as standalone', function() { var ast = Handlebars.parse(' {{> partial }} '); - equals(ast.statements[0].string, ''); - equals(ast.statements[1].indent, ' '); - equals(ast.statements[2].string, ''); + equals(ast.body[0].string, ''); + equals(ast.body[1].indent, ' '); + equals(ast.body[2].string, ''); }); it('marks those around content as not standalone', function() { var ast = Handlebars.parse('a{{> partial }}'); - equals(ast.statements[0].omit, undefined); + equals(ast.body[0].omit, undefined); ast = Handlebars.parse('{{> partial }}a'); - equals(ast.statements[1].omit, undefined); + equals(ast.body[1].omit, undefined); }); }); describe('comments', function() { it('marks comment as standalone', function() { var ast = Handlebars.parse('{{! comment }} '); - equals(ast.statements[1].string, ''); + equals(ast.body[1].string, ''); }); it('marks indented comment as standalone', function() { var ast = Handlebars.parse(' {{! comment }} '); - equals(ast.statements[0].string, ''); - equals(ast.statements[2].string, ''); + equals(ast.body[0].string, ''); + equals(ast.body[2].string, ''); }); it('marks those around content as not standalone', function() { var ast = Handlebars.parse('a{{! comment }}'); - equals(ast.statements[0].omit, undefined); + equals(ast.body[0].omit, undefined); ast = Handlebars.parse('{{! comment }}a'); - equals(ast.statements[1].omit, undefined); + equals(ast.body[1].omit, undefined); }); }); }); diff --git a/src/handlebars.yy b/src/handlebars.yy index f5c69ff6f..27d12e565 100644 --- a/src/handlebars.yy +++ b/src/handlebars.yy @@ -5,7 +5,7 @@ %% root - : program EOF { yy.prepareProgram($1.statements, true); return $1; } + : program EOF { yy.prepareProgram($1.body, true); return $1; } ; program From 5cfe6a0be16a912a5f00af251753a3d5746a6577 Mon Sep 17 00:00:00 2001 From: kpdecker Date: Wed, 26 Nov 2014 09:31:46 -0600 Subject: [PATCH 03/15] Update MustacheNode for new AST structure --- lib/handlebars/compiler/ast.js | 21 +++++---------------- lib/handlebars/compiler/compiler.js | 2 +- lib/handlebars/compiler/printer.js | 8 ++++---- lib/handlebars/compiler/visitor.js | 8 ++++---- spec/ast.js | 13 ++++--------- src/handlebars.yy | 6 +++--- 6 files changed, 21 insertions(+), 37 deletions(-) diff --git a/lib/handlebars/compiler/ast.js b/lib/handlebars/compiler/ast.js index a0ed42e28..2f95cfb96 100644 --- a/lib/handlebars/compiler/ast.js +++ b/lib/handlebars/compiler/ast.js @@ -10,10 +10,11 @@ var AST = { this.strip = strip; }, - MustacheNode: function(rawParams, hash, open, strip, locInfo) { + MustacheNode: function(rawParams, open, strip, locInfo) { this.loc = locInfo; - this.type = "mustache"; - this.strip = strip; + this.type = 'MustacheStatement'; + + this.sexpr = rawParams; // Open may be a string parsed from the parser or a passed boolean flag if (open != null && open.charAt) { @@ -24,19 +25,7 @@ var AST = { this.escaped = !!open; } - if (rawParams instanceof AST.SexprNode) { - this.sexpr = rawParams; - } else { - // Support old AST API - this.sexpr = new AST.SexprNode(rawParams, hash); - } - - // Support old AST API that stored this info in MustacheNode - this.id = this.sexpr.id; - this.params = this.sexpr.params; - this.hash = this.sexpr.hash; - this.eligibleHelper = this.sexpr.eligibleHelper; - this.isHelper = this.sexpr.isHelper; + this.strip = strip; }, SexprNode: function(rawParams, hash, locInfo) { diff --git a/lib/handlebars/compiler/compiler.js b/lib/handlebars/compiler/compiler.js index 683e3e50b..69937a5a4 100644 --- a/lib/handlebars/compiler/compiler.js +++ b/lib/handlebars/compiler/compiler.js @@ -193,7 +193,7 @@ Compiler.prototype = { } }, - mustache: function(mustache) { + MustacheStatement: function(mustache) { this.sexpr(mustache.sexpr); if(mustache.escaped && !this.options.noEscape) { diff --git a/lib/handlebars/compiler/printer.js b/lib/handlebars/compiler/printer.js index 8c2346ec1..481c7be10 100644 --- a/lib/handlebars/compiler/printer.js +++ b/lib/handlebars/compiler/printer.js @@ -44,6 +44,10 @@ PrintVisitor.prototype.Program = function(program) { return out; }; +PrintVisitor.prototype.MustacheStatement = function(mustache) { + return this.pad('{{ ' + this.accept(mustache.sexpr) + ' }}'); +}; + PrintVisitor.prototype.block = function(block) { var out = ""; @@ -83,10 +87,6 @@ PrintVisitor.prototype.sexpr = function(sexpr) { return this.accept(sexpr.id) + " " + params + hash; }; -PrintVisitor.prototype.mustache = function(mustache) { - return this.pad("{{ " + this.accept(mustache.sexpr) + " }}"); -}; - PrintVisitor.prototype.partial = function(partial) { var content = this.accept(partial.partialName); if(partial.context) { diff --git a/lib/handlebars/compiler/visitor.js b/lib/handlebars/compiler/visitor.js index 12d822ce9..dc7f527fe 100644 --- a/lib/handlebars/compiler/visitor.js +++ b/lib/handlebars/compiler/visitor.js @@ -16,16 +16,16 @@ Visitor.prototype = { } }, + MustacheStatement: function(mustache) { + this.accept(mustache.sexpr); + }, + block: function(block) { this.accept(block.mustache); this.accept(block.program); this.accept(block.inverse); }, - mustache: function(mustache) { - this.accept(mustache.sexpr); - }, - sexpr: function(sexpr) { var params = sexpr.params, paramStrings = [], hash; diff --git a/spec/ast.js b/spec/ast.js index e13e9569e..d41a591da 100644 --- a/spec/ast.js +++ b/spec/ast.js @@ -24,21 +24,16 @@ describe('ast', function() { describe('MustacheNode', function() { function testEscape(open, expected) { - var mustache = new handlebarsEnv.AST.MustacheNode([{}], {}, open, false); + var mustache = new handlebarsEnv.AST.MustacheNode([{}], open, false); equals(mustache.escaped, expected); } it('should store args', function() { var id = {isSimple: true}, hash = {}, - mustache = new handlebarsEnv.AST.MustacheNode([id, 'param1'], hash, '', false, LOCATION_INFO); - equals(mustache.type, 'mustache'); - equals(mustache.hash, hash); + mustache = new handlebarsEnv.AST.MustacheNode([id, 'param1'], '', false, LOCATION_INFO); + equals(mustache.type, 'MustacheStatement'); equals(mustache.escaped, true); - equals(mustache.id, id); - equals(mustache.params.length, 1); - equals(mustache.params[0], 'param1'); - equals(!!mustache.isHelper, true); testLocationInfoStorage(mustache); }); it('should accept token for escape', function() { @@ -280,7 +275,7 @@ describe('ast', function() { block = ast.body[0]; equals(block.program.body[0].string, ''); - equals(block.program.body[1].id.original, 'foo'); + equals(block.program.body[1].sexpr.id.original, 'foo'); equals(block.program.body[2].string, '\n'); }); it('marks nested block mustaches as standalone', function() { diff --git a/src/handlebars.yy b/src/handlebars.yy index 27d12e565..d7d117cf1 100644 --- a/src/handlebars.yy +++ b/src/handlebars.yy @@ -47,7 +47,7 @@ openInverse ; openInverseChain - : OPEN_INVERSE_CHAIN sexpr CLOSE -> new yy.MustacheNode($2, null, $1, yy.stripFlags($1, $3), yy.locInfo(@$)) + : OPEN_INVERSE_CHAIN sexpr CLOSE -> new yy.MustacheNode($2, $1, yy.stripFlags($1, $3), yy.locInfo(@$)) ; inverseAndProgram @@ -73,8 +73,8 @@ closeBlock mustache // Parsing out the '&' escape token at AST level saves ~500 bytes after min due to the removal of one parser node. // This also allows for handler unification as all mustache node instances can utilize the same handler - : OPEN sexpr CLOSE -> new yy.MustacheNode($2, null, $1, yy.stripFlags($1, $3), yy.locInfo(@$)) - | OPEN_UNESCAPED sexpr CLOSE_UNESCAPED -> new yy.MustacheNode($2, null, $1, yy.stripFlags($1, $3), yy.locInfo(@$)) + : OPEN sexpr CLOSE -> new yy.MustacheNode($2, $1, yy.stripFlags($1, $3), yy.locInfo(@$)) + | OPEN_UNESCAPED sexpr CLOSE_UNESCAPED -> new yy.MustacheNode($2, $1, yy.stripFlags($1, $3), yy.locInfo(@$)) ; partial From 2a4819d75cba7513946af5cf28ee22881561814f Mon Sep 17 00:00:00 2001 From: kpdecker Date: Wed, 26 Nov 2014 12:46:38 -0600 Subject: [PATCH 04/15] Update statement node ASTs --- lib/handlebars/compiler/ast.js | 81 ++++++++++------------- lib/handlebars/compiler/compiler.js | 99 +++++++++++++++-------------- lib/handlebars/compiler/helpers.js | 22 +++---- lib/handlebars/compiler/printer.js | 51 +++++++-------- lib/handlebars/compiler/visitor.js | 27 ++++---- spec/ast.js | 63 ++++++++---------- spec/parser.js | 2 + spec/partials.js | 6 ++ spec/visitor.js | 14 ++-- src/handlebars.yy | 13 ++-- 10 files changed, 179 insertions(+), 199 deletions(-) diff --git a/lib/handlebars/compiler/ast.js b/lib/handlebars/compiler/ast.js index 2f95cfb96..26ea4ab5c 100644 --- a/lib/handlebars/compiler/ast.js +++ b/lib/handlebars/compiler/ast.js @@ -28,6 +28,41 @@ var AST = { this.strip = strip; }, + BlockNode: function(sexpr, program, inverse, strip, locInfo) { + this.loc = locInfo; + + this.type = 'BlockStatement'; + this.sexpr = sexpr; + this.program = program; + this.inverse = inverse; + this.strip = strip; + }, + + PartialNode: function(sexpr, strip, locInfo) { + this.loc = locInfo; + this.type = 'PartialStatement'; + this.sexpr = sexpr; + this.indent = ''; + + this.strip = strip; + this.strip.inlineStandalone = true; + }, + + ContentNode: function(string, locInfo) { + this.loc = locInfo; + this.type = 'ContentStatement'; + this.original = this.value = string; + }, + + CommentNode: function(comment, strip, locInfo) { + this.loc = locInfo; + this.type = 'CommentStatement'; + this.value = comment; + + this.strip = strip; + strip.inlineStandalone = true; + }, + SexprNode: function(rawParams, hash, locInfo) { this.loc = locInfo; @@ -51,37 +86,6 @@ var AST = { // pass or at runtime. }, - PartialNode: function(partialName, context, hash, strip, locInfo) { - this.loc = locInfo; - this.type = "partial"; - this.partialName = partialName; - this.context = context; - this.hash = hash; - this.strip = strip; - - this.strip.inlineStandalone = true; - }, - - BlockNode: function(sexpr, program, inverse, strip, locInfo) { - this.loc = locInfo; - - this.type = 'block'; - this.sexpr = sexpr; - this.program = program; - this.inverse = inverse; - this.strip = strip; - - if (inverse && !program) { - this.isInverse = true; - } - }, - - ContentNode: function(string, locInfo) { - this.loc = locInfo; - this.type = "content"; - this.original = this.string = string; - }, - HashNode: function(pairs, locInfo) { this.loc = locInfo; this.type = "hash"; @@ -128,12 +132,6 @@ var AST = { this.stringModeValue = this.string; }, - PartialNameNode: function(name, locInfo) { - this.loc = locInfo; - this.type = "PARTIAL_NAME"; - this.name = name.original; - }, - DataNode: function(id, locInfo) { this.loc = locInfo; this.type = "DATA"; @@ -163,15 +161,6 @@ var AST = { this.type = "BOOLEAN"; this.bool = bool; this.stringModeValue = bool === "true"; - }, - - CommentNode: function(comment, strip, locInfo) { - this.loc = locInfo; - this.type = "comment"; - this.comment = comment; - - this.strip = strip; - strip.inlineStandalone = true; } }; diff --git a/lib/handlebars/compiler/compiler.js b/lib/handlebars/compiler/compiler.js index 69937a5a4..ea2272a1b 100644 --- a/lib/handlebars/compiler/compiler.js +++ b/lib/handlebars/compiler/compiler.js @@ -107,24 +107,19 @@ Compiler.prototype = { return guid; }, - block: function(block) { + BlockStatement: function(block) { var sexpr = block.sexpr, program = block.program, inverse = block.inverse; - if (program) { - program = this.compileProgram(program); - } - - if (inverse) { - inverse = this.compileProgram(inverse); - } + program = program && this.compileProgram(program); + inverse = inverse && this.compileProgram(inverse); var type = this.classifySexpr(sexpr); - if (type === "helper") { + if (type === 'helper') { this.helperSexpr(sexpr, program, inverse); - } else if (type === "simple") { + } else if (type === 'simple') { this.simpleSexpr(sexpr); // now that the simple mustache is resolved, we need to @@ -147,32 +142,23 @@ Compiler.prototype = { this.opcode('append', block); }, - hash: function(hash) { - var pairs = hash.pairs, i, l; - - this.opcode('pushHash', hash); - - for(i=0, l=pairs.length; i 1) { + throw new Exception('Unsupported number of partial arguments: ' + params.length, partial); + } + + this.pushParam(params[0]); } else { this.opcode('getContext', partial, 0); this.opcode('pushContext', partial); @@ -183,16 +169,10 @@ Compiler.prototype = { this.opcode('appendContent', partial, indent); indent = ''; } - this.opcode('invokePartial', partial, partialName.name, indent); + this.opcode('invokePartial', partial, partialName, indent); this.opcode('append', partial); }, - content: function(content) { - if (content.string) { - this.opcode('appendContent', content, content.string); - } - }, - MustacheStatement: function(mustache) { this.sexpr(mustache.sexpr); @@ -203,6 +183,25 @@ Compiler.prototype = { } }, + ContentStatement: function(content) { + if (content.value) { + this.opcode('appendContent', content, content.value); + } + }, + + CommentStatement: function() {}, + + sexpr: function(sexpr) { + var type = this.classifySexpr(sexpr); + + if (type === "simple") { + this.simpleSexpr(sexpr); + } else if (type === "helper") { + this.helperSexpr(sexpr); + } else { + this.ambiguousSexpr(sexpr); + } + }, ambiguousSexpr: function(sexpr, program, inverse) { var id = sexpr.id, name = id.parts[0], @@ -252,16 +251,18 @@ Compiler.prototype = { } }, - sexpr: function(sexpr) { - var type = this.classifySexpr(sexpr); + hash: function(hash) { + var pairs = hash.pairs, i, l; - if (type === "simple") { - this.simpleSexpr(sexpr); - } else if (type === "helper") { - this.helperSexpr(sexpr); - } else { - this.ambiguousSexpr(sexpr); + this.opcode('pushHash', hash); + + for(i=0, l=pairs.length; i ' + content + ' }}'); +}; + +PrintVisitor.prototype.ContentStatement = function(content) { + return this.pad("CONTENT[ '" + content.value + "' ]"); +}; + +PrintVisitor.prototype.CommentStatement = function(comment) { + return this.pad("{{! '" + comment.value + "' }}"); +}; + PrintVisitor.prototype.sexpr = function(sexpr) { var params = sexpr.params, paramStrings = [], hash; @@ -87,17 +107,6 @@ PrintVisitor.prototype.sexpr = function(sexpr) { return this.accept(sexpr.id) + " " + params + hash; }; -PrintVisitor.prototype.partial = function(partial) { - var content = this.accept(partial.partialName); - if(partial.context) { - content += " " + this.accept(partial.context); - } - if (partial.hash) { - content += " " + this.accept(partial.hash); - } - return this.pad("{{> " + content + " }}"); -}; - PrintVisitor.prototype.hash = function(hash) { var pairs = hash.pairs; var joinedPairs = [], left, right; @@ -132,19 +141,7 @@ PrintVisitor.prototype.ID = function(id) { } }; -PrintVisitor.prototype.PARTIAL_NAME = function(partialName) { - return "PARTIAL:" + partialName.name; -}; - PrintVisitor.prototype.DATA = function(data) { return "@" + this.accept(data.id); }; -PrintVisitor.prototype.content = function(content) { - return this.pad("CONTENT[ '" + content.string + "' ]"); -}; - -PrintVisitor.prototype.comment = function(comment) { - return this.pad("{{! '" + comment.comment + "' }}"); -}; - diff --git a/lib/handlebars/compiler/visitor.js b/lib/handlebars/compiler/visitor.js index dc7f527fe..e48c9bcee 100644 --- a/lib/handlebars/compiler/visitor.js +++ b/lib/handlebars/compiler/visitor.js @@ -4,7 +4,7 @@ Visitor.prototype = { constructor: Visitor, accept: function(object) { - return object && this[object.type] && this[object.type](object); + return object && this[object.type](object); }, Program: function(program) { @@ -20,12 +20,21 @@ Visitor.prototype = { this.accept(mustache.sexpr); }, - block: function(block) { - this.accept(block.mustache); + BlockStatement: function(block) { + this.accept(block.sexpr); this.accept(block.program); this.accept(block.inverse); }, + PartialStatement: function(partial) { + this.accept(partial.partialName); + this.accept(partial.context); + this.accept(partial.hash); + }, + + ContentStatement: function(content) {}, + CommentStatement: function(comment) {}, + sexpr: function(sexpr) { var params = sexpr.params, paramStrings = [], hash; @@ -44,13 +53,6 @@ Visitor.prototype = { } }, - partial: function(partial) { - this.accept(partial.partialName); - this.accept(partial.context); - this.accept(partial.hash); - }, - PARTIAL_NAME: function(partialName) {}, - DATA: function(data) { this.accept(data.id); }, @@ -58,10 +60,7 @@ Visitor.prototype = { STRING: function(string) {}, NUMBER: function(number) {}, BOOLEAN: function(bool) {}, - ID: function(id) {}, - - content: function(content) {}, - comment: function(comment) {} + ID: function(id) {} }; export default Visitor; diff --git a/spec/ast.js b/spec/ast.js index d41a591da..dc6e136ec 100644 --- a/spec/ast.js +++ b/spec/ast.js @@ -168,18 +168,9 @@ describe('ast', function() { }); }); - describe("PartialNameNode", function(){ - - it('stores location info', function(){ - var pnn = new handlebarsEnv.AST.PartialNameNode({original: "YES"}, LOCATION_INFO); - testLocationInfoStorage(pnn); - }); - }); - describe("PartialNode", function(){ - it('stores location info', function(){ - var pn = new handlebarsEnv.AST.PartialNode("so_partial", {}, {}, {}, LOCATION_INFO); + var pn = new handlebarsEnv.AST.PartialNode('so_partial', {}, LOCATION_INFO); testLocationInfoStorage(pn); }); }); @@ -248,8 +239,8 @@ describe('ast', function() { describe('mustache', function() { it('does not mark mustaches as standalone', function() { var ast = Handlebars.parse(' {{comment}} '); - equals(!!ast.body[0].string, true); - equals(!!ast.body[2].string, true); + equals(!!ast.body[0].value, true); + equals(!!ast.body[2].value, true); }); }); describe('blocks', function() { @@ -257,38 +248,38 @@ describe('ast', function() { var ast = Handlebars.parse(' {{# comment}} \nfoo\n {{else}} \n bar \n {{/comment}} '), block = ast.body[1]; - equals(ast.body[0].string, ''); + equals(ast.body[0].value, ''); - equals(block.program.body[0].string, 'foo\n'); - equals(block.inverse.body[0].string, ' bar \n'); + equals(block.program.body[0].value, 'foo\n'); + equals(block.inverse.body[0].value, ' bar \n'); - equals(ast.body[2].string, ''); + equals(ast.body[2].value, ''); }); it('marks initial block mustaches as standalone', function() { var ast = Handlebars.parse('{{# comment}} \nfoo\n {{/comment}}'), block = ast.body[0]; - equals(block.program.body[0].string, 'foo\n'); + equals(block.program.body[0].value, 'foo\n'); }); it('marks mustaches with children as standalone', function() { var ast = Handlebars.parse('{{# comment}} \n{{foo}}\n {{/comment}}'), block = ast.body[0]; - equals(block.program.body[0].string, ''); + equals(block.program.body[0].value, ''); equals(block.program.body[1].sexpr.id.original, 'foo'); - equals(block.program.body[2].string, '\n'); + equals(block.program.body[2].value, '\n'); }); it('marks nested block mustaches as standalone', function() { var ast = Handlebars.parse('{{#foo}} \n{{# comment}} \nfoo\n {{else}} \n bar \n {{/comment}} \n{{/foo}}'), body = ast.body[0].program.body, block = body[1]; - equals(body[0].string, ''); + equals(body[0].value, ''); - equals(block.program.body[0].string, 'foo\n'); - equals(block.inverse.body[0].string, ' bar \n'); + equals(block.program.body[0].value, 'foo\n'); + equals(block.inverse.body[0].value, ' bar \n'); - equals(body[0].string, ''); + equals(body[0].value, ''); }); it('does not mark nested block mustaches as standalone', function() { var ast = Handlebars.parse('{{#foo}} {{# comment}} \nfoo\n {{else}} \n bar \n {{/comment}} {{/foo}}'), @@ -297,8 +288,8 @@ describe('ast', function() { equals(body[0].omit, undefined); - equals(block.program.body[0].string, ' \nfoo\n'); - equals(block.inverse.body[0].string, ' bar \n '); + equals(block.program.body[0].value, ' \nfoo\n'); + equals(block.inverse.body[0].value, ' bar \n '); equals(body[0].omit, undefined); }); @@ -307,8 +298,8 @@ describe('ast', function() { body = ast.body[0].program.body, block = body[0]; - equals(block.program.body[0].string, ' \nfoo\n'); - equals(block.inverse.body[0].string, ' bar \n '); + equals(block.program.body[0].value, ' \nfoo\n'); + equals(block.inverse.body[0].value, ' bar \n '); equals(body[0].omit, undefined); }); @@ -319,22 +310,22 @@ describe('ast', function() { equals(ast.body[0].omit, undefined); - equals(block.program.body[0].string, 'foo\n'); - equals(block.inverse.body[0].string, ' bar \n'); + equals(block.program.body[0].value, 'foo\n'); + equals(block.inverse.body[0].value, ' bar \n'); - equals(ast.body[2].string, ''); + equals(ast.body[2].value, ''); }); }); describe('partials', function() { it('marks partial as standalone', function() { var ast = Handlebars.parse('{{> partial }} '); - equals(ast.body[1].string, ''); + equals(ast.body[1].value, ''); }); it('marks indented partial as standalone', function() { var ast = Handlebars.parse(' {{> partial }} '); - equals(ast.body[0].string, ''); + equals(ast.body[0].value, ''); equals(ast.body[1].indent, ' '); - equals(ast.body[2].string, ''); + equals(ast.body[2].value, ''); }); it('marks those around content as not standalone', function() { var ast = Handlebars.parse('a{{> partial }}'); @@ -347,12 +338,12 @@ describe('ast', function() { describe('comments', function() { it('marks comment as standalone', function() { var ast = Handlebars.parse('{{! comment }} '); - equals(ast.body[1].string, ''); + equals(ast.body[1].value, ''); }); it('marks indented comment as standalone', function() { var ast = Handlebars.parse(' {{! comment }} '); - equals(ast.body[0].string, ''); - equals(ast.body[2].string, ''); + equals(ast.body[0].value, ''); + equals(ast.body[2].value, ''); }); it('marks those around content as not standalone', function() { var ast = Handlebars.parse('a{{! comment }}'); diff --git a/spec/parser.js b/spec/parser.js index 056922905..f2b6e41e4 100644 --- a/spec/parser.js +++ b/spec/parser.js @@ -82,6 +82,8 @@ describe('parser', function() { it('parses a partial', function() { equals(ast_for("{{> foo }}"), "{{> PARTIAL:foo }}\n"); + equals(ast_for("{{> 'foo' }}"), "{{> PARTIAL:foo }}\n"); + equals(ast_for("{{> 1 }}"), "{{> PARTIAL:1 }}\n"); }); it('parses a partial with context', function() { diff --git a/spec/partials.js b/spec/partials.js index a1e05388e..b1509423a 100644 --- a/spec/partials.js +++ b/spec/partials.js @@ -23,6 +23,12 @@ describe('partials', function() { shouldCompileToWithPartials(string, [hash, {}, {dude: partial}], true, "Dudes: Empty"); }); + it('partials with duplicate parameters', function() { + shouldThrow(function() { + CompilerContext.compile('Dudes: {{>dude dudes foo bar=baz}}'); + }, Error, 'Unsupported number of partial arguments: 2 - 1:7'); + }); + it("partials with parameters", function() { var string = "Dudes: {{#dudes}}{{> dude others=..}}{{/dudes}}"; var partial = "{{others.foo}}{{name}} ({{url}}) "; diff --git a/spec/visitor.js b/spec/visitor.js index b64dc5698..15a0f029b 100644 --- a/spec/visitor.js +++ b/spec/visitor.js @@ -14,16 +14,12 @@ describe('Visitor', function() { // Simply run the thing and make sure it does not fail and that all of the // stub methods are executed var visitor = new Handlebars.Visitor(); - visitor.accept(Handlebars.parse('{{#foo (bar 1 "1" true) foo=@data}}{{!comment}}{{> bar }} {{/foo}}')); + visitor.accept(Handlebars.parse('{{foo}}{{#foo (bar 1 "1" true) foo=@data}}{{!comment}}{{> bar }} {{/foo}}')); }); it('should traverse to stubs', function() { var visitor = new Handlebars.Visitor(); - visitor.PARTIAL_NAME = function(partialName) { - equal(partialName.name, 'bar'); - }; - visitor.STRING = function(string) { equal(string.string, '2'); }; @@ -36,11 +32,11 @@ describe('Visitor', function() { visitor.ID = function(id) { equal(id.original, 'foo.bar'); }; - visitor.content = function(content) { - equal(content.string, ' '); + visitor.ContentStatement = function(content) { + equal(content.value, ' '); }; - visitor.comment = function(comment) { - equal(comment.comment, 'comment'); + visitor.CommentStatement = function(comment) { + equal(comment.value, 'comment'); }; visitor.accept(Handlebars.parse('{{#foo.bar (foo.bar 1 "2" true) foo=@foo.bar}}{{!comment}}{{> bar }} {{/foo.bar}}')); diff --git a/src/handlebars.yy b/src/handlebars.yy index d7d117cf1..0bd6fde87 100644 --- a/src/handlebars.yy +++ b/src/handlebars.yy @@ -78,12 +78,11 @@ mustache ; partial - : OPEN_PARTIAL partialName param hash? CLOSE -> new yy.PartialNode($2, $3, $4, yy.stripFlags($1, $5), yy.locInfo(@$)) - | OPEN_PARTIAL partialName hash? CLOSE -> new yy.PartialNode($2, undefined, $3, yy.stripFlags($1, $4), yy.locInfo(@$)) + : OPEN_PARTIAL sexpr CLOSE -> new yy.PartialNode($2, yy.stripFlags($1, $3), yy.locInfo(@$)) ; sexpr - : path param* hash? -> new yy.SexprNode([$1].concat($2), $3, yy.locInfo(@$)) + : helperName param* hash? -> new yy.SexprNode([$1].concat($2), $3, yy.locInfo(@$)) | dataName -> new yy.SexprNode([$1], null, yy.locInfo(@$)) ; @@ -108,10 +107,10 @@ blockParams : OPEN_BLOCK_PARAMS ID+ CLOSE_BLOCK_PARAMS -> $2 ; -partialName - : path -> new yy.PartialNameNode($1, yy.locInfo(@$)) - | STRING -> new yy.PartialNameNode(new yy.StringNode($1, yy.locInfo(@$)), yy.locInfo(@$)) - | NUMBER -> new yy.PartialNameNode(new yy.NumberNode($1, yy.locInfo(@$))) +helperName + : path -> $1 + | STRING -> new yy.StringNode($1, yy.locInfo(@$)), yy.locInfo(@$) + | NUMBER -> new yy.NumberNode($1, yy.locInfo(@$)) ; dataName From 697bbe59cad06bc74a945f7e26fc0af333a01d47 Mon Sep 17 00:00:00 2001 From: kpdecker Date: Wed, 26 Nov 2014 19:22:09 -0600 Subject: [PATCH 05/15] Update literal ast nodes for new spec --- lib/handlebars/compiler/ast.js | 15 ++++++--------- lib/handlebars/compiler/code-gen.js | 2 +- lib/handlebars/compiler/compiler.js | 14 +++++++------- lib/handlebars/compiler/printer.js | 12 ++++++------ lib/handlebars/compiler/visitor.js | 6 +++--- spec/string-params.js | 25 +++++++++++++------------ spec/visitor.js | 12 ++++++------ 7 files changed, 42 insertions(+), 44 deletions(-) diff --git a/lib/handlebars/compiler/ast.js b/lib/handlebars/compiler/ast.js index 26ea4ab5c..44b26ecbe 100644 --- a/lib/handlebars/compiler/ast.js +++ b/lib/handlebars/compiler/ast.js @@ -142,25 +142,22 @@ var AST = { StringNode: function(string, locInfo) { this.loc = locInfo; - this.type = "STRING"; + this.type = 'StringLiteral'; this.original = - this.string = - this.stringModeValue = string; + this.value = string; }, NumberNode: function(number, locInfo) { this.loc = locInfo; - this.type = "NUMBER"; + this.type = 'NumberLiteral'; this.original = - this.number = number; - this.stringModeValue = Number(number); + this.value = Number(number); }, BooleanNode: function(bool, locInfo) { this.loc = locInfo; - this.type = "BOOLEAN"; - this.bool = bool; - this.stringModeValue = bool === "true"; + this.type = 'BooleanLiteral'; + this.value = bool === 'true'; } }; diff --git a/lib/handlebars/compiler/code-gen.js b/lib/handlebars/compiler/code-gen.js index 30f0735e1..a7c16590d 100644 --- a/lib/handlebars/compiler/code-gen.js +++ b/lib/handlebars/compiler/code-gen.js @@ -99,7 +99,7 @@ CodeGen.prototype = { }, quotedString: function(str) { - return '"' + str + return '"' + (str + '') .replace(/\\/g, '\\\\') .replace(/"/g, '\\"') .replace(/\n/g, '\\n') diff --git a/lib/handlebars/compiler/compiler.js b/lib/handlebars/compiler/compiler.js index ea2272a1b..a5acb64c7 100644 --- a/lib/handlebars/compiler/compiler.js +++ b/lib/handlebars/compiler/compiler.js @@ -283,16 +283,16 @@ Compiler.prototype = { this.opcode('lookupData', data, data.id.depth, data.id.parts); }, - STRING: function(string) { - this.opcode('pushString', string, string.string); + StringLiteral: function(string) { + this.opcode('pushString', string, string.value); }, - NUMBER: function(number) { - this.opcode('pushLiteral', number, number.number); + NumberLiteral: function(number) { + this.opcode('pushLiteral', number, number.value); }, - BOOLEAN: function(bool) { - this.opcode('pushLiteral', bool, bool.bool); + BooleanLiteral: function(bool) { + this.opcode('pushLiteral', bool, bool.value); }, // HELPERS @@ -338,7 +338,7 @@ Compiler.prototype = { }, pushParam: function(val) { - var stringModeValue = val.stringModeValue != null ? val.stringModeValue : ''; + var stringModeValue = val.stringModeValue || (val.value != null ? val.value : ''); if (this.stringParams) { if(val.depth) { diff --git a/lib/handlebars/compiler/printer.js b/lib/handlebars/compiler/printer.js index dac8ec2d9..c86f7fa79 100644 --- a/lib/handlebars/compiler/printer.js +++ b/lib/handlebars/compiler/printer.js @@ -120,16 +120,16 @@ PrintVisitor.prototype.hash = function(hash) { return "HASH{" + joinedPairs.join(", ") + "}"; }; -PrintVisitor.prototype.STRING = function(string) { - return '"' + string.string + '"'; +PrintVisitor.prototype.StringLiteral = function(string) { + return '"' + string.value + '"'; }; -PrintVisitor.prototype.NUMBER = function(number) { - return "NUMBER{" + number.number + "}"; +PrintVisitor.prototype.NumberLiteral = function(number) { + return "NUMBER{" + number.value + "}"; }; -PrintVisitor.prototype.BOOLEAN = function(bool) { - return "BOOLEAN{" + bool.bool + "}"; +PrintVisitor.prototype.BooleanLiteral = function(bool) { + return "BOOLEAN{" + bool.value + "}"; }; PrintVisitor.prototype.ID = function(id) { diff --git a/lib/handlebars/compiler/visitor.js b/lib/handlebars/compiler/visitor.js index e48c9bcee..a2ad7bb5d 100644 --- a/lib/handlebars/compiler/visitor.js +++ b/lib/handlebars/compiler/visitor.js @@ -57,9 +57,9 @@ Visitor.prototype = { this.accept(data.id); }, - STRING: function(string) {}, - NUMBER: function(number) {}, - BOOLEAN: function(bool) {}, + StringLiteral: function(string) {}, + NumberLiteral: function(number) {}, + BooleanLiteral: function(bool) {}, ID: function(id) {} }; diff --git a/spec/string-params.js b/spec/string-params.js index 2e88cf15c..52ea5a8a2 100644 --- a/spec/string-params.js +++ b/spec/string-params.js @@ -1,3 +1,4 @@ +/*global CompilerContext */ describe('string params mode', function() { it("arguments to helpers can be retrieved from options hash in string form", function() { var template = CompilerContext.compile('{{wycats is.a slave.driver}}', {stringParams: true}); @@ -56,9 +57,9 @@ describe('string params mode', function() { var helpers = { tomdale: function(desire, noun, trueBool, falseBool, options) { - equal(options.types[0], 'STRING', "the string type is passed"); + equal(options.types[0], 'StringLiteral', "the string type is passed"); equal(options.types[1], 'ID', "the expression type is passed"); - equal(options.types[2], 'BOOLEAN', "the expression type is passed"); + equal(options.types[2], 'BooleanLiteral', "the expression type is passed"); equal(desire, "need", "the string form is passed for strings"); equal(noun, "dad.joke", "the string form is passed for expressions"); equal(trueBool, true, "raw booleans are passed through"); @@ -76,21 +77,21 @@ describe('string params mode', function() { var helpers = { tomdale: function(exclamation, options) { - equal(exclamation, "he.says"); - equal(options.types[0], "ID"); - - equal(options.hashTypes.desire, "STRING"); - equal(options.hashTypes.noun, "ID"); - equal(options.hashTypes.bool, "BOOLEAN"); - equal(options.hash.desire, "need"); - equal(options.hash.noun, "dad.joke"); + equal(exclamation, 'he.says'); + equal(options.types[0], 'ID'); + + equal(options.hashTypes.desire, 'StringLiteral'); + equal(options.hashTypes.noun, 'ID'); + equal(options.hashTypes.bool, 'BooleanLiteral'); + equal(options.hash.desire, 'need'); + equal(options.hash.noun, 'dad.joke'); equal(options.hash.bool, true); - return "Helper called"; + return 'Helper called'; } }; var result = template({}, { helpers: helpers }); - equal(result, "Helper called"); + equal(result, 'Helper called'); }); it("hash parameters get context information", function() { diff --git a/spec/visitor.js b/spec/visitor.js index 15a0f029b..e6f80f349 100644 --- a/spec/visitor.js +++ b/spec/visitor.js @@ -20,14 +20,14 @@ describe('Visitor', function() { it('should traverse to stubs', function() { var visitor = new Handlebars.Visitor(); - visitor.STRING = function(string) { - equal(string.string, '2'); + visitor.StringLiteral = function(string) { + equal(string.value, '2'); }; - visitor.NUMBER = function(number) { - equal(number.stringModeValue, 1); + visitor.NumberLiteral = function(number) { + equal(number.value, 1); }; - visitor.BOOLEAN = function(bool) { - equal(bool.stringModeValue, true); + visitor.BooleanLiteral = function(bool) { + equal(bool.value, true); }; visitor.ID = function(id) { equal(id.original, 'foo.bar'); From 5c921cafebee438fa27d417ae701b24323373a30 Mon Sep 17 00:00:00 2001 From: kpdecker Date: Wed, 26 Nov 2014 20:35:33 -0600 Subject: [PATCH 06/15] Replace DataNode and IdNode with PathNode This is a breaking change for string mode users as there is no longer a distinct type for data parameters. Instead data consumers should look for the @ prefix value. --- lib/handlebars/compiler/ast.js | 27 ++---- lib/handlebars/compiler/compiler.js | 39 ++++---- .../compiler/javascript-compiler.js | 2 +- lib/handlebars/compiler/printer.js | 20 ++--- lib/handlebars/compiler/visitor.js | 9 +- spec/ast.js | 18 ++-- spec/parser.js | 88 +++++++++---------- spec/string-params.js | 12 +-- spec/subexpressions.js | 4 +- spec/visitor.js | 4 +- src/handlebars.yy | 4 +- 11 files changed, 103 insertions(+), 124 deletions(-) diff --git a/lib/handlebars/compiler/ast.js b/lib/handlebars/compiler/ast.js index 44b26ecbe..287a9e554 100644 --- a/lib/handlebars/compiler/ast.js +++ b/lib/handlebars/compiler/ast.js @@ -92,11 +92,11 @@ var AST = { this.pairs = pairs; }, - IdNode: function(parts, locInfo) { + PathNode: function(data, parts, locInfo) { this.loc = locInfo; - this.type = "ID"; + this.type = 'PathExpression'; - var original = "", + var original = '', dig = [], depth = 0, depthString = ''; @@ -105,10 +105,10 @@ var AST = { var part = parts[i].part; original += (parts[i].separator || '') + part; - if (part === ".." || part === "." || part === "this") { + if (part === '..' || part === '.' || part === 'this') { if (dig.length > 0) { - throw new Exception("Invalid path: " + original, this); - } else if (part === "..") { + throw new Exception('Invalid path: ' + original, this); + } else if (part === '..') { depth++; depthString += '../'; } else { @@ -119,25 +119,14 @@ var AST = { } } - this.original = original; + this.data = data; + this.original = (data ? '@' : '') + original; this.parts = dig; - this.string = dig.join('.'); this.depth = depth; - this.idName = depthString + this.string; // an ID is simple if it only has one part, and that part is not // `..` or `this`. this.isSimple = parts.length === 1 && !this.isScoped && depth === 0; - - this.stringModeValue = this.string; - }, - - DataNode: function(id, locInfo) { - this.loc = locInfo; - this.type = "DATA"; - this.id = id; - this.stringModeValue = id.stringModeValue; - this.idName = '@' + id.stringModeValue; }, StringNode: function(string, locInfo) { diff --git a/lib/handlebars/compiler/compiler.js b/lib/handlebars/compiler/compiler.js index a5acb64c7..ea655838f 100644 --- a/lib/handlebars/compiler/compiler.js +++ b/lib/handlebars/compiler/compiler.js @@ -212,7 +212,7 @@ Compiler.prototype = { this.opcode('pushProgram', sexpr, program); this.opcode('pushProgram', sexpr, inverse); - this.ID(id); + this.accept(id); this.opcode('invokeAmbiguous', sexpr, name, isBlock); }, @@ -220,10 +220,8 @@ Compiler.prototype = { simpleSexpr: function(sexpr) { var id = sexpr.id; - if (id.type === 'DATA') { - this.DATA(id); - } else if (id.parts.length) { - this.ID(id); + if (id.parts.length) { + this.accept(id); } else { // Simplified ID for `this` this.addDepth(id.depth); @@ -246,7 +244,7 @@ Compiler.prototype = { } else { id.falsy = true; - this.ID(id); + this.accept(id); this.opcode('invokeHelper', sexpr, params.length, id.original, id.isSimple); } }, @@ -265,7 +263,7 @@ Compiler.prototype = { this.opcode('popHash', hash); }, - ID: function(id) { + PathExpression: function(id) { this.addDepth(id.depth); this.opcode('getContext', id, id.depth); @@ -273,16 +271,14 @@ Compiler.prototype = { if (!name) { // Context reference, i.e. `{{foo .}}` or `{{foo ..}}` this.opcode('pushContext', id); + } else if (id.data) { + this.options.data = true; + this.opcode('lookupData', id, id.depth, id.parts); } else { this.opcode('lookupOnContext', id, id.parts, id.falsy, id.isScoped); } }, - DATA: function(data) { - this.options.data = true; - this.opcode('lookupData', data, data.id.depth, data.id.parts); - }, - StringLiteral: function(string) { this.opcode('pushString', string, string.value); }, @@ -338,14 +334,20 @@ Compiler.prototype = { }, pushParam: function(val) { - var stringModeValue = val.stringModeValue || (val.value != null ? val.value : ''); + var value = val.value != null ? val.value : val.original || ''; if (this.stringParams) { + if (value.replace) { + value = value + .replace(/^(\.?\.\/)*/g, '') + .replace(/\//g, '.'); + } + if(val.depth) { this.addDepth(val.depth); } this.opcode('getContext', val, val.depth || 0); - this.opcode('pushStringParam', val, stringModeValue, val.type); + this.opcode('pushStringParam', val, value, val.type); if (val.type === 'sexpr') { // Subexpressions get evaluated and passed in @@ -354,7 +356,14 @@ Compiler.prototype = { } } else { if (this.trackIds) { - this.opcode('pushId', val, val.type, val.idName || stringModeValue); + value = val.original || value; + if (value.replace) { + value = value + .replace(/^\.\//g, '') + .replace(/^\.$/g, ''); + } + + this.opcode('pushId', val, val.type, value); } this.accept(val); } diff --git a/lib/handlebars/compiler/javascript-compiler.js b/lib/handlebars/compiler/javascript-compiler.js index 639aa4118..2916a3221 100644 --- a/lib/handlebars/compiler/javascript-compiler.js +++ b/lib/handlebars/compiler/javascript-compiler.js @@ -659,7 +659,7 @@ JavaScriptCompiler.prototype = { }, pushId: function(type, name) { - if (type === 'ID' || type === 'DATA') { + if (type === 'PathExpression') { this.pushString(name); } else if (type === 'sexpr') { this.pushStackLiteral('true'); diff --git a/lib/handlebars/compiler/printer.js b/lib/handlebars/compiler/printer.js index c86f7fa79..05e8ee7be 100644 --- a/lib/handlebars/compiler/printer.js +++ b/lib/handlebars/compiler/printer.js @@ -107,6 +107,12 @@ PrintVisitor.prototype.sexpr = function(sexpr) { return this.accept(sexpr.id) + " " + params + hash; }; +PrintVisitor.prototype.PathExpression = function(id) { + var path = id.parts.join('/'); + return (id.data ? '@' : '') + 'PATH:' + path; +}; + + PrintVisitor.prototype.hash = function(hash) { var pairs = hash.pairs; var joinedPairs = [], left, right; @@ -131,17 +137,3 @@ PrintVisitor.prototype.NumberLiteral = function(number) { PrintVisitor.prototype.BooleanLiteral = function(bool) { return "BOOLEAN{" + bool.value + "}"; }; - -PrintVisitor.prototype.ID = function(id) { - var path = id.parts.join("/"); - if(id.parts.length > 1) { - return "PATH:" + path; - } else { - return "ID:" + path; - } -}; - -PrintVisitor.prototype.DATA = function(data) { - return "@" + this.accept(data.id); -}; - diff --git a/lib/handlebars/compiler/visitor.js b/lib/handlebars/compiler/visitor.js index a2ad7bb5d..e191bbdc4 100644 --- a/lib/handlebars/compiler/visitor.js +++ b/lib/handlebars/compiler/visitor.js @@ -45,6 +45,8 @@ Visitor.prototype = { this.accept(sexpr.hash); }, + PathExpression: function(id) {}, + hash: function(hash) { var pairs = hash.pairs; @@ -53,14 +55,9 @@ Visitor.prototype = { } }, - DATA: function(data) { - this.accept(data.id); - }, - StringLiteral: function(string) {}, NumberLiteral: function(number) {}, - BooleanLiteral: function(bool) {}, - ID: function(id) {} + BooleanLiteral: function(bool) {} }; export default Visitor; diff --git a/spec/ast.js b/spec/ast.js index dc6e136ec..064bf6411 100644 --- a/spec/ast.js +++ b/spec/ast.js @@ -81,24 +81,24 @@ describe('ast', function() { testLocationInfoStorage(block); }); }); - describe('IdNode', function() { + describe('PathNode', function() { it('should throw on invalid path', function() { shouldThrow(function() { - new handlebarsEnv.AST.IdNode([ + new handlebarsEnv.AST.PathNode(false, [ {part: 'foo'}, {part: '..'}, {part: 'bar'} ], {start: {line: 1, column: 1}}); }, Handlebars.Exception, "Invalid path: foo.. - 1:1"); shouldThrow(function() { - new handlebarsEnv.AST.IdNode([ + new handlebarsEnv.AST.PathNode(false, [ {part: 'foo'}, {part: '.'}, {part: 'bar'} ], {start: {line: 1, column: 1}}); }, Handlebars.Exception, "Invalid path: foo. - 1:1"); shouldThrow(function() { - new handlebarsEnv.AST.IdNode([ + new handlebarsEnv.AST.PathNode(false, [ {part: 'foo'}, {part: 'this'}, {part: 'bar'} @@ -107,7 +107,7 @@ describe('ast', function() { }); it('stores location info', function(){ - var idNode = new handlebarsEnv.AST.IdNode([], LOCATION_INFO); + var idNode = new handlebarsEnv.AST.PathNode(false, [], LOCATION_INFO); testLocationInfoStorage(idNode); }); }); @@ -160,14 +160,6 @@ describe('ast', function() { }); }); - describe("DataNode", function(){ - - it('stores location info', function(){ - var data = new handlebarsEnv.AST.DataNode("YES", LOCATION_INFO); - testLocationInfoStorage(data); - }); - }); - describe("PartialNode", function(){ it('stores location info', function(){ var pn = new handlebarsEnv.AST.PartialNode('so_partial', {}, LOCATION_INFO); diff --git a/spec/parser.js b/spec/parser.js index f2b6e41e4..b25f222f9 100644 --- a/spec/parser.js +++ b/spec/parser.js @@ -10,19 +10,19 @@ describe('parser', function() { } it('parses simple mustaches', function() { - equals(ast_for('{{foo}}'), "{{ ID:foo [] }}\n"); - equals(ast_for('{{foo?}}'), "{{ ID:foo? [] }}\n"); - equals(ast_for('{{foo_}}'), "{{ ID:foo_ [] }}\n"); - equals(ast_for('{{foo-}}'), "{{ ID:foo- [] }}\n"); - equals(ast_for('{{foo:}}'), "{{ ID:foo: [] }}\n"); + equals(ast_for('{{foo}}'), "{{ PATH:foo [] }}\n"); + equals(ast_for('{{foo?}}'), "{{ PATH:foo? [] }}\n"); + equals(ast_for('{{foo_}}'), "{{ PATH:foo_ [] }}\n"); + equals(ast_for('{{foo-}}'), "{{ PATH:foo- [] }}\n"); + equals(ast_for('{{foo:}}'), "{{ PATH:foo: [] }}\n"); }); it('parses simple mustaches with data', function() { - equals(ast_for("{{@foo}}"), "{{ @ID:foo [] }}\n"); + equals(ast_for("{{@foo}}"), "{{ @PATH:foo [] }}\n"); }); it('parses simple mustaches with data paths', function() { - equals(ast_for("{{@../foo}}"), "{{ @ID:foo [] }}\n"); + equals(ast_for("{{@../foo}}"), "{{ @PATH:foo [] }}\n"); }); it('parses mustaches with paths', function() { @@ -30,54 +30,54 @@ describe('parser', function() { }); it('parses mustaches with this/foo', function() { - equals(ast_for("{{this/foo}}"), "{{ ID:foo [] }}\n"); + equals(ast_for("{{this/foo}}"), "{{ PATH:foo [] }}\n"); }); it('parses mustaches with - in a path', function() { - equals(ast_for("{{foo-bar}}"), "{{ ID:foo-bar [] }}\n"); + equals(ast_for("{{foo-bar}}"), "{{ PATH:foo-bar [] }}\n"); }); it('parses mustaches with parameters', function() { - equals(ast_for("{{foo bar}}"), "{{ ID:foo [ID:bar] }}\n"); + equals(ast_for("{{foo bar}}"), "{{ PATH:foo [PATH:bar] }}\n"); }); it('parses mustaches with string parameters', function() { - equals(ast_for("{{foo bar \"baz\" }}"), '{{ ID:foo [ID:bar, "baz"] }}\n'); + equals(ast_for("{{foo bar \"baz\" }}"), '{{ PATH:foo [PATH:bar, "baz"] }}\n'); }); it('parses mustaches with NUMBER parameters', function() { - equals(ast_for("{{foo 1}}"), "{{ ID:foo [NUMBER{1}] }}\n"); + equals(ast_for("{{foo 1}}"), "{{ PATH:foo [NUMBER{1}] }}\n"); }); it('parses mustaches with BOOLEAN parameters', function() { - equals(ast_for("{{foo true}}"), "{{ ID:foo [BOOLEAN{true}] }}\n"); - equals(ast_for("{{foo false}}"), "{{ ID:foo [BOOLEAN{false}] }}\n"); + equals(ast_for("{{foo true}}"), "{{ PATH:foo [BOOLEAN{true}] }}\n"); + equals(ast_for("{{foo false}}"), "{{ PATH:foo [BOOLEAN{false}] }}\n"); }); it('parses mutaches with DATA parameters', function() { - equals(ast_for("{{foo @bar}}"), "{{ ID:foo [@ID:bar] }}\n"); + equals(ast_for("{{foo @bar}}"), "{{ PATH:foo [@PATH:bar] }}\n"); }); it('parses mustaches with hash arguments', function() { - equals(ast_for("{{foo bar=baz}}"), "{{ ID:foo [] HASH{bar=ID:baz} }}\n"); - equals(ast_for("{{foo bar=1}}"), "{{ ID:foo [] HASH{bar=NUMBER{1}} }}\n"); - equals(ast_for("{{foo bar=true}}"), "{{ ID:foo [] HASH{bar=BOOLEAN{true}} }}\n"); - equals(ast_for("{{foo bar=false}}"), "{{ ID:foo [] HASH{bar=BOOLEAN{false}} }}\n"); - equals(ast_for("{{foo bar=@baz}}"), "{{ ID:foo [] HASH{bar=@ID:baz} }}\n"); + equals(ast_for("{{foo bar=baz}}"), "{{ PATH:foo [] HASH{bar=PATH:baz} }}\n"); + equals(ast_for("{{foo bar=1}}"), "{{ PATH:foo [] HASH{bar=NUMBER{1}} }}\n"); + equals(ast_for("{{foo bar=true}}"), "{{ PATH:foo [] HASH{bar=BOOLEAN{true}} }}\n"); + equals(ast_for("{{foo bar=false}}"), "{{ PATH:foo [] HASH{bar=BOOLEAN{false}} }}\n"); + equals(ast_for("{{foo bar=@baz}}"), "{{ PATH:foo [] HASH{bar=@PATH:baz} }}\n"); - equals(ast_for("{{foo bar=baz bat=bam}}"), "{{ ID:foo [] HASH{bar=ID:baz, bat=ID:bam} }}\n"); - equals(ast_for("{{foo bar=baz bat=\"bam\"}}"), '{{ ID:foo [] HASH{bar=ID:baz, bat="bam"} }}\n'); + equals(ast_for("{{foo bar=baz bat=bam}}"), "{{ PATH:foo [] HASH{bar=PATH:baz, bat=PATH:bam} }}\n"); + equals(ast_for("{{foo bar=baz bat=\"bam\"}}"), '{{ PATH:foo [] HASH{bar=PATH:baz, bat="bam"} }}\n'); - equals(ast_for("{{foo bat='bam'}}"), '{{ ID:foo [] HASH{bat="bam"} }}\n'); + equals(ast_for("{{foo bat='bam'}}"), '{{ PATH:foo [] HASH{bat="bam"} }}\n'); - equals(ast_for("{{foo omg bar=baz bat=\"bam\"}}"), '{{ ID:foo [ID:omg] HASH{bar=ID:baz, bat="bam"} }}\n'); - equals(ast_for("{{foo omg bar=baz bat=\"bam\" baz=1}}"), '{{ ID:foo [ID:omg] HASH{bar=ID:baz, bat="bam", baz=NUMBER{1}} }}\n'); - equals(ast_for("{{foo omg bar=baz bat=\"bam\" baz=true}}"), '{{ ID:foo [ID:omg] HASH{bar=ID:baz, bat="bam", baz=BOOLEAN{true}} }}\n'); - equals(ast_for("{{foo omg bar=baz bat=\"bam\" baz=false}}"), '{{ ID:foo [ID:omg] HASH{bar=ID:baz, bat="bam", baz=BOOLEAN{false}} }}\n'); + equals(ast_for("{{foo omg bar=baz bat=\"bam\"}}"), '{{ PATH:foo [PATH:omg] HASH{bar=PATH:baz, bat="bam"} }}\n'); + equals(ast_for("{{foo omg bar=baz bat=\"bam\" baz=1}}"), '{{ PATH:foo [PATH:omg] HASH{bar=PATH:baz, bat="bam", baz=NUMBER{1}} }}\n'); + equals(ast_for("{{foo omg bar=baz bat=\"bam\" baz=true}}"), '{{ PATH:foo [PATH:omg] HASH{bar=PATH:baz, bat="bam", baz=BOOLEAN{true}} }}\n'); + equals(ast_for("{{foo omg bar=baz bat=\"bam\" baz=false}}"), '{{ PATH:foo [PATH:omg] HASH{bar=PATH:baz, bat="bam", baz=BOOLEAN{false}} }}\n'); }); it('parses contents followed by a mustache', function() { - equals(ast_for("foo bar {{baz}}"), "CONTENT[ \'foo bar \' ]\n{{ ID:baz [] }}\n"); + equals(ast_for("foo bar {{baz}}"), "CONTENT[ \'foo bar \' ]\n{{ PATH:baz [] }}\n"); }); it('parses a partial', function() { @@ -87,15 +87,15 @@ describe('parser', function() { }); it('parses a partial with context', function() { - equals(ast_for("{{> foo bar}}"), "{{> PARTIAL:foo ID:bar }}\n"); + equals(ast_for("{{> foo bar}}"), "{{> PARTIAL:foo PATH:bar }}\n"); }); it('parses a partial with hash', function() { - equals(ast_for("{{> foo bar=bat}}"), "{{> PARTIAL:foo HASH{bar=ID:bat} }}\n"); + equals(ast_for("{{> foo bar=bat}}"), "{{> PARTIAL:foo HASH{bar=PATH:bat} }}\n"); }); it('parses a partial with context and hash', function() { - equals(ast_for("{{> foo bar bat=baz}}"), "{{> PARTIAL:foo ID:bar HASH{bat=ID:baz} }}\n"); + equals(ast_for("{{> foo bar bat=baz}}"), "{{> PARTIAL:foo PATH:bar HASH{bat=PATH:baz} }}\n"); }); it('parses a partial with a complex name', function() { @@ -111,47 +111,47 @@ describe('parser', function() { }); it('parses an inverse section', function() { - equals(ast_for("{{#foo}} bar {{^}} baz {{/foo}}"), "BLOCK:\n ID:foo []\n PROGRAM:\n CONTENT[ ' bar ' ]\n {{^}}\n CONTENT[ ' baz ' ]\n"); + equals(ast_for("{{#foo}} bar {{^}} baz {{/foo}}"), "BLOCK:\n PATH:foo []\n PROGRAM:\n CONTENT[ ' bar ' ]\n {{^}}\n CONTENT[ ' baz ' ]\n"); }); it('parses an inverse (else-style) section', function() { - equals(ast_for("{{#foo}} bar {{else}} baz {{/foo}}"), "BLOCK:\n ID:foo []\n PROGRAM:\n CONTENT[ ' bar ' ]\n {{^}}\n CONTENT[ ' baz ' ]\n"); + equals(ast_for("{{#foo}} bar {{else}} baz {{/foo}}"), "BLOCK:\n PATH:foo []\n PROGRAM:\n CONTENT[ ' bar ' ]\n {{^}}\n CONTENT[ ' baz ' ]\n"); }); it('parses multiple inverse sections', function() { - equals(ast_for("{{#foo}} bar {{else if bar}}{{else}} baz {{/foo}}"), "BLOCK:\n ID:foo []\n PROGRAM:\n CONTENT[ ' bar ' ]\n {{^}}\n BLOCK:\n ID:if [ID:bar]\n PROGRAM:\n {{^}}\n CONTENT[ ' baz ' ]\n"); + equals(ast_for("{{#foo}} bar {{else if bar}}{{else}} baz {{/foo}}"), "BLOCK:\n PATH:foo []\n PROGRAM:\n CONTENT[ ' bar ' ]\n {{^}}\n BLOCK:\n PATH:if [PATH:bar]\n PROGRAM:\n {{^}}\n CONTENT[ ' baz ' ]\n"); }); it('parses empty blocks', function() { - equals(ast_for("{{#foo}}{{/foo}}"), "BLOCK:\n ID:foo []\n PROGRAM:\n"); + equals(ast_for("{{#foo}}{{/foo}}"), "BLOCK:\n PATH:foo []\n PROGRAM:\n"); }); it('parses empty blocks with empty inverse section', function() { - equals(ast_for("{{#foo}}{{^}}{{/foo}}"), "BLOCK:\n ID:foo []\n PROGRAM:\n {{^}}\n"); + equals(ast_for("{{#foo}}{{^}}{{/foo}}"), "BLOCK:\n PATH:foo []\n PROGRAM:\n {{^}}\n"); }); it('parses empty blocks with empty inverse (else-style) section', function() { - equals(ast_for("{{#foo}}{{else}}{{/foo}}"), "BLOCK:\n ID:foo []\n PROGRAM:\n {{^}}\n"); + equals(ast_for("{{#foo}}{{else}}{{/foo}}"), "BLOCK:\n PATH:foo []\n PROGRAM:\n {{^}}\n"); }); it('parses non-empty blocks with empty inverse section', function() { - equals(ast_for("{{#foo}} bar {{^}}{{/foo}}"), "BLOCK:\n ID:foo []\n PROGRAM:\n CONTENT[ ' bar ' ]\n {{^}}\n"); + equals(ast_for("{{#foo}} bar {{^}}{{/foo}}"), "BLOCK:\n PATH:foo []\n PROGRAM:\n CONTENT[ ' bar ' ]\n {{^}}\n"); }); it('parses non-empty blocks with empty inverse (else-style) section', function() { - equals(ast_for("{{#foo}} bar {{else}}{{/foo}}"), "BLOCK:\n ID:foo []\n PROGRAM:\n CONTENT[ ' bar ' ]\n {{^}}\n"); + equals(ast_for("{{#foo}} bar {{else}}{{/foo}}"), "BLOCK:\n PATH:foo []\n PROGRAM:\n CONTENT[ ' bar ' ]\n {{^}}\n"); }); it('parses empty blocks with non-empty inverse section', function() { - equals(ast_for("{{#foo}}{{^}} bar {{/foo}}"), "BLOCK:\n ID:foo []\n PROGRAM:\n {{^}}\n CONTENT[ ' bar ' ]\n"); + equals(ast_for("{{#foo}}{{^}} bar {{/foo}}"), "BLOCK:\n PATH:foo []\n PROGRAM:\n {{^}}\n CONTENT[ ' bar ' ]\n"); }); it('parses empty blocks with non-empty inverse (else-style) section', function() { - equals(ast_for("{{#foo}}{{else}} bar {{/foo}}"), "BLOCK:\n ID:foo []\n PROGRAM:\n {{^}}\n CONTENT[ ' bar ' ]\n"); + equals(ast_for("{{#foo}}{{else}} bar {{/foo}}"), "BLOCK:\n PATH:foo []\n PROGRAM:\n {{^}}\n CONTENT[ ' bar ' ]\n"); }); it('parses a standalone inverse section', function() { - equals(ast_for("{{^foo}}bar{{/foo}}"), "BLOCK:\n ID:foo []\n {{^}}\n CONTENT[ 'bar' ]\n"); + equals(ast_for("{{^foo}}bar{{/foo}}"), "BLOCK:\n PATH:foo []\n {{^}}\n CONTENT[ 'bar' ]\n"); }); it('throws on old inverse section', function() { shouldThrow(function() { @@ -160,11 +160,11 @@ describe('parser', function() { }); it('parses block with block params', function() { - equals(ast_for("{{#foo as |bar baz|}}content{{/foo}}"), "BLOCK:\n ID:foo []\n PROGRAM:\n BLOCK PARAMS: [ bar baz ]\n CONTENT[ 'content' ]\n"); + equals(ast_for("{{#foo as |bar baz|}}content{{/foo}}"), "BLOCK:\n PATH:foo []\n PROGRAM:\n BLOCK PARAMS: [ bar baz ]\n CONTENT[ 'content' ]\n"); }); it('parses inverse block with block params', function() { - equals(ast_for("{{^foo as |bar baz|}}content{{/foo}}"), "BLOCK:\n ID:foo []\n {{^}}\n BLOCK PARAMS: [ bar baz ]\n CONTENT[ 'content' ]\n"); + equals(ast_for("{{^foo as |bar baz|}}content{{/foo}}"), "BLOCK:\n PATH:foo []\n {{^}}\n BLOCK PARAMS: [ bar baz ]\n CONTENT[ 'content' ]\n"); }); it("raises if there's a Parse error", function() { diff --git a/spec/string-params.js b/spec/string-params.js index 52ea5a8a2..4704a84f4 100644 --- a/spec/string-params.js +++ b/spec/string-params.js @@ -58,7 +58,7 @@ describe('string params mode', function() { var helpers = { tomdale: function(desire, noun, trueBool, falseBool, options) { equal(options.types[0], 'StringLiteral', "the string type is passed"); - equal(options.types[1], 'ID', "the expression type is passed"); + equal(options.types[1], 'PathExpression', "the expression type is passed"); equal(options.types[2], 'BooleanLiteral', "the expression type is passed"); equal(desire, "need", "the string form is passed for strings"); equal(noun, "dad.joke", "the string form is passed for expressions"); @@ -78,10 +78,10 @@ describe('string params mode', function() { var helpers = { tomdale: function(exclamation, options) { equal(exclamation, 'he.says'); - equal(options.types[0], 'ID'); + equal(options.types[0], 'PathExpression'); equal(options.hashTypes.desire, 'StringLiteral'); - equal(options.hashTypes.noun, 'ID'); + equal(options.hashTypes.noun, 'PathExpression'); equal(options.hashTypes.bool, 'BooleanLiteral'); equal(options.hash.desire, 'need'); equal(options.hash.noun, 'dad.joke'); @@ -102,7 +102,7 @@ describe('string params mode', function() { var helpers = { tomdale: function(exclamation, options) { equal(exclamation, "he.says"); - equal(options.types[0], "ID"); + equal(options.types[0], 'PathExpression'); equal(options.contexts.length, 1); equal(options.hashContexts.noun, context); @@ -165,8 +165,8 @@ describe('string params mode', function() { var helpers = { foo: function(bar, options) { - equal(bar, 'bar'); - equal(options.types[0], 'DATA'); + equal(bar, '@bar'); + equal(options.types[0], 'PathExpression'); return 'Foo!'; } }; diff --git a/spec/subexpressions.js b/spec/subexpressions.js index 5c9fdfc30..d892bb513 100644 --- a/spec/subexpressions.js +++ b/spec/subexpressions.js @@ -171,13 +171,13 @@ describe('subexpressions', function() { equals(a, 'foo'); equals(options.types.length, 2, "string params for outer helper processed correctly"); equals(options.types[0], 'sexpr', "string params for outer helper processed correctly"); - equals(options.types[1], 'ID', "string params for outer helper processed correctly"); + equals(options.types[1], 'PathExpression', "string params for outer helper processed correctly"); return a + b; }, blorg: function(a, options) { equals(options.types.length, 1, "string params for inner helper processed correctly"); - equals(options.types[0], 'ID', "string params for inner helper processed correctly"); + equals(options.types[0], 'PathExpression', "string params for inner helper processed correctly"); return a; } }; diff --git a/spec/visitor.js b/spec/visitor.js index e6f80f349..66c3b688f 100644 --- a/spec/visitor.js +++ b/spec/visitor.js @@ -29,8 +29,8 @@ describe('Visitor', function() { visitor.BooleanLiteral = function(bool) { equal(bool.value, true); }; - visitor.ID = function(id) { - equal(id.original, 'foo.bar'); + visitor.PathExpression = function(id) { + equal(/foo\.bar$/.test(id.original), true); }; visitor.ContentStatement = function(content) { equal(content.value, ' '); diff --git a/src/handlebars.yy b/src/handlebars.yy index 0bd6fde87..fb0837af5 100644 --- a/src/handlebars.yy +++ b/src/handlebars.yy @@ -114,11 +114,11 @@ helperName ; dataName - : DATA path -> new yy.DataNode($2, yy.locInfo(@$)) + : DATA pathSegments -> new yy.PathNode(true, $2, yy.locInfo(@$)) ; path - : pathSegments -> new yy.IdNode($1, yy.locInfo(@$)) + : pathSegments -> new yy.PathNode(false, $1, yy.locInfo(@$)) ; pathSegments From e1cba432a68fe208782f8f943ebd57d7641d705a Mon Sep 17 00:00:00 2001 From: kpdecker Date: Thu, 27 Nov 2014 07:32:15 -0600 Subject: [PATCH 07/15] Simplify Path and Sexpr calculated flags --- lib/handlebars/compiler/ast.js | 23 ++--------------- lib/handlebars/compiler/compiler.js | 40 ++++++++++++++++++++++++++--- src/handlebars.yy | 2 +- 3 files changed, 39 insertions(+), 26 deletions(-) diff --git a/lib/handlebars/compiler/ast.js b/lib/handlebars/compiler/ast.js index 287a9e554..75300745e 100644 --- a/lib/handlebars/compiler/ast.js +++ b/lib/handlebars/compiler/ast.js @@ -69,21 +69,8 @@ var AST = { this.type = "sexpr"; this.hash = hash; - var id = this.id = rawParams[0]; - var params = this.params = rawParams.slice(1); - - // a mustache is definitely a helper if: - // * it is an eligible helper, and - // * it has at least one parameter or hash segment - this.isHelper = !!(params.length || hash); - - // a mustache is an eligible helper if: - // * its id is simple (a single part, not `this` or `..`) - this.eligibleHelper = this.isHelper || id.isSimple; - - // if a mustache is an eligible helper but not a definite - // helper, it is ambiguous, and will be resolved in a later - // pass or at runtime. + this.id = rawParams[0]; + this.params = rawParams.slice(1); }, HashNode: function(pairs, locInfo) { @@ -111,8 +98,6 @@ var AST = { } else if (part === '..') { depth++; depthString += '../'; - } else { - this.isScoped = true; } } else { dig.push(part); @@ -123,10 +108,6 @@ var AST = { this.original = (data ? '@' : '') + original; this.parts = dig; this.depth = depth; - - // an ID is simple if it only has one part, and that part is not - // `..` or `this`. - this.isSimple = parts.length === 1 && !this.isScoped && depth === 0; }, StringNode: function(string, locInfo) { diff --git a/lib/handlebars/compiler/compiler.js b/lib/handlebars/compiler/compiler.js index ea655838f..5dbbfc44b 100644 --- a/lib/handlebars/compiler/compiler.js +++ b/lib/handlebars/compiler/compiler.js @@ -3,6 +3,26 @@ import {isArray} from "../utils"; var slice = [].slice; + +// a mustache is definitely a helper if: +// * it is an eligible helper, and +// * it has at least one parameter or hash segment +function helperExpr(sexpr) { + return !!(sexpr.isHelper || sexpr.params.length || sexpr.hash); +} + +function scopedId(id) { + return (/^\.|this\b/).test(id.original); +} + +// an ID is simple if it only has one part, and that part is not +// `..` or `this`. +function simpleId(id) { + var part = id.parts[0]; + + return id.parts.length === 1 && !scopedId(id) && !id.depth; +} + export function Compiler() {} // the foundHelper register will disambiguate helper lookup from finding a @@ -245,7 +265,7 @@ Compiler.prototype = { id.falsy = true; this.accept(id); - this.opcode('invokeHelper', sexpr, params.length, id.original, id.isSimple); + this.opcode('invokeHelper', sexpr, params.length, id.original, simpleId(id)); } }, @@ -275,7 +295,7 @@ Compiler.prototype = { this.options.data = true; this.opcode('lookupData', id, id.depth, id.parts); } else { - this.opcode('lookupOnContext', id, id.parts, id.falsy, id.isScoped); + this.opcode('lookupOnContext', id, id.parts, id.falsy, scopedId(id)); } }, @@ -306,8 +326,15 @@ Compiler.prototype = { }, classifySexpr: function(sexpr) { - var isHelper = sexpr.isHelper; - var isEligible = sexpr.eligibleHelper; + // a mustache is an eligible helper if: + // * its id is simple (a single part, not `this` or `..`) + var isHelper = helperExpr(sexpr); + + // if a mustache is an eligible helper but not a definite + // helper, it is ambiguous, and will be resolved in a later + // pass or at runtime. + var isEligible = isHelper || simpleId(sexpr.id); + var options = this.options; // if ambiguous, we can possibly resolve the ambiguity now @@ -336,6 +363,11 @@ Compiler.prototype = { pushParam: function(val) { var value = val.value != null ? val.value : val.original || ''; + // Force helper evaluation + if (val.type === 'sexpr') { + val.isHelper = true; + } + if (this.stringParams) { if (value.replace) { value = value diff --git a/src/handlebars.yy b/src/handlebars.yy index fb0837af5..b51aad2d7 100644 --- a/src/handlebars.yy +++ b/src/handlebars.yy @@ -92,7 +92,7 @@ param | NUMBER -> new yy.NumberNode($1, yy.locInfo(@$)) | BOOLEAN -> new yy.BooleanNode($1, yy.locInfo(@$)) | dataName -> $1 - | OPEN_SEXPR sexpr CLOSE_SEXPR {$2.isHelper = true; $$ = $2;} + | OPEN_SEXPR sexpr CLOSE_SEXPR -> $2 ; hash From 1124908d2a0d36493912eb2ce062545b1b5e5445 Mon Sep 17 00:00:00 2001 From: kpdecker Date: Thu, 27 Nov 2014 08:37:48 -0600 Subject: [PATCH 08/15] Update subexpression and hash AST constructs --- lib/handlebars/compiler/ast.js | 25 ++-- lib/handlebars/compiler/compiler.js | 118 ++++++++---------- lib/handlebars/compiler/helpers.js | 8 +- .../compiler/javascript-compiler.js | 4 +- lib/handlebars/compiler/printer.js | 33 ++--- lib/handlebars/compiler/visitor.js | 21 ++-- spec/ast.js | 2 +- spec/subexpressions.js | 10 +- src/handlebars.yy | 2 +- 9 files changed, 111 insertions(+), 112 deletions(-) diff --git a/lib/handlebars/compiler/ast.js b/lib/handlebars/compiler/ast.js index 75300745e..243957143 100644 --- a/lib/handlebars/compiler/ast.js +++ b/lib/handlebars/compiler/ast.js @@ -66,17 +66,10 @@ var AST = { SexprNode: function(rawParams, hash, locInfo) { this.loc = locInfo; - this.type = "sexpr"; - this.hash = hash; - - this.id = rawParams[0]; + this.type = 'SubExpression'; + this.path = rawParams[0]; this.params = rawParams.slice(1); - }, - - HashNode: function(pairs, locInfo) { - this.loc = locInfo; - this.type = "hash"; - this.pairs = pairs; + this.hash = hash; }, PathNode: function(data, parts, locInfo) { @@ -128,6 +121,18 @@ var AST = { this.loc = locInfo; this.type = 'BooleanLiteral'; this.value = bool === 'true'; + }, + + HashNode: function(pairs, locInfo) { + this.loc = locInfo; + this.type = 'Hash'; + this.pairs = pairs; + }, + HashPair: function(key, value, locInfo) { + this.loc = locInfo; + this.type = 'HashPair'; + this.key = key; + this.value = value; } }; diff --git a/lib/handlebars/compiler/compiler.js b/lib/handlebars/compiler/compiler.js index 5dbbfc44b..e73ce37b4 100644 --- a/lib/handlebars/compiler/compiler.js +++ b/lib/handlebars/compiler/compiler.js @@ -11,16 +11,16 @@ function helperExpr(sexpr) { return !!(sexpr.isHelper || sexpr.params.length || sexpr.hash); } -function scopedId(id) { - return (/^\.|this\b/).test(id.original); +function scopedId(path) { + return (/^\.|this\b/).test(path.original); } // an ID is simple if it only has one part, and that part is not // `..` or `this`. -function simpleId(id) { - var part = id.parts[0]; +function simpleId(path) { + var part = path.parts[0]; - return id.parts.length === 1 && !scopedId(id) && !id.depth; + return path.parts.length === 1 && !scopedId(path) && !path.depth; } export function Compiler() {} @@ -147,7 +147,7 @@ Compiler.prototype = { this.opcode('pushProgram', block, program); this.opcode('pushProgram', block, inverse); this.opcode('emptyHash', block); - this.opcode('blockValue', block, sexpr.id.original); + this.opcode('blockValue', block, sexpr.path.original); } else { this.ambiguousSexpr(sexpr, program, inverse); @@ -163,7 +163,7 @@ Compiler.prototype = { }, PartialStatement: function(partial) { - var partialName = partial.sexpr.id.original; + var partialName = partial.sexpr.path.original; this.usePartial = true; if (partial.sexpr.hash) { @@ -194,7 +194,7 @@ Compiler.prototype = { }, MustacheStatement: function(mustache) { - this.sexpr(mustache.sexpr); + this.accept(mustache.sexpr); if(mustache.escaped && !this.options.noEscape) { this.opcode('appendEscaped', mustache); @@ -211,91 +211,67 @@ Compiler.prototype = { CommentStatement: function() {}, - sexpr: function(sexpr) { + SubExpression: function(sexpr) { var type = this.classifySexpr(sexpr); - if (type === "simple") { + if (type === 'simple') { this.simpleSexpr(sexpr); - } else if (type === "helper") { + } else if (type === 'helper') { this.helperSexpr(sexpr); } else { this.ambiguousSexpr(sexpr); } }, ambiguousSexpr: function(sexpr, program, inverse) { - var id = sexpr.id, - name = id.parts[0], + var path = sexpr.path, + name = path.parts[0], isBlock = program != null || inverse != null; - this.opcode('getContext', sexpr, id.depth); + this.opcode('getContext', sexpr, path.depth); this.opcode('pushProgram', sexpr, program); this.opcode('pushProgram', sexpr, inverse); - this.accept(id); + this.accept(path); this.opcode('invokeAmbiguous', sexpr, name, isBlock); }, simpleSexpr: function(sexpr) { - var id = sexpr.id; - - if (id.parts.length) { - this.accept(id); - } else { - // Simplified ID for `this` - this.addDepth(id.depth); - this.opcode('getContext', sexpr, id.depth); - this.opcode('pushContext', sexpr); - } - + this.accept(sexpr.path); this.opcode('resolvePossibleLambda', sexpr); }, helperSexpr: function(sexpr, program, inverse) { var params = this.setupFullMustacheParams(sexpr, program, inverse), - id = sexpr.id, - name = id.parts[0]; + path = sexpr.path, + name = path.parts[0]; if (this.options.knownHelpers[name]) { this.opcode('invokeKnownHelper', sexpr, params.length, name); } else if (this.options.knownHelpersOnly) { throw new Exception("You specified knownHelpersOnly, but used the unknown helper " + name, sexpr); } else { - id.falsy = true; - - this.accept(id); - this.opcode('invokeHelper', sexpr, params.length, id.original, simpleId(id)); - } - }, - - hash: function(hash) { - var pairs = hash.pairs, i, l; - - this.opcode('pushHash', hash); + path.falsy = true; - for(i=0, l=pairs.length; i'); }); @@ -159,7 +159,7 @@ describe('subexpressions', function() { t: function(defaultString) { return new Handlebars.SafeString(defaultString); } - } + }; shouldCompileTo(string, [context, helpers], ''); }); @@ -170,7 +170,7 @@ describe('subexpressions', function() { snog: function(a, b, options) { equals(a, 'foo'); equals(options.types.length, 2, "string params for outer helper processed correctly"); - equals(options.types[0], 'sexpr', "string params for outer helper processed correctly"); + equals(options.types[0], 'SubExpression', "string params for outer helper processed correctly"); equals(options.types[1], 'PathExpression', "string params for outer helper processed correctly"); return a + b; }, @@ -196,7 +196,7 @@ describe('subexpressions', function() { var helpers = { blog: function(options) { - equals(options.hashTypes.fun, 'sexpr'); + equals(options.hashTypes.fun, 'SubExpression'); return "val is " + options.hash.fun; }, bork: function() { diff --git a/src/handlebars.yy b/src/handlebars.yy index b51aad2d7..1975f63af 100644 --- a/src/handlebars.yy +++ b/src/handlebars.yy @@ -100,7 +100,7 @@ hash ; hashSegment - : ID EQUALS param -> [$1, $3] + : ID EQUALS param -> new yy.HashPair($1, $3, yy.locInfo(@$)) ; blockParams From f990cf006422fbcbbae7d8a8a866831e58300ea4 Mon Sep 17 00:00:00 2001 From: kpdecker Date: Thu, 27 Nov 2014 09:11:03 -0600 Subject: [PATCH 09/15] Treat partial exec in a manner closer to helpers This helps unify the code handling and will also be needed to support string/id tracking on partials. --- lib/handlebars/compiler/code-gen.js | 5 ++- lib/handlebars/compiler/compiler.js | 25 ++++------- .../compiler/javascript-compiler.js | 41 +++++++++++-------- lib/handlebars/runtime.js | 27 ++++++------ 4 files changed, 50 insertions(+), 48 deletions(-) diff --git a/lib/handlebars/compiler/code-gen.js b/lib/handlebars/compiler/code-gen.js index a7c16590d..0fddb7c65 100644 --- a/lib/handlebars/compiler/code-gen.js +++ b/lib/handlebars/compiler/code-gen.js @@ -113,7 +113,10 @@ CodeGen.prototype = { for (var key in obj) { if (obj.hasOwnProperty(key)) { - pairs.push([this.quotedString(key), ':', castChunk(obj[key], this)]); + var value = castChunk(obj[key], this); + if (value !== 'undefined') { + pairs.push([this.quotedString(key), ':', value]); + } } } diff --git a/lib/handlebars/compiler/compiler.js b/lib/handlebars/compiler/compiler.js index e73ce37b4..df5a10f4f 100644 --- a/lib/handlebars/compiler/compiler.js +++ b/lib/handlebars/compiler/compiler.js @@ -166,24 +166,15 @@ Compiler.prototype = { var partialName = partial.sexpr.path.original; this.usePartial = true; - if (partial.sexpr.hash) { - this.accept(partial.sexpr.hash); - } else { - this.opcode('pushLiteral', partial, 'undefined'); - } - var params = partial.sexpr.params; - if (params.length) { - if (params.length > 1) { - throw new Exception('Unsupported number of partial arguments: ' + params.length, partial); - } - - this.pushParam(params[0]); - } else { - this.opcode('getContext', partial, 0); - this.opcode('pushContext', partial); + if (params.length > 1) { + throw new Exception('Unsupported number of partial arguments: ' + params.length, partial); + } else if (!params.length) { + params.push({type: 'PathExpression', parts: [], depth: 0}); } + this.setupFullMustacheParams(partial.sexpr, undefined, undefined, true); + var indent = partial.indent || ''; if (this.options.preventIndent && indent) { this.opcode('appendContent', partial, indent); @@ -391,7 +382,7 @@ Compiler.prototype = { } }, - setupFullMustacheParams: function(sexpr, program, inverse) { + setupFullMustacheParams: function(sexpr, program, inverse, omitEmpty) { var params = sexpr.params; this.pushParams(params); @@ -401,7 +392,7 @@ Compiler.prototype = { if (sexpr.hash) { this.accept(sexpr.hash); } else { - this.opcode('emptyHash', sexpr); + this.opcode('emptyHash', sexpr, omitEmpty); } return params; diff --git a/lib/handlebars/compiler/javascript-compiler.js b/lib/handlebars/compiler/javascript-compiler.js index cde2ab507..db1977811 100644 --- a/lib/handlebars/compiler/javascript-compiler.js +++ b/lib/handlebars/compiler/javascript-compiler.js @@ -275,7 +275,7 @@ JavaScriptCompiler.prototype = { blockValue: function(name) { var blockHelperMissing = this.aliasable('helpers.blockHelperMissing'), params = [this.contextName(0)]; - this.setupParams(name, 0, params); + this.setupHelperArgs(name, 0, params); var blockName = this.popStack(); params.splice(1, 0, blockName); @@ -293,7 +293,7 @@ JavaScriptCompiler.prototype = { // We're being a bit cheeky and reusing the options value from the prior exec var blockHelperMissing = this.aliasable('helpers.blockHelperMissing'), params = [this.contextName(0)]; - this.setupParams('', 0, params, true); + this.setupHelperArgs('', 0, params, true); this.flushInline(); @@ -469,9 +469,7 @@ JavaScriptCompiler.prototype = { } }, - emptyHash: function() { - this.pushStackLiteral('{}'); - + emptyHash: function(omitEmpty) { if (this.trackIds) { this.push('{}'); // hashIds } @@ -479,6 +477,7 @@ JavaScriptCompiler.prototype = { this.push('{}'); // hashContexts this.push('{}'); // hashTypes } + this.pushStackLiteral(omitEmpty ? 'undefined' : '{}'); }, pushHash: function() { if (this.hash) { @@ -611,16 +610,22 @@ JavaScriptCompiler.prototype = { // This operation pops off a context, invokes a partial with that context, // and pushes the result of the invocation back. invokePartial: function(name, indent) { - var params = [this.nameLookup('partials', name, 'partial'), "'" + indent + "'", "'" + name + "'", this.popStack(), this.popStack(), "helpers", "partials"]; + var params = [], + options = this.setupParams(name, 1, params, false); - if (this.options.data) { - params.push("data"); - } else if (this.options.compat) { - params.push('undefined'); + if (indent) { + options.indent = JSON.stringify(indent); } + options.helpers = 'helpers'; + options.partials = 'partials'; + + params.unshift(this.nameLookup('partials', name, 'partial')); + if (this.options.compat) { - params.push('depths'); + options.depths = 'depths'; } + options = this.objectLiteral(options); + params.push(options); this.push(this.source.functionCall('this.invokePartial', '', params)); }, @@ -881,7 +886,7 @@ JavaScriptCompiler.prototype = { setupHelper: function(paramSize, name, blockHelper) { var params = [], - paramsInit = this.setupParams(name, paramSize, params, blockHelper); + paramsInit = this.setupHelperArgs(name, paramSize, params, blockHelper); var foundHelper = this.nameLookup('helpers', name, 'helper'); return { @@ -892,8 +897,8 @@ JavaScriptCompiler.prototype = { }; }, - setupParams: function(helper, paramSize, params, useRegister) { - var options = {}, contexts = [], types = [], ids = [], param, inverse, program; + setupParams: function(helper, paramSize, params) { + var options = {}, contexts = [], types = [], ids = [], param; options.name = this.quotedString(helper); options.hash = this.popStack(); @@ -906,8 +911,8 @@ JavaScriptCompiler.prototype = { options.hashContexts = this.popStack(); } - inverse = this.popStack(); - program = this.popStack(); + var inverse = this.popStack(), + program = this.popStack(); // Avoid setting fn and inverse if neither are set. This allows // helpers to do a check for `if (options.fn)` @@ -943,7 +948,11 @@ JavaScriptCompiler.prototype = { if (this.options.data) { options.data = "data"; } + return options; + }, + setupHelperArgs: function(helper, paramSize, params, useRegister) { + var options = this.setupParams(helper, paramSize, params, true); options = this.objectLiteral(options); if (useRegister) { this.useRegister('options'); diff --git a/lib/handlebars/runtime.js b/lib/handlebars/runtime.js index 05759cba9..455dd332a 100644 --- a/lib/handlebars/runtime.js +++ b/lib/handlebars/runtime.js @@ -35,36 +35,35 @@ export function template(templateSpec, env) { // for external users to override these as psuedo-supported APIs. env.VM.checkRevision(templateSpec.compiler); - var invokePartialWrapper = function(partial, indent, name, context, hash, helpers, partials, data, depths) { - if (hash) { - context = Utils.extend({}, context, hash); + var invokePartialWrapper = function(partial, context, options) { + if (options.hash) { + context = Utils.extend({}, context, options.hash); } if (!partial) { - partial = partials[name]; + partial = options.partials[options.name]; } - var result = env.VM.invokePartial.call(this, partial, name, context, helpers, partials, data, depths); + var result = env.VM.invokePartial.call(this, partial, context, options); if (result == null && env.compile) { - var options = { helpers: helpers, partials: partials, data: data, depths: depths }; - partials[name] = env.compile(partial, templateSpec.compilerOptions, env); - result = partials[name](context, options); + options.partials[options.name] = env.compile(partial, templateSpec.compilerOptions, env); + result = options.partials[options.name](context, options); } if (result != null) { - if (indent) { + if (options.indent) { var lines = result.split('\n'); for (var i = 0, l = lines.length; i < l; i++) { if (!lines[i] && i + 1 === l) { break; } - lines[i] = indent + lines[i]; + lines[i] = options.indent + lines[i]; } result = lines.join('\n'); } return result; } else { - throw new Exception("The partial " + name + " could not be compiled when running in runtime-only mode"); + throw new Exception("The partial " + options.name + " could not be compiled when running in runtime-only mode"); } }; @@ -172,11 +171,11 @@ export function program(container, i, fn, data, depths) { return prog; } -export function invokePartial(partial, name, context, helpers, partials, data, depths) { - var options = { partial: true, helpers: helpers, partials: partials, data: data, depths: depths }; +export function invokePartial(partial, context, options) { + options.partial = true; if(partial === undefined) { - throw new Exception("The partial " + name + " could not be found"); + throw new Exception("The partial " + options.name + " could not be found"); } else if(partial instanceof Function) { return partial(context, options); } From 6a7a8c803d48b8a5768d00e8d1c73cf5012106d2 Mon Sep 17 00:00:00 2001 From: kpdecker Date: Fri, 28 Nov 2014 14:42:24 -0600 Subject: [PATCH 10/15] Rename AST objects to match type names --- lib/handlebars/compiler/ast.js | 24 +++++----- lib/handlebars/compiler/helpers.js | 8 ++-- spec/ast.js | 70 ++++++++++++++---------------- spec/compiler.js | 4 +- spec/parser.js | 2 +- src/handlebars.yy | 36 +++++++-------- 6 files changed, 69 insertions(+), 75 deletions(-) diff --git a/lib/handlebars/compiler/ast.js b/lib/handlebars/compiler/ast.js index 243957143..69581f281 100644 --- a/lib/handlebars/compiler/ast.js +++ b/lib/handlebars/compiler/ast.js @@ -1,7 +1,7 @@ import Exception from "../exception"; var AST = { - ProgramNode: function(statements, blockParams, strip, locInfo) { + Program: function(statements, blockParams, strip, locInfo) { this.loc = locInfo; this.type = 'Program'; this.body = statements; @@ -10,7 +10,7 @@ var AST = { this.strip = strip; }, - MustacheNode: function(rawParams, open, strip, locInfo) { + MustacheStatement: function(rawParams, open, strip, locInfo) { this.loc = locInfo; this.type = 'MustacheStatement'; @@ -28,7 +28,7 @@ var AST = { this.strip = strip; }, - BlockNode: function(sexpr, program, inverse, strip, locInfo) { + BlockStatement: function(sexpr, program, inverse, strip, locInfo) { this.loc = locInfo; this.type = 'BlockStatement'; @@ -38,7 +38,7 @@ var AST = { this.strip = strip; }, - PartialNode: function(sexpr, strip, locInfo) { + PartialStatement: function(sexpr, strip, locInfo) { this.loc = locInfo; this.type = 'PartialStatement'; this.sexpr = sexpr; @@ -48,13 +48,13 @@ var AST = { this.strip.inlineStandalone = true; }, - ContentNode: function(string, locInfo) { + ContentStatement: function(string, locInfo) { this.loc = locInfo; this.type = 'ContentStatement'; this.original = this.value = string; }, - CommentNode: function(comment, strip, locInfo) { + CommentStatement: function(comment, strip, locInfo) { this.loc = locInfo; this.type = 'CommentStatement'; this.value = comment; @@ -63,7 +63,7 @@ var AST = { strip.inlineStandalone = true; }, - SexprNode: function(rawParams, hash, locInfo) { + SubExpression: function(rawParams, hash, locInfo) { this.loc = locInfo; this.type = 'SubExpression'; @@ -72,7 +72,7 @@ var AST = { this.hash = hash; }, - PathNode: function(data, parts, locInfo) { + PathExpression: function(data, parts, locInfo) { this.loc = locInfo; this.type = 'PathExpression'; @@ -103,27 +103,27 @@ var AST = { this.depth = depth; }, - StringNode: function(string, locInfo) { + StringLiteral: function(string, locInfo) { this.loc = locInfo; this.type = 'StringLiteral'; this.original = this.value = string; }, - NumberNode: function(number, locInfo) { + NumberLiteral: function(number, locInfo) { this.loc = locInfo; this.type = 'NumberLiteral'; this.original = this.value = Number(number); }, - BooleanNode: function(bool, locInfo) { + BooleanLiteral: function(bool, locInfo) { this.loc = locInfo; this.type = 'BooleanLiteral'; this.value = bool === 'true'; }, - HashNode: function(pairs, locInfo) { + Hash: function(pairs, locInfo) { this.loc = locInfo; this.type = 'Hash'; this.pairs = pairs; diff --git a/lib/handlebars/compiler/helpers.js b/lib/handlebars/compiler/helpers.js index f75c10b8c..3d5144f70 100644 --- a/lib/handlebars/compiler/helpers.js +++ b/lib/handlebars/compiler/helpers.js @@ -32,9 +32,9 @@ export function prepareRawBlock(openRawBlock, content, close, locInfo) { throw new Exception(openRawBlock.sexpr.path.original + " doesn't match " + close, errorNode); } - var program = new this.ProgramNode([content], null, {}, locInfo); + var program = new this.Program([content], null, {}, locInfo); - return new this.BlockNode(openRawBlock.sexpr, program, undefined, undefined, locInfo); + return new this.BlockStatement(openRawBlock.sexpr, program, undefined, undefined, locInfo); } export function prepareBlock(openBlock, program, inverseAndProgram, close, inverted, locInfo) { @@ -109,9 +109,9 @@ export function prepareBlock(openBlock, program, inverseAndProgram, close, inver } if (inverted) { - return new this.BlockNode(openBlock.sexpr, inverse, program, strip, locInfo); + return new this.BlockStatement(openBlock.sexpr, inverse, program, strip, locInfo); } else { - return new this.BlockNode(openBlock.sexpr, program, inverse, strip, locInfo); + return new this.BlockStatement(openBlock.sexpr, program, inverse, strip, locInfo); } } diff --git a/spec/ast.js b/spec/ast.js index 0efe96342..53267493d 100644 --- a/spec/ast.js +++ b/spec/ast.js @@ -22,16 +22,16 @@ describe('ast', function() { equals(node.loc.end.column, 1); } - describe('MustacheNode', function() { + describe('MustacheStatement', function() { function testEscape(open, expected) { - var mustache = new handlebarsEnv.AST.MustacheNode([{}], open, false); + var mustache = new handlebarsEnv.AST.MustacheStatement([{}], open, false); equals(mustache.escaped, expected); } it('should store args', function() { var id = {isSimple: true}, hash = {}, - mustache = new handlebarsEnv.AST.MustacheNode([id, 'param1'], '', false, LOCATION_INFO); + mustache = new handlebarsEnv.AST.MustacheStatement([id, 'param1'], '', false, LOCATION_INFO); equals(mustache.type, 'MustacheStatement'); equals(mustache.escaped, true); testLocationInfoStorage(mustache); @@ -61,7 +61,7 @@ describe('ast', function() { testEscape(undefined, false); }); }); - describe('BlockNode', function() { + describe('BlockStatement', function() { it('should throw on mustache mismatch', function() { shouldThrow(function() { handlebarsEnv.parse("\n {{#foo}}{{/bar}}"); @@ -69,9 +69,9 @@ describe('ast', function() { }); it('stores location info', function(){ - var sexprNode = new handlebarsEnv.AST.SexprNode([{ original: 'foo'}], null); - var mustacheNode = new handlebarsEnv.AST.MustacheNode(sexprNode, null, '{{', {}); - var block = new handlebarsEnv.AST.BlockNode(mustacheNode, + var sexprNode = new handlebarsEnv.AST.SubExpression([{ original: 'foo'}], null); + var mustacheNode = new handlebarsEnv.AST.MustacheStatement(sexprNode, null, '{{', {}); + var block = new handlebarsEnv.AST.BlockStatement(mustacheNode, {body: [], strip: {}}, {body: [], strip: {}}, { strip: {}, @@ -81,24 +81,24 @@ describe('ast', function() { testLocationInfoStorage(block); }); }); - describe('PathNode', function() { + describe('PathExpression', function() { it('should throw on invalid path', function() { shouldThrow(function() { - new handlebarsEnv.AST.PathNode(false, [ + new handlebarsEnv.AST.PathExpression(false, [ {part: 'foo'}, {part: '..'}, {part: 'bar'} ], {start: {line: 1, column: 1}}); }, Handlebars.Exception, "Invalid path: foo.. - 1:1"); shouldThrow(function() { - new handlebarsEnv.AST.PathNode(false, [ + new handlebarsEnv.AST.PathExpression(false, [ {part: 'foo'}, {part: '.'}, {part: 'bar'} ], {start: {line: 1, column: 1}}); }, Handlebars.Exception, "Invalid path: foo. - 1:1"); shouldThrow(function() { - new handlebarsEnv.AST.PathNode(false, [ + new handlebarsEnv.AST.PathExpression(false, [ {part: 'foo'}, {part: 'this'}, {part: 'bar'} @@ -107,69 +107,63 @@ describe('ast', function() { }); it('stores location info', function(){ - var idNode = new handlebarsEnv.AST.PathNode(false, [], LOCATION_INFO); + var idNode = new handlebarsEnv.AST.PathExpression(false, [], LOCATION_INFO); testLocationInfoStorage(idNode); }); }); - describe("HashNode", function(){ - + describe('Hash', function(){ it('stores location info', function(){ - var hash = new handlebarsEnv.AST.HashNode([], LOCATION_INFO); + var hash = new handlebarsEnv.AST.Hash([], LOCATION_INFO); testLocationInfoStorage(hash); }); }); - describe("ContentNode", function(){ - + describe('ContentStatement', function(){ it('stores location info', function(){ - var content = new handlebarsEnv.AST.ContentNode("HI", LOCATION_INFO); + var content = new handlebarsEnv.AST.ContentStatement("HI", LOCATION_INFO); testLocationInfoStorage(content); }); }); - describe("CommentNode", function(){ - + describe('CommentStatement', function(){ it('stores location info', function(){ - var comment = new handlebarsEnv.AST.CommentNode("HI", {}, LOCATION_INFO); + var comment = new handlebarsEnv.AST.CommentStatement("HI", {}, LOCATION_INFO); testLocationInfoStorage(comment); }); }); - describe("NumberNode", function(){ - + describe('NumberLiteral', function(){ it('stores location info', function(){ - var integer = new handlebarsEnv.AST.NumberNode("6", LOCATION_INFO); + var integer = new handlebarsEnv.AST.NumberLiteral("6", LOCATION_INFO); testLocationInfoStorage(integer); }); }); - describe("StringNode", function(){ - + describe('StringLiteral', function(){ it('stores location info', function(){ - var string = new handlebarsEnv.AST.StringNode("6", LOCATION_INFO); + var string = new handlebarsEnv.AST.StringLiteral("6", LOCATION_INFO); testLocationInfoStorage(string); }); }); - describe("BooleanNode", function(){ - + describe('BooleanLiteral', function(){ it('stores location info', function(){ - var bool = new handlebarsEnv.AST.BooleanNode("true", LOCATION_INFO); + var bool = new handlebarsEnv.AST.BooleanLiteral("true", LOCATION_INFO); testLocationInfoStorage(bool); }); }); - describe("PartialNode", function(){ + describe('PartialStatement', function(){ it('stores location info', function(){ - var pn = new handlebarsEnv.AST.PartialNode('so_partial', {}, LOCATION_INFO); + var pn = new handlebarsEnv.AST.PartialStatement('so_partial', {}, LOCATION_INFO); testLocationInfoStorage(pn); }); }); - describe('ProgramNode', function(){ + describe('Program', function(){ it('storing location info', function(){ - var pn = new handlebarsEnv.AST.ProgramNode([], null, {}, LOCATION_INFO); + var pn = new handlebarsEnv.AST.Program([], null, {}, LOCATION_INFO); testLocationInfoStorage(pn); }); }); @@ -193,7 +187,7 @@ describe('ast', function() { testColumns(contentNode, 1, 1, 0, 7); }); - it('gets MustacheNode line numbers', function(){ + it('gets MustacheStatement line numbers', function(){ var mustacheNode = body[1]; testColumns(mustacheNode, 1, 1, 7, 21); }); @@ -202,9 +196,9 @@ describe('ast', function() { testColumns(body[2], 1, 2, 21, 8); }); - it('gets MustacheNode line numbers correct across newlines', function(){ - var secondMustacheNode = body[3]; - testColumns(secondMustacheNode, 2, 2, 8, 22); + it('gets MustacheStatement line numbers correct across newlines', function(){ + var secondMustacheStatement = body[3]; + testColumns(secondMustacheStatement, 2, 2, 8, 22); }); it('gets the block helper information correct', function(){ diff --git a/spec/compiler.js b/spec/compiler.js index eead00b58..f9eba28fb 100644 --- a/spec/compiler.js +++ b/spec/compiler.js @@ -41,7 +41,7 @@ describe('compiler', function() { }); it('can utilize AST instance', function() { - equal(Handlebars.compile(new Handlebars.AST.ProgramNode([ new Handlebars.AST.ContentNode("Hello")], null, {}))(), 'Hello'); + equal(Handlebars.compile(new Handlebars.AST.Program([ new Handlebars.AST.ContentStatement("Hello")], null, {}))(), 'Hello'); }); it("can pass through an empty string", function() { @@ -60,7 +60,7 @@ describe('compiler', function() { }); it('can utilize AST instance', function() { - equal(/return "Hello"/.test(Handlebars.precompile(new Handlebars.AST.ProgramNode([ new Handlebars.AST.ContentNode("Hello")]), null, {})), true); + equal(/return "Hello"/.test(Handlebars.precompile(new Handlebars.AST.Program([ new Handlebars.AST.ContentStatement("Hello")]), null, {})), true); }); it("can pass through an empty string", function() { diff --git a/spec/parser.js b/spec/parser.js index b25f222f9..ad5273446 100644 --- a/spec/parser.js +++ b/spec/parser.js @@ -203,7 +203,7 @@ describe('parser', function() { describe('externally compiled AST', function() { it('can pass through an already-compiled AST', function() { - equals(ast_for(new Handlebars.AST.ProgramNode([new Handlebars.AST.ContentNode("Hello")], null)), "CONTENT[ \'Hello\' ]\n"); + equals(ast_for(new Handlebars.AST.Program([new Handlebars.AST.ContentStatement("Hello")], null)), "CONTENT[ \'Hello\' ]\n"); }); }); }); diff --git a/src/handlebars.yy b/src/handlebars.yy index 1975f63af..8b63df045 100644 --- a/src/handlebars.yy +++ b/src/handlebars.yy @@ -9,7 +9,7 @@ root ; program - : statement* -> new yy.ProgramNode(yy.prepareProgram($1), null, {}, yy.locInfo(@$)) + : statement* -> new yy.Program(yy.prepareProgram($1), null, {}, yy.locInfo(@$)) ; statement @@ -18,11 +18,11 @@ statement | rawBlock -> $1 | partial -> $1 | content -> $1 - | COMMENT -> new yy.CommentNode(yy.stripComment($1), yy.stripFlags($1, $1), yy.locInfo(@$)) + | COMMENT -> new yy.CommentStatement(yy.stripComment($1), yy.stripFlags($1, $1), yy.locInfo(@$)) ; content - : CONTENT -> new yy.ContentNode($1, yy.locInfo(@$)) + : CONTENT -> new yy.ContentStatement($1, yy.locInfo(@$)) ; rawBlock @@ -47,7 +47,7 @@ openInverse ; openInverseChain - : OPEN_INVERSE_CHAIN sexpr CLOSE -> new yy.MustacheNode($2, $1, yy.stripFlags($1, $3), yy.locInfo(@$)) + : OPEN_INVERSE_CHAIN sexpr CLOSE -> new yy.MustacheStatement($2, $1, yy.stripFlags($1, $3), yy.locInfo(@$)) ; inverseAndProgram @@ -57,7 +57,7 @@ inverseAndProgram inverseChain : openInverseChain program inverseChain? { var inverse = yy.prepareBlock($1, $2, $3, $3, false, yy.locInfo(@$)), - program = new yy.ProgramNode(yy.prepareProgram([inverse]), null, {}, yy.locInfo(@$)); + program = new yy.Program(yy.prepareProgram([inverse]), null, {}, yy.locInfo(@$)); program.inverse = inverse; @@ -73,30 +73,30 @@ closeBlock mustache // Parsing out the '&' escape token at AST level saves ~500 bytes after min due to the removal of one parser node. // This also allows for handler unification as all mustache node instances can utilize the same handler - : OPEN sexpr CLOSE -> new yy.MustacheNode($2, $1, yy.stripFlags($1, $3), yy.locInfo(@$)) - | OPEN_UNESCAPED sexpr CLOSE_UNESCAPED -> new yy.MustacheNode($2, $1, yy.stripFlags($1, $3), yy.locInfo(@$)) + : OPEN sexpr CLOSE -> new yy.MustacheStatement($2, $1, yy.stripFlags($1, $3), yy.locInfo(@$)) + | OPEN_UNESCAPED sexpr CLOSE_UNESCAPED -> new yy.MustacheStatement($2, $1, yy.stripFlags($1, $3), yy.locInfo(@$)) ; partial - : OPEN_PARTIAL sexpr CLOSE -> new yy.PartialNode($2, yy.stripFlags($1, $3), yy.locInfo(@$)) + : OPEN_PARTIAL sexpr CLOSE -> new yy.PartialStatement($2, yy.stripFlags($1, $3), yy.locInfo(@$)) ; sexpr - : helperName param* hash? -> new yy.SexprNode([$1].concat($2), $3, yy.locInfo(@$)) - | dataName -> new yy.SexprNode([$1], null, yy.locInfo(@$)) + : helperName param* hash? -> new yy.SubExpression([$1].concat($2), $3, yy.locInfo(@$)) + | dataName -> new yy.SubExpression([$1], null, yy.locInfo(@$)) ; param : path -> $1 - | STRING -> new yy.StringNode($1, yy.locInfo(@$)) - | NUMBER -> new yy.NumberNode($1, yy.locInfo(@$)) - | BOOLEAN -> new yy.BooleanNode($1, yy.locInfo(@$)) + | STRING -> new yy.StringLiteral($1, yy.locInfo(@$)) + | NUMBER -> new yy.NumberLiteral($1, yy.locInfo(@$)) + | BOOLEAN -> new yy.BooleanLiteral($1, yy.locInfo(@$)) | dataName -> $1 | OPEN_SEXPR sexpr CLOSE_SEXPR -> $2 ; hash - : hashSegment+ -> new yy.HashNode($1, yy.locInfo(@$)) + : hashSegment+ -> new yy.Hash($1, yy.locInfo(@$)) ; hashSegment @@ -109,16 +109,16 @@ blockParams helperName : path -> $1 - | STRING -> new yy.StringNode($1, yy.locInfo(@$)), yy.locInfo(@$) - | NUMBER -> new yy.NumberNode($1, yy.locInfo(@$)) + | STRING -> new yy.StringLiteral($1, yy.locInfo(@$)), yy.locInfo(@$) + | NUMBER -> new yy.NumberLiteral($1, yy.locInfo(@$)) ; dataName - : DATA pathSegments -> new yy.PathNode(true, $2, yy.locInfo(@$)) + : DATA pathSegments -> new yy.PathExpression(true, $2, yy.locInfo(@$)) ; path - : pathSegments -> new yy.PathNode(false, $1, yy.locInfo(@$)) + : pathSegments -> new yy.PathExpression(false, $1, yy.locInfo(@$)) ; pathSegments From ffc9fb5007f516350ddeca90640982e4e12503f5 Mon Sep 17 00:00:00 2001 From: kpdecker Date: Fri, 28 Nov 2014 16:55:41 -0600 Subject: [PATCH 11/15] Add original to BooleanLiteral --- lib/handlebars/compiler/ast.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/handlebars/compiler/ast.js b/lib/handlebars/compiler/ast.js index 69581f281..efb4ece98 100644 --- a/lib/handlebars/compiler/ast.js +++ b/lib/handlebars/compiler/ast.js @@ -120,7 +120,8 @@ var AST = { BooleanLiteral: function(bool, locInfo) { this.loc = locInfo; this.type = 'BooleanLiteral'; - this.value = bool === 'true'; + this.original = + this.value = bool === 'true'; }, Hash: function(pairs, locInfo) { From 95b23095c097447ff4e1059720abfd2132cb9b2d Mon Sep 17 00:00:00 2001 From: kpdecker Date: Fri, 28 Nov 2014 16:55:53 -0600 Subject: [PATCH 12/15] First crack at compiler API docs --- docs/compiler-api.md | 224 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 224 insertions(+) create mode 100644 docs/compiler-api.md diff --git a/docs/compiler-api.md b/docs/compiler-api.md new file mode 100644 index 000000000..7e89b01e4 --- /dev/null +++ b/docs/compiler-api.md @@ -0,0 +1,224 @@ +# Handlebars Compiler APIs + +There are a number of formal APIs that tool implementors may interact with. + +## AST + +Other tools may interact with the formal AST as defined below. Any JSON structure matching this pattern may be used and passed into the `compile` and `precompile` methods in the same way as the text for a template. + +AST structures may be generated either with the `Handlebars.parse` method and then manipulated, via the `Handlebars.AST` objects of the same name, or constructed manually as a generic JavaScript object matching the structure defined below. + +```javascript +var ast = Handlebars.parse(myTemplate); + +// Modify ast + +Handlebars.precompile(ast); +``` + + +### Basic + +```java +interface Node { + type: string; + loc: SourceLocation | null; +} + +interface SourceLocation { + source: string | null; + start: Position; + end: Position; +} + +interface Position { + line: uint >= 1; + column: uint >= 0; +} +``` + +### Programs + +```java +interface Program <: Node { + type: "Program"; + body: [ Statement ]; + + blockParams: [ string ]; + strip: StripFlags | null; +} +``` + +### Statements + +```java +interface Statement <: Node { + strip: StripFlags | null; +} + +interface MustacheStatement <: Statement { + type: "MustacheStatement"; + sexpr: Subexpression; + escaped: boolean; +} + +interface BlockStatement <: Statement { + type: "BlockStatement"; + sexpr: Subexpression; + program: Program; + inverse: Program | null; +} + +interface PartialStatement <: Statement { + type: "PartialStatement"; + sexpr: Subexpression; + + indent: string; +} + +interface ContentStatement <: Statement { + type: "ContentStatement"; + value: string; + original: string; +} + +interface CommentStatement <: Statement { + type: "CommentStatement"; + value: string; +} +``` + +### Expressions + +```java +interface Expression <: Node { } +``` + +##### Subexpressions + +```java +interface SubExpression <: Expression { + type: "SubExpression"; + path: PathExpression; + params: [ Expression ]; + hash: Hash; + + isHelper: true | null; +} +``` + +`isHelper` is not required and is used to disambiguate between cases such as `{{foo}}` and `(foo)`, which have slightly different call behaviors. + +##### Paths + +```java +interface PathExpression <: Expression { + type: "PathExpression"; + data: boolean; + depth: uint >= 0; + parts: [ string ]; + original: string; +} +``` + +- `data` is true when the given expression is a `@data` reference. +- `depth` is an integer representation of which context the expression references. `0` represents the current context, `1` would be `../`, etc. +- `parts` is an array of the names in the path. `foo.bar` would be `['foo', 'bar']`. Scope references, `.`, `..`, and `this` should be omitted from this array. +- `original` is the path as entered by the user. Separator and scope references are left untouched. + + +##### Literals + +```java +interface Literal <: Expression { } + +interface StringLiteral <: Literal { + type: "StringLiteral"; + value: string; + original: string; +} + +interface BooleanLiteral <: Literal { + type: "BooleanLiteral"; + value: boolean; + original: boolean; +} + +interface NumberLiteral <: Literal { + type: "NumberLiteral"; + value: number; + original: number; +} +``` + + +### Miscellaneous + +```java +interface Hash <: Node { + type: "Hash"; + pairs: [ HashPair ]; +} + +interface HashPair <: Node { + type: "HashPair"; + key: string; + value: Expression; +} + +interface StripFlags { + left: boolean; + right: boolean; +} +``` + +`StripFlags` are used to signify whitespace control character that may have been entered on a given statement. + +TODO : Document what the flags mean or drop this from things like Program. + +## AST Visitor + +`Handlebars.Visitor` is available as a base class for general interaction with AST structures. This will by default traverse the entire tree and individual methods may be overridden to provide specific responses to particular nodes. + +Recording all referenced partial names: + +```javascript +var Visitor = Handlebars.Visitor; + +function ImportScanner() { + this.partials = []; +} +ImportScanner.prototype = new Visitor(); + +ImportScanner.prototype.PartialStatement = function(partial) { + this.partials.push({request: partial.sexpr.original}); + + Visitor.prototype.PartialStatement.call(this, partial); +}; + +var scanner = new ImportScanner(); +scanner.accept(ast); +``` + +## JavaScript Compiler + +The `Handlebars.JavaScriptCompiler` object has a number of methods that may be customized to alter the output of the compiler: + +```javascript +function MyCompiler() { + Handlebars.JavaScriptCompiler.apply(this, arguments); +} +MyCompiler.prototype = Object.create(Handlebars.JavaScriptCompiler); + +MyCompiler.nameLookup = function(parent, name, type) { + if (type === 'partial') { + return 'MyPartialList[' + JSON.stringify(name) ']'; + } else { + return Handlebars.JavaScriptCompiler.prototype.nameLookup.call(this, parent, name, type); + } +}; + +var env = Handlebars.create(); +env.JavaScriptCompiler = MyCompiler; +env.compile('my template'); +``` From 8a6796e5c09686b47945a35826d77680d589d07c Mon Sep 17 00:00:00 2001 From: kpdecker Date: Fri, 28 Nov 2014 17:26:52 -0600 Subject: [PATCH 13/15] Move Jison parsing out of AST into helpers --- lib/handlebars/compiler/ast.js | 47 +++++------------------- lib/handlebars/compiler/helpers.js | 37 +++++++++++++++++++ spec/ast.js | 59 ++---------------------------- spec/parser.js | 13 ++++++- src/handlebars.yy | 14 +++---- 5 files changed, 68 insertions(+), 102 deletions(-) diff --git a/lib/handlebars/compiler/ast.js b/lib/handlebars/compiler/ast.js index efb4ece98..9f2544362 100644 --- a/lib/handlebars/compiler/ast.js +++ b/lib/handlebars/compiler/ast.js @@ -10,20 +10,12 @@ var AST = { this.strip = strip; }, - MustacheStatement: function(rawParams, open, strip, locInfo) { + MustacheStatement: function(sexpr, escaped, strip, locInfo) { this.loc = locInfo; this.type = 'MustacheStatement'; - this.sexpr = rawParams; - - // Open may be a string parsed from the parser or a passed boolean flag - if (open != null && open.charAt) { - // Must use charAt to support IE pre-10 - var escapeFlag = open.charAt(3) || open.charAt(2); - this.escaped = escapeFlag !== '{' && escapeFlag !== '&'; - } else { - this.escaped = !!open; - } + this.sexpr = sexpr; + this.escaped = escaped; this.strip = strip; }, @@ -63,43 +55,22 @@ var AST = { strip.inlineStandalone = true; }, - SubExpression: function(rawParams, hash, locInfo) { + SubExpression: function(path, params, hash, locInfo) { this.loc = locInfo; this.type = 'SubExpression'; - this.path = rawParams[0]; - this.params = rawParams.slice(1); + this.path = path; + this.params = params || []; this.hash = hash; }, - PathExpression: function(data, parts, locInfo) { + PathExpression: function(data, depth, parts, original, locInfo) { this.loc = locInfo; this.type = 'PathExpression'; - var original = '', - dig = [], - depth = 0, - depthString = ''; - - for(var i=0,l=parts.length; i 0) { - throw new Exception('Invalid path: ' + original, this); - } else if (part === '..') { - depth++; - depthString += '../'; - } - } else { - dig.push(part); - } - } - this.data = data; - this.original = (data ? '@' : '') + original; - this.parts = dig; + this.original = original; + this.parts = parts; this.depth = depth; }, diff --git a/lib/handlebars/compiler/helpers.js b/lib/handlebars/compiler/helpers.js index 3d5144f70..f215049f3 100644 --- a/lib/handlebars/compiler/helpers.js +++ b/lib/handlebars/compiler/helpers.js @@ -24,6 +24,43 @@ export function stripComment(comment) { .replace(/-?-?~?\}\}$/, ''); } +export function preparePath(data, parts, locInfo) { + /*jshint -W040 */ + locInfo = this.locInfo(locInfo); + + var original = data ? '@' : '', + dig = [], + depth = 0, + depthString = ''; + + for(var i=0,l=parts.length; i 0) { + throw new Exception('Invalid path: ' + original, {loc: locInfo}); + } else if (part === '..') { + depth++; + depthString += '../'; + } + } else { + dig.push(part); + } + } + + return new this.PathExpression(data, depth, dig, original, locInfo); +} + +export function prepareMustache(sexpr, open, strip, locInfo) { + /*jshint -W040 */ + // Must use charAt to support IE pre-10 + var escapeFlag = open.charAt(3) || open.charAt(2), + escaped = escapeFlag !== '{' && escapeFlag !== '&'; + + return new this.MustacheStatement(sexpr, escaped, strip, this.locInfo(locInfo)); +} + export function prepareRawBlock(openRawBlock, content, close, locInfo) { /*jshint -W040 */ if (openRawBlock.sexpr.path.original !== close) { diff --git a/spec/ast.js b/spec/ast.js index 53267493d..3e36730a4 100644 --- a/spec/ast.js +++ b/spec/ast.js @@ -23,43 +23,14 @@ describe('ast', function() { } describe('MustacheStatement', function() { - function testEscape(open, expected) { - var mustache = new handlebarsEnv.AST.MustacheStatement([{}], open, false); - equals(mustache.escaped, expected); - } - it('should store args', function() { var id = {isSimple: true}, hash = {}, - mustache = new handlebarsEnv.AST.MustacheStatement([id, 'param1'], '', false, LOCATION_INFO); + mustache = new handlebarsEnv.AST.MustacheStatement({}, true, {}, LOCATION_INFO); equals(mustache.type, 'MustacheStatement'); equals(mustache.escaped, true); testLocationInfoStorage(mustache); }); - it('should accept token for escape', function() { - testEscape('{{', true); - testEscape('{{~', true); - testEscape('{{#', true); - testEscape('{{~#', true); - testEscape('{{/', true); - testEscape('{{~/', true); - testEscape('{{^', true); - testEscape('{{~^', true); - testEscape('{', true); - testEscape('{', true); - - testEscape('{{&', false); - testEscape('{{~&', false); - testEscape('{{{', false); - testEscape('{{~{', false); - }); - it('should accept boolean for escape', function() { - testEscape(true, true); - testEscape({}, true); - - testEscape(false, false); - testEscape(undefined, false); - }); }); describe('BlockStatement', function() { it('should throw on mustache mismatch', function() { @@ -70,7 +41,7 @@ describe('ast', function() { it('stores location info', function(){ var sexprNode = new handlebarsEnv.AST.SubExpression([{ original: 'foo'}], null); - var mustacheNode = new handlebarsEnv.AST.MustacheStatement(sexprNode, null, '{{', {}); + var mustacheNode = new handlebarsEnv.AST.MustacheStatement(sexprNode, false, {}); var block = new handlebarsEnv.AST.BlockStatement(mustacheNode, {body: [], strip: {}}, {body: [], strip: {}}, { @@ -82,32 +53,8 @@ describe('ast', function() { }); }); describe('PathExpression', function() { - it('should throw on invalid path', function() { - shouldThrow(function() { - new handlebarsEnv.AST.PathExpression(false, [ - {part: 'foo'}, - {part: '..'}, - {part: 'bar'} - ], {start: {line: 1, column: 1}}); - }, Handlebars.Exception, "Invalid path: foo.. - 1:1"); - shouldThrow(function() { - new handlebarsEnv.AST.PathExpression(false, [ - {part: 'foo'}, - {part: '.'}, - {part: 'bar'} - ], {start: {line: 1, column: 1}}); - }, Handlebars.Exception, "Invalid path: foo. - 1:1"); - shouldThrow(function() { - new handlebarsEnv.AST.PathExpression(false, [ - {part: 'foo'}, - {part: 'this'}, - {part: 'bar'} - ], {start: {line: 1, column: 1}}); - }, Handlebars.Exception, "Invalid path: foothis - 1:1"); - }); - it('stores location info', function(){ - var idNode = new handlebarsEnv.AST.PathExpression(false, [], LOCATION_INFO); + var idNode = new handlebarsEnv.AST.PathExpression(false, 0, [], 'foo', LOCATION_INFO); testLocationInfoStorage(idNode); }); }); diff --git a/spec/parser.js b/spec/parser.js index ad5273446..26eb4ddfb 100644 --- a/spec/parser.js +++ b/spec/parser.js @@ -166,7 +166,6 @@ describe('parser', function() { it('parses inverse block with block params', function() { equals(ast_for("{{^foo as |bar baz|}}content{{/foo}}"), "BLOCK:\n PATH:foo []\n {{^}}\n BLOCK PARAMS: [ bar baz ]\n CONTENT[ 'content' ]\n"); }); - it("raises if there's a Parse error", function() { shouldThrow(function() { ast_for("foo{{^}}bar"); @@ -186,6 +185,18 @@ describe('parser', function() { }, Error, /goodbyes doesn't match hellos/); }); + it('should handle invalid paths', function() { + shouldThrow(function() { + ast_for("{{foo/../bar}}"); + }, Error, /Invalid path: foo\/\.\. - 1:2/); + shouldThrow(function() { + ast_for("{{foo/./bar}}"); + }, Error, /Invalid path: foo\/\. - 1:2/); + shouldThrow(function() { + ast_for("{{foo/this/bar}}"); + }, Error, /Invalid path: foo\/this - 1:2/); + }); + it('knows how to report the correct line number in errors', function() { shouldThrow(function() { ast_for("hello\nmy\n{{foo}"); diff --git a/src/handlebars.yy b/src/handlebars.yy index 8b63df045..7f2765cbb 100644 --- a/src/handlebars.yy +++ b/src/handlebars.yy @@ -47,7 +47,7 @@ openInverse ; openInverseChain - : OPEN_INVERSE_CHAIN sexpr CLOSE -> new yy.MustacheStatement($2, $1, yy.stripFlags($1, $3), yy.locInfo(@$)) + : OPEN_INVERSE_CHAIN sexpr CLOSE -> yy.prepareMustache($2, $1, yy.stripFlags($1, $3), @$) ; inverseAndProgram @@ -73,8 +73,8 @@ closeBlock mustache // Parsing out the '&' escape token at AST level saves ~500 bytes after min due to the removal of one parser node. // This also allows for handler unification as all mustache node instances can utilize the same handler - : OPEN sexpr CLOSE -> new yy.MustacheStatement($2, $1, yy.stripFlags($1, $3), yy.locInfo(@$)) - | OPEN_UNESCAPED sexpr CLOSE_UNESCAPED -> new yy.MustacheStatement($2, $1, yy.stripFlags($1, $3), yy.locInfo(@$)) + : OPEN sexpr CLOSE -> yy.prepareMustache($2, $1, yy.stripFlags($1, $3), @$) + | OPEN_UNESCAPED sexpr CLOSE_UNESCAPED -> yy.prepareMustache($2, $1, yy.stripFlags($1, $3), @$) ; partial @@ -82,8 +82,8 @@ partial ; sexpr - : helperName param* hash? -> new yy.SubExpression([$1].concat($2), $3, yy.locInfo(@$)) - | dataName -> new yy.SubExpression([$1], null, yy.locInfo(@$)) + : helperName param* hash? -> new yy.SubExpression($1, $2, $3, yy.locInfo(@$)) + | dataName -> new yy.SubExpression($1, null, null, yy.locInfo(@$)) ; param @@ -114,11 +114,11 @@ helperName ; dataName - : DATA pathSegments -> new yy.PathExpression(true, $2, yy.locInfo(@$)) + : DATA pathSegments -> yy.preparePath(true, $2, @$) ; path - : pathSegments -> new yy.PathExpression(false, $1, yy.locInfo(@$)) + : pathSegments -> yy.preparePath(false, $1, @$) ; pathSegments From 928ba56b9577fd6cd874f0a83178f1265a6d0526 Mon Sep 17 00:00:00 2001 From: kpdecker Date: Fri, 28 Nov 2014 22:58:21 -0600 Subject: [PATCH 14/15] Rework strip flags to make clearer at in AST level Rather than keeping state in the AST, which requires some gymnastics, we create a separate visitor flow which does the top down iteration necessary to calculate all of the state needed for proper whitespace control evaluation. --- docs/compiler-api.md | 22 +- lib/handlebars/compiler/ast.js | 9 +- lib/handlebars/compiler/base.js | 4 +- lib/handlebars/compiler/helpers.js | 204 ++--------------- lib/handlebars/compiler/whitespace-control.js | 210 ++++++++++++++++++ spec/ast.js | 15 +- src/handlebars.yy | 17 +- 7 files changed, 268 insertions(+), 213 deletions(-) create mode 100644 lib/handlebars/compiler/whitespace-control.js diff --git a/docs/compiler-api.md b/docs/compiler-api.md index 7e89b01e4..89dcc4ccf 100644 --- a/docs/compiler-api.md +++ b/docs/compiler-api.md @@ -45,28 +45,31 @@ interface Program <: Node { body: [ Statement ]; blockParams: [ string ]; - strip: StripFlags | null; } ``` ### Statements ```java -interface Statement <: Node { - strip: StripFlags | null; -} +interface Statement <: Node { } interface MustacheStatement <: Statement { type: "MustacheStatement"; sexpr: Subexpression; escaped: boolean; + + strip: StripFlags | null; } interface BlockStatement <: Statement { type: "BlockStatement"; sexpr: Subexpression; - program: Program; + program: Program | null; inverse: Program | null; + + openStrip: StripFlags | null; + inverseStrip: StripFlags | null; + closeStrip: StripFlags | null; } interface PartialStatement <: Statement { @@ -74,6 +77,7 @@ interface PartialStatement <: Statement { sexpr: Subexpression; indent: string; + strip: StripFlags | null; } interface ContentStatement <: Statement { @@ -85,6 +89,8 @@ interface ContentStatement <: Statement { interface CommentStatement <: Statement { type: "CommentStatement"; value: string; + + strip: StripFlags | null; } ``` @@ -167,15 +173,13 @@ interface HashPair <: Node { } interface StripFlags { - left: boolean; - right: boolean; + open: boolean; + close: boolean; } ``` `StripFlags` are used to signify whitespace control character that may have been entered on a given statement. -TODO : Document what the flags mean or drop this from things like Program. - ## AST Visitor `Handlebars.Visitor` is available as a base class for general interaction with AST structures. This will by default traverse the entire tree and individual methods may be overridden to provide specific responses to particular nodes. diff --git a/lib/handlebars/compiler/ast.js b/lib/handlebars/compiler/ast.js index 9f2544362..72a56aa3f 100644 --- a/lib/handlebars/compiler/ast.js +++ b/lib/handlebars/compiler/ast.js @@ -20,14 +20,17 @@ var AST = { this.strip = strip; }, - BlockStatement: function(sexpr, program, inverse, strip, locInfo) { + BlockStatement: function(sexpr, program, inverse, openStrip, inverseStrip, closeStrip, locInfo) { this.loc = locInfo; this.type = 'BlockStatement'; this.sexpr = sexpr; this.program = program; this.inverse = inverse; - this.strip = strip; + + this.openStrip = openStrip; + this.inverseStrip = inverseStrip; + this.closeStrip = closeStrip; }, PartialStatement: function(sexpr, strip, locInfo) { @@ -37,7 +40,6 @@ var AST = { this.indent = ''; this.strip = strip; - this.strip.inlineStandalone = true; }, ContentStatement: function(string, locInfo) { @@ -52,7 +54,6 @@ var AST = { this.value = comment; this.strip = strip; - strip.inlineStandalone = true; }, SubExpression: function(path, params, hash, locInfo) { diff --git a/lib/handlebars/compiler/base.js b/lib/handlebars/compiler/base.js index 786c37ebd..ff237ec23 100644 --- a/lib/handlebars/compiler/base.js +++ b/lib/handlebars/compiler/base.js @@ -1,5 +1,6 @@ import parser from "./parser"; import AST from "./ast"; +import WhitespaceControl from "./whitespace-control"; module Helpers from "./helpers"; import { extend } from "../utils"; @@ -19,5 +20,6 @@ export function parse(input, options) { return new yy.SourceLocation(options && options.srcName, locInfo); }; - return parser.parse(input); + var strip = new WhitespaceControl(); + return strip.accept(parser.parse(input)); } diff --git a/lib/handlebars/compiler/helpers.js b/lib/handlebars/compiler/helpers.js index f215049f3..1daddf650 100644 --- a/lib/handlebars/compiler/helpers.js +++ b/lib/handlebars/compiler/helpers.js @@ -14,8 +14,8 @@ export function SourceLocation(source, locInfo) { export function stripFlags(open, close) { return { - left: open.charAt(2) === '~', - right: close.charAt(close.length-3) === '~' + open: open.charAt(2) === '~', + close: close.charAt(close.length-3) === '~' }; } @@ -69,9 +69,13 @@ export function prepareRawBlock(openRawBlock, content, close, locInfo) { throw new Exception(openRawBlock.sexpr.path.original + " doesn't match " + close, errorNode); } + locInfo = this.locInfo(locInfo); var program = new this.Program([content], null, {}, locInfo); - return new this.BlockStatement(openRawBlock.sexpr, program, undefined, undefined, locInfo); + return new this.BlockStatement( + openRawBlock.sexpr, program, undefined, + {}, {}, {}, + locInfo); } export function prepareBlock(openBlock, program, inverseAndProgram, close, inverted, locInfo) { @@ -85,192 +89,26 @@ export function prepareBlock(openBlock, program, inverseAndProgram, close, inver program.blockParams = openBlock.blockParams; - // Safely handle a chained inverse that does not have a non-conditional inverse - // (i.e. both inverseAndProgram AND close are undefined) - if (!close) { - close = {strip: {}}; - } - - // Find the inverse program that is involed with whitespace stripping. - var inverse = inverseAndProgram && inverseAndProgram.program, - firstInverse = inverse, - lastInverse = inverse; - if (inverse && inverse.inverse) { - firstInverse = inverse.body[0].program; + var inverse, + inverseStrip; - // Walk the inverse chain to find the last inverse that is actually in the chain. - while (lastInverse.inverse) { - lastInverse = lastInverse.body[lastInverse.body.length-1].program; + if (inverseAndProgram) { + if (inverseAndProgram.chain) { + inverseAndProgram.program.body[0].closeStrip = close.strip || close.openStrip; } - } - var strip = { - left: openBlock.strip.left, - right: close.strip.right, - - // Determine the standalone candiacy. Basically flag our content as being possibly standalone - // so our parent can determine if we actually are standalone - openStandalone: isNextWhitespace(program.body), - closeStandalone: isPrevWhitespace((firstInverse || program).body) - }; - - if (openBlock.strip.right) { - omitRight(program.body, null, true); - } - - if (inverse) { - var inverseStrip = inverseAndProgram.strip; - - if (inverseStrip.left) { - omitLeft(program.body, null, true); - } - - if (inverseStrip.right) { - omitRight(firstInverse.body, null, true); - } - if (close.strip.left) { - omitLeft(lastInverse.body, null, true); - } - - // Find standalone else statments - if (isPrevWhitespace(program.body) - && isNextWhitespace(firstInverse.body)) { - - omitLeft(program.body); - omitRight(firstInverse.body); - } - } else { - if (close.strip.left) { - omitLeft(program.body, null, true); - } + inverseStrip = inverseAndProgram.strip; + inverse = inverseAndProgram.program; } if (inverted) { - return new this.BlockStatement(openBlock.sexpr, inverse, program, strip, locInfo); - } else { - return new this.BlockStatement(openBlock.sexpr, program, inverse, strip, locInfo); - } -} - - -export function prepareProgram(body, isRoot) { - for (var i = 0, l = body.length; i < l; i++) { - var current = body[i], - strip = current.strip; - - if (!strip) { - continue; - } - - var _isPrevWhitespace = isPrevWhitespace(body, i, isRoot, current.type === 'partial'), - _isNextWhitespace = isNextWhitespace(body, i, isRoot), - - openStandalone = strip.openStandalone && _isPrevWhitespace, - closeStandalone = strip.closeStandalone && _isNextWhitespace, - inlineStandalone = strip.inlineStandalone && _isPrevWhitespace && _isNextWhitespace; - - if (strip.right) { - omitRight(body, i, true); - } - if (strip.left) { - omitLeft(body, i, true); - } - - if (inlineStandalone) { - omitRight(body, i); - - if (omitLeft(body, i)) { - // If we are on a standalone node, save the indent info for partials - if (current.type === 'PartialStatement') { - // Pull out the whitespace from the final line - current.indent = (/([ \t]+$)/).exec(body[i-1].original)[1]; - } - } - } - if (openStandalone) { - omitRight((current.program || current.inverse).body); - - // Strip out the previous content node if it's whitespace only - omitLeft(body, i); - } - if (closeStandalone) { - // Always strip the next node - omitRight(body, i); - - omitLeft((current.inverse || current.program).body); - } - } - - return body; -} - -function isPrevWhitespace(body, i, isRoot) { - if (i === undefined) { - i = body.length; - } - - // Nodes that end with newlines are considered whitespace (but are special - // cased for strip operations) - var prev = body[i-1], - sibling = body[i-2]; - if (!prev) { - return isRoot; - } - - if (prev.type === 'ContentStatement') { - return (sibling || !isRoot ? (/\r?\n\s*?$/) : (/(^|\r?\n)\s*?$/)).test(prev.original); - } -} -function isNextWhitespace(body, i, isRoot) { - if (i === undefined) { - i = -1; - } - - var next = body[i+1], - sibling = body[i+2]; - if (!next) { - return isRoot; - } - - if (next.type === 'ContentStatement') { - return (sibling || !isRoot ? (/^\s*?\r?\n/) : (/^\s*?(\r?\n|$)/)).test(next.original); - } -} - -// Marks the node to the right of the position as omitted. -// I.e. {{foo}}' ' will mark the ' ' node as omitted. -// -// If i is undefined, then the first child will be marked as such. -// -// If mulitple is truthy then all whitespace will be stripped out until non-whitespace -// content is met. -function omitRight(body, i, multiple) { - var current = body[i == null ? 0 : i + 1]; - if (!current || current.type !== 'ContentStatement' || (!multiple && current.rightStripped)) { - return; - } - - var original = current.value; - current.value = current.value.replace(multiple ? (/^\s+/) : (/^[ \t]*\r?\n?/), ''); - current.rightStripped = current.value !== original; -} - -// Marks the node to the left of the position as omitted. -// I.e. ' '{{foo}} will mark the ' ' node as omitted. -// -// If i is undefined then the last child will be marked as such. -// -// If mulitple is truthy then all whitespace will be stripped out until non-whitespace -// content is met. -function omitLeft(body, i, multiple) { - var current = body[i == null ? body.length - 1 : i - 1]; - if (!current || current.type !== 'ContentStatement' || (!multiple && current.leftStripped)) { - return; + inverted = inverse; + inverse = program; + program = inverted; } - // We omit the last node if it's whitespace only and not preceeded by a non-content node. - var original = current.value; - current.value = current.value.replace(multiple ? (/\s+$/) : (/[ \t]+$/), ''); - current.leftStripped = current.value !== original; - return current.leftStripped; + return new this.BlockStatement( + openBlock.sexpr, program, inverse, + openBlock.strip, inverseStrip, close && (close.strip || close.openStrip), + this.locInfo(locInfo)); } diff --git a/lib/handlebars/compiler/whitespace-control.js b/lib/handlebars/compiler/whitespace-control.js new file mode 100644 index 000000000..e10eb1570 --- /dev/null +++ b/lib/handlebars/compiler/whitespace-control.js @@ -0,0 +1,210 @@ +import Visitor from "./visitor"; + +function WhitespaceControl() { +} +WhitespaceControl.prototype = new Visitor(); + +WhitespaceControl.prototype.Program = function(program) { + var isRoot = !this.isRootSeen; + this.isRootSeen = true; + + var body = program.body; + for (var i = 0, l = body.length; i < l; i++) { + var current = body[i], + strip = this.accept(current); + + if (!strip) { + continue; + } + + var _isPrevWhitespace = isPrevWhitespace(body, i, isRoot), + _isNextWhitespace = isNextWhitespace(body, i, isRoot), + + openStandalone = strip.openStandalone && _isPrevWhitespace, + closeStandalone = strip.closeStandalone && _isNextWhitespace, + inlineStandalone = strip.inlineStandalone && _isPrevWhitespace && _isNextWhitespace; + + if (strip.close) { + omitRight(body, i, true); + } + if (strip.open) { + omitLeft(body, i, true); + } + + if (inlineStandalone) { + omitRight(body, i); + + if (omitLeft(body, i)) { + // If we are on a standalone node, save the indent info for partials + if (current.type === 'PartialStatement') { + // Pull out the whitespace from the final line + current.indent = (/([ \t]+$)/).exec(body[i-1].original)[1]; + } + } + } + if (openStandalone) { + omitRight((current.program || current.inverse).body); + + // Strip out the previous content node if it's whitespace only + omitLeft(body, i); + } + if (closeStandalone) { + // Always strip the next node + omitRight(body, i); + + omitLeft((current.inverse || current.program).body); + } + } + + return program; +}; +WhitespaceControl.prototype.BlockStatement = function(block) { + this.accept(block.program); + this.accept(block.inverse); + + // Find the inverse program that is involed with whitespace stripping. + var program = block.program || block.inverse, + inverse = block.program && block.inverse, + firstInverse = inverse, + lastInverse = inverse; + + if (inverse && inverse.chained) { + firstInverse = inverse.body[0].program; + + // Walk the inverse chain to find the last inverse that is actually in the chain. + while (lastInverse.chained) { + lastInverse = lastInverse.body[lastInverse.body.length-1].program; + } + } + + var strip = { + open: block.openStrip.open, + close: block.closeStrip.close, + + // Determine the standalone candiacy. Basically flag our content as being possibly standalone + // so our parent can determine if we actually are standalone + openStandalone: isNextWhitespace(program.body), + closeStandalone: isPrevWhitespace((firstInverse || program).body) + }; + + if (block.openStrip.close) { + omitRight(program.body, null, true); + } + + if (inverse) { + var inverseStrip = block.inverseStrip; + + if (inverseStrip.open) { + omitLeft(program.body, null, true); + } + + if (inverseStrip.close) { + omitRight(firstInverse.body, null, true); + } + if (block.closeStrip.open) { + omitLeft(lastInverse.body, null, true); + } + + // Find standalone else statments + if (isPrevWhitespace(program.body) + && isNextWhitespace(firstInverse.body)) { + + omitLeft(program.body); + omitRight(firstInverse.body); + } + } else { + if (block.closeStrip.open) { + omitLeft(program.body, null, true); + } + } + + return strip; +}; + +WhitespaceControl.prototype.MustacheStatement = function(mustache) { + return mustache.strip; +}; + +WhitespaceControl.prototype.PartialStatement = + WhitespaceControl.prototype.CommentStatement = function(node) { + var strip = node.strip || {}; + return { + inlineStandalone: true, + open: strip.open, + close: strip.close + }; +}; + + +function isPrevWhitespace(body, i, isRoot) { + if (i === undefined) { + i = body.length; + } + + // Nodes that end with newlines are considered whitespace (but are special + // cased for strip operations) + var prev = body[i-1], + sibling = body[i-2]; + if (!prev) { + return isRoot; + } + + if (prev.type === 'ContentStatement') { + return (sibling || !isRoot ? (/\r?\n\s*?$/) : (/(^|\r?\n)\s*?$/)).test(prev.original); + } +} +function isNextWhitespace(body, i, isRoot) { + if (i === undefined) { + i = -1; + } + + var next = body[i+1], + sibling = body[i+2]; + if (!next) { + return isRoot; + } + + if (next.type === 'ContentStatement') { + return (sibling || !isRoot ? (/^\s*?\r?\n/) : (/^\s*?(\r?\n|$)/)).test(next.original); + } +} + +// Marks the node to the right of the position as omitted. +// I.e. {{foo}}' ' will mark the ' ' node as omitted. +// +// If i is undefined, then the first child will be marked as such. +// +// If mulitple is truthy then all whitespace will be stripped out until non-whitespace +// content is met. +function omitRight(body, i, multiple) { + var current = body[i == null ? 0 : i + 1]; + if (!current || current.type !== 'ContentStatement' || (!multiple && current.rightStripped)) { + return; + } + + var original = current.value; + current.value = current.value.replace(multiple ? (/^\s+/) : (/^[ \t]*\r?\n?/), ''); + current.rightStripped = current.value !== original; +} + +// Marks the node to the left of the position as omitted. +// I.e. ' '{{foo}} will mark the ' ' node as omitted. +// +// If i is undefined then the last child will be marked as such. +// +// If mulitple is truthy then all whitespace will be stripped out until non-whitespace +// content is met. +function omitLeft(body, i, multiple) { + var current = body[i == null ? body.length - 1 : i - 1]; + if (!current || current.type !== 'ContentStatement' || (!multiple && current.leftStripped)) { + return; + } + + // We omit the last node if it's whitespace only and not preceeded by a non-content node. + var original = current.value; + current.value = current.value.replace(multiple ? (/\s+$/) : (/[ \t]+$/), ''); + current.leftStripped = current.value !== original; + return current.leftStripped; +} + +export default WhitespaceControl; diff --git a/spec/ast.js b/spec/ast.js index 3e36730a4..d464cf110 100644 --- a/spec/ast.js +++ b/spec/ast.js @@ -42,13 +42,14 @@ describe('ast', function() { it('stores location info', function(){ var sexprNode = new handlebarsEnv.AST.SubExpression([{ original: 'foo'}], null); var mustacheNode = new handlebarsEnv.AST.MustacheStatement(sexprNode, false, {}); - var block = new handlebarsEnv.AST.BlockStatement(mustacheNode, - {body: [], strip: {}}, {body: [], strip: {}}, - { - strip: {}, - path: {original: 'foo'} - }, - LOCATION_INFO); + var block = new handlebarsEnv.AST.BlockStatement( + mustacheNode, + {body: []}, + {body: []}, + {}, + {}, + {}, + LOCATION_INFO); testLocationInfoStorage(block); }); }); diff --git a/src/handlebars.yy b/src/handlebars.yy index 7f2765cbb..0b2062e39 100644 --- a/src/handlebars.yy +++ b/src/handlebars.yy @@ -5,11 +5,11 @@ %% root - : program EOF { yy.prepareProgram($1.body, true); return $1; } + : program EOF { return $1; } ; program - : statement* -> new yy.Program(yy.prepareProgram($1), null, {}, yy.locInfo(@$)) + : statement* -> new yy.Program($1, null, {}, yy.locInfo(@$)) ; statement @@ -26,7 +26,7 @@ content ; rawBlock - : openRawBlock content END_RAW_BLOCK -> yy.prepareRawBlock($1, $2, $3, yy.locInfo(@$)) + : openRawBlock content END_RAW_BLOCK -> yy.prepareRawBlock($1, $2, $3, @$) ; openRawBlock @@ -34,8 +34,8 @@ openRawBlock ; block - : openBlock program inverseChain? closeBlock -> yy.prepareBlock($1, $2, $3, $4, false, yy.locInfo(@$)) - | openInverse program inverseAndProgram? closeBlock -> yy.prepareBlock($1, $2, $3, $4, true, yy.locInfo(@$)) + : openBlock program inverseChain? closeBlock -> yy.prepareBlock($1, $2, $3, $4, false, @$) + | openInverse program inverseAndProgram? closeBlock -> yy.prepareBlock($1, $2, $3, $4, true, @$) ; openBlock @@ -56,10 +56,9 @@ inverseAndProgram inverseChain : openInverseChain program inverseChain? { - var inverse = yy.prepareBlock($1, $2, $3, $3, false, yy.locInfo(@$)), - program = new yy.Program(yy.prepareProgram([inverse]), null, {}, yy.locInfo(@$)); - - program.inverse = inverse; + var inverse = yy.prepareBlock($1, $2, $3, $3, false, @$), + program = new yy.Program([inverse], null, {}, yy.locInfo(@$)); + program.chained = true; $$ = { strip: $1.strip, program: program, chain: true }; } From a655aedb5cf523430b08ada5f8cc4730d1db3e5b Mon Sep 17 00:00:00 2001 From: kpdecker Date: Sat, 29 Nov 2014 17:59:09 -0600 Subject: [PATCH 15/15] s/Subexpression/SubExpression/ --- docs/compiler-api.md | 8 ++++---- lib/handlebars/compiler/compiler.js | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/compiler-api.md b/docs/compiler-api.md index 89dcc4ccf..5431a9830 100644 --- a/docs/compiler-api.md +++ b/docs/compiler-api.md @@ -55,7 +55,7 @@ interface Statement <: Node { } interface MustacheStatement <: Statement { type: "MustacheStatement"; - sexpr: Subexpression; + sexpr: SubExpression; escaped: boolean; strip: StripFlags | null; @@ -63,7 +63,7 @@ interface MustacheStatement <: Statement { interface BlockStatement <: Statement { type: "BlockStatement"; - sexpr: Subexpression; + sexpr: SubExpression; program: Program | null; inverse: Program | null; @@ -74,7 +74,7 @@ interface BlockStatement <: Statement { interface PartialStatement <: Statement { type: "PartialStatement"; - sexpr: Subexpression; + sexpr: SubExpression; indent: string; strip: StripFlags | null; @@ -100,7 +100,7 @@ interface CommentStatement <: Statement { interface Expression <: Node { } ``` -##### Subexpressions +##### SubExpressions ```java interface SubExpression <: Expression { diff --git a/lib/handlebars/compiler/compiler.js b/lib/handlebars/compiler/compiler.js index df5a10f4f..5ba091650 100644 --- a/lib/handlebars/compiler/compiler.js +++ b/lib/handlebars/compiler/compiler.js @@ -363,7 +363,7 @@ Compiler.prototype = { this.opcode('pushStringParam', val, value, val.type); if (val.type === 'SubExpression') { - // Subexpressions get evaluated and passed in + // SubExpressions get evaluated and passed in // in string params mode. this.accept(val); }