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