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