source-map-consumer.js
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 8 var util = require('./util'); 9 var binarySearch = require('./binary-search'); 10 var ArraySet = require('./array-set').ArraySet; 11 var base64VLQ = require('./base64-vlq'); 12 var quickSort = require('./quick-sort').quickSort; 13 14 function SourceMapConsumer(aSourceMap, aSourceMapURL) { 15 var sourceMap = aSourceMap; 16 if (typeof aSourceMap === 'string') { 17 sourceMap = util.parseSourceMapInput(aSourceMap); 18 } 19 20 return sourceMap.sections != null 21 ? new IndexedSourceMapConsumer(sourceMap, aSourceMapURL) 22 : new BasicSourceMapConsumer(sourceMap, aSourceMapURL); 23 } 24 25 SourceMapConsumer.fromSourceMap = function(aSourceMap, aSourceMapURL) { 26 return BasicSourceMapConsumer.fromSourceMap(aSourceMap, aSourceMapURL); 27 } 28 29 /** 30 * The version of the source mapping spec that we are consuming. 31 */ 32 SourceMapConsumer.prototype._version = 3; 33 34 // `__generatedMappings` and `__originalMappings` are arrays that hold the 35 // parsed mapping coordinates from the source map's "mappings" attribute. They 36 // are lazily instantiated, accessed via the `_generatedMappings` and 37 // `_originalMappings` getters respectively, and we only parse the mappings 38 // and create these arrays once queried for a source location. We jump through 39 // these hoops because there can be many thousands of mappings, and parsing 40 // them is expensive, so we only want to do it if we must. 41 // 42 // Each object in the arrays is of the form: 43 // 44 // { 45 // generatedLine: The line number in the generated code, 46 // generatedColumn: The column number in the generated code, 47 // source: The path to the original source file that generated this 48 // chunk of code, 49 // originalLine: The line number in the original source that 50 // corresponds to this chunk of generated code, 51 // originalColumn: The column number in the original source that 52 // corresponds to this chunk of generated code, 53 // name: The name of the original symbol which generated this chunk of 54 // code. 55 // } 56 // 57 // All properties except for `generatedLine` and `generatedColumn` can be 58 // `null`. 59 // 60 // `_generatedMappings` is ordered by the generated positions. 61 // 62 // `_originalMappings` is ordered by the original positions. 63 64 SourceMapConsumer.prototype.__generatedMappings = null; 65 Object.defineProperty(SourceMapConsumer.prototype, '_generatedMappings', { 66 configurable: true, 67 enumerable: true, 68 get: function () { 69 if (!this.__generatedMappings) { 70 this._parseMappings(this._mappings, this.sourceRoot); 71 } 72 73 return this.__generatedMappings; 74 } 75 }); 76 77 SourceMapConsumer.prototype.__originalMappings = null; 78 Object.defineProperty(SourceMapConsumer.prototype, '_originalMappings', { 79 configurable: true, 80 enumerable: true, 81 get: function () { 82 if (!this.__originalMappings) { 83 this._parseMappings(this._mappings, this.sourceRoot); 84 } 85 86 return this.__originalMappings; 87 } 88 }); 89 90 SourceMapConsumer.prototype._charIsMappingSeparator = 91 function SourceMapConsumer_charIsMappingSeparator(aStr, index) { 92 var c = aStr.charAt(index); 93 return c === ";" || c === ","; 94 }; 95 96 /** 97 * Parse the mappings in a string in to a data structure which we can easily 98 * query (the ordered arrays in the `this.__generatedMappings` and 99 * `this.__originalMappings` properties). 100 */ 101 SourceMapConsumer.prototype._parseMappings = 102 function SourceMapConsumer_parseMappings(aStr, aSourceRoot) { 103 throw new Error("Subclasses must implement _parseMappings"); 104 }; 105 106 SourceMapConsumer.GENERATED_ORDER = 1; 107 SourceMapConsumer.ORIGINAL_ORDER = 2; 108 109 SourceMapConsumer.GREATEST_LOWER_BOUND = 1; 110 SourceMapConsumer.LEAST_UPPER_BOUND = 2; 111 112 /** 113 * Iterate over each mapping between an original source/line/column and a 114 * generated line/column in this source map. 115 * 116 * @param Function aCallback 117 * The function that is called with each mapping. 118 * @param Object aContext 119 * Optional. If specified, this object will be the value of `this` every 120 * time that `aCallback` is called. 121 * @param aOrder 122 * Either `SourceMapConsumer.GENERATED_ORDER` or 123 * `SourceMapConsumer.ORIGINAL_ORDER`. Specifies whether you want to 124 * iterate over the mappings sorted by the generated file's line/column 125 * order or the original's source/line/column order, respectively. Defaults to 126 * `SourceMapConsumer.GENERATED_ORDER`. 127 */ 128 SourceMapConsumer.prototype.eachMapping = 129 function SourceMapConsumer_eachMapping(aCallback, aContext, aOrder) { 130 var context = aContext || null; 131 var order = aOrder || SourceMapConsumer.GENERATED_ORDER; 132 133 var mappings; 134 switch (order) { 135 case SourceMapConsumer.GENERATED_ORDER: 136 mappings = this._generatedMappings; 137 break; 138 case SourceMapConsumer.ORIGINAL_ORDER: 139 mappings = this._originalMappings; 140 break; 141 default: 142 throw new Error("Unknown order of iteration."); 143 } 144 145 var sourceRoot = this.sourceRoot; 146 mappings.map(function (mapping) { 147 var source = mapping.source === null ? null : this._sources.at(mapping.source); 148 source = util.computeSourceURL(sourceRoot, source, this._sourceMapURL); 149 return { 150 source: source, 151 generatedLine: mapping.generatedLine, 152 generatedColumn: mapping.generatedColumn, 153 originalLine: mapping.originalLine, 154 originalColumn: mapping.originalColumn, 155 name: mapping.name === null ? null : this._names.at(mapping.name) 156 }; 157 }, this).forEach(aCallback, context); 158 }; 159 160 /** 161 * Returns all generated line and column information for the original source, 162 * line, and column provided. If no column is provided, returns all mappings 163 * corresponding to a either the line we are searching for or the next 164 * closest line that has any mappings. Otherwise, returns all mappings 165 * corresponding to the given line and either the column we are searching for 166 * or the next closest column that has any offsets. 167 * 168 * The only argument is an object with the following properties: 169 * 170 * - source: The filename of the original source. 171 * - line: The line number in the original source. The line number is 1-based. 172 * - column: Optional. the column number in the original source. 173 * The column number is 0-based. 174 * 175 * and an array of objects is returned, each with the following properties: 176 * 177 * - line: The line number in the generated source, or null. The 178 * line number is 1-based. 179 * - column: The column number in the generated source, or null. 180 * The column number is 0-based. 181 */ 182 SourceMapConsumer.prototype.allGeneratedPositionsFor = 183 function SourceMapConsumer_allGeneratedPositionsFor(aArgs) { 184 var line = util.getArg(aArgs, 'line'); 185 186 // When there is no exact match, BasicSourceMapConsumer.prototype._findMapping 187 // returns the index of the closest mapping less than the needle. By 188 // setting needle.originalColumn to 0, we thus find the last mapping for 189 // the given line, provided such a mapping exists. 190 var needle = { 191 source: util.getArg(aArgs, 'source'), 192 originalLine: line, 193 originalColumn: util.getArg(aArgs, 'column', 0) 194 }; 195 196 needle.source = this._findSourceIndex(needle.source); 197 if (needle.source < 0) { 198 return []; 199 } 200 201 var mappings = []; 202 203 var index = this._findMapping(needle, 204 this._originalMappings, 205 "originalLine", 206 "originalColumn", 207 util.compareByOriginalPositions, 208 binarySearch.LEAST_UPPER_BOUND); 209 if (index >= 0) { 210 var mapping = this._originalMappings[index]; 211 212 if (aArgs.column === undefined) { 213 var originalLine = mapping.originalLine; 214 215 // Iterate until either we run out of mappings, or we run into 216 // a mapping for a different line than the one we found. Since 217 // mappings are sorted, this is guaranteed to find all mappings for 218 // the line we found. 219 while (mapping && mapping.originalLine === originalLine) { 220 mappings.push({ 221 line: util.getArg(mapping, 'generatedLine', null), 222 column: util.getArg(mapping, 'generatedColumn', null), 223 lastColumn: util.getArg(mapping, 'lastGeneratedColumn', null) 224 }); 225 226 mapping = this._originalMappings[++index]; 227 } 228 } else { 229 var originalColumn = mapping.originalColumn; 230 231 // Iterate until either we run out of mappings, or we run into 232 // a mapping for a different line than the one we were searching for. 233 // Since mappings are sorted, this is guaranteed to find all mappings for 234 // the line we are searching for. 235 while (mapping && 236 mapping.originalLine === line && 237 mapping.originalColumn == originalColumn) { 238 mappings.push({ 239 line: util.getArg(mapping, 'generatedLine', null), 240 column: util.getArg(mapping, 'generatedColumn', null), 241 lastColumn: util.getArg(mapping, 'lastGeneratedColumn', null) 242 }); 243 244 mapping = this._originalMappings[++index]; 245 } 246 } 247 } 248 249 return mappings; 250 }; 251 252 exports.SourceMapConsumer = SourceMapConsumer; 253 254 /** 255 * A BasicSourceMapConsumer instance represents a parsed source map which we can 256 * query for information about the original file positions by giving it a file 257 * position in the generated source. 258 * 259 * The first parameter is the raw source map (either as a JSON string, or 260 * already parsed to an object). According to the spec, source maps have the 261 * following attributes: 262 * 263 * - version: Which version of the source map spec this map is following. 264 * - sources: An array of URLs to the original source files. 265 * - names: An array of identifiers which can be referrenced by individual mappings. 266 * - sourceRoot: Optional. The URL root from which all sources are relative. 267 * - sourcesContent: Optional. An array of contents of the original source files. 268 * - mappings: A string of base64 VLQs which contain the actual mappings. 269 * - file: Optional. The generated file this source map is associated with. 270 * 271 * Here is an example source map, taken from the source map spec[0]: 272 * 273 * { 274 * version : 3, 275 * file: "out.js", 276 * sourceRoot : "", 277 * sources: ["foo.js", "bar.js"], 278 * names: ["src", "maps", "are", "fun"], 279 * mappings: "AA,AB;;ABCDE;" 280 * } 281 * 282 * The second parameter, if given, is a string whose value is the URL 283 * at which the source map was found. This URL is used to compute the 284 * sources array. 285 * 286 * [0]: https://docs.google.com/document/d/1U1RGAehQwRypUTovF1KRlpiOFze0b-_2gc6fAH0KY0k/edit?pli=1# 287 */ 288 function BasicSourceMapConsumer(aSourceMap, aSourceMapURL) { 289 var sourceMap = aSourceMap; 290 if (typeof aSourceMap === 'string') { 291 sourceMap = util.parseSourceMapInput(aSourceMap); 292 } 293 294 var version = util.getArg(sourceMap, 'version'); 295 var sources = util.getArg(sourceMap, 'sources'); 296 // Sass 3.3 leaves out the 'names' array, so we deviate from the spec (which 297 // requires the array) to play nice here. 298 var names = util.getArg(sourceMap, 'names', []); 299 var sourceRoot = util.getArg(sourceMap, 'sourceRoot', null); 300 var sourcesContent = util.getArg(sourceMap, 'sourcesContent', null); 301 var mappings = util.getArg(sourceMap, 'mappings'); 302 var file = util.getArg(sourceMap, 'file', null); 303 304 // Once again, Sass deviates from the spec and supplies the version as a 305 // string rather than a number, so we use loose equality checking here. 306 if (version != this._version) { 307 throw new Error('Unsupported version: ' + version); 308 } 309 310 if (sourceRoot) { 311 sourceRoot = util.normalize(sourceRoot); 312 } 313 314 sources = sources 315 .map(String) 316 // Some source maps produce relative source paths like "./foo.js" instead of 317 // "foo.js". Normalize these first so that future comparisons will succeed. 318 // See bugzil.la/1090768. 319 .map(util.normalize) 320 // Always ensure that absolute sources are internally stored relative to 321 // the source root, if the source root is absolute. Not doing this would 322 // be particularly problematic when the source root is a prefix of the 323 // source (valid, but why??). See github issue #199 and bugzil.la/1188982. 324 .map(function (source) { 325 return sourceRoot && util.isAbsolute(sourceRoot) && util.isAbsolute(source) 326 ? util.relative(sourceRoot, source) 327 : source; 328 }); 329 330 // Pass `true` below to allow duplicate names and sources. While source maps 331 // are intended to be compressed and deduplicated, the TypeScript compiler 332 // sometimes generates source maps with duplicates in them. See Github issue 333 // #72 and bugzil.la/889492. 334 this._names = ArraySet.fromArray(names.map(String), true); 335 this._sources = ArraySet.fromArray(sources, true); 336 337 this._absoluteSources = this._sources.toArray().map(function (s) { 338 return util.computeSourceURL(sourceRoot, s, aSourceMapURL); 339 }); 340 341 this.sourceRoot = sourceRoot; 342 this.sourcesContent = sourcesContent; 343 this._mappings = mappings; 344 this._sourceMapURL = aSourceMapURL; 345 this.file = file; 346 } 347 348 BasicSourceMapConsumer.prototype = Object.create(SourceMapConsumer.prototype); 349 BasicSourceMapConsumer.prototype.consumer = SourceMapConsumer; 350 351 /** 352 * Utility function to find the index of a source. Returns -1 if not 353 * found. 354 */ 355 BasicSourceMapConsumer.prototype._findSourceIndex = function(aSource) { 356 var relativeSource = aSource; 357 if (this.sourceRoot != null) { 358 relativeSource = util.relative(this.sourceRoot, relativeSource); 359 } 360 361 if (this._sources.has(relativeSource)) { 362 return this._sources.indexOf(relativeSource); 363 } 364 365 // Maybe aSource is an absolute URL as returned by |sources|. In 366 // this case we can't simply undo the transform. 367 var i; 368 for (i = 0; i < this._absoluteSources.length; ++i) { 369 if (this._absoluteSources[i] == aSource) { 370 return i; 371 } 372 } 373 374 return -1; 375 }; 376 377 /** 378 * Create a BasicSourceMapConsumer from a SourceMapGenerator. 379 * 380 * @param SourceMapGenerator aSourceMap 381 * The source map that will be consumed. 382 * @param String aSourceMapURL 383 * The URL at which the source map can be found (optional) 384 * @returns BasicSourceMapConsumer 385 */ 386 BasicSourceMapConsumer.fromSourceMap = 387 function SourceMapConsumer_fromSourceMap(aSourceMap, aSourceMapURL) { 388 var smc = Object.create(BasicSourceMapConsumer.prototype); 389 390 var names = smc._names = ArraySet.fromArray(aSourceMap._names.toArray(), true); 391 var sources = smc._sources = ArraySet.fromArray(aSourceMap._sources.toArray(), true); 392 smc.sourceRoot = aSourceMap._sourceRoot; 393 smc.sourcesContent = aSourceMap._generateSourcesContent(smc._sources.toArray(), 394 smc.sourceRoot); 395 smc.file = aSourceMap._file; 396 smc._sourceMapURL = aSourceMapURL; 397 smc._absoluteSources = smc._sources.toArray().map(function (s) { 398 return util.computeSourceURL(smc.sourceRoot, s, aSourceMapURL); 399 }); 400 401 // Because we are modifying the entries (by converting string sources and 402 // names to indices into the sources and names ArraySets), we have to make 403 // a copy of the entry or else bad things happen. Shared mutable state 404 // strikes again! See github issue #191. 405 406 var generatedMappings = aSourceMap._mappings.toArray().slice(); 407 var destGeneratedMappings = smc.__generatedMappings = []; 408 var destOriginalMappings = smc.__originalMappings = []; 409 410 for (var i = 0, length = generatedMappings.length; i < length; i++) { 411 var srcMapping = generatedMappings[i]; 412 var destMapping = new Mapping; 413 destMapping.generatedLine = srcMapping.generatedLine; 414 destMapping.generatedColumn = srcMapping.generatedColumn; 415 416 if (srcMapping.source) { 417 destMapping.source = sources.indexOf(srcMapping.source); 418 destMapping.originalLine = srcMapping.originalLine; 419 destMapping.originalColumn = srcMapping.originalColumn; 420 421 if (srcMapping.name) { 422 destMapping.name = names.indexOf(srcMapping.name); 423 } 424 425 destOriginalMappings.push(destMapping); 426 } 427 428 destGeneratedMappings.push(destMapping); 429 } 430 431 quickSort(smc.__originalMappings, util.compareByOriginalPositions); 432 433 return smc; 434 }; 435 436 /** 437 * The version of the source mapping spec that we are consuming. 438 */ 439 BasicSourceMapConsumer.prototype._version = 3; 440 441 /** 442 * The list of original sources. 443 */ 444 Object.defineProperty(BasicSourceMapConsumer.prototype, 'sources', { 445 get: function () { 446 return this._absoluteSources.slice(); 447 } 448 }); 449 450 /** 451 * Provide the JIT with a nice shape / hidden class. 452 */ 453 function Mapping() { 454 this.generatedLine = 0; 455 this.generatedColumn = 0; 456 this.source = null; 457 this.originalLine = null; 458 this.originalColumn = null; 459 this.name = null; 460 } 461 462 /** 463 * Parse the mappings in a string in to a data structure which we can easily 464 * query (the ordered arrays in the `this.__generatedMappings` and 465 * `this.__originalMappings` properties). 466 */ 467 BasicSourceMapConsumer.prototype._parseMappings = 468 function SourceMapConsumer_parseMappings(aStr, aSourceRoot) { 469 var generatedLine = 1; 470 var previousGeneratedColumn = 0; 471 var previousOriginalLine = 0; 472 var previousOriginalColumn = 0; 473 var previousSource = 0; 474 var previousName = 0; 475 var length = aStr.length; 476 var index = 0; 477 var cachedSegments = {}; 478 var temp = {}; 479 var originalMappings = []; 480 var generatedMappings = []; 481 var mapping, str, segment, end, value; 482 483 while (index < length) { 484 if (aStr.charAt(index) === ';') { 485 generatedLine++; 486 index++; 487 previousGeneratedColumn = 0; 488 } 489 else if (aStr.charAt(index) === ',') { 490 index++; 491 } 492 else { 493 mapping = new Mapping(); 494 mapping.generatedLine = generatedLine; 495 496 // Because each offset is encoded relative to the previous one, 497 // many segments often have the same encoding. We can exploit this 498 // fact by caching the parsed variable length fields of each segment, 499 // allowing us to avoid a second parse if we encounter the same 500 // segment again. 501 for (end = index; end < length; end++) { 502 if (this._charIsMappingSeparator(aStr, end)) { 503 break; 504 } 505 } 506 str = aStr.slice(index, end); 507 508 segment = cachedSegments[str]; 509 if (segment) { 510 index += str.length; 511 } else { 512 segment = []; 513 while (index < end) { 514 base64VLQ.decode(aStr, index, temp); 515 value = temp.value; 516 index = temp.rest; 517 segment.push(value); 518 } 519 520 if (segment.length === 2) { 521 throw new Error('Found a source, but no line and column'); 522 } 523 524 if (segment.length === 3) { 525 throw new Error('Found a source and line, but no column'); 526 } 527 528 cachedSegments[str] = segment; 529 } 530 531 // Generated column. 532 mapping.generatedColumn = previousGeneratedColumn + segment[0]; 533 previousGeneratedColumn = mapping.generatedColumn; 534 535 if (segment.length > 1) { 536 // Original source. 537 mapping.source = previousSource + segment[1]; 538 previousSource += segment[1]; 539 540 // Original line. 541 mapping.originalLine = previousOriginalLine + segment[2]; 542 previousOriginalLine = mapping.originalLine; 543 // Lines are stored 0-based 544 mapping.originalLine += 1; 545 546 // Original column. 547 mapping.originalColumn = previousOriginalColumn + segment[3]; 548 previousOriginalColumn = mapping.originalColumn; 549 550 if (segment.length > 4) { 551 // Original name. 552 mapping.name = previousName + segment[4]; 553 previousName += segment[4]; 554 } 555 } 556 557 generatedMappings.push(mapping); 558 if (typeof mapping.originalLine === 'number') { 559 originalMappings.push(mapping); 560 } 561 } 562 } 563 564 quickSort(generatedMappings, util.compareByGeneratedPositionsDeflated); 565 this.__generatedMappings = generatedMappings; 566 567 quickSort(originalMappings, util.compareByOriginalPositions); 568 this.__originalMappings = originalMappings; 569 }; 570 571 /** 572 * Find the mapping that best matches the hypothetical "needle" mapping that 573 * we are searching for in the given "haystack" of mappings. 574 */ 575 BasicSourceMapConsumer.prototype._findMapping = 576 function SourceMapConsumer_findMapping(aNeedle, aMappings, aLineName, 577 aColumnName, aComparator, aBias) { 578 // To return the position we are searching for, we must first find the 579 // mapping for the given position and then return the opposite position it 580 // points to. Because the mappings are sorted, we can use binary search to 581 // find the best mapping. 582 583 if (aNeedle[aLineName] <= 0) { 584 throw new TypeError('Line must be greater than or equal to 1, got ' 585 + aNeedle[aLineName]); 586 } 587 if (aNeedle[aColumnName] < 0) { 588 throw new TypeError('Column must be greater than or equal to 0, got ' 589 + aNeedle[aColumnName]); 590 } 591 592 return binarySearch.search(aNeedle, aMappings, aComparator, aBias); 593 }; 594 595 /** 596 * Compute the last column for each generated mapping. The last column is 597 * inclusive. 598 */ 599 BasicSourceMapConsumer.prototype.computeColumnSpans = 600 function SourceMapConsumer_computeColumnSpans() { 601 for (var index = 0; index < this._generatedMappings.length; ++index) { 602 var mapping = this._generatedMappings[index]; 603 604 // Mappings do not contain a field for the last generated columnt. We 605 // can come up with an optimistic estimate, however, by assuming that 606 // mappings are contiguous (i.e. given two consecutive mappings, the 607 // first mapping ends where the second one starts). 608 if (index + 1 < this._generatedMappings.length) { 609 var nextMapping = this._generatedMappings[index + 1]; 610 611 if (mapping.generatedLine === nextMapping.generatedLine) { 612 mapping.lastGeneratedColumn = nextMapping.generatedColumn - 1; 613 continue; 614 } 615 } 616 617 // The last mapping for each line spans the entire line. 618 mapping.lastGeneratedColumn = Infinity; 619 } 620 }; 621 622 /** 623 * Returns the original source, line, and column information for the generated 624 * source's line and column positions provided. The only argument is an object 625 * with the following properties: 626 * 627 * - line: The line number in the generated source. The line number 628 * is 1-based. 629 * - column: The column number in the generated source. The column 630 * number is 0-based. 631 * - bias: Either 'SourceMapConsumer.GREATEST_LOWER_BOUND' or 632 * 'SourceMapConsumer.LEAST_UPPER_BOUND'. Specifies whether to return the 633 * closest element that is smaller than or greater than the one we are 634 * searching for, respectively, if the exact element cannot be found. 635 * Defaults to 'SourceMapConsumer.GREATEST_LOWER_BOUND'. 636 * 637 * and an object is returned with the following properties: 638 * 639 * - source: The original source file, or null. 640 * - line: The line number in the original source, or null. The 641 * line number is 1-based. 642 * - column: The column number in the original source, or null. The 643 * column number is 0-based. 644 * - name: The original identifier, or null. 645 */ 646 BasicSourceMapConsumer.prototype.originalPositionFor = 647 function SourceMapConsumer_originalPositionFor(aArgs) { 648 var needle = { 649 generatedLine: util.getArg(aArgs, 'line'), 650 generatedColumn: util.getArg(aArgs, 'column') 651 }; 652 653 var index = this._findMapping( 654 needle, 655 this._generatedMappings, 656 "generatedLine", 657 "generatedColumn", 658 util.compareByGeneratedPositionsDeflated, 659 util.getArg(aArgs, 'bias', SourceMapConsumer.GREATEST_LOWER_BOUND) 660 ); 661 662 if (index >= 0) { 663 var mapping = this._generatedMappings[index]; 664 665 if (mapping.generatedLine === needle.generatedLine) { 666 var source = util.getArg(mapping, 'source', null); 667 if (source !== null) { 668 source = this._sources.at(source); 669 source = util.computeSourceURL(this.sourceRoot, source, this._sourceMapURL); 670 } 671 var name = util.getArg(mapping, 'name', null); 672 if (name !== null) { 673 name = this._names.at(name); 674 } 675 return { 676 source: source, 677 line: util.getArg(mapping, 'originalLine', null), 678 column: util.getArg(mapping, 'originalColumn', null), 679 name: name 680 }; 681 } 682 } 683 684 return { 685 source: null, 686 line: null, 687 column: null, 688 name: null 689 }; 690 }; 691 692 /** 693 * Return true if we have the source content for every source in the source 694 * map, false otherwise. 695 */ 696 BasicSourceMapConsumer.prototype.hasContentsOfAllSources = 697 function BasicSourceMapConsumer_hasContentsOfAllSources() { 698 if (!this.sourcesContent) { 699 return false; 700 } 701 return this.sourcesContent.length >= this._sources.size() && 702 !this.sourcesContent.some(function (sc) { return sc == null; }); 703 }; 704 705 /** 706 * Returns the original source content. The only argument is the url of the 707 * original source file. Returns null if no original source content is 708 * available. 709 */ 710 BasicSourceMapConsumer.prototype.sourceContentFor = 711 function SourceMapConsumer_sourceContentFor(aSource, nullOnMissing) { 712 if (!this.sourcesContent) { 713 return null; 714 } 715 716 var index = this._findSourceIndex(aSource); 717 if (index >= 0) { 718 return this.sourcesContent[index]; 719 } 720 721 var relativeSource = aSource; 722 if (this.sourceRoot != null) { 723 relativeSource = util.relative(this.sourceRoot, relativeSource); 724 } 725 726 var url; 727 if (this.sourceRoot != null 728 && (url = util.urlParse(this.sourceRoot))) { 729 // XXX: file:// URIs and absolute paths lead to unexpected behavior for 730 // many users. We can help them out when they expect file:// URIs to 731 // behave like it would if they were running a local HTTP server. See 732 // https://bugzilla.mozilla.org/show_bug.cgi?id=885597. 733 var fileUriAbsPath = relativeSource.replace(/^file:\/\//, ""); 734 if (url.scheme == "file" 735 && this._sources.has(fileUriAbsPath)) { 736 return this.sourcesContent[this._sources.indexOf(fileUriAbsPath)] 737 } 738 739 if ((!url.path || url.path == "/") 740 && this._sources.has("/" + relativeSource)) { 741 return this.sourcesContent[this._sources.indexOf("/" + relativeSource)]; 742 } 743 } 744 745 // This function is used recursively from 746 // IndexedSourceMapConsumer.prototype.sourceContentFor. In that case, we 747 // don't want to throw if we can't find the source - we just want to 748 // return null, so we provide a flag to exit gracefully. 749 if (nullOnMissing) { 750 return null; 751 } 752 else { 753 throw new Error('"' + relativeSource + '" is not in the SourceMap.'); 754 } 755 }; 756 757 /** 758 * Returns the generated line and column information for the original source, 759 * line, and column positions provided. The only argument is an object with 760 * the following properties: 761 * 762 * - source: The filename of the original source. 763 * - line: The line number in the original source. The line number 764 * is 1-based. 765 * - column: The column number in the original source. The column 766 * number is 0-based. 767 * - bias: Either 'SourceMapConsumer.GREATEST_LOWER_BOUND' or 768 * 'SourceMapConsumer.LEAST_UPPER_BOUND'. Specifies whether to return the 769 * closest element that is smaller than or greater than the one we are 770 * searching for, respectively, if the exact element cannot be found. 771 * Defaults to 'SourceMapConsumer.GREATEST_LOWER_BOUND'. 772 * 773 * and an object is returned with the following properties: 774 * 775 * - line: The line number in the generated source, or null. The 776 * line number is 1-based. 777 * - column: The column number in the generated source, or null. 778 * The column number is 0-based. 779 */ 780 BasicSourceMapConsumer.prototype.generatedPositionFor = 781 function SourceMapConsumer_generatedPositionFor(aArgs) { 782 var source = util.getArg(aArgs, 'source'); 783 source = this._findSourceIndex(source); 784 if (source < 0) { 785 return { 786 line: null, 787 column: null, 788 lastColumn: null 789 }; 790 } 791 792 var needle = { 793 source: source, 794 originalLine: util.getArg(aArgs, 'line'), 795 originalColumn: util.getArg(aArgs, 'column') 796 }; 797 798 var index = this._findMapping( 799 needle, 800 this._originalMappings, 801 "originalLine", 802 "originalColumn", 803 util.compareByOriginalPositions, 804 util.getArg(aArgs, 'bias', SourceMapConsumer.GREATEST_LOWER_BOUND) 805 ); 806 807 if (index >= 0) { 808 var mapping = this._originalMappings[index]; 809 810 if (mapping.source === needle.source) { 811 return { 812 line: util.getArg(mapping, 'generatedLine', null), 813 column: util.getArg(mapping, 'generatedColumn', null), 814 lastColumn: util.getArg(mapping, 'lastGeneratedColumn', null) 815 }; 816 } 817 } 818 819 return { 820 line: null, 821 column: null, 822 lastColumn: null 823 }; 824 }; 825 826 exports.BasicSourceMapConsumer = BasicSourceMapConsumer; 827 828 /** 829 * An IndexedSourceMapConsumer instance represents a parsed source map which 830 * we can query for information. It differs from BasicSourceMapConsumer in 831 * that it takes "indexed" source maps (i.e. ones with a "sections" field) as 832 * input. 833 * 834 * The first parameter is a raw source map (either as a JSON string, or already 835 * parsed to an object). According to the spec for indexed source maps, they 836 * have the following attributes: 837 * 838 * - version: Which version of the source map spec this map is following. 839 * - file: Optional. The generated file this source map is associated with. 840 * - sections: A list of section definitions. 841 * 842 * Each value under the "sections" field has two fields: 843 * - offset: The offset into the original specified at which this section 844 * begins to apply, defined as an object with a "line" and "column" 845 * field. 846 * - map: A source map definition. This source map could also be indexed, 847 * but doesn't have to be. 848 * 849 * Instead of the "map" field, it's also possible to have a "url" field 850 * specifying a URL to retrieve a source map from, but that's currently 851 * unsupported. 852 * 853 * Here's an example source map, taken from the source map spec[0], but 854 * modified to omit a section which uses the "url" field. 855 * 856 * { 857 * version : 3, 858 * file: "app.js", 859 * sections: [{ 860 * offset: {line:100, column:10}, 861 * map: { 862 * version : 3, 863 * file: "section.js", 864 * sources: ["foo.js", "bar.js"], 865 * names: ["src", "maps", "are", "fun"], 866 * mappings: "AAAA,E;;ABCDE;" 867 * } 868 * }], 869 * } 870 * 871 * The second parameter, if given, is a string whose value is the URL 872 * at which the source map was found. This URL is used to compute the 873 * sources array. 874 * 875 * [0]: https://docs.google.com/document/d/1U1RGAehQwRypUTovF1KRlpiOFze0b-_2gc6fAH0KY0k/edit#heading=h.535es3xeprgt 876 */ 877 function IndexedSourceMapConsumer(aSourceMap, aSourceMapURL) { 878 var sourceMap = aSourceMap; 879 if (typeof aSourceMap === 'string') { 880 sourceMap = util.parseSourceMapInput(aSourceMap); 881 } 882 883 var version = util.getArg(sourceMap, 'version'); 884 var sections = util.getArg(sourceMap, 'sections'); 885 886 if (version != this._version) { 887 throw new Error('Unsupported version: ' + version); 888 } 889 890 this._sources = new ArraySet(); 891 this._names = new ArraySet(); 892 893 var lastOffset = { 894 line: -1, 895 column: 0 896 }; 897 this._sections = sections.map(function (s) { 898 if (s.url) { 899 // The url field will require support for asynchronicity. 900 // See https://github.com/mozilla/source-map/issues/16 901 throw new Error('Support for url field in sections not implemented.'); 902 } 903 var offset = util.getArg(s, 'offset'); 904 var offsetLine = util.getArg(offset, 'line'); 905 var offsetColumn = util.getArg(offset, 'column'); 906 907 if (offsetLine < lastOffset.line || 908 (offsetLine === lastOffset.line && offsetColumn < lastOffset.column)) { 909 throw new Error('Section offsets must be ordered and non-overlapping.'); 910 } 911 lastOffset = offset; 912 913 return { 914 generatedOffset: { 915 // The offset fields are 0-based, but we use 1-based indices when 916 // encoding/decoding from VLQ. 917 generatedLine: offsetLine + 1, 918 generatedColumn: offsetColumn + 1 919 }, 920 consumer: new SourceMapConsumer(util.getArg(s, 'map'), aSourceMapURL) 921 } 922 }); 923 } 924 925 IndexedSourceMapConsumer.prototype = Object.create(SourceMapConsumer.prototype); 926 IndexedSourceMapConsumer.prototype.constructor = SourceMapConsumer; 927 928 /** 929 * The version of the source mapping spec that we are consuming. 930 */ 931 IndexedSourceMapConsumer.prototype._version = 3; 932 933 /** 934 * The list of original sources. 935 */ 936 Object.defineProperty(IndexedSourceMapConsumer.prototype, 'sources', { 937 get: function () { 938 var sources = []; 939 for (var i = 0; i < this._sections.length; i++) { 940 for (var j = 0; j < this._sections[i].consumer.sources.length; j++) { 941 sources.push(this._sections[i].consumer.sources[j]); 942 } 943 } 944 return sources; 945 } 946 }); 947 948 /** 949 * Returns the original source, line, and column information for the generated 950 * source's line and column positions provided. The only argument is an object 951 * with the following properties: 952 * 953 * - line: The line number in the generated source. The line number 954 * is 1-based. 955 * - column: The column number in the generated source. The column 956 * number is 0-based. 957 * 958 * and an object is returned with the following properties: 959 * 960 * - source: The original source file, or null. 961 * - line: The line number in the original source, or null. The 962 * line number is 1-based. 963 * - column: The column number in the original source, or null. The 964 * column number is 0-based. 965 * - name: The original identifier, or null. 966 */ 967 IndexedSourceMapConsumer.prototype.originalPositionFor = 968 function IndexedSourceMapConsumer_originalPositionFor(aArgs) { 969 var needle = { 970 generatedLine: util.getArg(aArgs, 'line'), 971 generatedColumn: util.getArg(aArgs, 'column') 972 }; 973 974 // Find the section containing the generated position we're trying to map 975 // to an original position. 976 var sectionIndex = binarySearch.search(needle, this._sections, 977 function(needle, section) { 978 var cmp = needle.generatedLine - section.generatedOffset.generatedLine; 979 if (cmp) { 980 return cmp; 981 } 982 983 return (needle.generatedColumn - 984 section.generatedOffset.generatedColumn); 985 }); 986 var section = this._sections[sectionIndex]; 987 988 if (!section) { 989 return { 990 source: null, 991 line: null, 992 column: null, 993 name: null 994 }; 995 } 996 997 return section.consumer.originalPositionFor({ 998 line: needle.generatedLine - 999 (section.generatedOffset.generatedLine - 1), 1000 column: needle.generatedColumn - 1001 (section.generatedOffset.generatedLine === needle.generatedLine 1002 ? section.generatedOffset.generatedColumn - 1 1003 : 0), 1004 bias: aArgs.bias 1005 }); 1006 }; 1007 1008 /** 1009 * Return true if we have the source content for every source in the source 1010 * map, false otherwise. 1011 */ 1012 IndexedSourceMapConsumer.prototype.hasContentsOfAllSources = 1013 function IndexedSourceMapConsumer_hasContentsOfAllSources() { 1014 return this._sections.every(function (s) { 1015 return s.consumer.hasContentsOfAllSources(); 1016 }); 1017 }; 1018 1019 /** 1020 * Returns the original source content. The only argument is the url of the 1021 * original source file. Returns null if no original source content is 1022 * available. 1023 */ 1024 IndexedSourceMapConsumer.prototype.sourceContentFor = 1025 function IndexedSourceMapConsumer_sourceContentFor(aSource, nullOnMissing) { 1026 for (var i = 0; i < this._sections.length; i++) { 1027 var section = this._sections[i]; 1028 1029 var content = section.consumer.sourceContentFor(aSource, true); 1030 if (content) { 1031 return content; 1032 } 1033 } 1034 if (nullOnMissing) { 1035 return null; 1036 } 1037 else { 1038 throw new Error('"' + aSource + '" is not in the SourceMap.'); 1039 } 1040 }; 1041 1042 /** 1043 * Returns the generated line and column information for the original source, 1044 * line, and column positions provided. The only argument is an object with 1045 * the following properties: 1046 * 1047 * - source: The filename of the original source. 1048 * - line: The line number in the original source. The line number 1049 * is 1-based. 1050 * - column: The column number in the original source. The column 1051 * number is 0-based. 1052 * 1053 * and an object is returned with the following properties: 1054 * 1055 * - line: The line number in the generated source, or null. The 1056 * line number is 1-based. 1057 * - column: The column number in the generated source, or null. 1058 * The column number is 0-based. 1059 */ 1060 IndexedSourceMapConsumer.prototype.generatedPositionFor = 1061 function IndexedSourceMapConsumer_generatedPositionFor(aArgs) { 1062 for (var i = 0; i < this._sections.length; i++) { 1063 var section = this._sections[i]; 1064 1065 // Only consider this section if the requested source is in the list of 1066 // sources of the consumer. 1067 if (section.consumer._findSourceIndex(util.getArg(aArgs, 'source')) === -1) { 1068 continue; 1069 } 1070 var generatedPosition = section.consumer.generatedPositionFor(aArgs); 1071 if (generatedPosition) { 1072 var ret = { 1073 line: generatedPosition.line + 1074 (section.generatedOffset.generatedLine - 1), 1075 column: generatedPosition.column + 1076 (section.generatedOffset.generatedLine === generatedPosition.line 1077 ? section.generatedOffset.generatedColumn - 1 1078 : 0) 1079 }; 1080 return ret; 1081 } 1082 } 1083 1084 return { 1085 line: null, 1086 column: null 1087 }; 1088 }; 1089 1090 /** 1091 * Parse the mappings in a string in to a data structure which we can easily 1092 * query (the ordered arrays in the `this.__generatedMappings` and 1093 * `this.__originalMappings` properties). 1094 */ 1095 IndexedSourceMapConsumer.prototype._parseMappings = 1096 function IndexedSourceMapConsumer_parseMappings(aStr, aSourceRoot) { 1097 this.__generatedMappings = []; 1098 this.__originalMappings = []; 1099 for (var i = 0; i < this._sections.length; i++) { 1100 var section = this._sections[i]; 1101 var sectionMappings = section.consumer._generatedMappings; 1102 for (var j = 0; j < sectionMappings.length; j++) { 1103 var mapping = sectionMappings[j]; 1104 1105 var source = section.consumer._sources.at(mapping.source); 1106 source = util.computeSourceURL(section.consumer.sourceRoot, source, this._sourceMapURL); 1107 this._sources.add(source); 1108 source = this._sources.indexOf(source); 1109 1110 var name = null; 1111 if (mapping.name) { 1112 name = section.consumer._names.at(mapping.name); 1113 this._names.add(name); 1114 name = this._names.indexOf(name); 1115 } 1116 1117 // The mappings coming from the consumer for the section have 1118 // generated positions relative to the start of the section, so we 1119 // need to offset them to be relative to the start of the concatenated 1120 // generated file. 1121 var adjustedMapping = { 1122 source: source, 1123 generatedLine: mapping.generatedLine + 1124 (section.generatedOffset.generatedLine - 1), 1125 generatedColumn: mapping.generatedColumn + 1126 (section.generatedOffset.generatedLine === mapping.generatedLine 1127 ? section.generatedOffset.generatedColumn - 1 1128 : 0), 1129 originalLine: mapping.originalLine, 1130 originalColumn: mapping.originalColumn, 1131 name: name 1132 }; 1133 1134 this.__generatedMappings.push(adjustedMapping); 1135 if (typeof adjustedMapping.originalLine === 'number') { 1136 this.__originalMappings.push(adjustedMapping); 1137 } 1138 } 1139 } 1140 1141 quickSort(this.__generatedMappings, util.compareByGeneratedPositionsDeflated); 1142 quickSort(this.__originalMappings, util.compareByOriginalPositions); 1143 }; 1144 1145 exports.IndexedSourceMapConsumer = IndexedSourceMapConsumer;