Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
Download
80559 views
1
/* -*- Mode: js; js-indent-level: 2; -*- */
2
/*
3
* Copyright 2011 Mozilla Foundation and contributors
4
* Licensed under the New BSD license. See LICENSE or:
5
* http://opensource.org/licenses/BSD-3-Clause
6
*/
7
if (typeof define !== 'function') {
8
var define = require('amdefine')(module, require);
9
}
10
define(function (require, exports, module) {
11
12
var util = require('./util');
13
var binarySearch = require('./binary-search');
14
var ArraySet = require('./array-set').ArraySet;
15
var base64VLQ = require('./base64-vlq');
16
17
/**
18
* A SourceMapConsumer instance represents a parsed source map which we can
19
* query for information about the original file positions by giving it a file
20
* position in the generated source.
21
*
22
* The only parameter is the raw source map (either as a JSON string, or
23
* already parsed to an object). According to the spec, source maps have the
24
* following attributes:
25
*
26
* - version: Which version of the source map spec this map is following.
27
* - sources: An array of URLs to the original source files.
28
* - names: An array of identifiers which can be referrenced by individual mappings.
29
* - sourceRoot: Optional. The URL root from which all sources are relative.
30
* - sourcesContent: Optional. An array of contents of the original source files.
31
* - mappings: A string of base64 VLQs which contain the actual mappings.
32
* - file: Optional. The generated file this source map is associated with.
33
*
34
* Here is an example source map, taken from the source map spec[0]:
35
*
36
* {
37
* version : 3,
38
* file: "out.js",
39
* sourceRoot : "",
40
* sources: ["foo.js", "bar.js"],
41
* names: ["src", "maps", "are", "fun"],
42
* mappings: "AA,AB;;ABCDE;"
43
* }
44
*
45
* [0]: https://docs.google.com/document/d/1U1RGAehQwRypUTovF1KRlpiOFze0b-_2gc6fAH0KY0k/edit?pli=1#
46
*/
47
function SourceMapConsumer(aSourceMap) {
48
var sourceMap = aSourceMap;
49
if (typeof aSourceMap === 'string') {
50
sourceMap = JSON.parse(aSourceMap.replace(/^\)\]\}'/, ''));
51
}
52
53
var version = util.getArg(sourceMap, 'version');
54
var sources = util.getArg(sourceMap, 'sources');
55
// Sass 3.3 leaves out the 'names' array, so we deviate from the spec (which
56
// requires the array) to play nice here.
57
var names = util.getArg(sourceMap, 'names', []);
58
var sourceRoot = util.getArg(sourceMap, 'sourceRoot', null);
59
var sourcesContent = util.getArg(sourceMap, 'sourcesContent', null);
60
var mappings = util.getArg(sourceMap, 'mappings');
61
var file = util.getArg(sourceMap, 'file', null);
62
63
// Once again, Sass deviates from the spec and supplies the version as a
64
// string rather than a number, so we use loose equality checking here.
65
if (version != this._version) {
66
throw new Error('Unsupported version: ' + version);
67
}
68
69
// Some source maps produce relative source paths like "./foo.js" instead of
70
// "foo.js". Normalize these first so that future comparisons will succeed.
71
// See bugzil.la/1090768.
72
sources = sources.map(util.normalize);
73
74
// Pass `true` below to allow duplicate names and sources. While source maps
75
// are intended to be compressed and deduplicated, the TypeScript compiler
76
// sometimes generates source maps with duplicates in them. See Github issue
77
// #72 and bugzil.la/889492.
78
this._names = ArraySet.fromArray(names, true);
79
this._sources = ArraySet.fromArray(sources, true);
80
81
this.sourceRoot = sourceRoot;
82
this.sourcesContent = sourcesContent;
83
this._mappings = mappings;
84
this.file = file;
85
}
86
87
/**
88
* Create a SourceMapConsumer from a SourceMapGenerator.
89
*
90
* @param SourceMapGenerator aSourceMap
91
* The source map that will be consumed.
92
* @returns SourceMapConsumer
93
*/
94
SourceMapConsumer.fromSourceMap =
95
function SourceMapConsumer_fromSourceMap(aSourceMap) {
96
var smc = Object.create(SourceMapConsumer.prototype);
97
98
smc._names = ArraySet.fromArray(aSourceMap._names.toArray(), true);
99
smc._sources = ArraySet.fromArray(aSourceMap._sources.toArray(), true);
100
smc.sourceRoot = aSourceMap._sourceRoot;
101
smc.sourcesContent = aSourceMap._generateSourcesContent(smc._sources.toArray(),
102
smc.sourceRoot);
103
smc.file = aSourceMap._file;
104
105
smc.__generatedMappings = aSourceMap._mappings.toArray().slice();
106
smc.__originalMappings = aSourceMap._mappings.toArray().slice()
107
.sort(util.compareByOriginalPositions);
108
109
return smc;
110
};
111
112
/**
113
* The version of the source mapping spec that we are consuming.
114
*/
115
SourceMapConsumer.prototype._version = 3;
116
117
/**
118
* The list of original sources.
119
*/
120
Object.defineProperty(SourceMapConsumer.prototype, 'sources', {
121
get: function () {
122
return this._sources.toArray().map(function (s) {
123
return this.sourceRoot != null ? util.join(this.sourceRoot, s) : s;
124
}, this);
125
}
126
});
127
128
// `__generatedMappings` and `__originalMappings` are arrays that hold the
129
// parsed mapping coordinates from the source map's "mappings" attribute. They
130
// are lazily instantiated, accessed via the `_generatedMappings` and
131
// `_originalMappings` getters respectively, and we only parse the mappings
132
// and create these arrays once queried for a source location. We jump through
133
// these hoops because there can be many thousands of mappings, and parsing
134
// them is expensive, so we only want to do it if we must.
135
//
136
// Each object in the arrays is of the form:
137
//
138
// {
139
// generatedLine: The line number in the generated code,
140
// generatedColumn: The column number in the generated code,
141
// source: The path to the original source file that generated this
142
// chunk of code,
143
// originalLine: The line number in the original source that
144
// corresponds to this chunk of generated code,
145
// originalColumn: The column number in the original source that
146
// corresponds to this chunk of generated code,
147
// name: The name of the original symbol which generated this chunk of
148
// code.
149
// }
150
//
151
// All properties except for `generatedLine` and `generatedColumn` can be
152
// `null`.
153
//
154
// `_generatedMappings` is ordered by the generated positions.
155
//
156
// `_originalMappings` is ordered by the original positions.
157
158
SourceMapConsumer.prototype.__generatedMappings = null;
159
Object.defineProperty(SourceMapConsumer.prototype, '_generatedMappings', {
160
get: function () {
161
if (!this.__generatedMappings) {
162
this.__generatedMappings = [];
163
this.__originalMappings = [];
164
this._parseMappings(this._mappings, this.sourceRoot);
165
}
166
167
return this.__generatedMappings;
168
}
169
});
170
171
SourceMapConsumer.prototype.__originalMappings = null;
172
Object.defineProperty(SourceMapConsumer.prototype, '_originalMappings', {
173
get: function () {
174
if (!this.__originalMappings) {
175
this.__generatedMappings = [];
176
this.__originalMappings = [];
177
this._parseMappings(this._mappings, this.sourceRoot);
178
}
179
180
return this.__originalMappings;
181
}
182
});
183
184
SourceMapConsumer.prototype._nextCharIsMappingSeparator =
185
function SourceMapConsumer_nextCharIsMappingSeparator(aStr) {
186
var c = aStr.charAt(0);
187
return c === ";" || c === ",";
188
};
189
190
/**
191
* Parse the mappings in a string in to a data structure which we can easily
192
* query (the ordered arrays in the `this.__generatedMappings` and
193
* `this.__originalMappings` properties).
194
*/
195
SourceMapConsumer.prototype._parseMappings =
196
function SourceMapConsumer_parseMappings(aStr, aSourceRoot) {
197
var generatedLine = 1;
198
var previousGeneratedColumn = 0;
199
var previousOriginalLine = 0;
200
var previousOriginalColumn = 0;
201
var previousSource = 0;
202
var previousName = 0;
203
var str = aStr;
204
var temp = {};
205
var mapping;
206
207
while (str.length > 0) {
208
if (str.charAt(0) === ';') {
209
generatedLine++;
210
str = str.slice(1);
211
previousGeneratedColumn = 0;
212
}
213
else if (str.charAt(0) === ',') {
214
str = str.slice(1);
215
}
216
else {
217
mapping = {};
218
mapping.generatedLine = generatedLine;
219
220
// Generated column.
221
base64VLQ.decode(str, temp);
222
mapping.generatedColumn = previousGeneratedColumn + temp.value;
223
previousGeneratedColumn = mapping.generatedColumn;
224
str = temp.rest;
225
226
if (str.length > 0 && !this._nextCharIsMappingSeparator(str)) {
227
// Original source.
228
base64VLQ.decode(str, temp);
229
mapping.source = this._sources.at(previousSource + temp.value);
230
previousSource += temp.value;
231
str = temp.rest;
232
if (str.length === 0 || this._nextCharIsMappingSeparator(str)) {
233
throw new Error('Found a source, but no line and column');
234
}
235
236
// Original line.
237
base64VLQ.decode(str, temp);
238
mapping.originalLine = previousOriginalLine + temp.value;
239
previousOriginalLine = mapping.originalLine;
240
// Lines are stored 0-based
241
mapping.originalLine += 1;
242
str = temp.rest;
243
if (str.length === 0 || this._nextCharIsMappingSeparator(str)) {
244
throw new Error('Found a source and line, but no column');
245
}
246
247
// Original column.
248
base64VLQ.decode(str, temp);
249
mapping.originalColumn = previousOriginalColumn + temp.value;
250
previousOriginalColumn = mapping.originalColumn;
251
str = temp.rest;
252
253
if (str.length > 0 && !this._nextCharIsMappingSeparator(str)) {
254
// Original name.
255
base64VLQ.decode(str, temp);
256
mapping.name = this._names.at(previousName + temp.value);
257
previousName += temp.value;
258
str = temp.rest;
259
}
260
}
261
262
this.__generatedMappings.push(mapping);
263
if (typeof mapping.originalLine === 'number') {
264
this.__originalMappings.push(mapping);
265
}
266
}
267
}
268
269
this.__generatedMappings.sort(util.compareByGeneratedPositions);
270
this.__originalMappings.sort(util.compareByOriginalPositions);
271
};
272
273
/**
274
* Find the mapping that best matches the hypothetical "needle" mapping that
275
* we are searching for in the given "haystack" of mappings.
276
*/
277
SourceMapConsumer.prototype._findMapping =
278
function SourceMapConsumer_findMapping(aNeedle, aMappings, aLineName,
279
aColumnName, aComparator) {
280
// To return the position we are searching for, we must first find the
281
// mapping for the given position and then return the opposite position it
282
// points to. Because the mappings are sorted, we can use binary search to
283
// find the best mapping.
284
285
if (aNeedle[aLineName] <= 0) {
286
throw new TypeError('Line must be greater than or equal to 1, got '
287
+ aNeedle[aLineName]);
288
}
289
if (aNeedle[aColumnName] < 0) {
290
throw new TypeError('Column must be greater than or equal to 0, got '
291
+ aNeedle[aColumnName]);
292
}
293
294
return binarySearch.search(aNeedle, aMappings, aComparator);
295
};
296
297
/**
298
* Compute the last column for each generated mapping. The last column is
299
* inclusive.
300
*/
301
SourceMapConsumer.prototype.computeColumnSpans =
302
function SourceMapConsumer_computeColumnSpans() {
303
for (var index = 0; index < this._generatedMappings.length; ++index) {
304
var mapping = this._generatedMappings[index];
305
306
// Mappings do not contain a field for the last generated columnt. We
307
// can come up with an optimistic estimate, however, by assuming that
308
// mappings are contiguous (i.e. given two consecutive mappings, the
309
// first mapping ends where the second one starts).
310
if (index + 1 < this._generatedMappings.length) {
311
var nextMapping = this._generatedMappings[index + 1];
312
313
if (mapping.generatedLine === nextMapping.generatedLine) {
314
mapping.lastGeneratedColumn = nextMapping.generatedColumn - 1;
315
continue;
316
}
317
}
318
319
// The last mapping for each line spans the entire line.
320
mapping.lastGeneratedColumn = Infinity;
321
}
322
};
323
324
/**
325
* Returns the original source, line, and column information for the generated
326
* source's line and column positions provided. The only argument is an object
327
* with the following properties:
328
*
329
* - line: The line number in the generated source.
330
* - column: The column number in the generated source.
331
*
332
* and an object is returned with the following properties:
333
*
334
* - source: The original source file, or null.
335
* - line: The line number in the original source, or null.
336
* - column: The column number in the original source, or null.
337
* - name: The original identifier, or null.
338
*/
339
SourceMapConsumer.prototype.originalPositionFor =
340
function SourceMapConsumer_originalPositionFor(aArgs) {
341
var needle = {
342
generatedLine: util.getArg(aArgs, 'line'),
343
generatedColumn: util.getArg(aArgs, 'column')
344
};
345
346
var index = this._findMapping(needle,
347
this._generatedMappings,
348
"generatedLine",
349
"generatedColumn",
350
util.compareByGeneratedPositions);
351
352
if (index >= 0) {
353
var mapping = this._generatedMappings[index];
354
355
if (mapping.generatedLine === needle.generatedLine) {
356
var source = util.getArg(mapping, 'source', null);
357
if (source != null && this.sourceRoot != null) {
358
source = util.join(this.sourceRoot, source);
359
}
360
return {
361
source: source,
362
line: util.getArg(mapping, 'originalLine', null),
363
column: util.getArg(mapping, 'originalColumn', null),
364
name: util.getArg(mapping, 'name', null)
365
};
366
}
367
}
368
369
return {
370
source: null,
371
line: null,
372
column: null,
373
name: null
374
};
375
};
376
377
/**
378
* Returns the original source content. The only argument is the url of the
379
* original source file. Returns null if no original source content is
380
* availible.
381
*/
382
SourceMapConsumer.prototype.sourceContentFor =
383
function SourceMapConsumer_sourceContentFor(aSource) {
384
if (!this.sourcesContent) {
385
return null;
386
}
387
388
if (this.sourceRoot != null) {
389
aSource = util.relative(this.sourceRoot, aSource);
390
}
391
392
if (this._sources.has(aSource)) {
393
return this.sourcesContent[this._sources.indexOf(aSource)];
394
}
395
396
var url;
397
if (this.sourceRoot != null
398
&& (url = util.urlParse(this.sourceRoot))) {
399
// XXX: file:// URIs and absolute paths lead to unexpected behavior for
400
// many users. We can help them out when they expect file:// URIs to
401
// behave like it would if they were running a local HTTP server. See
402
// https://bugzilla.mozilla.org/show_bug.cgi?id=885597.
403
var fileUriAbsPath = aSource.replace(/^file:\/\//, "");
404
if (url.scheme == "file"
405
&& this._sources.has(fileUriAbsPath)) {
406
return this.sourcesContent[this._sources.indexOf(fileUriAbsPath)]
407
}
408
409
if ((!url.path || url.path == "/")
410
&& this._sources.has("/" + aSource)) {
411
return this.sourcesContent[this._sources.indexOf("/" + aSource)];
412
}
413
}
414
415
throw new Error('"' + aSource + '" is not in the SourceMap.');
416
};
417
418
/**
419
* Returns the generated line and column information for the original source,
420
* line, and column positions provided. The only argument is an object with
421
* the following properties:
422
*
423
* - source: The filename of the original source.
424
* - line: The line number in the original source.
425
* - column: The column number in the original source.
426
*
427
* and an object is returned with the following properties:
428
*
429
* - line: The line number in the generated source, or null.
430
* - column: The column number in the generated source, or null.
431
*/
432
SourceMapConsumer.prototype.generatedPositionFor =
433
function SourceMapConsumer_generatedPositionFor(aArgs) {
434
var needle = {
435
source: util.getArg(aArgs, 'source'),
436
originalLine: util.getArg(aArgs, 'line'),
437
originalColumn: util.getArg(aArgs, 'column')
438
};
439
440
if (this.sourceRoot != null) {
441
needle.source = util.relative(this.sourceRoot, needle.source);
442
}
443
444
var index = this._findMapping(needle,
445
this._originalMappings,
446
"originalLine",
447
"originalColumn",
448
util.compareByOriginalPositions);
449
450
if (index >= 0) {
451
var mapping = this._originalMappings[index];
452
453
return {
454
line: util.getArg(mapping, 'generatedLine', null),
455
column: util.getArg(mapping, 'generatedColumn', null),
456
lastColumn: util.getArg(mapping, 'lastGeneratedColumn', null)
457
};
458
}
459
460
return {
461
line: null,
462
column: null,
463
lastColumn: null
464
};
465
};
466
467
/**
468
* Returns all generated line and column information for the original source
469
* and line provided. The only argument is an object with the following
470
* properties:
471
*
472
* - source: The filename of the original source.
473
* - line: The line number in the original source.
474
*
475
* and an array of objects is returned, each with the following properties:
476
*
477
* - line: The line number in the generated source, or null.
478
* - column: The column number in the generated source, or null.
479
*/
480
SourceMapConsumer.prototype.allGeneratedPositionsFor =
481
function SourceMapConsumer_allGeneratedPositionsFor(aArgs) {
482
// When there is no exact match, SourceMapConsumer.prototype._findMapping
483
// returns the index of the closest mapping less than the needle. By
484
// setting needle.originalColumn to Infinity, we thus find the last
485
// mapping for the given line, provided such a mapping exists.
486
var needle = {
487
source: util.getArg(aArgs, 'source'),
488
originalLine: util.getArg(aArgs, 'line'),
489
originalColumn: Infinity
490
};
491
492
if (this.sourceRoot != null) {
493
needle.source = util.relative(this.sourceRoot, needle.source);
494
}
495
496
var mappings = [];
497
498
var index = this._findMapping(needle,
499
this._originalMappings,
500
"originalLine",
501
"originalColumn",
502
util.compareByOriginalPositions);
503
if (index >= 0) {
504
var mapping = this._originalMappings[index];
505
506
while (mapping && mapping.originalLine === needle.originalLine) {
507
mappings.push({
508
line: util.getArg(mapping, 'generatedLine', null),
509
column: util.getArg(mapping, 'generatedColumn', null),
510
lastColumn: util.getArg(mapping, 'lastGeneratedColumn', null)
511
});
512
513
mapping = this._originalMappings[--index];
514
}
515
}
516
517
return mappings.reverse();
518
};
519
520
SourceMapConsumer.GENERATED_ORDER = 1;
521
SourceMapConsumer.ORIGINAL_ORDER = 2;
522
523
/**
524
* Iterate over each mapping between an original source/line/column and a
525
* generated line/column in this source map.
526
*
527
* @param Function aCallback
528
* The function that is called with each mapping.
529
* @param Object aContext
530
* Optional. If specified, this object will be the value of `this` every
531
* time that `aCallback` is called.
532
* @param aOrder
533
* Either `SourceMapConsumer.GENERATED_ORDER` or
534
* `SourceMapConsumer.ORIGINAL_ORDER`. Specifies whether you want to
535
* iterate over the mappings sorted by the generated file's line/column
536
* order or the original's source/line/column order, respectively. Defaults to
537
* `SourceMapConsumer.GENERATED_ORDER`.
538
*/
539
SourceMapConsumer.prototype.eachMapping =
540
function SourceMapConsumer_eachMapping(aCallback, aContext, aOrder) {
541
var context = aContext || null;
542
var order = aOrder || SourceMapConsumer.GENERATED_ORDER;
543
544
var mappings;
545
switch (order) {
546
case SourceMapConsumer.GENERATED_ORDER:
547
mappings = this._generatedMappings;
548
break;
549
case SourceMapConsumer.ORIGINAL_ORDER:
550
mappings = this._originalMappings;
551
break;
552
default:
553
throw new Error("Unknown order of iteration.");
554
}
555
556
var sourceRoot = this.sourceRoot;
557
mappings.map(function (mapping) {
558
var source = mapping.source;
559
if (source != null && sourceRoot != null) {
560
source = util.join(sourceRoot, source);
561
}
562
return {
563
source: source,
564
generatedLine: mapping.generatedLine,
565
generatedColumn: mapping.generatedColumn,
566
originalLine: mapping.originalLine,
567
originalColumn: mapping.originalColumn,
568
name: mapping.name
569
};
570
}).forEach(aCallback, context);
571
};
572
573
exports.SourceMapConsumer = SourceMapConsumer;
574
575
});
576
577