From ec1c91ae805e91b3555cd3576b21daa12649b7d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20Mu=C3=B1oz?= <im.mmun@gmail.com> Date: Tue, 22 Apr 2014 23:07:01 -0400 Subject: [PATCH] Implement stripping --- lib/htmlbars/ast.js | 16 +++- lib/htmlbars/parser.js | 72 +++++++++++------- test/tests/combined_ast_test.js | 128 +++++++++++++++++++++++++++----- 3 files changed, 167 insertions(+), 49 deletions(-) diff --git a/lib/htmlbars/ast.js b/lib/htmlbars/ast.js index 300b99c7..73e16f01 100644 --- a/lib/htmlbars/ast.js +++ b/lib/htmlbars/ast.js @@ -1,13 +1,25 @@ import AST from "handlebars/compiler/ast"; -export var ProgramNode = AST.ProgramNode; -export var BlockNode = AST.BlockNode; export var MustacheNode = AST.MustacheNode; export var SexprNode = AST.SexprNode; export var HashNode = AST.HashNode; export var IdNode = AST.IdNode; export var StringNode = AST.StringNode; +export function ProgramNode(statements, strip) { + this.type = 'program'; + this.statements = statements; + this.strip = strip; +} + +export function BlockNode(mustache, program, inverse, strip) { + this.type = 'block'; + this.mustache = mustache; + this.program = program; + this.inverse = inverse; + this.strip = strip; +} + export function ElementNode(tag, attributes, helpers, children) { this.type = 'element'; this.tag = tag; diff --git a/lib/htmlbars/parser.js b/lib/htmlbars/parser.js index 78169971..ec8215bc 100644 --- a/lib/htmlbars/parser.js +++ b/lib/htmlbars/parser.js @@ -22,29 +22,56 @@ processor.accept = function(node) { }; processor.program = function(program) { - var children = []; - var node = new ProgramNode(children, program.strip); - var statements = program.statements; - var c, l = statements.length; + var statements = []; + var node = new ProgramNode(statements, program.strip); + var i, l = program.statements.length; + var statement; this.elementStack.push(node); if (l === 0) return this.elementStack.pop(); - c = statements[0]; - if (c.type === 'block' || c.type === 'mustache') { - children.push(new TextNode('')); + statement = program.statements[0]; + if (statement.type === 'block' || statement.type === 'mustache') { + statements.push(new TextNode('')); } - for (var i = 0; i < l; i++) { - this.accept(statements[i]); + for (i = 0; i < l; i++) { + this.accept(program.statements[i]); } process(this, this.tokenizer.tokenizeEOF()); - c = statements[l-1]; - if (c.type === 'block' || c.type === 'mustache') { - children.push(new TextNode('')); + statement = program.statements[l-1]; + if (statement.type === 'block' || statement.type === 'mustache') { + statements.push(new TextNode('')); + } + + // Remove any stripped whitespace + l = statements.length; + for (i = 0; i < l; i++) { + statement = statements[i]; + if (statement.type !== 'text') continue; + + if ((i > 0 && statements[i-1].strip && statements[i-1].strip.right) || + (i === 0 && program.strip.left)) { + statement.chars = statement.chars.replace(/^\s+/, ''); + } + + if ((i < l-1 && statements[i+1].strip && statements[i+1].strip.left) || + (i === l-1 && program.strip.right)) { + statement.chars = statement.chars.replace(/\s+$/, ''); + } + + // Remove unnecessary text nodes + if (statement.chars.length === 0) { + if ((i > 0 && statements[i-1].type === 'element') || + (i < l-1 && statements[i+1].type === 'element')) { + statements.splice(i, 1); + i--; + l--; + } + } } return this.elementStack.pop(); @@ -58,11 +85,14 @@ processor.block = function(block) { var mustache = block.mustache; var program = this.accept(block.program); var inverse = block.inverse ? this.accept(block.inverse) : null; + var strip = block.strip; - // TODO: Clean up Handlebars AST upstream to remove this hack. - var close = buildClose(mustache, program, inverse, block.strip.right); + // Normalize inverse's strip + if (inverse && !inverse.strip.left) { + inverse.strip.left = false; + } - var node = new BlockNode(mustache, program, inverse, close); + var node = new BlockNode(mustache, program, inverse, strip); var parentProgram = currentElement(this); appendChild(parentProgram, node); }; @@ -142,15 +172,3 @@ StartTag.prototype.addTagHelper = function(helper) { var helpers = this.helpers = this.helpers || []; helpers.push(helper); }; - -export function buildClose(mustache, program, inverse, stripRight) { - return { - path: { - original: mustache.sexpr.id.original - }, - strip: { - left: (inverse || program).strip.right, - right: stripRight || false - }, - }; -} diff --git a/test/tests/combined_ast_test.js b/test/tests/combined_ast_test.js index 75cc8300..5ee4a978 100644 --- a/test/tests/combined_ast_test.js +++ b/test/tests/combined_ast_test.js @@ -1,9 +1,12 @@ -import { preprocess, buildClose } from "htmlbars/parser"; +import { preprocess } from "htmlbars/parser"; import { ProgramNode, BlockNode, ElementNode, MustacheNode, SexprNode, HashNode, IdNode, StringNode, AttrNode, TextNode } from "htmlbars/ast"; module("HTML-based compiler (AST)"); +var stripLeft = { left: true, right: false }; +var stripRight = { left: false, right: true }; +var stripBoth = { left: true, right: true }; var stripNone = { left: false, right: false }; function id(string) { @@ -23,7 +26,7 @@ function hash(pairs) { return pairs ? new HashNode(pairs) : undefined; } -function mustache(string, pairs, raw) { +function mustache(string, pairs, strip, raw) { var params; if (({}).toString.call(string) === '[object Array]') { @@ -32,7 +35,7 @@ function mustache(string, pairs, raw) { params = [id(string)]; } - return new MustacheNode(params, hash(pairs), raw ? '{{{' : '{{', { left: false, right: false }); + return new MustacheNode(params, hash(pairs), raw ? '{{{' : '{{', strip || stripNone); } function string(data) { @@ -54,13 +57,16 @@ function text(chars) { return new TextNode(chars); } -function block(mustache, program, inverse, stripRight) { - var close = buildClose(mustache, program, inverse, stripRight); - return new BlockNode(mustache, program, inverse || null, close); +function block(mustache, program, inverse, strip) { + return new BlockNode(mustache, program, inverse || null, strip || stripNone); } function program(children, strip) { - return new ProgramNode(children, strip || stripNone); + return new ProgramNode(children || [], strip || stripNone); +} + +function root(children) { + return program(children || [], {}); } function removeLocInfo(obj) { @@ -89,14 +95,14 @@ function astEqual(template, expected, message) { test("a simple piece of content", function() { var t = 'some content'; - astEqual(t, program([ + astEqual(t, root([ text('some content') ])); }); test("a piece of content with HTML", function() { var t = 'some <div>content</div> done'; - astEqual(t, program([ + astEqual(t, root([ text("some "), element("div", [ text("content") @@ -107,7 +113,7 @@ test("a piece of content with HTML", function() { test("a piece of Handlebars with HTML", function() { var t = 'some <div>{{content}}</div> done'; - astEqual(t, program([ + astEqual(t, root([ text("some "), element("div", [ mustache('content') @@ -118,7 +124,7 @@ test("a piece of Handlebars with HTML", function() { test("Handlebars embedded in an attribute", function() { var t = 'some <div class="{{foo}}">content</div> done'; - astEqual(t, program([ + astEqual(t, root([ text("some "), element("div", [attr("class", [mustache('foo')])], [ text("content") @@ -129,7 +135,7 @@ test("Handlebars embedded in an attribute", function() { test("Handlebars embedded in an attribute (sexprs)", function() { var t = 'some <div class="{{foo (foo "abc")}}">content</div> done'; - astEqual(t, program([ + astEqual(t, root([ text("some "), element("div", [attr("class", [ mustache([id('foo'), sexpr([id('foo'), string('abc')])]) @@ -143,7 +149,7 @@ test("Handlebars embedded in an attribute (sexprs)", function() { test("Handlebars embedded in an attribute with other content surrounding it", function() { var t = 'some <a href="http://{{link}}/">content</a> done'; - astEqual(t, program([ + astEqual(t, root([ text("some "), element("a", [attr("href", [ text("http://"), @@ -159,7 +165,7 @@ test("A more complete embedding example", function() { var t = "{{embed}} {{some 'content'}} " + "<div class='{{foo}} {{bind-class isEnabled truthy='enabled'}}'>{{ content }}</div>" + " {{more 'embed'}}"; - astEqual(t, program([ + astEqual(t, root([ text(''), mustache('embed'), text(' '), @@ -182,7 +188,7 @@ test("A more complete embedding example", function() { test("Simple embedded block helpers", function() { var t = "{{#if foo}}<div>{{content}}</div>{{/if}}"; - astEqual(t, program([ + astEqual(t, root([ text(''), block(mustache([id('if'), id('foo')]), program([ element('div', [ @@ -195,7 +201,7 @@ test("Simple embedded block helpers", function() { test("Involved block helper", function() { var t = '<p>hi</p> content {{#testing shouldRender}}<p>Appears!</p>{{/testing}} more <em>content</em> here'; - astEqual(t, program([ + astEqual(t, root([ element('p', [ text('hi') ]), @@ -215,7 +221,7 @@ test("Involved block helper", function() { test("Node helpers", function() { var t = "<p {{action 'boom'}} class='bar'>Some content</p>"; - astEqual(t, program([ + astEqual(t, root([ element('p', [attr('class', [text('bar')])], [mustache([id('action'), string('boom')])], [ text('Some content') ]) @@ -224,17 +230,99 @@ test("Node helpers", function() { test('Auto insertion of text nodes between blocks and mustaches', function () { var t = "{{one}}{{two}}{{#three}}{{/three}}{{#four}}{{/four}}{{five}}"; - astEqual(t, program([ + astEqual(t, root([ text(''), mustache([id('one')]), text(''), mustache([id('two')]), text(''), - block(mustache([id('three')]), program([])), + block(mustache([id('three')]), program()), text(''), - block(mustache([id('four')]), program([])), + block(mustache([id('four')]), program()), text(''), mustache([id('five')]), text('') ])); +}); + +test("Stripping - mustaches", function() { + var t = "foo {{~content}} bar"; + astEqual(t, root([ + text('foo'), + mustache([id('content')], null, stripLeft), + text(' bar') + ])); + + t = "foo {{content~}} bar"; + astEqual(t, root([ + text('foo '), + mustache([id('content')], null, stripRight), + text('bar') + ])); +}); + +test("Stripping - blocks", function() { + var t = "foo {{~#wat}}{{/wat}} bar"; + astEqual(t, root([ + text('foo'), + block(mustache([id('wat')], null, stripLeft), program(), null, stripLeft), + text(' bar') + ])); + + t = "foo {{#wat}}{{/wat~}} bar"; + astEqual(t, root([ + text('foo '), + block(mustache([id('wat')]), program(), null, stripRight), + text('bar') + ])); +}); + + +test("Stripping - programs", function() { + var t = "{{#wat~}} foo {{else}}{{/wat}}"; + astEqual(t, root([ + text(''), + block(mustache([id('wat')], null, stripRight), program([ + text('foo ') + ], stripLeft), program()), + text('') + ])); + + t = "{{#wat}} foo {{~else}}{{/wat}}"; + astEqual(t, root([ + text(''), + block(mustache([id('wat')]), program([ + text(' foo') + ], stripRight), program()), + text('') + ])); + + t = "{{#wat}}{{else~}} foo {{/wat}}"; + astEqual(t, root([ + text(''), + block(mustache([id('wat')]), program(), program([ + text('foo ') + ], stripLeft)), + text('') + ])); + + t = "{{#wat}}{{else}} foo {{~/wat}}"; + astEqual(t, root([ + text(''), + block(mustache([id('wat')]), program(), program([ + text(' foo') + ], stripRight)), + text('') + ])); +}); + +test("Stripping - removes unnecessary text nodes", function() { + var t = "{{#each~}}\n <li> foo </li>\n{{~/each}}"; + astEqual(t, root([ + text(''), + block(mustache([id('each')], null, stripRight), program([ + element('li', [text(' foo ')]) + ], stripBoth)), + text('') + ])); }); \ No newline at end of file