896 lines
		
	
	
		
			23 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			896 lines
		
	
	
		
			23 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| 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("");
 | 
