| var assert = require("assert"); |
| var sourceMap = require("source-map"); |
| var normalizeOptions = require("./options").normalize; |
| var secretKey = require("private").makeUniqueKey(); |
| var types = require("./types"); |
| var isString = types.builtInTypes.string; |
| var comparePos = require("./util").comparePos; |
| var Mapping = require("./mapping"); |
| |
| // Goals: |
| // 1. Minimize new string creation. |
| // 2. Keep (de)identation O(lines) time. |
| // 3. Permit negative indentations. |
| // 4. Enforce immutability. |
| // 5. No newline characters. |
| |
| function getSecret(lines) { |
| return lines[secretKey]; |
| } |
| |
| function Lines(infos, sourceFileName) { |
| assert.ok(this instanceof Lines); |
| assert.ok(infos.length > 0); |
| |
| if (sourceFileName) { |
| isString.assert(sourceFileName); |
| } else { |
| sourceFileName = null; |
| } |
| |
| Object.defineProperty(this, secretKey, { |
| value: { |
| infos: infos, |
| mappings: [], |
| name: sourceFileName, |
| cachedSourceMap: null |
| } |
| }); |
| |
| if (sourceFileName) { |
| getSecret(this).mappings.push(new Mapping(this, { |
| start: this.firstPos(), |
| end: this.lastPos() |
| })); |
| } |
| } |
| |
| // Exposed for instanceof checks. The fromString function should be used |
| // to create new Lines objects. |
| exports.Lines = Lines; |
| var Lp = Lines.prototype; |
| |
| // These properties used to be assigned to each new object in the Lines |
| // constructor, but we can more efficiently stuff them into the secret and |
| // let these lazy accessors compute their values on-the-fly. |
| Object.defineProperties(Lp, { |
| length: { |
| get: function() { |
| return getSecret(this).infos.length; |
| } |
| }, |
| |
| name: { |
| get: function() { |
| return getSecret(this).name; |
| } |
| } |
| }); |
| |
| function copyLineInfo(info) { |
| return { |
| line: info.line, |
| indent: info.indent, |
| locked: info.locked, |
| sliceStart: info.sliceStart, |
| sliceEnd: info.sliceEnd |
| }; |
| } |
| |
| var fromStringCache = {}; |
| var hasOwn = fromStringCache.hasOwnProperty; |
| var maxCacheKeyLen = 10; |
| |
| function countSpaces(spaces, tabWidth) { |
| var count = 0; |
| var len = spaces.length; |
| |
| for (var i = 0; i < len; ++i) { |
| switch (spaces.charCodeAt(i)) { |
| case 9: // '\t' |
| assert.strictEqual(typeof tabWidth, "number"); |
| assert.ok(tabWidth > 0); |
| |
| var next = Math.ceil(count / tabWidth) * tabWidth; |
| if (next === count) { |
| count += tabWidth; |
| } else { |
| count = next; |
| } |
| |
| break; |
| |
| case 11: // '\v' |
| case 12: // '\f' |
| case 13: // '\r' |
| case 0xfeff: // zero-width non-breaking space |
| // These characters contribute nothing to indentation. |
| break; |
| |
| case 32: // ' ' |
| default: // Treat all other whitespace like ' '. |
| count += 1; |
| break; |
| } |
| } |
| |
| return count; |
| } |
| exports.countSpaces = countSpaces; |
| |
| var leadingSpaceExp = /^\s*/; |
| |
| // As specified here: http://www.ecma-international.org/ecma-262/6.0/#sec-line-terminators |
| var lineTerminatorSeqExp = |
| /\u000D\u000A|\u000D(?!\u000A)|\u000A|\u2028|\u2029/; |
| |
| /** |
| * @param {Object} options - Options object that configures printing. |
| */ |
| function fromString(string, options) { |
| if (string instanceof Lines) |
| return string; |
| |
| string += ""; |
| |
| var tabWidth = options && options.tabWidth; |
| var tabless = string.indexOf("\t") < 0; |
| var locked = !! (options && options.locked); |
| var cacheable = !options && tabless && (string.length <= maxCacheKeyLen); |
| |
| assert.ok(tabWidth || tabless, "No tab width specified but encountered tabs in string\n" + string); |
| |
| if (cacheable && hasOwn.call(fromStringCache, string)) |
| return fromStringCache[string]; |
| |
| var lines = new Lines(string.split(lineTerminatorSeqExp).map(function(line) { |
| var spaces = leadingSpaceExp.exec(line)[0]; |
| return { |
| line: line, |
| indent: countSpaces(spaces, tabWidth), |
| // Boolean indicating whether this line can be reindented. |
| locked: locked, |
| sliceStart: spaces.length, |
| sliceEnd: line.length |
| }; |
| }), normalizeOptions(options).sourceFileName); |
| |
| if (cacheable) |
| fromStringCache[string] = lines; |
| |
| return lines; |
| } |
| exports.fromString = fromString; |
| |
| function isOnlyWhitespace(string) { |
| return !/\S/.test(string); |
| } |
| |
| Lp.toString = function(options) { |
| return this.sliceString(this.firstPos(), this.lastPos(), options); |
| }; |
| |
| Lp.getSourceMap = function(sourceMapName, sourceRoot) { |
| if (!sourceMapName) { |
| // Although we could make up a name or generate an anonymous |
| // source map, instead we assume that any consumer who does not |
| // provide a name does not actually want a source map. |
| return null; |
| } |
| |
| var targetLines = this; |
| |
| function updateJSON(json) { |
| json = json || {}; |
| |
| isString.assert(sourceMapName); |
| json.file = sourceMapName; |
| |
| if (sourceRoot) { |
| isString.assert(sourceRoot); |
| json.sourceRoot = sourceRoot; |
| } |
| |
| return json; |
| } |
| |
| var secret = getSecret(targetLines); |
| if (secret.cachedSourceMap) { |
| // Since Lines objects are immutable, we can reuse any source map |
| // that was previously generated. Nevertheless, we return a new |
| // JSON object here to protect the cached source map from outside |
| // modification. |
| return updateJSON(secret.cachedSourceMap.toJSON()); |
| } |
| |
| var smg = new sourceMap.SourceMapGenerator(updateJSON()); |
| var sourcesToContents = {}; |
| |
| secret.mappings.forEach(function(mapping) { |
| var sourceCursor = mapping.sourceLines.skipSpaces( |
| mapping.sourceLoc.start |
| ) || mapping.sourceLines.lastPos(); |
| |
| var targetCursor = targetLines.skipSpaces( |
| mapping.targetLoc.start |
| ) || targetLines.lastPos(); |
| |
| while (comparePos(sourceCursor, mapping.sourceLoc.end) < 0 && |
| comparePos(targetCursor, mapping.targetLoc.end) < 0) { |
| |
| var sourceChar = mapping.sourceLines.charAt(sourceCursor); |
| var targetChar = targetLines.charAt(targetCursor); |
| assert.strictEqual(sourceChar, targetChar); |
| |
| var sourceName = mapping.sourceLines.name; |
| |
| // Add mappings one character at a time for maximum resolution. |
| smg.addMapping({ |
| source: sourceName, |
| original: { line: sourceCursor.line, |
| column: sourceCursor.column }, |
| generated: { line: targetCursor.line, |
| column: targetCursor.column } |
| }); |
| |
| if (!hasOwn.call(sourcesToContents, sourceName)) { |
| var sourceContent = mapping.sourceLines.toString(); |
| smg.setSourceContent(sourceName, sourceContent); |
| sourcesToContents[sourceName] = sourceContent; |
| } |
| |
| targetLines.nextPos(targetCursor, true); |
| mapping.sourceLines.nextPos(sourceCursor, true); |
| } |
| }); |
| |
| secret.cachedSourceMap = smg; |
| |
| return smg.toJSON(); |
| }; |
| |
| Lp.bootstrapCharAt = function(pos) { |
| assert.strictEqual(typeof pos, "object"); |
| assert.strictEqual(typeof pos.line, "number"); |
| assert.strictEqual(typeof pos.column, "number"); |
| |
| var line = pos.line, |
| column = pos.column, |
| strings = this.toString().split(lineTerminatorSeqExp), |
| string = strings[line - 1]; |
| |
| if (typeof string === "undefined") |
| return ""; |
| |
| if (column === string.length && |
| line < strings.length) |
| return "\n"; |
| |
| if (column >= string.length) |
| return ""; |
| |
| return string.charAt(column); |
| }; |
| |
| Lp.charAt = function(pos) { |
| assert.strictEqual(typeof pos, "object"); |
| assert.strictEqual(typeof pos.line, "number"); |
| assert.strictEqual(typeof pos.column, "number"); |
| |
| var line = pos.line, |
| column = pos.column, |
| secret = getSecret(this), |
| infos = secret.infos, |
| info = infos[line - 1], |
| c = column; |
| |
| if (typeof info === "undefined" || c < 0) |
| return ""; |
| |
| var indent = this.getIndentAt(line); |
| if (c < indent) |
| return " "; |
| |
| c += info.sliceStart - indent; |
| |
| if (c === info.sliceEnd && |
| line < this.length) |
| return "\n"; |
| |
| if (c >= info.sliceEnd) |
| return ""; |
| |
| return info.line.charAt(c); |
| }; |
| |
| Lp.stripMargin = function(width, skipFirstLine) { |
| if (width === 0) |
| return this; |
| |
| assert.ok(width > 0, "negative margin: " + width); |
| |
| if (skipFirstLine && this.length === 1) |
| return this; |
| |
| var secret = getSecret(this); |
| |
| var lines = new Lines(secret.infos.map(function(info, i) { |
| if (info.line && (i > 0 || !skipFirstLine)) { |
| info = copyLineInfo(info); |
| info.indent = Math.max(0, info.indent - width); |
| } |
| return info; |
| })); |
| |
| if (secret.mappings.length > 0) { |
| var newMappings = getSecret(lines).mappings; |
| assert.strictEqual(newMappings.length, 0); |
| secret.mappings.forEach(function(mapping) { |
| newMappings.push(mapping.indent(width, skipFirstLine, true)); |
| }); |
| } |
| |
| return lines; |
| }; |
| |
| Lp.indent = function(by) { |
| if (by === 0) |
| return this; |
| |
| var secret = getSecret(this); |
| |
| var lines = new Lines(secret.infos.map(function(info) { |
| if (info.line && ! info.locked) { |
| info = copyLineInfo(info); |
| info.indent += by; |
| } |
| return info |
| })); |
| |
| if (secret.mappings.length > 0) { |
| var newMappings = getSecret(lines).mappings; |
| assert.strictEqual(newMappings.length, 0); |
| secret.mappings.forEach(function(mapping) { |
| newMappings.push(mapping.indent(by)); |
| }); |
| } |
| |
| return lines; |
| }; |
| |
| Lp.indentTail = function(by) { |
| if (by === 0) |
| return this; |
| |
| if (this.length < 2) |
| return this; |
| |
| var secret = getSecret(this); |
| |
| var lines = new Lines(secret.infos.map(function(info, i) { |
| if (i > 0 && info.line && ! info.locked) { |
| info = copyLineInfo(info); |
| info.indent += by; |
| } |
| |
| return info; |
| })); |
| |
| if (secret.mappings.length > 0) { |
| var newMappings = getSecret(lines).mappings; |
| assert.strictEqual(newMappings.length, 0); |
| secret.mappings.forEach(function(mapping) { |
| newMappings.push(mapping.indent(by, true)); |
| }); |
| } |
| |
| return lines; |
| }; |
| |
| Lp.lockIndentTail = function () { |
| if (this.length < 2) { |
| return this; |
| } |
| |
| var infos = getSecret(this).infos; |
| |
| return new Lines(infos.map(function (info, i) { |
| info = copyLineInfo(info); |
| info.locked = i > 0; |
| return info; |
| })); |
| }; |
| |
| Lp.getIndentAt = function(line) { |
| assert.ok(line >= 1, "no line " + line + " (line numbers start from 1)"); |
| var secret = getSecret(this), |
| info = secret.infos[line - 1]; |
| return Math.max(info.indent, 0); |
| }; |
| |
| Lp.guessTabWidth = function() { |
| var secret = getSecret(this); |
| if (hasOwn.call(secret, "cachedTabWidth")) { |
| return secret.cachedTabWidth; |
| } |
| |
| var counts = []; // Sparse array. |
| var lastIndent = 0; |
| |
| for (var line = 1, last = this.length; line <= last; ++line) { |
| var info = secret.infos[line - 1]; |
| var sliced = info.line.slice(info.sliceStart, info.sliceEnd); |
| |
| // Whitespace-only lines don't tell us much about the likely tab |
| // width of this code. |
| if (isOnlyWhitespace(sliced)) { |
| continue; |
| } |
| |
| var diff = Math.abs(info.indent - lastIndent); |
| counts[diff] = ~~counts[diff] + 1; |
| lastIndent = info.indent; |
| } |
| |
| var maxCount = -1; |
| var result = 2; |
| |
| for (var tabWidth = 1; |
| tabWidth < counts.length; |
| tabWidth += 1) { |
| if (hasOwn.call(counts, tabWidth) && |
| counts[tabWidth] > maxCount) { |
| maxCount = counts[tabWidth]; |
| result = tabWidth; |
| } |
| } |
| |
| return secret.cachedTabWidth = result; |
| }; |
| |
| // Determine if the list of lines has a first line that starts with a // |
| // or /* comment. If this is the case, the code may need to be wrapped in |
| // parens to avoid ASI issues. |
| Lp.startsWithComment = function () { |
| var secret = getSecret(this); |
| if (secret.infos.length === 0) { |
| return false; |
| } |
| var firstLineInfo = secret.infos[0], |
| sliceStart = firstLineInfo.sliceStart, |
| sliceEnd = firstLineInfo.sliceEnd, |
| firstLine = firstLineInfo.line.slice(sliceStart, sliceEnd).trim(); |
| return firstLine.length === 0 || |
| firstLine.slice(0, 2) === "//" || |
| firstLine.slice(0, 2) === "/*"; |
| }; |
| |
| Lp.isOnlyWhitespace = function() { |
| return isOnlyWhitespace(this.toString()); |
| }; |
| |
| Lp.isPrecededOnlyByWhitespace = function(pos) { |
| var secret = getSecret(this); |
| var info = secret.infos[pos.line - 1]; |
| var indent = Math.max(info.indent, 0); |
| |
| var diff = pos.column - indent; |
| if (diff <= 0) { |
| // If pos.column does not exceed the indentation amount, then |
| // there must be only whitespace before it. |
| return true; |
| } |
| |
| var start = info.sliceStart; |
| var end = Math.min(start + diff, info.sliceEnd); |
| var prefix = info.line.slice(start, end); |
| |
| return isOnlyWhitespace(prefix); |
| }; |
| |
| Lp.getLineLength = function(line) { |
| var secret = getSecret(this), |
| info = secret.infos[line - 1]; |
| return this.getIndentAt(line) + info.sliceEnd - info.sliceStart; |
| }; |
| |
| Lp.nextPos = function(pos, skipSpaces) { |
| var l = Math.max(pos.line, 0), |
| c = Math.max(pos.column, 0); |
| |
| if (c < this.getLineLength(l)) { |
| pos.column += 1; |
| |
| return skipSpaces |
| ? !!this.skipSpaces(pos, false, true) |
| : true; |
| } |
| |
| if (l < this.length) { |
| pos.line += 1; |
| pos.column = 0; |
| |
| return skipSpaces |
| ? !!this.skipSpaces(pos, false, true) |
| : true; |
| } |
| |
| return false; |
| }; |
| |
| Lp.prevPos = function(pos, skipSpaces) { |
| var l = pos.line, |
| c = pos.column; |
| |
| if (c < 1) { |
| l -= 1; |
| |
| if (l < 1) |
| return false; |
| |
| c = this.getLineLength(l); |
| |
| } else { |
| c = Math.min(c - 1, this.getLineLength(l)); |
| } |
| |
| pos.line = l; |
| pos.column = c; |
| |
| return skipSpaces |
| ? !!this.skipSpaces(pos, true, true) |
| : true; |
| }; |
| |
| Lp.firstPos = function() { |
| // Trivial, but provided for completeness. |
| return { line: 1, column: 0 }; |
| }; |
| |
| Lp.lastPos = function() { |
| return { |
| line: this.length, |
| column: this.getLineLength(this.length) |
| }; |
| }; |
| |
| Lp.skipSpaces = function(pos, backward, modifyInPlace) { |
| if (pos) { |
| pos = modifyInPlace ? pos : { |
| line: pos.line, |
| column: pos.column |
| }; |
| } else if (backward) { |
| pos = this.lastPos(); |
| } else { |
| pos = this.firstPos(); |
| } |
| |
| if (backward) { |
| while (this.prevPos(pos)) { |
| if (!isOnlyWhitespace(this.charAt(pos)) && |
| this.nextPos(pos)) { |
| return pos; |
| } |
| } |
| |
| return null; |
| |
| } else { |
| while (isOnlyWhitespace(this.charAt(pos))) { |
| if (!this.nextPos(pos)) { |
| return null; |
| } |
| } |
| |
| return pos; |
| } |
| }; |
| |
| Lp.trimLeft = function() { |
| var pos = this.skipSpaces(this.firstPos(), false, true); |
| return pos ? this.slice(pos) : emptyLines; |
| }; |
| |
| Lp.trimRight = function() { |
| var pos = this.skipSpaces(this.lastPos(), true, true); |
| return pos ? this.slice(this.firstPos(), pos) : emptyLines; |
| }; |
| |
| Lp.trim = function() { |
| var start = this.skipSpaces(this.firstPos(), false, true); |
| if (start === null) |
| return emptyLines; |
| |
| var end = this.skipSpaces(this.lastPos(), true, true); |
| assert.notStrictEqual(end, null); |
| |
| return this.slice(start, end); |
| }; |
| |
| Lp.eachPos = function(callback, startPos, skipSpaces) { |
| var pos = this.firstPos(); |
| |
| if (startPos) { |
| pos.line = startPos.line, |
| pos.column = startPos.column |
| } |
| |
| if (skipSpaces && !this.skipSpaces(pos, false, true)) { |
| return; // Encountered nothing but spaces. |
| } |
| |
| do callback.call(this, pos); |
| while (this.nextPos(pos, skipSpaces)); |
| }; |
| |
| Lp.bootstrapSlice = function(start, end) { |
| var strings = this.toString().split( |
| lineTerminatorSeqExp |
| ).slice( |
| start.line - 1, |
| end.line |
| ); |
| |
| strings.push(strings.pop().slice(0, end.column)); |
| strings[0] = strings[0].slice(start.column); |
| |
| return fromString(strings.join("\n")); |
| }; |
| |
| Lp.slice = function(start, end) { |
| if (!end) { |
| if (!start) { |
| // The client seems to want a copy of this Lines object, but |
| // Lines objects are immutable, so it's perfectly adequate to |
| // return the same object. |
| return this; |
| } |
| |
| // Slice to the end if no end position was provided. |
| end = this.lastPos(); |
| } |
| |
| var secret = getSecret(this); |
| var sliced = secret.infos.slice(start.line - 1, end.line); |
| |
| if (start.line === end.line) { |
| sliced[0] = sliceInfo(sliced[0], start.column, end.column); |
| } else { |
| assert.ok(start.line < end.line); |
| sliced[0] = sliceInfo(sliced[0], start.column); |
| sliced.push(sliceInfo(sliced.pop(), 0, end.column)); |
| } |
| |
| var lines = new Lines(sliced); |
| |
| if (secret.mappings.length > 0) { |
| var newMappings = getSecret(lines).mappings; |
| assert.strictEqual(newMappings.length, 0); |
| secret.mappings.forEach(function(mapping) { |
| var sliced = mapping.slice(this, start, end); |
| if (sliced) { |
| newMappings.push(sliced); |
| } |
| }, this); |
| } |
| |
| return lines; |
| }; |
| |
| function sliceInfo(info, startCol, endCol) { |
| var sliceStart = info.sliceStart; |
| var sliceEnd = info.sliceEnd; |
| var indent = Math.max(info.indent, 0); |
| var lineLength = indent + sliceEnd - sliceStart; |
| |
| if (typeof endCol === "undefined") { |
| endCol = lineLength; |
| } |
| |
| startCol = Math.max(startCol, 0); |
| endCol = Math.min(endCol, lineLength); |
| endCol = Math.max(endCol, startCol); |
| |
| if (endCol < indent) { |
| indent = endCol; |
| sliceEnd = sliceStart; |
| } else { |
| sliceEnd -= lineLength - endCol; |
| } |
| |
| lineLength = endCol; |
| lineLength -= startCol; |
| |
| if (startCol < indent) { |
| indent -= startCol; |
| } else { |
| startCol -= indent; |
| indent = 0; |
| sliceStart += startCol; |
| } |
| |
| assert.ok(indent >= 0); |
| assert.ok(sliceStart <= sliceEnd); |
| assert.strictEqual(lineLength, indent + sliceEnd - sliceStart); |
| |
| if (info.indent === indent && |
| info.sliceStart === sliceStart && |
| info.sliceEnd === sliceEnd) { |
| return info; |
| } |
| |
| return { |
| line: info.line, |
| indent: indent, |
| // A destructive slice always unlocks indentation. |
| locked: false, |
| sliceStart: sliceStart, |
| sliceEnd: sliceEnd |
| }; |
| } |
| |
| Lp.bootstrapSliceString = function(start, end, options) { |
| return this.slice(start, end).toString(options); |
| }; |
| |
| Lp.sliceString = function(start, end, options) { |
| if (!end) { |
| if (!start) { |
| // The client seems to want a copy of this Lines object, but |
| // Lines objects are immutable, so it's perfectly adequate to |
| // return the same object. |
| return this; |
| } |
| |
| // Slice to the end if no end position was provided. |
| end = this.lastPos(); |
| } |
| |
| options = normalizeOptions(options); |
| |
| var infos = getSecret(this).infos; |
| var parts = []; |
| var tabWidth = options.tabWidth; |
| |
| for (var line = start.line; line <= end.line; ++line) { |
| var info = infos[line - 1]; |
| |
| if (line === start.line) { |
| if (line === end.line) { |
| info = sliceInfo(info, start.column, end.column); |
| } else { |
| info = sliceInfo(info, start.column); |
| } |
| } else if (line === end.line) { |
| info = sliceInfo(info, 0, end.column); |
| } |
| |
| var indent = Math.max(info.indent, 0); |
| |
| var before = info.line.slice(0, info.sliceStart); |
| if (options.reuseWhitespace && |
| isOnlyWhitespace(before) && |
| countSpaces(before, options.tabWidth) === indent) { |
| // Reuse original spaces if the indentation is correct. |
| parts.push(info.line.slice(0, info.sliceEnd)); |
| continue; |
| } |
| |
| var tabs = 0; |
| var spaces = indent; |
| |
| if (options.useTabs) { |
| tabs = Math.floor(indent / tabWidth); |
| spaces -= tabs * tabWidth; |
| } |
| |
| var result = ""; |
| |
| if (tabs > 0) { |
| result += new Array(tabs + 1).join("\t"); |
| } |
| |
| if (spaces > 0) { |
| result += new Array(spaces + 1).join(" "); |
| } |
| |
| result += info.line.slice(info.sliceStart, info.sliceEnd); |
| |
| parts.push(result); |
| } |
| |
| return parts.join(options.lineTerminator); |
| }; |
| |
| Lp.isEmpty = function() { |
| return this.length < 2 && this.getLineLength(1) < 1; |
| }; |
| |
| Lp.join = function(elements) { |
| var separator = this; |
| var separatorSecret = getSecret(separator); |
| var infos = []; |
| var mappings = []; |
| var prevInfo; |
| |
| function appendSecret(secret) { |
| if (secret === null) |
| return; |
| |
| if (prevInfo) { |
| var info = secret.infos[0]; |
| var indent = new Array(info.indent + 1).join(" "); |
| var prevLine = infos.length; |
| var prevColumn = Math.max(prevInfo.indent, 0) + |
| prevInfo.sliceEnd - prevInfo.sliceStart; |
| |
| prevInfo.line = prevInfo.line.slice( |
| 0, prevInfo.sliceEnd) + indent + info.line.slice( |
| info.sliceStart, info.sliceEnd); |
| |
| // If any part of a line is indentation-locked, the whole line |
| // will be indentation-locked. |
| prevInfo.locked = prevInfo.locked || info.locked; |
| |
| prevInfo.sliceEnd = prevInfo.line.length; |
| |
| if (secret.mappings.length > 0) { |
| secret.mappings.forEach(function(mapping) { |
| mappings.push(mapping.add(prevLine, prevColumn)); |
| }); |
| } |
| |
| } else if (secret.mappings.length > 0) { |
| mappings.push.apply(mappings, secret.mappings); |
| } |
| |
| secret.infos.forEach(function(info, i) { |
| if (!prevInfo || i > 0) { |
| prevInfo = copyLineInfo(info); |
| infos.push(prevInfo); |
| } |
| }); |
| } |
| |
| function appendWithSeparator(secret, i) { |
| if (i > 0) |
| appendSecret(separatorSecret); |
| appendSecret(secret); |
| } |
| |
| elements.map(function(elem) { |
| var lines = fromString(elem); |
| if (lines.isEmpty()) |
| return null; |
| return getSecret(lines); |
| }).forEach(separator.isEmpty() |
| ? appendSecret |
| : appendWithSeparator); |
| |
| if (infos.length < 1) |
| return emptyLines; |
| |
| var lines = new Lines(infos); |
| |
| getSecret(lines).mappings = mappings; |
| |
| return lines; |
| }; |
| |
| exports.concat = function(elements) { |
| return emptyLines.join(elements); |
| }; |
| |
| Lp.concat = function(other) { |
| var args = arguments, |
| list = [this]; |
| list.push.apply(list, args); |
| assert.strictEqual(list.length, args.length + 1); |
| return emptyLines.join(list); |
| }; |
| |
| // The emptyLines object needs to be created all the way down here so that |
| // Lines.prototype will be fully populated. |
| var emptyLines = fromString(""); |