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');
16 var SourceMapConsumer = require('./source-map-consumer').SourceMapConsumer;
19 * A BasicSourceMapConsumer instance represents a parsed source map which we can
20 * query for information about the original file positions by giving it a file
21 * position in the generated source.
23 * The only parameter is the raw source map (either as a JSON string, or
24 * already parsed to an object). According to the spec, source maps have the
25 * following attributes:
27 * - version: Which version of the source map spec this map is following.
28 * - sources: An array of URLs to the original source files.
29 * - names: An array of identifiers which can be referrenced by individual mappings.
30 * - sourceRoot: Optional. The URL root from which all sources are relative.
31 * - sourcesContent: Optional. An array of contents of the original source files.
32 * - mappings: A string of base64 VLQs which contain the actual mappings.
33 * - file: Optional. The generated file this source map is associated with.
35 * Here is an example source map, taken from the source map spec[0]:
41 * sources: ["foo.js", "bar.js"],
42 * names: ["src", "maps", "are", "fun"],
43 * mappings: "AA,AB;;ABCDE;"
46 * [0]: https://docs.google.com/document/d/1U1RGAehQwRypUTovF1KRlpiOFze0b-_2gc6fAH0KY0k/edit?pli=1#
48 function BasicSourceMapConsumer(aSourceMap) {
49 var sourceMap = aSourceMap;
50 if (typeof aSourceMap === 'string') {
51 sourceMap = JSON.parse(aSourceMap.replace(/^\)\]\}'/, ''));
54 var version = util.getArg(sourceMap, 'version');
55 var sources = util.getArg(sourceMap, 'sources');
56 // Sass 3.3 leaves out the 'names' array, so we deviate from the spec (which
57 // requires the array) to play nice here.
58 var names = util.getArg(sourceMap, 'names', []);
59 var sourceRoot = util.getArg(sourceMap, 'sourceRoot', null);
60 var sourcesContent = util.getArg(sourceMap, 'sourcesContent', null);
61 var mappings = util.getArg(sourceMap, 'mappings');
62 var file = util.getArg(sourceMap, 'file', null);
64 // Once again, Sass deviates from the spec and supplies the version as a
65 // string rather than a number, so we use loose equality checking here.
66 if (version != this._version) {
67 throw new Error('Unsupported version: ' + version);
70 // Some source maps produce relative source paths like "./foo.js" instead of
71 // "foo.js". Normalize these first so that future comparisons will succeed.
72 // See bugzil.la/1090768.
73 sources = sources.map(util.normalize);
75 // Pass `true` below to allow duplicate names and sources. While source maps
76 // are intended to be compressed and deduplicated, the TypeScript compiler
77 // sometimes generates source maps with duplicates in them. See Github issue
78 // #72 and bugzil.la/889492.
79 this._names = ArraySet.fromArray(names, true);
80 this._sources = ArraySet.fromArray(sources, true);
82 this.sourceRoot = sourceRoot;
83 this.sourcesContent = sourcesContent;
84 this._mappings = mappings;
88 BasicSourceMapConsumer.prototype = Object.create(SourceMapConsumer.prototype);
89 BasicSourceMapConsumer.prototype.consumer = SourceMapConsumer;
92 * Create a BasicSourceMapConsumer from a SourceMapGenerator.
94 * @param SourceMapGenerator aSourceMap
95 * The source map that will be consumed.
96 * @returns BasicSourceMapConsumer
98 BasicSourceMapConsumer.fromSourceMap =
99 function SourceMapConsumer_fromSourceMap(aSourceMap) {
100 var smc = Object.create(BasicSourceMapConsumer.prototype);
102 smc._names = ArraySet.fromArray(aSourceMap._names.toArray(), true);
103 smc._sources = ArraySet.fromArray(aSourceMap._sources.toArray(), true);
104 smc.sourceRoot = aSourceMap._sourceRoot;
105 smc.sourcesContent = aSourceMap._generateSourcesContent(smc._sources.toArray(),
107 smc.file = aSourceMap._file;
109 smc.__generatedMappings = aSourceMap._mappings.toArray().slice();
110 smc.__originalMappings = aSourceMap._mappings.toArray().slice()
111 .sort(util.compareByOriginalPositions);
117 * The version of the source mapping spec that we are consuming.
119 BasicSourceMapConsumer.prototype._version = 3;
122 * The list of original sources.
124 Object.defineProperty(BasicSourceMapConsumer.prototype, 'sources', {
126 return this._sources.toArray().map(function (s) {
127 return this.sourceRoot != null ? util.join(this.sourceRoot, s) : s;
133 * Parse the mappings in a string in to a data structure which we can easily
134 * query (the ordered arrays in the `this.__generatedMappings` and
135 * `this.__originalMappings` properties).
137 BasicSourceMapConsumer.prototype._parseMappings =
138 function SourceMapConsumer_parseMappings(aStr, aSourceRoot) {
139 var generatedLine = 1;
140 var previousGeneratedColumn = 0;
141 var previousOriginalLine = 0;
142 var previousOriginalColumn = 0;
143 var previousSource = 0;
144 var previousName = 0;
149 while (str.length > 0) {
150 if (str.charAt(0) === ';') {
153 previousGeneratedColumn = 0;
155 else if (str.charAt(0) === ',') {
160 mapping.generatedLine = generatedLine;
163 base64VLQ.decode(str, temp);
164 mapping.generatedColumn = previousGeneratedColumn + temp.value;
165 previousGeneratedColumn = mapping.generatedColumn;
168 if (str.length > 0 && !this._nextCharIsMappingSeparator(str)) {
170 base64VLQ.decode(str, temp);
171 mapping.source = this._sources.at(previousSource + temp.value);
172 previousSource += temp.value;
174 if (str.length === 0 || this._nextCharIsMappingSeparator(str)) {
175 throw new Error('Found a source, but no line and column');
179 base64VLQ.decode(str, temp);
180 mapping.originalLine = previousOriginalLine + temp.value;
181 previousOriginalLine = mapping.originalLine;
182 // Lines are stored 0-based
183 mapping.originalLine += 1;
185 if (str.length === 0 || this._nextCharIsMappingSeparator(str)) {
186 throw new Error('Found a source and line, but no column');
190 base64VLQ.decode(str, temp);
191 mapping.originalColumn = previousOriginalColumn + temp.value;
192 previousOriginalColumn = mapping.originalColumn;
195 if (str.length > 0 && !this._nextCharIsMappingSeparator(str)) {
197 base64VLQ.decode(str, temp);
198 mapping.name = this._names.at(previousName + temp.value);
199 previousName += temp.value;
204 this.__generatedMappings.push(mapping);
205 if (typeof mapping.originalLine === 'number') {
206 this.__originalMappings.push(mapping);
211 this.__generatedMappings.sort(util.compareByGeneratedPositions);
212 this.__originalMappings.sort(util.compareByOriginalPositions);
216 * Find the mapping that best matches the hypothetical "needle" mapping that
217 * we are searching for in the given "haystack" of mappings.
219 BasicSourceMapConsumer.prototype._findMapping =
220 function SourceMapConsumer_findMapping(aNeedle, aMappings, aLineName,
221 aColumnName, aComparator) {
222 // To return the position we are searching for, we must first find the
223 // mapping for the given position and then return the opposite position it
224 // points to. Because the mappings are sorted, we can use binary search to
225 // find the best mapping.
227 if (aNeedle[aLineName] <= 0) {
228 throw new TypeError('Line must be greater than or equal to 1, got '
229 + aNeedle[aLineName]);
231 if (aNeedle[aColumnName] < 0) {
232 throw new TypeError('Column must be greater than or equal to 0, got '
233 + aNeedle[aColumnName]);
236 return binarySearch.search(aNeedle, aMappings, aComparator);
240 * Compute the last column for each generated mapping. The last column is
243 BasicSourceMapConsumer.prototype.computeColumnSpans =
244 function SourceMapConsumer_computeColumnSpans() {
245 for (var index = 0; index < this._generatedMappings.length; ++index) {
246 var mapping = this._generatedMappings[index];
248 // Mappings do not contain a field for the last generated columnt. We
249 // can come up with an optimistic estimate, however, by assuming that
250 // mappings are contiguous (i.e. given two consecutive mappings, the
251 // first mapping ends where the second one starts).
252 if (index + 1 < this._generatedMappings.length) {
253 var nextMapping = this._generatedMappings[index + 1];
255 if (mapping.generatedLine === nextMapping.generatedLine) {
256 mapping.lastGeneratedColumn = nextMapping.generatedColumn - 1;
261 // The last mapping for each line spans the entire line.
262 mapping.lastGeneratedColumn = Infinity;
267 * Returns the original source, line, and column information for the generated
268 * source's line and column positions provided. The only argument is an object
269 * with the following properties:
271 * - line: The line number in the generated source.
272 * - column: The column number in the generated source.
274 * and an object is returned with the following properties:
276 * - source: The original source file, or null.
277 * - line: The line number in the original source, or null.
278 * - column: The column number in the original source, or null.
279 * - name: The original identifier, or null.
281 BasicSourceMapConsumer.prototype.originalPositionFor =
282 function SourceMapConsumer_originalPositionFor(aArgs) {
284 generatedLine: util.getArg(aArgs, 'line'),
285 generatedColumn: util.getArg(aArgs, 'column')
288 var index = this._findMapping(needle,
289 this._generatedMappings,
292 util.compareByGeneratedPositions);
295 var mapping = this._generatedMappings[index];
297 if (mapping.generatedLine === needle.generatedLine) {
298 var source = util.getArg(mapping, 'source', null);
299 if (source != null && this.sourceRoot != null) {
300 source = util.join(this.sourceRoot, source);
304 line: util.getArg(mapping, 'originalLine', null),
305 column: util.getArg(mapping, 'originalColumn', null),
306 name: util.getArg(mapping, 'name', null)
320 * Returns the original source content. The only argument is the url of the
321 * original source file. Returns null if no original source content is
324 BasicSourceMapConsumer.prototype.sourceContentFor =
325 function SourceMapConsumer_sourceContentFor(aSource, nullOnMissing) {
326 if (!this.sourcesContent) {
330 if (this.sourceRoot != null) {
331 aSource = util.relative(this.sourceRoot, aSource);
334 if (this._sources.has(aSource)) {
335 return this.sourcesContent[this._sources.indexOf(aSource)];
339 if (this.sourceRoot != null
340 && (url = util.urlParse(this.sourceRoot))) {
341 // XXX: file:// URIs and absolute paths lead to unexpected behavior for
342 // many users. We can help them out when they expect file:// URIs to
343 // behave like it would if they were running a local HTTP server. See
344 // https://bugzilla.mozilla.org/show_bug.cgi?id=885597.
345 var fileUriAbsPath = aSource.replace(/^file:\/\//, "");
346 if (url.scheme == "file"
347 && this._sources.has(fileUriAbsPath)) {
348 return this.sourcesContent[this._sources.indexOf(fileUriAbsPath)]
351 if ((!url.path || url.path == "/")
352 && this._sources.has("/" + aSource)) {
353 return this.sourcesContent[this._sources.indexOf("/" + aSource)];
357 // This function is used recursively from
358 // IndexedSourceMapConsumer.prototype.sourceContentFor. In that case, we
359 // don't want to throw if we can't find the source - we just want to
360 // return null, so we provide a flag to exit gracefully.
365 throw new Error('"' + aSource + '" is not in the SourceMap.');
370 * Returns the generated line and column information for the original source,
371 * line, and column positions provided. The only argument is an object with
372 * the following properties:
374 * - source: The filename of the original source.
375 * - line: The line number in the original source.
376 * - column: The column number in the original source.
378 * and an object is returned with the following properties:
380 * - line: The line number in the generated source, or null.
381 * - column: The column number in the generated source, or null.
383 BasicSourceMapConsumer.prototype.generatedPositionFor =
384 function SourceMapConsumer_generatedPositionFor(aArgs) {
386 source: util.getArg(aArgs, 'source'),
387 originalLine: util.getArg(aArgs, 'line'),
388 originalColumn: util.getArg(aArgs, 'column')
391 if (this.sourceRoot != null) {
392 needle.source = util.relative(this.sourceRoot, needle.source);
395 var index = this._findMapping(needle,
396 this._originalMappings,
399 util.compareByOriginalPositions);
402 var mapping = this._originalMappings[index];
405 line: util.getArg(mapping, 'generatedLine', null),
406 column: util.getArg(mapping, 'generatedColumn', null),
407 lastColumn: util.getArg(mapping, 'lastGeneratedColumn', null)
418 exports.BasicSourceMapConsumer = BasicSourceMapConsumer;