diff options
author | Kevin Decker <kpdecker@gmail.com> | 2014-11-29 18:02:12 -0600 |
---|---|---|
committer | Kevin Decker <kpdecker@gmail.com> | 2014-11-29 18:02:12 -0600 |
commit | d4070c36675bfecee290f20bd2d9c23a50e9e00b (patch) | |
tree | 0fdf5adfe0824f0310fe1745effcc1576d060933 /lib/handlebars/compiler | |
parent | 3a9440f954092558275cd4c05a35ba34bcbfa210 (diff) | |
parent | a655aedb5cf523430b08ada5f8cc4730d1db3e5b (diff) | |
download | handlebars.js-d4070c36675bfecee290f20bd2d9c23a50e9e00b.zip handlebars.js-d4070c36675bfecee290f20bd2d9c23a50e9e00b.tar.gz handlebars.js-d4070c36675bfecee290f20bd2d9c23a50e9e00b.tar.bz2 |
Merge pull request #915 from wycats/ast-update
Ast update
Diffstat (limited to 'lib/handlebars/compiler')
-rw-r--r-- | lib/handlebars/compiler/ast.js | 222 | ||||
-rw-r--r-- | lib/handlebars/compiler/base.js | 13 | ||||
-rw-r--r-- | lib/handlebars/compiler/code-gen.js | 15 | ||||
-rw-r--r-- | lib/handlebars/compiler/compiler.js | 271 | ||||
-rw-r--r-- | lib/handlebars/compiler/helpers.js | 271 | ||||
-rw-r--r-- | lib/handlebars/compiler/javascript-compiler.js | 49 | ||||
-rw-r--r-- | lib/handlebars/compiler/printer.js | 120 | ||||
-rw-r--r-- | lib/handlebars/compiler/visitor.js | 63 | ||||
-rw-r--r-- | lib/handlebars/compiler/whitespace-control.js | 210 |
9 files changed, 634 insertions, 600 deletions
diff --git a/lib/handlebars/compiler/ast.js b/lib/handlebars/compiler/ast.js index 0bc70e9..72a56aa 100644 --- a/lib/handlebars/compiler/ast.js +++ b/lib/handlebars/compiler/ast.js @@ -1,195 +1,111 @@ 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.type = "program"; - this.statements = statements; + Program: function(statements, blockParams, strip, locInfo) { + this.loc = locInfo; + this.type = 'Program'; + this.body = statements; + this.blockParams = blockParams; this.strip = strip; }, - MustacheNode: function(rawParams, hash, open, strip, locInfo) { - LocationInfo.call(this, locInfo); - this.type = "mustache"; - this.strip = strip; - - // 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; - } - - 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; - }, - - SexprNode: function(rawParams, hash, locInfo) { - LocationInfo.call(this, locInfo); - - this.type = "sexpr"; - this.hash = hash; - - var id = this.id = rawParams[0]; - var params = this.params = rawParams.slice(1); + MustacheStatement: function(sexpr, escaped, strip, locInfo) { + this.loc = locInfo; + this.type = 'MustacheStatement'; - // 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.sexpr = sexpr; + this.escaped = escaped; - PartialNode: function(partialName, context, hash, strip, locInfo) { - LocationInfo.call(this, 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) { - LocationInfo.call(this, locInfo); + BlockStatement: function(sexpr, program, inverse, openStrip, inverseStrip, closeStrip, locInfo) { + this.loc = locInfo; - this.type = 'block'; + this.type = 'BlockStatement'; this.sexpr = sexpr; this.program = program; this.inverse = inverse; - this.strip = strip; - if (inverse && !program) { - this.isInverse = true; - } + this.openStrip = openStrip; + this.inverseStrip = inverseStrip; + this.closeStrip = closeStrip; }, - ContentNode: function(string, locInfo) { - LocationInfo.call(this, locInfo); - this.type = "content"; - this.original = this.string = string; + PartialStatement: function(sexpr, strip, locInfo) { + this.loc = locInfo; + this.type = 'PartialStatement'; + this.sexpr = sexpr; + this.indent = ''; + + this.strip = strip; }, - HashNode: function(pairs, locInfo) { - LocationInfo.call(this, locInfo); - this.type = "hash"; - this.pairs = pairs; + ContentStatement: function(string, locInfo) { + this.loc = locInfo; + this.type = 'ContentStatement'; + this.original = this.value = string; }, - IdNode: function(parts, locInfo) { - LocationInfo.call(this, locInfo); - this.type = "ID"; - - var original = "", - dig = [], - depth = 0, - depthString = ''; - - for(var i=0,l=parts.length; i<l; i++) { - var part = parts[i].part; - original += (parts[i].separator || '') + part; - - if (part === ".." || part === "." || part === "this") { - if (dig.length > 0) { - throw new Exception("Invalid path: " + original, this); - } else if (part === "..") { - depth++; - depthString += '../'; - } else { - this.isScoped = true; - } - } else { - dig.push(part); - } - } + CommentStatement: function(comment, strip, locInfo) { + this.loc = locInfo; + this.type = 'CommentStatement'; + this.value = comment; - this.original = original; - this.parts = dig; - this.string = dig.join('.'); - this.depth = depth; - this.idName = depthString + this.string; + this.strip = strip; + }, - // 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; + SubExpression: function(path, params, hash, locInfo) { + this.loc = locInfo; - this.stringModeValue = this.string; + this.type = 'SubExpression'; + this.path = path; + this.params = params || []; + this.hash = hash; }, - PartialNameNode: function(name, locInfo) { - LocationInfo.call(this, locInfo); - this.type = "PARTIAL_NAME"; - this.name = name.original; - }, + PathExpression: function(data, depth, parts, original, locInfo) { + this.loc = locInfo; + this.type = 'PathExpression'; - DataNode: function(id, locInfo) { - LocationInfo.call(this, locInfo); - this.type = "DATA"; - this.id = id; - this.stringModeValue = id.stringModeValue; - this.idName = '@' + id.stringModeValue; + this.data = data; + this.original = original; + this.parts = parts; + this.depth = depth; }, - StringNode: function(string, locInfo) { - LocationInfo.call(this, locInfo); - this.type = "STRING"; + StringLiteral: function(string, locInfo) { + this.loc = locInfo; + this.type = 'StringLiteral'; this.original = - this.string = - this.stringModeValue = string; + this.value = string; }, - NumberNode: function(number, locInfo) { - LocationInfo.call(this, locInfo); - this.type = "NUMBER"; + NumberLiteral: function(number, locInfo) { + this.loc = locInfo; + this.type = 'NumberLiteral'; this.original = - this.number = number; - this.stringModeValue = Number(number); + this.value = Number(number); }, - BooleanNode: function(bool, locInfo) { - LocationInfo.call(this, locInfo); - this.type = "BOOLEAN"; - this.bool = bool; - this.stringModeValue = bool === "true"; + BooleanLiteral: function(bool, locInfo) { + this.loc = locInfo; + this.type = 'BooleanLiteral'; + this.original = + this.value = bool === 'true'; }, - CommentNode: function(comment, strip, locInfo) { - LocationInfo.call(this, locInfo); - this.type = "comment"; - this.comment = comment; - - this.strip = strip; - strip.inlineStandalone = true; + Hash: 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/base.js b/lib/handlebars/compiler/base.js index 1378463..ff237ec 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"; @@ -8,11 +9,17 @@ 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; } + if (input.type === 'Program') { return input; } parser.yy = yy; - return parser.parse(input); + // 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); + }; + + var strip = new WhitespaceControl(); + return strip.accept(parser.parse(input)); } diff --git a/lib/handlebars/compiler/code-gen.js b/lib/handlebars/compiler/code-gen.js index 7d1b4ca..0fddb7c 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) { @@ -99,7 +99,7 @@ CodeGen.prototype = { }, quotedString: function(str) { - return '"' + str + return '"' + (str + '') .replace(/\\/g, '\\\\') .replace(/"/g, '\\"') .replace(/\n/g, '\\n') @@ -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 1e5d07a..5ba0916 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(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(path) { + var part = path.parts[0]; + + return path.parts.length === 1 && !scopedId(path) && !path.depth; +} + export function Compiler() {} // the foundHelper register will disambiguate helper lookup from finding a @@ -74,12 +94,12 @@ Compiler.prototype = { return this[node.type](node); }, - program: function(program) { - var statements = program.statements; - - for(var i=0, l=statements.length; i<l; i++) { - this.accept(statements[i]); + Program: function(program) { + var body = program.body; + for(var i=0, l=body.length; i<l; i++) { + this.accept(body[i]); } + this.isSimple = l === 1; this.depths.list = this.depths.list.sort(function(a, b) { @@ -107,24 +127,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 @@ -132,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); @@ -147,54 +162,30 @@ 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<l; i++) { - this.pushParam(pairs[i][1]); - } - while(i--) { - this.opcode('assignToHash', hash, pairs[i][0]); - } - this.opcode('popHash', hash); - }, - - partial: function(partial) { - var partialName = partial.partialName; + PartialStatement: function(partial) { + var partialName = partial.sexpr.path.original; this.usePartial = true; - if (partial.hash) { - this.accept(partial.hash); - } else { - this.opcode('pushLiteral', partial, 'undefined'); + var params = partial.sexpr.params; + 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}); } - if (partial.context) { - this.accept(partial.context); - } else { - this.opcode('getContext', partial, 0); - this.opcode('pushContext', partial); - } + this.setupFullMustacheParams(partial.sexpr, undefined, undefined, true); var indent = partial.indent || ''; if (this.options.preventIndent && indent) { 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); - } - }, - - mustache: function(mustache) { - this.sexpr(mustache.sexpr); + MustacheStatement: function(mustache) { + this.accept(mustache.sexpr); if(mustache.escaped && !this.options.noEscape) { this.opcode('appendEscaped', mustache); @@ -203,106 +194,107 @@ Compiler.prototype = { } }, + ContentStatement: function(content) { + if (content.value) { + this.opcode('appendContent', content, content.value); + } + }, + + CommentStatement: function() {}, + + SubExpression: 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], + 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.ID(id); + this.accept(path); this.opcode('invokeAmbiguous', sexpr, name, isBlock); }, simpleSexpr: function(sexpr) { - var id = sexpr.id; - - if (id.type === 'DATA') { - this.DATA(id); - } else if (id.parts.length) { - this.ID(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; + path.falsy = true; - this.ID(id); - this.opcode('invokeHelper', sexpr, params.length, id.original, id.isSimple); - } - }, - - 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); + this.accept(path); + this.opcode('invokeHelper', sexpr, params.length, path.original, simpleId(path)); } }, - ID: function(id) { - this.addDepth(id.depth); - this.opcode('getContext', id, id.depth); + PathExpression: function(path) { + this.addDepth(path.depth); + this.opcode('getContext', path, path.depth); - var name = id.parts[0]; + var name = path.parts[0]; if (!name) { // Context reference, i.e. `{{foo .}}` or `{{foo ..}}` - this.opcode('pushContext', id); + this.opcode('pushContext', path); + } else if (path.data) { + this.options.data = true; + this.opcode('lookupData', path, path.depth, path.parts); } else { - this.opcode('lookupOnContext', id, id.parts, id.falsy, id.isScoped); + this.opcode('lookupOnContext', path, path.parts, path.falsy, scopedId(path)); } }, - 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); }, - STRING: function(string) { - this.opcode('pushString', string, string.string); + NumberLiteral: function(number) { + this.opcode('pushLiteral', number, number.value); }, - NUMBER: function(number) { - this.opcode('pushLiteral', number, number.number); + BooleanLiteral: function(bool) { + this.opcode('pushLiteral', bool, bool.value); }, - BOOLEAN: function(bool) { - this.opcode('pushLiteral', bool, bool.bool); - }, + Hash: function(hash) { + var pairs = hash.pairs, i, l; + + this.opcode('pushHash', hash); - comment: function() {}, + for (i=0, l=pairs.length; i<l; i++) { + this.pushParam(pairs[i].value); + } + while (i--) { + this.opcode('assignToHash', hash, pairs[i].key); + } + this.opcode('popHash', hash); + }, // 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) { @@ -315,14 +307,21 @@ Compiler.prototype = { }, classifySexpr: function(sexpr) { - var isHelper = sexpr.isHelper; - var isEligible = sexpr.eligibleHelper; - var options = this.options; + // 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.path); + + var options = this.options; // if ambiguous, we can possibly resolve the ambiguity now // An eligible helper is one that does not have a complex path, i.e. `this.foo`, `../foo` etc. if (isEligible && !isHelper) { - var name = sexpr.id.parts[0]; + var name = sexpr.path.parts[0]; if (options.knownHelpers[name]) { isHelper = true; @@ -331,9 +330,9 @@ Compiler.prototype = { } } - if (isHelper) { return "helper"; } - else if (isEligible) { return "ambiguous"; } - else { return "simple"; } + if (isHelper) { return 'helper'; } + else if (isEligible) { return 'ambiguous'; } + else { return 'simple'; } }, pushParams: function(params) { @@ -343,27 +342,47 @@ Compiler.prototype = { }, pushParam: function(val) { + var value = val.value != null ? val.value : val.original || ''; + + // Force helper evaluation + if (val.type === 'SubExpression') { + val.isHelper = true; + } + 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, val.stringModeValue, val.type); + this.opcode('pushStringParam', val, value, val.type); - if (val.type === 'sexpr') { - // Subexpressions get evaluated and passed in + if (val.type === 'SubExpression') { + // SubExpressions get evaluated and passed in // in string params mode. - this.sexpr(val); + this.accept(val); } } else { if (this.trackIds) { - this.opcode('pushId', val, val.type, val.idName || val.stringModeValue); + value = val.original || value; + if (value.replace) { + value = value + .replace(/^\.\//g, '') + .replace(/^\.$/g, ''); + } + + this.opcode('pushId', val, val.type, value); } this.accept(val); } }, - setupFullMustacheParams: function(sexpr, program, inverse) { + setupFullMustacheParams: function(sexpr, program, inverse, omitEmpty) { var params = sexpr.params; this.pushParams(params); @@ -371,9 +390,9 @@ Compiler.prototype = { this.opcode('pushProgram', sexpr, inverse); if (sexpr.hash) { - this.hash(sexpr.hash); + this.accept(sexpr.hash); } else { - this.opcode('emptyHash', sexpr); + this.opcode('emptyHash', sexpr, omitEmpty); } return params; @@ -381,7 +400,7 @@ Compiler.prototype = { }; export function precompile(input, options, env) { - if (input == null || (typeof input !== 'string' && input.constructor !== env.AST.ProgramNode)) { + if (input == null || (typeof input !== 'string' && input.type !== 'Program')) { throw new Exception("You must pass a string or Handlebars AST to Handlebars.precompile. You passed " + input); } @@ -393,13 +412,13 @@ 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); } export function compile(input, options, env) { - if (input == null || (typeof input !== 'string' && input.constructor !== env.AST.ProgramNode)) { + if (input == null || (typeof input !== 'string' && input.type !== 'Program')) { throw new Exception("You must pass a string or Handlebars AST to Handlebars.compile. You passed " + input); } @@ -415,7 +434,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 50a3c53..1daddf6 100644 --- a/lib/handlebars/compiler/helpers.js +++ b/lib/handlebars/compiler/helpers.js @@ -1,9 +1,21 @@ 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) === '~', - right: close.charAt(close.length-3) === '~' + open: open.charAt(2) === '~', + close: close.charAt(close.length-3) === '~' }; } @@ -12,222 +24,91 @@ export function stripComment(comment) { .replace(/-?-?~?\}\}$/, ''); } -export function prepareRawBlock(openRawBlock, content, close, locInfo) { +export function preparePath(data, parts, locInfo) { /*jshint -W040 */ - if (openRawBlock.sexpr.id.original !== close) { - var errorNode = { - firstLine: openRawBlock.sexpr.firstLine, - firstColumn: openRawBlock.sexpr.firstColumn - }; - - throw new Exception(openRawBlock.sexpr.id.original + " doesn't match " + close, errorNode); + locInfo = this.locInfo(locInfo); + + var original = data ? '@' : '', + dig = [], + depth = 0, + depthString = ''; + + for(var i=0,l=parts.length; i<l; i++) { + var part = parts[i].part; + original += (parts[i].separator || '') + part; + + if (part === '..' || part === '.' || part === 'this') { + if (dig.length > 0) { + throw new Exception('Invalid path: ' + original, {loc: locInfo}); + } else if (part === '..') { + depth++; + depthString += '../'; + } + } else { + dig.push(part); + } } - var program = new this.ProgramNode([content], null, {}, locInfo); - - return new this.BlockNode(openRawBlock.sexpr, program, undefined, undefined, locInfo); + return new this.PathExpression(data, depth, dig, original, locInfo); } -export function prepareBlock(openBlock, program, inverseAndProgram, close, inverted, locInfo) { +export function prepareMustache(sexpr, open, strip, locInfo) { /*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 - }; + // Must use charAt to support IE pre-10 + var escapeFlag = open.charAt(3) || open.charAt(2), + escaped = escapeFlag !== '{' && escapeFlag !== '&'; - throw new Exception(openBlock.sexpr.id.original + ' doesn\'t match ' + close.path.original, errorNode); - } + return new this.MustacheStatement(sexpr, escaped, strip, this.locInfo(locInfo)); +} - program.blockParams = openBlock.blockParams; +export function prepareRawBlock(openRawBlock, content, close, locInfo) { + /*jshint -W040 */ + if (openRawBlock.sexpr.path.original !== close) { + var errorNode = {loc: openRawBlock.sexpr.loc}; - // 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: {}}; + throw new Exception(openRawBlock.sexpr.path.original + " doesn't match " + close, errorNode); } - // 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.statements[0].program; + locInfo = this.locInfo(locInfo); + var program = new this.Program([content], null, {}, locInfo); - // Walk the inverse chain to find the last inverse that is actually in the chain. - while (lastInverse.inverse) { - lastInverse = lastInverse.statements[lastInverse.statements.length-1].program; - } - } - - var strip = { - left: openBlock.strip.left, - right: close.strip.right, + return new this.BlockStatement( + openRawBlock.sexpr, program, undefined, + {}, {}, {}, + locInfo); +} - // 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.statements), - closeStandalone: isPrevWhitespace((firstInverse || program).statements) - }; +export function prepareBlock(openBlock, program, inverseAndProgram, close, inverted, locInfo) { + /*jshint -W040 */ + // When we are chaining inverse calls, we will not have a close path + if (close && close.path && openBlock.sexpr.path.original !== close.path.original) { + var errorNode = {loc: openBlock.sexpr.loc}; - if (openBlock.strip.right) { - omitRight(program.statements, null, true); + throw new Exception(openBlock.sexpr.path.original + ' doesn\'t match ' + close.path.original, errorNode); } - if (inverse) { - var inverseStrip = inverseAndProgram.strip; + program.blockParams = openBlock.blockParams; - if (inverseStrip.left) { - omitLeft(program.statements, null, true); - } + var inverse, + inverseStrip; - if (inverseStrip.right) { - omitRight(firstInverse.statements, null, true); - } - if (close.strip.left) { - omitLeft(lastInverse.statements, null, true); + if (inverseAndProgram) { + if (inverseAndProgram.chain) { + inverseAndProgram.program.body[0].closeStrip = close.strip || close.openStrip; } - // Find standalone else statments - if (isPrevWhitespace(program.statements) - && isNextWhitespace(firstInverse.statements)) { - - omitLeft(program.statements); - omitRight(firstInverse.statements); - } - } else { - if (close.strip.left) { - omitLeft(program.statements, null, true); - } + inverseStrip = inverseAndProgram.strip; + inverse = inverseAndProgram.program; } if (inverted) { - return new this.BlockNode(openBlock.sexpr, inverse, program, strip, locInfo); - } else { - return new this.BlockNode(openBlock.sexpr, program, inverse, strip, locInfo); - } -} - - -export function prepareProgram(statements, isRoot) { - for (var i = 0, l = statements.length; i < l; i++) { - var current = statements[i], - strip = current.strip; - - if (!strip) { - continue; - } - - var _isPrevWhitespace = isPrevWhitespace(statements, i, isRoot, current.type === 'partial'), - _isNextWhitespace = isNextWhitespace(statements, i, isRoot), - - openStandalone = strip.openStandalone && _isPrevWhitespace, - closeStandalone = strip.closeStandalone && _isNextWhitespace, - inlineStandalone = strip.inlineStandalone && _isPrevWhitespace && _isNextWhitespace; - - if (strip.right) { - omitRight(statements, i, true); - } - if (strip.left) { - omitLeft(statements, i, true); - } - - if (inlineStandalone) { - omitRight(statements, i); - - if (omitLeft(statements, i)) { - // If we are on a standalone node, save the indent info for partials - if (current.type === 'partial') { - // Pull out the whitespace from the final line - current.indent = (/([ \t]+$)/).exec(statements[i-1].original)[1]; - } - } - } - if (openStandalone) { - omitRight((current.program || current.inverse).statements); - - // Strip out the previous content node if it's whitespace only - omitLeft(statements, i); - } - if (closeStandalone) { - // Always strip the next node - omitRight(statements, i); - - omitLeft((current.inverse || current.program).statements); - } - } - - return statements; -} - -function isPrevWhitespace(statements, i, isRoot) { - if (i === undefined) { - i = statements.length; - } - - // Nodes that end with newlines are considered whitespace (but are special - // cased for strip operations) - var prev = statements[i-1], - sibling = statements[i-2]; - if (!prev) { - return isRoot; - } - - if (prev.type === 'content') { - return (sibling || !isRoot ? (/\r?\n\s*?$/) : (/(^|\r?\n)\s*?$/)).test(prev.original); - } -} -function isNextWhitespace(statements, i, isRoot) { - if (i === undefined) { - i = -1; - } - - var next = statements[i+1], - sibling = statements[i+2]; - if (!next) { - return isRoot; - } - - if (next.type === 'content') { - 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(statements, i, multiple) { - var current = statements[i == null ? 0 : i + 1]; - if (!current || current.type !== 'content' || (!multiple && current.rightStripped)) { - return; - } - - var original = current.string; - current.string = current.string.replace(multiple ? (/^\s+/) : (/^[ \t]*\r?\n?/), ''); - current.rightStripped = current.string !== 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(statements, i, multiple) { - var current = statements[i == null ? statements.length - 1 : i - 1]; - if (!current || current.type !== 'content' || (!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.string; - current.string = current.string.replace(multiple ? (/\s+$/) : (/[ \t]+$/), ''); - current.leftStripped = current.string !== 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/javascript-compiler.js b/lib/handlebars/compiler/javascript-compiler.js index ba84a3e..db19778 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) { @@ -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(); @@ -460,7 +460,7 @@ JavaScriptCompiler.prototype = { // If it's a subexpression, the string result // will be pushed after this opcode. - if (type !== 'sexpr') { + if (type !== 'SubExpression') { if (typeof string === 'string') { this.pushString(string); } else { @@ -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)); }, @@ -659,9 +664,9 @@ JavaScriptCompiler.prototype = { }, pushId: function(type, name) { - if (type === 'ID' || type === 'DATA') { + if (type === 'PathExpression') { this.pushString(name); - } else if (type === 'sexpr') { + } else if (type === 'SubExpression') { this.pushStackLiteral('true'); } else { this.pushStackLiteral('null'); @@ -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/compiler/printer.js b/lib/handlebars/compiler/printer.js index e93652c..b549d61 100644 --- a/lib/handlebars/compiler/printer.js +++ b/lib/handlebars/compiler/printer.js @@ -21,22 +21,22 @@ PrintVisitor.prototype.pad = function(string) { return out; }; -PrintVisitor.prototype.program = function(program) { - var out = "", - statements = program.statements, +PrintVisitor.prototype.Program = function(program) { + var out = '', + body = program.body, i, l; if (program.blockParams) { - var blockParams = "BLOCK PARAMS: ["; + var blockParams = 'BLOCK PARAMS: ['; for(i=0, l=program.blockParams.length; i<l; i++) { - blockParams += " " + program.blockParams[i]; + blockParams += ' ' + program.blockParams[i]; } - blockParams += " ]"; + blockParams += ' ]'; out += this.pad(blockParams); } - for(i=0, l=statements.length; i<l; i++) { - out = out + this.accept(statements[i]); + for(i=0, l=body.length; i<l; i++) { + out = out + this.accept(body[i]); } this.padding--; @@ -44,21 +44,25 @@ PrintVisitor.prototype.program = function(program) { return out; }; -PrintVisitor.prototype.block = function(block) { +PrintVisitor.prototype.MustacheStatement = function(mustache) { + return this.pad('{{ ' + this.accept(mustache.sexpr) + ' }}'); +}; + +PrintVisitor.prototype.BlockStatement = function(block) { var out = ""; - out = out + this.pad("BLOCK:"); + out = out + this.pad('BLOCK:'); this.padding++; out = out + this.pad(this.accept(block.sexpr)); if (block.program) { - out = out + this.pad("PROGRAM:"); + out = out + this.pad('PROGRAM:'); this.padding++; out = out + this.accept(block.program); this.padding--; } if (block.inverse) { if (block.program) { this.padding++; } - out = out + this.pad("{{^}}"); + out = out + this.pad('{{^}}'); this.padding++; out = out + this.accept(block.inverse); this.padding--; @@ -69,7 +73,27 @@ PrintVisitor.prototype.block = function(block) { return out; }; -PrintVisitor.prototype.sexpr = function(sexpr) { +PrintVisitor.prototype.PartialStatement = function(partial) { + var sexpr = partial.sexpr, + content = 'PARTIAL:' + sexpr.path.original; + if(sexpr.params[0]) { + content += ' ' + this.accept(sexpr.params[0]); + } + if (sexpr.hash) { + content += ' ' + this.accept(sexpr.hash); + } + return this.pad('{{> ' + content + ' }}'); +}; + +PrintVisitor.prototype.ContentStatement = function(content) { + return this.pad("CONTENT[ '" + content.value + "' ]"); +}; + +PrintVisitor.prototype.CommentStatement = function(comment) { + return this.pad("{{! '" + comment.value + "' }}"); +}; + +PrintVisitor.prototype.SubExpression = function(sexpr) { var params = sexpr.params, paramStrings = [], hash; for(var i=0, l=params.length; i<l; i++) { @@ -80,71 +104,37 @@ PrintVisitor.prototype.sexpr = function(sexpr) { hash = sexpr.hash ? " " + this.accept(sexpr.hash) : ""; - return this.accept(sexpr.id) + " " + params + hash; + return this.accept(sexpr.path) + " " + 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) { - content += " " + this.accept(partial.context); - } - if (partial.hash) { - content += " " + this.accept(partial.hash); - } - return this.pad("{{> " + content + " }}"); +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; - - for(var i=0, l=pairs.length; i<l; i++) { - left = pairs[i][0]; - right = this.accept(pairs[i][1]); - joinedPairs.push( left + "=" + right ); - } - return "HASH{" + joinedPairs.join(", ") + "}"; +PrintVisitor.prototype.StringLiteral = function(string) { + return '"' + string.value + '"'; }; -PrintVisitor.prototype.STRING = function(string) { - return '"' + string.string + '"'; +PrintVisitor.prototype.NumberLiteral = function(number) { + return "NUMBER{" + number.value + "}"; }; -PrintVisitor.prototype.NUMBER = function(number) { - return "NUMBER{" + number.number + "}"; +PrintVisitor.prototype.BooleanLiteral = function(bool) { + return "BOOLEAN{" + bool.value + "}"; }; -PrintVisitor.prototype.BOOLEAN = function(bool) { - return "BOOLEAN{" + bool.bool + "}"; -}; +PrintVisitor.prototype.Hash = function(hash) { + var pairs = hash.pairs; + var joinedPairs = [], left, right; -PrintVisitor.prototype.ID = function(id) { - var path = id.parts.join("/"); - if(id.parts.length > 1) { - return "PATH:" + path; - } else { - return "ID:" + path; + for (var i=0, l=pairs.length; i<l; i++) { + joinedPairs.push(this.accept(pairs[i])); } -}; - -PrintVisitor.prototype.PARTIAL_NAME = function(partialName) { - return "PARTIAL:" + partialName.name; -}; -PrintVisitor.prototype.DATA = function(data) { - return "@" + this.accept(data.id); + return 'HASH{' + joinedPairs.join(', ') + '}'; }; - -PrintVisitor.prototype.content = function(content) { - return this.pad("CONTENT[ '" + content.string + "' ]"); +PrintVisitor.prototype.HashPair = function(pair) { + return pair.key + '=' + this.accept(pair.value); }; - -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 a4eb2b4..c0cfab6 100644 --- a/lib/handlebars/compiler/visitor.js +++ b/lib/handlebars/compiler/visitor.js @@ -4,64 +4,63 @@ 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) { - var statements = program.statements, + Program: function(program) { + var body = program.body, i, l; - for(i=0, l=statements.length; i<l; i++) { - this.accept(statements[i]); + for(i=0, l=body.length; i<l; i++) { + this.accept(body[i]); } }, - block: function(block) { - this.accept(block.mustache); + MustacheStatement: function(mustache) { + this.accept(mustache.sexpr); + }, + + BlockStatement: function(block) { + this.accept(block.sexpr); this.accept(block.program); this.accept(block.inverse); }, - mustache: function(mustache) { - this.accept(mustache.sexpr); + PartialStatement: function(partial) { + this.accept(partial.partialName); + this.accept(partial.context); + this.accept(partial.hash); }, - sexpr: function(sexpr) { + ContentStatement: function(content) {}, + CommentStatement: function(comment) {}, + + SubExpression: function(sexpr) { var params = sexpr.params, paramStrings = [], hash; - this.accept(sexpr.id); + this.accept(sexpr.path); for(var i=0, l=params.length; i<l; i++) { this.accept(params[i]); } this.accept(sexpr.hash); }, - hash: function(hash) { + PathExpression: function(path) {}, + + StringLiteral: function(string) {}, + NumberLiteral: function(number) {}, + BooleanLiteral: function(bool) {}, + + Hash: function(hash) { var pairs = hash.pairs; for(var i=0, l=pairs.length; i<l; i++) { - this.accept(pairs[i][1]); + this.accept(pairs[i]); } }, - - 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); - }, - - STRING: function(string) {}, - NUMBER: function(number) {}, - BOOLEAN: function(bool) {}, - ID: function(id) {}, - - content: function(content) {}, - comment: function(comment) {} + HashPair: function(pair) { + this.accept(pair.value); + } }; export default Visitor; diff --git a/lib/handlebars/compiler/whitespace-control.js b/lib/handlebars/compiler/whitespace-control.js new file mode 100644 index 0000000..e10eb15 --- /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; |