1 /* -*- Mode: js; js-indent-level: 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
7 if (typeof define !== 'function') {
8 var define = require('amdefine')(module, require);
10 define(function (require, exports, module) {
12 var util = require('./util');
13 var binarySearch = require('./binary-search');
14 var ArraySet = require('./array-set').ArraySet;
15 var base64VLQ = require('./base64-vlq');
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.
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:
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.
34 * Here is an example source map, taken from the source map spec[0]:
40 * sources: ["foo.js", "bar.js"],
41 * names: ["src", "maps", "are", "fun"],
42 * mappings: "AA,AB;;ABCDE;"
45 * [0]: https://docs.google.com/document/d/1U1RGAehQwRypUTovF1KRlpiOFze0b-_2gc6fAH0KY0k/edit?pli=1#
47 function SourceMapConsumer(aSourceMap) {
48 var sourceMap = aSourceMap;
49 if (typeof aSourceMap === 'string') {
50 sourceMap = JSON.parse(aSourceMap.replace(/^\)\]\}'/, ''));
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);
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);
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);
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);
81 this.sourceRoot = sourceRoot;
82 this.sourcesContent = sourcesContent;
83 this._mappings = mappings;
88 * Create a SourceMapConsumer from a SourceMapGenerator.
90 * @param SourceMapGenerator aSourceMap
91 * The source map that will be consumed.
92 * @returns SourceMapConsumer
94 SourceMapConsumer.fromSourceMap =
95 function SourceMapConsumer_fromSourceMap(aSourceMap) {
96 var smc = Object.create(SourceMapConsumer.prototype);
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(),
103 smc.file = aSourceMap._file;
105 smc.__generatedMappings = aSourceMap._mappings.toArray().slice();
106 smc.__originalMappings = aSourceMap._mappings.toArray().slice()
107 .sort(util.compareByOriginalPositions);
113 * The version of the source mapping spec that we are consuming.
115 SourceMapConsumer.prototype._version = 3;
118 * The list of original sources.
120 Object.defineProperty(SourceMapConsumer.prototype, 'sources', {
122 return this._sources.toArray().map(function (s) {
123 return this.sourceRoot != null ? util.join(this.sourceRoot, s) : s;
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.
136 // Each object in the arrays is of the form:
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
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
151 // All properties except for `generatedLine` and `generatedColumn` can be
154 // `_generatedMappings` is ordered by the generated positions.
156 // `_originalMappings` is ordered by the original positions.
158 SourceMapConsumer.prototype.__generatedMappings = null;
159 Object.defineProperty(SourceMapConsumer.prototype, '_generatedMappings', {
161 if (!this.__generatedMappings) {
162 this.__generatedMappings = [];
163 this.__originalMappings = [];
164 this._parseMappings(this._mappings, this.sourceRoot);
167 return this.__generatedMappings;
171 SourceMapConsumer.prototype.__originalMappings = null;
172 Object.defineProperty(SourceMapConsumer.prototype, '_originalMappings', {
174 if (!this.__originalMappings) {
175 this.__generatedMappings = [];
176 this.__originalMappings = [];
177 this._parseMappings(this._mappings, this.sourceRoot);
180 return this.__originalMappings;
184 SourceMapConsumer.prototype._nextCharIsMappingSeparator =
185 function SourceMapConsumer_nextCharIsMappingSeparator(aStr) {
186 var c = aStr.charAt(0);
187 return c === ";" || c === ",";
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).
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;
207 while (str.length > 0) {
208 if (str.charAt(0) === ';') {
211 previousGeneratedColumn = 0;
213 else if (str.charAt(0) === ',') {
218 mapping.generatedLine = generatedLine;
221 base64VLQ.decode(str, temp);
222 mapping.generatedColumn = previousGeneratedColumn + temp.value;
223 previousGeneratedColumn = mapping.generatedColumn;
226 if (str.length > 0 && !this._nextCharIsMappingSeparator(str)) {
228 base64VLQ.decode(str, temp);
229 mapping.source = this._sources.at(previousSource + temp.value);
230 previousSource += temp.value;
232 if (str.length === 0 || this._nextCharIsMappingSeparator(str)) {
233 throw new Error('Found a source, but no line and column');
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;
243 if (str.length === 0 || this._nextCharIsMappingSeparator(str)) {
244 throw new Error('Found a source and line, but no column');
248 base64VLQ.decode(str, temp);
249 mapping.originalColumn = previousOriginalColumn + temp.value;
250 previousOriginalColumn = mapping.originalColumn;
253 if (str.length > 0 && !this._nextCharIsMappingSeparator(str)) {
255 base64VLQ.decode(str, temp);
256 mapping.name = this._names.at(previousName + temp.value);
257 previousName += temp.value;
262 this.__generatedMappings.push(mapping);
263 if (typeof mapping.originalLine === 'number') {
264 this.__originalMappings.push(mapping);
269 this.__generatedMappings.sort(util.compareByGeneratedPositions);
270 this.__originalMappings.sort(util.compareByOriginalPositions);
274 * Find the mapping that best matches the hypothetical "needle" mapping that
275 * we are searching for in the given "haystack" of mappings.
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.
285 if (aNeedle[aLineName] <= 0) {
286 throw new TypeError('Line must be greater than or equal to 1, got '
287 + aNeedle[aLineName]);
289 if (aNeedle[aColumnName] < 0) {
290 throw new TypeError('Column must be greater than or equal to 0, got '
291 + aNeedle[aColumnName]);
294 return binarySearch.search(aNeedle, aMappings, aComparator);
298 * Compute the last column for each generated mapping. The last column is
301 SourceMapConsumer.prototype.computeColumnSpans =
302 function SourceMapConsumer_computeColumnSpans() {
303 for (var index = 0; index < this._generatedMappings.length; ++index) {
304 var mapping = this._generatedMappings[index];
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];
313 if (mapping.generatedLine === nextMapping.generatedLine) {
314 mapping.lastGeneratedColumn = nextMapping.generatedColumn - 1;
319 // The last mapping for each line spans the entire line.
320 mapping.lastGeneratedColumn = Infinity;
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:
329 * - line: The line number in the generated source.
330 * - column: The column number in the generated source.
332 * and an object is returned with the following properties:
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.
339 SourceMapConsumer.prototype.originalPositionFor =
340 function SourceMapConsumer_originalPositionFor(aArgs) {
342 generatedLine: util.getArg(aArgs, 'line'),
343 generatedColumn: util.getArg(aArgs, 'column')
346 var index = this._findMapping(needle,
347 this._generatedMappings,
350 util.compareByGeneratedPositions);
353 var mapping = this._generatedMappings[index];
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);
362 line: util.getArg(mapping, 'originalLine', null),
363 column: util.getArg(mapping, 'originalColumn', null),
364 name: util.getArg(mapping, 'name', null)
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
382 SourceMapConsumer.prototype.sourceContentFor =
383 function SourceMapConsumer_sourceContentFor(aSource) {
384 if (!this.sourcesContent) {
388 if (this.sourceRoot != null) {
389 aSource = util.relative(this.sourceRoot, aSource);
392 if (this._sources.has(aSource)) {
393 return this.sourcesContent[this._sources.indexOf(aSource)];
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)]
409 if ((!url.path || url.path == "/")
410 && this._sources.has("/" + aSource)) {
411 return this.sourcesContent[this._sources.indexOf("/" + aSource)];
415 throw new Error('"' + aSource + '" is not in the SourceMap.');
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:
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.
427 * and an object is returned with the following properties:
429 * - line: The line number in the generated source, or null.
430 * - column: The column number in the generated source, or null.
432 SourceMapConsumer.prototype.generatedPositionFor =
433 function SourceMapConsumer_generatedPositionFor(aArgs) {
435 source: util.getArg(aArgs, 'source'),
436 originalLine: util.getArg(aArgs, 'line'),
437 originalColumn: util.getArg(aArgs, 'column')
440 if (this.sourceRoot != null) {
441 needle.source = util.relative(this.sourceRoot, needle.source);
444 var index = this._findMapping(needle,
445 this._originalMappings,
448 util.compareByOriginalPositions);
451 var mapping = this._originalMappings[index];
454 line: util.getArg(mapping, 'generatedLine', null),
455 column: util.getArg(mapping, 'generatedColumn', null),
456 lastColumn: util.getArg(mapping, 'lastGeneratedColumn', null)
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
472 * - source: The filename of the original source.
473 * - line: The line number in the original source.
475 * and an array of objects is returned, each with the following properties:
477 * - line: The line number in the generated source, or null.
478 * - column: The column number in the generated source, or null.
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.
487 source: util.getArg(aArgs, 'source'),
488 originalLine: util.getArg(aArgs, 'line'),
489 originalColumn: Infinity
492 if (this.sourceRoot != null) {
493 needle.source = util.relative(this.sourceRoot, needle.source);
498 var index = this._findMapping(needle,
499 this._originalMappings,
502 util.compareByOriginalPositions);
504 var mapping = this._originalMappings[index];
506 while (mapping && mapping.originalLine === needle.originalLine) {
508 line: util.getArg(mapping, 'generatedLine', null),
509 column: util.getArg(mapping, 'generatedColumn', null),
510 lastColumn: util.getArg(mapping, 'lastGeneratedColumn', null)
513 mapping = this._originalMappings[--index];
517 return mappings.reverse();
520 SourceMapConsumer.GENERATED_ORDER = 1;
521 SourceMapConsumer.ORIGINAL_ORDER = 2;
524 * Iterate over each mapping between an original source/line/column and a
525 * generated line/column in this source map.
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.
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`.
539 SourceMapConsumer.prototype.eachMapping =
540 function SourceMapConsumer_eachMapping(aCallback, aContext, aOrder) {
541 var context = aContext || null;
542 var order = aOrder || SourceMapConsumer.GENERATED_ORDER;
546 case SourceMapConsumer.GENERATED_ORDER:
547 mappings = this._generatedMappings;
549 case SourceMapConsumer.ORIGINAL_ORDER:
550 mappings = this._originalMappings;
553 throw new Error("Unknown order of iteration.");
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);
564 generatedLine: mapping.generatedLine,
565 generatedColumn: mapping.generatedColumn,
566 originalLine: mapping.originalLine,
567 originalColumn: mapping.originalColumn,
570 }).forEach(aCallback, context);
573 exports.SourceMapConsumer = SourceMapConsumer;