lines.js 23 KB

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