| /* -*- Mode: js; js-indent-level: 2; -*- */ |
| /* |
| * Copyright 2011 Mozilla Foundation and contributors |
| * Licensed under the New BSD license. See LICENSE or: |
| * http://opensource.org/licenses/BSD-3-Clause |
| */ |
| if (typeof define !== 'function') { |
| var define = require('amdefine')(module, require); |
| } |
| define(function (require, exports, module) { |
| |
| var util = require('./util'); |
| var binarySearch = require('./binary-search'); |
| var ArraySet = require('./array-set').ArraySet; |
| var base64VLQ = require('./base64-vlq'); |
| var SourceMapConsumer = require('./source-map-consumer').SourceMapConsumer; |
| |
| /** |
| * A BasicSourceMapConsumer instance represents a parsed source map which we can |
| * query for information about the original file positions by giving it a file |
| * position in the generated source. |
| * |
| * The only parameter is the raw source map (either as a JSON string, or |
| * already parsed to an object). According to the spec, source maps have the |
| * following attributes: |
| * |
| * - version: Which version of the source map spec this map is following. |
| * - sources: An array of URLs to the original source files. |
| * - names: An array of identifiers which can be referrenced by individual mappings. |
| * - sourceRoot: Optional. The URL root from which all sources are relative. |
| * - sourcesContent: Optional. An array of contents of the original source files. |
| * - mappings: A string of base64 VLQs which contain the actual mappings. |
| * - file: Optional. The generated file this source map is associated with. |
| * |
| * Here is an example source map, taken from the source map spec[0]: |
| * |
| * { |
| * version : 3, |
| * file: "out.js", |
| * sourceRoot : "", |
| * sources: ["foo.js", "bar.js"], |
| * names: ["src", "maps", "are", "fun"], |
| * mappings: "AA,AB;;ABCDE;" |
| * } |
| * |
| * [0]: https://docs.google.com/document/d/1U1RGAehQwRypUTovF1KRlpiOFze0b-_2gc6fAH0KY0k/edit?pli=1# |
| */ |
| function BasicSourceMapConsumer(aSourceMap) { |
| var sourceMap = aSourceMap; |
| if (typeof aSourceMap === 'string') { |
| sourceMap = JSON.parse(aSourceMap.replace(/^\)\]\}'/, '')); |
| } |
| |
| var version = util.getArg(sourceMap, 'version'); |
| var sources = util.getArg(sourceMap, 'sources'); |
| // Sass 3.3 leaves out the 'names' array, so we deviate from the spec (which |
| // requires the array) to play nice here. |
| var names = util.getArg(sourceMap, 'names', []); |
| var sourceRoot = util.getArg(sourceMap, 'sourceRoot', null); |
| var sourcesContent = util.getArg(sourceMap, 'sourcesContent', null); |
| var mappings = util.getArg(sourceMap, 'mappings'); |
| var file = util.getArg(sourceMap, 'file', null); |
| |
| // Once again, Sass deviates from the spec and supplies the version as a |
| // string rather than a number, so we use loose equality checking here. |
| if (version != this._version) { |
| throw new Error('Unsupported version: ' + version); |
| } |
| |
| // Some source maps produce relative source paths like "./foo.js" instead of |
| // "foo.js". Normalize these first so that future comparisons will succeed. |
| // See bugzil.la/1090768. |
| sources = sources.map(util.normalize); |
| |
| // Pass `true` below to allow duplicate names and sources. While source maps |
| // are intended to be compressed and deduplicated, the TypeScript compiler |
| // sometimes generates source maps with duplicates in them. See Github issue |
| // #72 and bugzil.la/889492. |
| this._names = ArraySet.fromArray(names, true); |
| this._sources = ArraySet.fromArray(sources, true); |
| |
| this.sourceRoot = sourceRoot; |
| this.sourcesContent = sourcesContent; |
| this._mappings = mappings; |
| this.file = file; |
| } |
| |
| BasicSourceMapConsumer.prototype = Object.create(SourceMapConsumer.prototype); |
| BasicSourceMapConsumer.prototype.consumer = SourceMapConsumer; |
| |
| /** |
| * Create a BasicSourceMapConsumer from a SourceMapGenerator. |
| * |
| * @param SourceMapGenerator aSourceMap |
| * The source map that will be consumed. |
| * @returns BasicSourceMapConsumer |
| */ |
| BasicSourceMapConsumer.fromSourceMap = |
| function SourceMapConsumer_fromSourceMap(aSourceMap) { |
| var smc = Object.create(BasicSourceMapConsumer.prototype); |
| |
| smc._names = ArraySet.fromArray(aSourceMap._names.toArray(), true); |
| smc._sources = ArraySet.fromArray(aSourceMap._sources.toArray(), true); |
| smc.sourceRoot = aSourceMap._sourceRoot; |
| smc.sourcesContent = aSourceMap._generateSourcesContent(smc._sources.toArray(), |
| smc.sourceRoot); |
| smc.file = aSourceMap._file; |
| |
| smc.__generatedMappings = aSourceMap._mappings.toArray().slice(); |
| smc.__originalMappings = aSourceMap._mappings.toArray().slice() |
| .sort(util.compareByOriginalPositions); |
| |
| return smc; |
| }; |
| |
| /** |
| * The version of the source mapping spec that we are consuming. |
| */ |
| BasicSourceMapConsumer.prototype._version = 3; |
| |
| /** |
| * The list of original sources. |
| */ |
| Object.defineProperty(BasicSourceMapConsumer.prototype, 'sources', { |
| get: function () { |
| return this._sources.toArray().map(function (s) { |
| return this.sourceRoot != null ? util.join(this.sourceRoot, s) : s; |
| }, this); |
| } |
| }); |
| |
| /** |
| * Parse the mappings in a string in to a data structure which we can easily |
| * query (the ordered arrays in the `this.__generatedMappings` and |
| * `this.__originalMappings` properties). |
| */ |
| BasicSourceMapConsumer.prototype._parseMappings = |
| function SourceMapConsumer_parseMappings(aStr, aSourceRoot) { |
| var generatedLine = 1; |
| var previousGeneratedColumn = 0; |
| var previousOriginalLine = 0; |
| var previousOriginalColumn = 0; |
| var previousSource = 0; |
| var previousName = 0; |
| var str = aStr; |
| var temp = {}; |
| var mapping; |
| |
| while (str.length > 0) { |
| if (str.charAt(0) === ';') { |
| generatedLine++; |
| str = str.slice(1); |
| previousGeneratedColumn = 0; |
| } |
| else if (str.charAt(0) === ',') { |
| str = str.slice(1); |
| } |
| else { |
| mapping = {}; |
| mapping.generatedLine = generatedLine; |
| |
| // Generated column. |
| base64VLQ.decode(str, temp); |
| mapping.generatedColumn = previousGeneratedColumn + temp.value; |
| previousGeneratedColumn = mapping.generatedColumn; |
| str = temp.rest; |
| |
| if (str.length > 0 && !this._nextCharIsMappingSeparator(str)) { |
| // Original source. |
| base64VLQ.decode(str, temp); |
| mapping.source = this._sources.at(previousSource + temp.value); |
| previousSource += temp.value; |
| str = temp.rest; |
| if (str.length === 0 || this._nextCharIsMappingSeparator(str)) { |
| throw new Error('Found a source, but no line and column'); |
| } |
| |
| // Original line. |
| base64VLQ.decode(str, temp); |
| mapping.originalLine = previousOriginalLine + temp.value; |
| previousOriginalLine = mapping.originalLine; |
| // Lines are stored 0-based |
| mapping.originalLine += 1; |
| str = temp.rest; |
| if (str.length === 0 || this._nextCharIsMappingSeparator(str)) { |
| throw new Error('Found a source and line, but no column'); |
| } |
| |
| // Original column. |
| base64VLQ.decode(str, temp); |
| mapping.originalColumn = previousOriginalColumn + temp.value; |
| previousOriginalColumn = mapping.originalColumn; |
| str = temp.rest; |
| |
| if (str.length > 0 && !this._nextCharIsMappingSeparator(str)) { |
| // Original name. |
| base64VLQ.decode(str, temp); |
| mapping.name = this._names.at(previousName + temp.value); |
| previousName += temp.value; |
| str = temp.rest; |
| } |
| } |
| |
| this.__generatedMappings.push(mapping); |
| if (typeof mapping.originalLine === 'number') { |
| this.__originalMappings.push(mapping); |
| } |
| } |
| } |
| |
| this.__generatedMappings.sort(util.compareByGeneratedPositions); |
| this.__originalMappings.sort(util.compareByOriginalPositions); |
| }; |
| |
| /** |
| * Find the mapping that best matches the hypothetical "needle" mapping that |
| * we are searching for in the given "haystack" of mappings. |
| */ |
| BasicSourceMapConsumer.prototype._findMapping = |
| function SourceMapConsumer_findMapping(aNeedle, aMappings, aLineName, |
| aColumnName, aComparator) { |
| // To return the position we are searching for, we must first find the |
| // mapping for the given position and then return the opposite position it |
| // points to. Because the mappings are sorted, we can use binary search to |
| // find the best mapping. |
| |
| if (aNeedle[aLineName] <= 0) { |
| throw new TypeError('Line must be greater than or equal to 1, got ' |
| + aNeedle[aLineName]); |
| } |
| if (aNeedle[aColumnName] < 0) { |
| throw new TypeError('Column must be greater than or equal to 0, got ' |
| + aNeedle[aColumnName]); |
| } |
| |
| return binarySearch.search(aNeedle, aMappings, aComparator); |
| }; |
| |
| /** |
| * Compute the last column for each generated mapping. The last column is |
| * inclusive. |
| */ |
| BasicSourceMapConsumer.prototype.computeColumnSpans = |
| function SourceMapConsumer_computeColumnSpans() { |
| for (var index = 0; index < this._generatedMappings.length; ++index) { |
| var mapping = this._generatedMappings[index]; |
| |
| // Mappings do not contain a field for the last generated columnt. We |
| // can come up with an optimistic estimate, however, by assuming that |
| // mappings are contiguous (i.e. given two consecutive mappings, the |
| // first mapping ends where the second one starts). |
| if (index + 1 < this._generatedMappings.length) { |
| var nextMapping = this._generatedMappings[index + 1]; |
| |
| if (mapping.generatedLine === nextMapping.generatedLine) { |
| mapping.lastGeneratedColumn = nextMapping.generatedColumn - 1; |
| continue; |
| } |
| } |
| |
| // The last mapping for each line spans the entire line. |
| mapping.lastGeneratedColumn = Infinity; |
| } |
| }; |
| |
| /** |
| * Returns the original source, line, and column information for the generated |
| * source's line and column positions provided. The only argument is an object |
| * with the following properties: |
| * |
| * - line: The line number in the generated source. |
| * - column: The column number in the generated source. |
| * |
| * and an object is returned with the following properties: |
| * |
| * - source: The original source file, or null. |
| * - line: The line number in the original source, or null. |
| * - column: The column number in the original source, or null. |
| * - name: The original identifier, or null. |
| */ |
| BasicSourceMapConsumer.prototype.originalPositionFor = |
| function SourceMapConsumer_originalPositionFor(aArgs) { |
| var needle = { |
| generatedLine: util.getArg(aArgs, 'line'), |
| generatedColumn: util.getArg(aArgs, 'column') |
| }; |
| |
| var index = this._findMapping(needle, |
| this._generatedMappings, |
| "generatedLine", |
| "generatedColumn", |
| util.compareByGeneratedPositions); |
| |
| if (index >= 0) { |
| var mapping = this._generatedMappings[index]; |
| |
| if (mapping.generatedLine === needle.generatedLine) { |
| var source = util.getArg(mapping, 'source', null); |
| if (source != null && this.sourceRoot != null) { |
| source = util.join(this.sourceRoot, source); |
| } |
| return { |
| source: source, |
| line: util.getArg(mapping, 'originalLine', null), |
| column: util.getArg(mapping, 'originalColumn', null), |
| name: util.getArg(mapping, 'name', null) |
| }; |
| } |
| } |
| |
| return { |
| source: null, |
| line: null, |
| column: null, |
| name: null |
| }; |
| }; |
| |
| /** |
| * Returns the original source content. The only argument is the url of the |
| * original source file. Returns null if no original source content is |
| * availible. |
| */ |
| BasicSourceMapConsumer.prototype.sourceContentFor = |
| function SourceMapConsumer_sourceContentFor(aSource, nullOnMissing) { |
| if (!this.sourcesContent) { |
| return null; |
| } |
| |
| if (this.sourceRoot != null) { |
| aSource = util.relative(this.sourceRoot, aSource); |
| } |
| |
| if (this._sources.has(aSource)) { |
| return this.sourcesContent[this._sources.indexOf(aSource)]; |
| } |
| |
| var url; |
| if (this.sourceRoot != null |
| && (url = util.urlParse(this.sourceRoot))) { |
| // XXX: file:// URIs and absolute paths lead to unexpected behavior for |
| // many users. We can help them out when they expect file:// URIs to |
| // behave like it would if they were running a local HTTP server. See |
| // https://bugzilla.mozilla.org/show_bug.cgi?id=885597. |
| var fileUriAbsPath = aSource.replace(/^file:\/\//, ""); |
| if (url.scheme == "file" |
| && this._sources.has(fileUriAbsPath)) { |
| return this.sourcesContent[this._sources.indexOf(fileUriAbsPath)] |
| } |
| |
| if ((!url.path || url.path == "/") |
| && this._sources.has("/" + aSource)) { |
| return this.sourcesContent[this._sources.indexOf("/" + aSource)]; |
| } |
| } |
| |
| // This function is used recursively from |
| // IndexedSourceMapConsumer.prototype.sourceContentFor. In that case, we |
| // don't want to throw if we can't find the source - we just want to |
| // return null, so we provide a flag to exit gracefully. |
| if (nullOnMissing) { |
| return null; |
| } |
| else { |
| throw new Error('"' + aSource + '" is not in the SourceMap.'); |
| } |
| }; |
| |
| /** |
| * Returns the generated line and column information for the original source, |
| * line, and column positions provided. The only argument is an object with |
| * the following properties: |
| * |
| * - source: The filename of the original source. |
| * - line: The line number in the original source. |
| * - column: The column number in the original source. |
| * |
| * and an object is returned with the following properties: |
| * |
| * - line: The line number in the generated source, or null. |
| * - column: The column number in the generated source, or null. |
| */ |
| BasicSourceMapConsumer.prototype.generatedPositionFor = |
| function SourceMapConsumer_generatedPositionFor(aArgs) { |
| var needle = { |
| source: util.getArg(aArgs, 'source'), |
| originalLine: util.getArg(aArgs, 'line'), |
| originalColumn: util.getArg(aArgs, 'column') |
| }; |
| |
| if (this.sourceRoot != null) { |
| needle.source = util.relative(this.sourceRoot, needle.source); |
| } |
| |
| var index = this._findMapping(needle, |
| this._originalMappings, |
| "originalLine", |
| "originalColumn", |
| util.compareByOriginalPositions); |
| |
| if (index >= 0) { |
| var mapping = this._originalMappings[index]; |
| |
| return { |
| line: util.getArg(mapping, 'generatedLine', null), |
| column: util.getArg(mapping, 'generatedColumn', null), |
| lastColumn: util.getArg(mapping, 'lastGeneratedColumn', null) |
| }; |
| } |
| |
| return { |
| line: null, |
| column: null, |
| lastColumn: null |
| }; |
| }; |
| |
| exports.BasicSourceMapConsumer = BasicSourceMapConsumer; |
| |
| }); |