| 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.DecoratorBlock = |
| 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.Decorator = |
| 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 preceded 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; |