import Visitor from './visitor'; function WhitespaceControl(options = {}) { this.options = options; } WhitespaceControl.prototype = new Visitor(); WhitespaceControl.prototype.Program = function(program) { const doStandalone = !this.options.ignoreStandalone; let isRoot = !this.isRootSeen; this.isRootSeen = true; let body = program.body; for (let i = 0, l = body.length; i < l; i++) { let current = body[i], strip = this.accept(current); if (!strip) { continue; } let _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 (doStandalone && 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 (doStandalone && openStandalone) { omitRight((current.program || current.inverse).body); // Strip out the previous content node if it's whitespace only omitLeft(body, i); } if (doStandalone && closeStandalone) { // Always strip the next node omitRight(body, i); omitLeft((current.inverse || current.program).body); } } return program; }; WhitespaceControl.prototype.BlockStatement = WhitespaceControl.prototype.PartialBlockStatement = function(block) { this.accept(block.program); this.accept(block.inverse); // Find the inverse program that is involed with whitespace stripping. let 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; } } let 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) { let 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 (!this.options.ignoreStandalone && 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) { /* istanbul ignore next */ let 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) let 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; } let 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) { let current = body[i == null ? 0 : i + 1]; if (!current || current.type !== 'ContentStatement' || (!multiple && current.rightStripped)) { return; } let 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) { let 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. let original = current.value; current.value = current.value.replace(multiple ? (/\s+$/) : (/[ \t]+$/), ''); current.leftStripped = current.value !== original; return current.leftStripped; } export default WhitespaceControl;