928ba56b95
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.
211 lines
5.8 KiB
JavaScript
211 lines
5.8 KiB
JavaScript
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;
|