lines.js 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900
  1. var assert = require("assert");
  2. var sourceMap = require("source-map");
  3. var normalizeOptions = require("./options").normalize;
  4. var secretKey = require("private").makeUniqueKey();
  5. var types = require("./types");
  6. var isString = types.builtInTypes.string;
  7. var comparePos = require("./util").comparePos;
  8. var Mapping = require("./mapping");
  9. // Goals:
  10. // 1. Minimize new string creation.
  11. // 2. Keep (de)identation O(lines) time.
  12. // 3. Permit negative indentations.
  13. // 4. Enforce immutability.
  14. // 5. No newline characters.
  15. var useSymbol = typeof Symbol === "function";
  16. var secretKey = "recastLinesSecret";
  17. if (useSymbol) {
  18. secretKey = Symbol.for(secretKey);
  19. }
  20. function getSecret(lines) {
  21. return lines[secretKey];
  22. }
  23. function Lines(infos, sourceFileName) {
  24. assert.ok(this instanceof Lines);
  25. assert.ok(infos.length > 0);
  26. if (sourceFileName) {
  27. isString.assert(sourceFileName);
  28. } else {
  29. sourceFileName = null;
  30. }
  31. setSymbolOrKey(this, secretKey, {
  32. infos: infos,
  33. mappings: [],
  34. name: sourceFileName,
  35. cachedSourceMap: null
  36. });
  37. this.length = infos.length;
  38. this.name = sourceFileName;
  39. if (sourceFileName) {
  40. getSecret(this).mappings.push(new Mapping(this, {
  41. start: this.firstPos(),
  42. end: this.lastPos()
  43. }));
  44. }
  45. }
  46. function setSymbolOrKey(obj, key, value) {
  47. if (useSymbol) {
  48. return obj[key] = value;
  49. }
  50. Object.defineProperty(obj, key, {
  51. value: value,
  52. enumerable: false,
  53. writable: false,
  54. configurable: true
  55. });
  56. return value;
  57. }
  58. // Exposed for instanceof checks. The fromString function should be used
  59. // to create new Lines objects.
  60. exports.Lines = Lines;
  61. var Lp = Lines.prototype;
  62. function copyLineInfo(info) {
  63. return {
  64. line: info.line,
  65. indent: info.indent,
  66. locked: info.locked,
  67. sliceStart: info.sliceStart,
  68. sliceEnd: info.sliceEnd
  69. };
  70. }
  71. var fromStringCache = {};
  72. var hasOwn = fromStringCache.hasOwnProperty;
  73. var maxCacheKeyLen = 10;
  74. function countSpaces(spaces, tabWidth) {
  75. var count = 0;
  76. var len = spaces.length;
  77. for (var i = 0; i < len; ++i) {
  78. switch (spaces.charCodeAt(i)) {
  79. case 9: // '\t'
  80. assert.strictEqual(typeof tabWidth, "number");
  81. assert.ok(tabWidth > 0);
  82. var next = Math.ceil(count / tabWidth) * tabWidth;
  83. if (next === count) {
  84. count += tabWidth;
  85. } else {
  86. count = next;
  87. }
  88. break;
  89. case 11: // '\v'
  90. case 12: // '\f'
  91. case 13: // '\r'
  92. case 0xfeff: // zero-width non-breaking space
  93. // These characters contribute nothing to indentation.
  94. break;
  95. case 32: // ' '
  96. default: // Treat all other whitespace like ' '.
  97. count += 1;
  98. break;
  99. }
  100. }
  101. return count;
  102. }
  103. exports.countSpaces = countSpaces;
  104. var leadingSpaceExp = /^\s*/;
  105. // As specified here: http://www.ecma-international.org/ecma-262/6.0/#sec-line-terminators
  106. var lineTerminatorSeqExp =
  107. /\u000D\u000A|\u000D(?!\u000A)|\u000A|\u2028|\u2029/;
  108. /**
  109. * @param {Object} options - Options object that configures printing.
  110. */
  111. function fromString(string, options) {
  112. if (string instanceof Lines)
  113. return string;
  114. string += "";
  115. var tabWidth = options && options.tabWidth;
  116. var tabless = string.indexOf("\t") < 0;
  117. var locked = !! (options && options.locked);
  118. var cacheable = !options && tabless && (string.length <= maxCacheKeyLen);
  119. assert.ok(tabWidth || tabless, "No tab width specified but encountered tabs in string\n" + string);
  120. if (cacheable && hasOwn.call(fromStringCache, string))
  121. return fromStringCache[string];
  122. var lines = new Lines(string.split(lineTerminatorSeqExp).map(function(line) {
  123. var spaces = leadingSpaceExp.exec(line)[0];
  124. return {
  125. line: line,
  126. indent: countSpaces(spaces, tabWidth),
  127. // Boolean indicating whether this line can be reindented.
  128. locked: locked,
  129. sliceStart: spaces.length,
  130. sliceEnd: line.length
  131. };
  132. }), normalizeOptions(options).sourceFileName);
  133. if (cacheable)
  134. fromStringCache[string] = lines;
  135. return lines;
  136. }
  137. exports.fromString = fromString;
  138. function isOnlyWhitespace(string) {
  139. return !/\S/.test(string);
  140. }
  141. Lp.toString = function(options) {
  142. return this.sliceString(this.firstPos(), this.lastPos(), options);
  143. };
  144. Lp.getSourceMap = function(sourceMapName, sourceRoot) {
  145. if (!sourceMapName) {
  146. // Although we could make up a name or generate an anonymous
  147. // source map, instead we assume that any consumer who does not
  148. // provide a name does not actually want a source map.
  149. return null;
  150. }
  151. var targetLines = this;
  152. function updateJSON(json) {
  153. json = json || {};
  154. isString.assert(sourceMapName);
  155. json.file = sourceMapName;
  156. if (sourceRoot) {
  157. isString.assert(sourceRoot);
  158. json.sourceRoot = sourceRoot;
  159. }
  160. return json;
  161. }
  162. var secret = getSecret(targetLines);
  163. if (secret.cachedSourceMap) {
  164. // Since Lines objects are immutable, we can reuse any source map
  165. // that was previously generated. Nevertheless, we return a new
  166. // JSON object here to protect the cached source map from outside
  167. // modification.
  168. return updateJSON(secret.cachedSourceMap.toJSON());
  169. }
  170. var smg = new sourceMap.SourceMapGenerator(updateJSON());
  171. var sourcesToContents = {};
  172. secret.mappings.forEach(function(mapping) {
  173. var sourceCursor = mapping.sourceLines.skipSpaces(
  174. mapping.sourceLoc.start
  175. ) || mapping.sourceLines.lastPos();
  176. var targetCursor = targetLines.skipSpaces(
  177. mapping.targetLoc.start
  178. ) || targetLines.lastPos();
  179. while (comparePos(sourceCursor, mapping.sourceLoc.end) < 0 &&
  180. comparePos(targetCursor, mapping.targetLoc.end) < 0) {
  181. var sourceChar = mapping.sourceLines.charAt(sourceCursor);
  182. var targetChar = targetLines.charAt(targetCursor);
  183. assert.strictEqual(sourceChar, targetChar);
  184. var sourceName = mapping.sourceLines.name;
  185. // Add mappings one character at a time for maximum resolution.
  186. smg.addMapping({
  187. source: sourceName,
  188. original: { line: sourceCursor.line,
  189. column: sourceCursor.column },
  190. generated: { line: targetCursor.line,
  191. column: targetCursor.column }
  192. });
  193. if (!hasOwn.call(sourcesToContents, sourceName)) {
  194. var sourceContent = mapping.sourceLines.toString();
  195. smg.setSourceContent(sourceName, sourceContent);
  196. sourcesToContents[sourceName] = sourceContent;
  197. }
  198. targetLines.nextPos(targetCursor, true);
  199. mapping.sourceLines.nextPos(sourceCursor, true);
  200. }
  201. });
  202. secret.cachedSourceMap = smg;
  203. return smg.toJSON();
  204. };
  205. Lp.bootstrapCharAt = function(pos) {
  206. assert.strictEqual(typeof pos, "object");
  207. assert.strictEqual(typeof pos.line, "number");
  208. assert.strictEqual(typeof pos.column, "number");
  209. var line = pos.line,
  210. column = pos.column,
  211. strings = this.toString().split(lineTerminatorSeqExp),
  212. string = strings[line - 1];
  213. if (typeof string === "undefined")
  214. return "";
  215. if (column === string.length &&
  216. line < strings.length)
  217. return "\n";
  218. if (column >= string.length)
  219. return "";
  220. return string.charAt(column);
  221. };
  222. Lp.charAt = function(pos) {
  223. assert.strictEqual(typeof pos, "object");
  224. assert.strictEqual(typeof pos.line, "number");
  225. assert.strictEqual(typeof pos.column, "number");
  226. var line = pos.line,
  227. column = pos.column,
  228. secret = getSecret(this),
  229. infos = secret.infos,
  230. info = infos[line - 1],
  231. c = column;
  232. if (typeof info === "undefined" || c < 0)
  233. return "";
  234. var indent = this.getIndentAt(line);
  235. if (c < indent)
  236. return " ";
  237. c += info.sliceStart - indent;
  238. if (c === info.sliceEnd &&
  239. line < this.length)
  240. return "\n";
  241. if (c >= info.sliceEnd)
  242. return "";
  243. return info.line.charAt(c);
  244. };
  245. Lp.stripMargin = function(width, skipFirstLine) {
  246. if (width === 0)
  247. return this;
  248. assert.ok(width > 0, "negative margin: " + width);
  249. if (skipFirstLine && this.length === 1)
  250. return this;
  251. var secret = getSecret(this);
  252. var lines = new Lines(secret.infos.map(function(info, i) {
  253. if (info.line && (i > 0 || !skipFirstLine)) {
  254. info = copyLineInfo(info);
  255. info.indent = Math.max(0, info.indent - width);
  256. }
  257. return info;
  258. }));
  259. if (secret.mappings.length > 0) {
  260. var newMappings = getSecret(lines).mappings;
  261. assert.strictEqual(newMappings.length, 0);
  262. secret.mappings.forEach(function(mapping) {
  263. newMappings.push(mapping.indent(width, skipFirstLine, true));
  264. });
  265. }
  266. return lines;
  267. };
  268. Lp.indent = function(by) {
  269. if (by === 0)
  270. return this;
  271. var secret = getSecret(this);
  272. var lines = new Lines(secret.infos.map(function(info) {
  273. if (info.line && ! info.locked) {
  274. info = copyLineInfo(info);
  275. info.indent += by;
  276. }
  277. return info
  278. }));
  279. if (secret.mappings.length > 0) {
  280. var newMappings = getSecret(lines).mappings;
  281. assert.strictEqual(newMappings.length, 0);
  282. secret.mappings.forEach(function(mapping) {
  283. newMappings.push(mapping.indent(by));
  284. });
  285. }
  286. return lines;
  287. };
  288. Lp.indentTail = function(by) {
  289. if (by === 0)
  290. return this;
  291. if (this.length < 2)
  292. return this;
  293. var secret = getSecret(this);
  294. var lines = new Lines(secret.infos.map(function(info, i) {
  295. if (i > 0 && info.line && ! info.locked) {
  296. info = copyLineInfo(info);
  297. info.indent += by;
  298. }
  299. return info;
  300. }));
  301. if (secret.mappings.length > 0) {
  302. var newMappings = getSecret(lines).mappings;
  303. assert.strictEqual(newMappings.length, 0);
  304. secret.mappings.forEach(function(mapping) {
  305. newMappings.push(mapping.indent(by, true));
  306. });
  307. }
  308. return lines;
  309. };
  310. Lp.lockIndentTail = function () {
  311. if (this.length < 2) {
  312. return this;
  313. }
  314. var infos = getSecret(this).infos;
  315. return new Lines(infos.map(function (info, i) {
  316. info = copyLineInfo(info);
  317. info.locked = i > 0;
  318. return info;
  319. }));
  320. };
  321. Lp.getIndentAt = function(line) {
  322. assert.ok(line >= 1, "no line " + line + " (line numbers start from 1)");
  323. var secret = getSecret(this),
  324. info = secret.infos[line - 1];
  325. return Math.max(info.indent, 0);
  326. };
  327. Lp.guessTabWidth = function() {
  328. var secret = getSecret(this);
  329. if (hasOwn.call(secret, "cachedTabWidth")) {
  330. return secret.cachedTabWidth;
  331. }
  332. var counts = []; // Sparse array.
  333. var lastIndent = 0;
  334. for (var line = 1, last = this.length; line <= last; ++line) {
  335. var info = secret.infos[line - 1];
  336. var sliced = info.line.slice(info.sliceStart, info.sliceEnd);
  337. // Whitespace-only lines don't tell us much about the likely tab
  338. // width of this code.
  339. if (isOnlyWhitespace(sliced)) {
  340. continue;
  341. }
  342. var diff = Math.abs(info.indent - lastIndent);
  343. counts[diff] = ~~counts[diff] + 1;
  344. lastIndent = info.indent;
  345. }
  346. var maxCount = -1;
  347. var result = 2;
  348. for (var tabWidth = 1;
  349. tabWidth < counts.length;
  350. tabWidth += 1) {
  351. if (hasOwn.call(counts, tabWidth) &&
  352. counts[tabWidth] > maxCount) {
  353. maxCount = counts[tabWidth];
  354. result = tabWidth;
  355. }
  356. }
  357. return secret.cachedTabWidth = result;
  358. };
  359. // Determine if the list of lines has a first line that starts with a //
  360. // or /* comment. If this is the case, the code may need to be wrapped in
  361. // parens to avoid ASI issues.
  362. Lp.startsWithComment = function () {
  363. var secret = getSecret(this);
  364. if (secret.infos.length === 0) {
  365. return false;
  366. }
  367. var firstLineInfo = secret.infos[0],
  368. sliceStart = firstLineInfo.sliceStart,
  369. sliceEnd = firstLineInfo.sliceEnd,
  370. firstLine = firstLineInfo.line.slice(sliceStart, sliceEnd).trim();
  371. return firstLine.length === 0 ||
  372. firstLine.slice(0, 2) === "//" ||
  373. firstLine.slice(0, 2) === "/*";
  374. };
  375. Lp.isOnlyWhitespace = function() {
  376. return isOnlyWhitespace(this.toString());
  377. };
  378. Lp.isPrecededOnlyByWhitespace = function(pos) {
  379. var secret = getSecret(this);
  380. var info = secret.infos[pos.line - 1];
  381. var indent = Math.max(info.indent, 0);
  382. var diff = pos.column - indent;
  383. if (diff <= 0) {
  384. // If pos.column does not exceed the indentation amount, then
  385. // there must be only whitespace before it.
  386. return true;
  387. }
  388. var start = info.sliceStart;
  389. var end = Math.min(start + diff, info.sliceEnd);
  390. var prefix = info.line.slice(start, end);
  391. return isOnlyWhitespace(prefix);
  392. };
  393. Lp.getLineLength = function(line) {
  394. var secret = getSecret(this),
  395. info = secret.infos[line - 1];
  396. return this.getIndentAt(line) + info.sliceEnd - info.sliceStart;
  397. };
  398. Lp.nextPos = function(pos, skipSpaces) {
  399. var l = Math.max(pos.line, 0),
  400. c = Math.max(pos.column, 0);
  401. if (c < this.getLineLength(l)) {
  402. pos.column += 1;
  403. return skipSpaces
  404. ? !!this.skipSpaces(pos, false, true)
  405. : true;
  406. }
  407. if (l < this.length) {
  408. pos.line += 1;
  409. pos.column = 0;
  410. return skipSpaces
  411. ? !!this.skipSpaces(pos, false, true)
  412. : true;
  413. }
  414. return false;
  415. };
  416. Lp.prevPos = function(pos, skipSpaces) {
  417. var l = pos.line,
  418. c = pos.column;
  419. if (c < 1) {
  420. l -= 1;
  421. if (l < 1)
  422. return false;
  423. c = this.getLineLength(l);
  424. } else {
  425. c = Math.min(c - 1, this.getLineLength(l));
  426. }
  427. pos.line = l;
  428. pos.column = c;
  429. return skipSpaces
  430. ? !!this.skipSpaces(pos, true, true)
  431. : true;
  432. };
  433. Lp.firstPos = function() {
  434. // Trivial, but provided for completeness.
  435. return { line: 1, column: 0 };
  436. };
  437. Lp.lastPos = function() {
  438. return {
  439. line: this.length,
  440. column: this.getLineLength(this.length)
  441. };
  442. };
  443. Lp.skipSpaces = function(pos, backward, modifyInPlace) {
  444. if (pos) {
  445. pos = modifyInPlace ? pos : {
  446. line: pos.line,
  447. column: pos.column
  448. };
  449. } else if (backward) {
  450. pos = this.lastPos();
  451. } else {
  452. pos = this.firstPos();
  453. }
  454. if (backward) {
  455. while (this.prevPos(pos)) {
  456. if (!isOnlyWhitespace(this.charAt(pos)) &&
  457. this.nextPos(pos)) {
  458. return pos;
  459. }
  460. }
  461. return null;
  462. } else {
  463. while (isOnlyWhitespace(this.charAt(pos))) {
  464. if (!this.nextPos(pos)) {
  465. return null;
  466. }
  467. }
  468. return pos;
  469. }
  470. };
  471. Lp.trimLeft = function() {
  472. var pos = this.skipSpaces(this.firstPos(), false, true);
  473. return pos ? this.slice(pos) : emptyLines;
  474. };
  475. Lp.trimRight = function() {
  476. var pos = this.skipSpaces(this.lastPos(), true, true);
  477. return pos ? this.slice(this.firstPos(), pos) : emptyLines;
  478. };
  479. Lp.trim = function() {
  480. var start = this.skipSpaces(this.firstPos(), false, true);
  481. if (start === null)
  482. return emptyLines;
  483. var end = this.skipSpaces(this.lastPos(), true, true);
  484. assert.notStrictEqual(end, null);
  485. return this.slice(start, end);
  486. };
  487. Lp.eachPos = function(callback, startPos, skipSpaces) {
  488. var pos = this.firstPos();
  489. if (startPos) {
  490. pos.line = startPos.line,
  491. pos.column = startPos.column
  492. }
  493. if (skipSpaces && !this.skipSpaces(pos, false, true)) {
  494. return; // Encountered nothing but spaces.
  495. }
  496. do callback.call(this, pos);
  497. while (this.nextPos(pos, skipSpaces));
  498. };
  499. Lp.bootstrapSlice = function(start, end) {
  500. var strings = this.toString().split(
  501. lineTerminatorSeqExp
  502. ).slice(
  503. start.line - 1,
  504. end.line
  505. );
  506. strings.push(strings.pop().slice(0, end.column));
  507. strings[0] = strings[0].slice(start.column);
  508. return fromString(strings.join("\n"));
  509. };
  510. Lp.slice = function(start, end) {
  511. if (!end) {
  512. if (!start) {
  513. // The client seems to want a copy of this Lines object, but
  514. // Lines objects are immutable, so it's perfectly adequate to
  515. // return the same object.
  516. return this;
  517. }
  518. // Slice to the end if no end position was provided.
  519. end = this.lastPos();
  520. }
  521. var secret = getSecret(this);
  522. var sliced = secret.infos.slice(start.line - 1, end.line);
  523. if (start.line === end.line) {
  524. sliced[0] = sliceInfo(sliced[0], start.column, end.column);
  525. } else {
  526. assert.ok(start.line < end.line);
  527. sliced[0] = sliceInfo(sliced[0], start.column);
  528. sliced.push(sliceInfo(sliced.pop(), 0, end.column));
  529. }
  530. var lines = new Lines(sliced);
  531. if (secret.mappings.length > 0) {
  532. var newMappings = getSecret(lines).mappings;
  533. assert.strictEqual(newMappings.length, 0);
  534. secret.mappings.forEach(function(mapping) {
  535. var sliced = mapping.slice(this, start, end);
  536. if (sliced) {
  537. newMappings.push(sliced);
  538. }
  539. }, this);
  540. }
  541. return lines;
  542. };
  543. function sliceInfo(info, startCol, endCol) {
  544. var sliceStart = info.sliceStart;
  545. var sliceEnd = info.sliceEnd;
  546. var indent = Math.max(info.indent, 0);
  547. var lineLength = indent + sliceEnd - sliceStart;
  548. if (typeof endCol === "undefined") {
  549. endCol = lineLength;
  550. }
  551. startCol = Math.max(startCol, 0);
  552. endCol = Math.min(endCol, lineLength);
  553. endCol = Math.max(endCol, startCol);
  554. if (endCol < indent) {
  555. indent = endCol;
  556. sliceEnd = sliceStart;
  557. } else {
  558. sliceEnd -= lineLength - endCol;
  559. }
  560. lineLength = endCol;
  561. lineLength -= startCol;
  562. if (startCol < indent) {
  563. indent -= startCol;
  564. } else {
  565. startCol -= indent;
  566. indent = 0;
  567. sliceStart += startCol;
  568. }
  569. assert.ok(indent >= 0);
  570. assert.ok(sliceStart <= sliceEnd);
  571. assert.strictEqual(lineLength, indent + sliceEnd - sliceStart);
  572. if (info.indent === indent &&
  573. info.sliceStart === sliceStart &&
  574. info.sliceEnd === sliceEnd) {
  575. return info;
  576. }
  577. return {
  578. line: info.line,
  579. indent: indent,
  580. // A destructive slice always unlocks indentation.
  581. locked: false,
  582. sliceStart: sliceStart,
  583. sliceEnd: sliceEnd
  584. };
  585. }
  586. Lp.bootstrapSliceString = function(start, end, options) {
  587. return this.slice(start, end).toString(options);
  588. };
  589. Lp.sliceString = function(start, end, options) {
  590. if (!end) {
  591. if (!start) {
  592. // The client seems to want a copy of this Lines object, but
  593. // Lines objects are immutable, so it's perfectly adequate to
  594. // return the same object.
  595. return this;
  596. }
  597. // Slice to the end if no end position was provided.
  598. end = this.lastPos();
  599. }
  600. options = normalizeOptions(options);
  601. var infos = getSecret(this).infos;
  602. var parts = [];
  603. var tabWidth = options.tabWidth;
  604. for (var line = start.line; line <= end.line; ++line) {
  605. var info = infos[line - 1];
  606. if (line === start.line) {
  607. if (line === end.line) {
  608. info = sliceInfo(info, start.column, end.column);
  609. } else {
  610. info = sliceInfo(info, start.column);
  611. }
  612. } else if (line === end.line) {
  613. info = sliceInfo(info, 0, end.column);
  614. }
  615. var indent = Math.max(info.indent, 0);
  616. var before = info.line.slice(0, info.sliceStart);
  617. if (options.reuseWhitespace &&
  618. isOnlyWhitespace(before) &&
  619. countSpaces(before, options.tabWidth) === indent) {
  620. // Reuse original spaces if the indentation is correct.
  621. parts.push(info.line.slice(0, info.sliceEnd));
  622. continue;
  623. }
  624. var tabs = 0;
  625. var spaces = indent;
  626. if (options.useTabs) {
  627. tabs = Math.floor(indent / tabWidth);
  628. spaces -= tabs * tabWidth;
  629. }
  630. var result = "";
  631. if (tabs > 0) {
  632. result += new Array(tabs + 1).join("\t");
  633. }
  634. if (spaces > 0) {
  635. result += new Array(spaces + 1).join(" ");
  636. }
  637. result += info.line.slice(info.sliceStart, info.sliceEnd);
  638. parts.push(result);
  639. }
  640. return parts.join(options.lineTerminator);
  641. };
  642. Lp.isEmpty = function() {
  643. return this.length < 2 && this.getLineLength(1) < 1;
  644. };
  645. Lp.join = function(elements) {
  646. var separator = this;
  647. var separatorSecret = getSecret(separator);
  648. var infos = [];
  649. var mappings = [];
  650. var prevInfo;
  651. function appendSecret(secret) {
  652. if (secret === null)
  653. return;
  654. if (prevInfo) {
  655. var info = secret.infos[0];
  656. var indent = new Array(info.indent + 1).join(" ");
  657. var prevLine = infos.length;
  658. var prevColumn = Math.max(prevInfo.indent, 0) +
  659. prevInfo.sliceEnd - prevInfo.sliceStart;
  660. prevInfo.line = prevInfo.line.slice(
  661. 0, prevInfo.sliceEnd) + indent + info.line.slice(
  662. info.sliceStart, info.sliceEnd);
  663. // If any part of a line is indentation-locked, the whole line
  664. // will be indentation-locked.
  665. prevInfo.locked = prevInfo.locked || info.locked;
  666. prevInfo.sliceEnd = prevInfo.line.length;
  667. if (secret.mappings.length > 0) {
  668. secret.mappings.forEach(function(mapping) {
  669. mappings.push(mapping.add(prevLine, prevColumn));
  670. });
  671. }
  672. } else if (secret.mappings.length > 0) {
  673. mappings.push.apply(mappings, secret.mappings);
  674. }
  675. secret.infos.forEach(function(info, i) {
  676. if (!prevInfo || i > 0) {
  677. prevInfo = copyLineInfo(info);
  678. infos.push(prevInfo);
  679. }
  680. });
  681. }
  682. function appendWithSeparator(secret, i) {
  683. if (i > 0)
  684. appendSecret(separatorSecret);
  685. appendSecret(secret);
  686. }
  687. elements.map(function(elem) {
  688. var lines = fromString(elem);
  689. if (lines.isEmpty())
  690. return null;
  691. return getSecret(lines);
  692. }).forEach(separator.isEmpty()
  693. ? appendSecret
  694. : appendWithSeparator);
  695. if (infos.length < 1)
  696. return emptyLines;
  697. var lines = new Lines(infos);
  698. getSecret(lines).mappings = mappings;
  699. return lines;
  700. };
  701. exports.concat = function(elements) {
  702. return emptyLines.join(elements);
  703. };
  704. Lp.concat = function(other) {
  705. var args = arguments,
  706. list = [this];
  707. list.push.apply(list, args);
  708. assert.strictEqual(list.length, args.length + 1);
  709. return emptyLines.join(list);
  710. };
  711. // The emptyLines object needs to be created all the way down here so that
  712. // Lines.prototype will be fully populated.
  713. var emptyLines = fromString("");