Skip to content
This repository has been archived by the owner on Apr 4, 2019. It is now read-only.

Implement stripping #20

Merged
merged 1 commit into from
Apr 23, 2014
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 14 additions & 2 deletions lib/htmlbars/ast.js
Original file line number Diff line number Diff line change
@@ -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;
Expand Down
72 changes: 45 additions & 27 deletions lib/htmlbars/parser.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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);
};
Expand Down Expand Up @@ -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
},
};
}
128 changes: 108 additions & 20 deletions test/tests/combined_ast_test.js
Original file line number Diff line number Diff line change
@@ -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) {
Expand All @@ -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]') {
Expand All @@ -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) {
Expand All @@ -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) {
Expand Down Expand Up @@ -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")
Expand All @@ -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')
Expand All @@ -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")
Expand All @@ -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')])])
Expand All @@ -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://"),
Expand All @@ -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(' '),
Expand All @@ -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', [
Expand All @@ -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')
]),
Expand All @@ -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')
])
Expand All @@ -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('')
]));
});