util.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  /**
  9   * This is a helper function for getting values from parameter/options
 10   * objects.
 11   *
 12   * @param args The object we are extracting values from
 13   * @param name The name of the property we are getting.
 14   * @param defaultValue An optional value to return if the property is missing
 15   * from the object. If this is not specified and the property is missing, an
 16   * error will be thrown.
 17   */
 18  function getArg(aArgs, aName, aDefaultValue) {
 19    if (aName in aArgs) {
 20      return aArgs[aName];
 21    } else if (arguments.length === 3) {
 22      return aDefaultValue;
 23    } else {
 24      throw new Error('"' + aName + '" is a required argument.');
 25    }
 26  }
 27  exports.getArg = getArg;
 28  
 29  var urlRegexp = /^(?:([\w+\-.]+):)?\/\/(?:(\w+:\w+)@)?([\w.-]*)(?::(\d+))?(.*)$/;
 30  var dataUrlRegexp = /^data:.+\,.+$/;
 31  
 32  function urlParse(aUrl) {
 33    var match = aUrl.match(urlRegexp);
 34    if (!match) {
 35      return null;
 36    }
 37    return {
 38      scheme: match[1],
 39      auth: match[2],
 40      host: match[3],
 41      port: match[4],
 42      path: match[5]
 43    };
 44  }
 45  exports.urlParse = urlParse;
 46  
 47  function urlGenerate(aParsedUrl) {
 48    var url = '';
 49    if (aParsedUrl.scheme) {
 50      url += aParsedUrl.scheme + ':';
 51    }
 52    url += '//';
 53    if (aParsedUrl.auth) {
 54      url += aParsedUrl.auth + '@';
 55    }
 56    if (aParsedUrl.host) {
 57      url += aParsedUrl.host;
 58    }
 59    if (aParsedUrl.port) {
 60      url += ":" + aParsedUrl.port
 61    }
 62    if (aParsedUrl.path) {
 63      url += aParsedUrl.path;
 64    }
 65    return url;
 66  }
 67  exports.urlGenerate = urlGenerate;
 68  
 69  /**
 70   * Normalizes a path, or the path portion of a URL:
 71   *
 72   * - Replaces consecutive slashes with one slash.
 73   * - Removes unnecessary '.' parts.
 74   * - Removes unnecessary '<dir>/..' parts.
 75   *
 76   * Based on code in the Node.js 'path' core module.
 77   *
 78   * @param aPath The path or url to normalize.
 79   */
 80  function normalize(aPath) {
 81    var path = aPath;
 82    var url = urlParse(aPath);
 83    if (url) {
 84      if (!url.path) {
 85        return aPath;
 86      }
 87      path = url.path;
 88    }
 89    var isAbsolute = exports.isAbsolute(path);
 90  
 91    var parts = path.split(/\/+/);
 92    for (var part, up = 0, i = parts.length - 1; i >= 0; i--) {
 93      part = parts[i];
 94      if (part === '.') {
 95        parts.splice(i, 1);
 96      } else if (part === '..') {
 97        up++;
 98      } else if (up > 0) {
 99        if (part === '') {
100          // The first part is blank if the path is absolute. Trying to go
101          // above the root is a no-op. Therefore we can remove all '..' parts
102          // directly after the root.
103          parts.splice(i + 1, up);
104          up = 0;
105        } else {
106          parts.splice(i, 2);
107          up--;
108        }
109      }
110    }
111    path = parts.join('/');
112  
113    if (path === '') {
114      path = isAbsolute ? '/' : '.';
115    }
116  
117    if (url) {
118      url.path = path;
119      return urlGenerate(url);
120    }
121    return path;
122  }
123  exports.normalize = normalize;
124  
125  /**
126   * Joins two paths/URLs.
127   *
128   * @param aRoot The root path or URL.
129   * @param aPath The path or URL to be joined with the root.
130   *
131   * - If aPath is a URL or a data URI, aPath is returned, unless aPath is a
132   *   scheme-relative URL: Then the scheme of aRoot, if any, is prepended
133   *   first.
134   * - Otherwise aPath is a path. If aRoot is a URL, then its path portion
135   *   is updated with the result and aRoot is returned. Otherwise the result
136   *   is returned.
137   *   - If aPath is absolute, the result is aPath.
138   *   - Otherwise the two paths are joined with a slash.
139   * - Joining for example 'http://' and 'www.example.com' is also supported.
140   */
141  function join(aRoot, aPath) {
142    if (aRoot === "") {
143      aRoot = ".";
144    }
145    if (aPath === "") {
146      aPath = ".";
147    }
148    var aPathUrl = urlParse(aPath);
149    var aRootUrl = urlParse(aRoot);
150    if (aRootUrl) {
151      aRoot = aRootUrl.path || '/';
152    }
153  
154    // `join(foo, '//www.example.org')`
155    if (aPathUrl && !aPathUrl.scheme) {
156      if (aRootUrl) {
157        aPathUrl.scheme = aRootUrl.scheme;
158      }
159      return urlGenerate(aPathUrl);
160    }
161  
162    if (aPathUrl || aPath.match(dataUrlRegexp)) {
163      return aPath;
164    }
165  
166    // `join('http://', 'www.example.com')`
167    if (aRootUrl && !aRootUrl.host && !aRootUrl.path) {
168      aRootUrl.host = aPath;
169      return urlGenerate(aRootUrl);
170    }
171  
172    var joined = aPath.charAt(0) === '/'
173      ? aPath
174      : normalize(aRoot.replace(/\/+$/, '') + '/' + aPath);
175  
176    if (aRootUrl) {
177      aRootUrl.path = joined;
178      return urlGenerate(aRootUrl);
179    }
180    return joined;
181  }
182  exports.join = join;
183  
184  exports.isAbsolute = function (aPath) {
185    return aPath.charAt(0) === '/' || urlRegexp.test(aPath);
186  };
187  
188  /**
189   * Make a path relative to a URL or another path.
190   *
191   * @param aRoot The root path or URL.
192   * @param aPath The path or URL to be made relative to aRoot.
193   */
194  function relative(aRoot, aPath) {
195    if (aRoot === "") {
196      aRoot = ".";
197    }
198  
199    aRoot = aRoot.replace(/\/$/, '');
200  
201    // It is possible for the path to be above the root. In this case, simply
202    // checking whether the root is a prefix of the path won't work. Instead, we
203    // need to remove components from the root one by one, until either we find
204    // a prefix that fits, or we run out of components to remove.
205    var level = 0;
206    while (aPath.indexOf(aRoot + '/') !== 0) {
207      var index = aRoot.lastIndexOf("/");
208      if (index < 0) {
209        return aPath;
210      }
211  
212      // If the only part of the root that is left is the scheme (i.e. http://,
213      // file:///, etc.), one or more slashes (/), or simply nothing at all, we
214      // have exhausted all components, so the path is not relative to the root.
215      aRoot = aRoot.slice(0, index);
216      if (aRoot.match(/^([^\/]+:\/)?\/*$/)) {
217        return aPath;
218      }
219  
220      ++level;
221    }
222  
223    // Make sure we add a "../" for each component we removed from the root.
224    return Array(level + 1).join("../") + aPath.substr(aRoot.length + 1);
225  }
226  exports.relative = relative;
227  
228  var supportsNullProto = (function () {
229    var obj = Object.create(null);
230    return !('__proto__' in obj);
231  }());
232  
233  function identity (s) {
234    return s;
235  }
236  
237  /**
238   * Because behavior goes wacky when you set `__proto__` on objects, we
239   * have to prefix all the strings in our set with an arbitrary character.
240   *
241   * See https://github.com/mozilla/source-map/pull/31 and
242   * https://github.com/mozilla/source-map/issues/30
243   *
244   * @param String aStr
245   */
246  function toSetString(aStr) {
247    if (isProtoString(aStr)) {
248      return '$' + aStr;
249    }
250  
251    return aStr;
252  }
253  exports.toSetString = supportsNullProto ? identity : toSetString;
254  
255  function fromSetString(aStr) {
256    if (isProtoString(aStr)) {
257      return aStr.slice(1);
258    }
259  
260    return aStr;
261  }
262  exports.fromSetString = supportsNullProto ? identity : fromSetString;
263  
264  function isProtoString(s) {
265    if (!s) {
266      return false;
267    }
268  
269    var length = s.length;
270  
271    if (length < 9 /* "__proto__".length */) {
272      return false;
273    }
274  
275    if (s.charCodeAt(length - 1) !== 95  /* '_' */ ||
276        s.charCodeAt(length - 2) !== 95  /* '_' */ ||
277        s.charCodeAt(length - 3) !== 111 /* 'o' */ ||
278        s.charCodeAt(length - 4) !== 116 /* 't' */ ||
279        s.charCodeAt(length - 5) !== 111 /* 'o' */ ||
280        s.charCodeAt(length - 6) !== 114 /* 'r' */ ||
281        s.charCodeAt(length - 7) !== 112 /* 'p' */ ||
282        s.charCodeAt(length - 8) !== 95  /* '_' */ ||
283        s.charCodeAt(length - 9) !== 95  /* '_' */) {
284      return false;
285    }
286  
287    for (var i = length - 10; i >= 0; i--) {
288      if (s.charCodeAt(i) !== 36 /* '$' */) {
289        return false;
290      }
291    }
292  
293    return true;
294  }
295  
296  /**
297   * Comparator between two mappings where the original positions are compared.
298   *
299   * Optionally pass in `true` as `onlyCompareGenerated` to consider two
300   * mappings with the same original source/line/column, but different generated
301   * line and column the same. Useful when searching for a mapping with a
302   * stubbed out mapping.
303   */
304  function compareByOriginalPositions(mappingA, mappingB, onlyCompareOriginal) {
305    var cmp = strcmp(mappingA.source, mappingB.source);
306    if (cmp !== 0) {
307      return cmp;
308    }
309  
310    cmp = mappingA.originalLine - mappingB.originalLine;
311    if (cmp !== 0) {
312      return cmp;
313    }
314  
315    cmp = mappingA.originalColumn - mappingB.originalColumn;
316    if (cmp !== 0 || onlyCompareOriginal) {
317      return cmp;
318    }
319  
320    cmp = mappingA.generatedColumn - mappingB.generatedColumn;
321    if (cmp !== 0) {
322      return cmp;
323    }
324  
325    cmp = mappingA.generatedLine - mappingB.generatedLine;
326    if (cmp !== 0) {
327      return cmp;
328    }
329  
330    return strcmp(mappingA.name, mappingB.name);
331  }
332  exports.compareByOriginalPositions = compareByOriginalPositions;
333  
334  /**
335   * Comparator between two mappings with deflated source and name indices where
336   * the generated positions are compared.
337   *
338   * Optionally pass in `true` as `onlyCompareGenerated` to consider two
339   * mappings with the same generated line and column, but different
340   * source/name/original line and column the same. Useful when searching for a
341   * mapping with a stubbed out mapping.
342   */
343  function compareByGeneratedPositionsDeflated(mappingA, mappingB, onlyCompareGenerated) {
344    var cmp = mappingA.generatedLine - mappingB.generatedLine;
345    if (cmp !== 0) {
346      return cmp;
347    }
348  
349    cmp = mappingA.generatedColumn - mappingB.generatedColumn;
350    if (cmp !== 0 || onlyCompareGenerated) {
351      return cmp;
352    }
353  
354    cmp = strcmp(mappingA.source, mappingB.source);
355    if (cmp !== 0) {
356      return cmp;
357    }
358  
359    cmp = mappingA.originalLine - mappingB.originalLine;
360    if (cmp !== 0) {
361      return cmp;
362    }
363  
364    cmp = mappingA.originalColumn - mappingB.originalColumn;
365    if (cmp !== 0) {
366      return cmp;
367    }
368  
369    return strcmp(mappingA.name, mappingB.name);
370  }
371  exports.compareByGeneratedPositionsDeflated = compareByGeneratedPositionsDeflated;
372  
373  function strcmp(aStr1, aStr2) {
374    if (aStr1 === aStr2) {
375      return 0;
376    }
377  
378    if (aStr1 === null) {
379      return 1; // aStr2 !== null
380    }
381  
382    if (aStr2 === null) {
383      return -1; // aStr1 !== null
384    }
385  
386    if (aStr1 > aStr2) {
387      return 1;
388    }
389  
390    return -1;
391  }
392  
393  /**
394   * Comparator between two mappings with inflated source and name strings where
395   * the generated positions are compared.
396   */
397  function compareByGeneratedPositionsInflated(mappingA, mappingB) {
398    var cmp = mappingA.generatedLine - mappingB.generatedLine;
399    if (cmp !== 0) {
400      return cmp;
401    }
402  
403    cmp = mappingA.generatedColumn - mappingB.generatedColumn;
404    if (cmp !== 0) {
405      return cmp;
406    }
407  
408    cmp = strcmp(mappingA.source, mappingB.source);
409    if (cmp !== 0) {
410      return cmp;
411    }
412  
413    cmp = mappingA.originalLine - mappingB.originalLine;
414    if (cmp !== 0) {
415      return cmp;
416    }
417  
418    cmp = mappingA.originalColumn - mappingB.originalColumn;
419    if (cmp !== 0) {
420      return cmp;
421    }
422  
423    return strcmp(mappingA.name, mappingB.name);
424  }
425  exports.compareByGeneratedPositionsInflated = compareByGeneratedPositionsInflated;
426  
427  /**
428   * Strip any JSON XSSI avoidance prefix from the string (as documented
429   * in the source maps specification), and then parse the string as
430   * JSON.
431   */
432  function parseSourceMapInput(str) {
433    return JSON.parse(str.replace(/^\)]}'[^\n]*\n/, ''));
434  }
435  exports.parseSourceMapInput = parseSourceMapInput;
436  
437  /**
438   * Compute the URL of a source given the the source root, the source's
439   * URL, and the source map's URL.
440   */
441  function computeSourceURL(sourceRoot, sourceURL, sourceMapURL) {
442    sourceURL = sourceURL || '';
443  
444    if (sourceRoot) {
445      // This follows what Chrome does.
446      if (sourceRoot[sourceRoot.length - 1] !== '/' && sourceURL[0] !== '/') {
447        sourceRoot += '/';
448      }
449      // The spec says:
450      //   Line 4: An optional source root, useful for relocating source
451      //   files on a server or removing repeated values in the
452      //   “sources” entry.  This value is prepended to the individual
453      //   entries in the “source” field.
454      sourceURL = sourceRoot + sourceURL;
455    }
456  
457    // Historically, SourceMapConsumer did not take the sourceMapURL as
458    // a parameter.  This mode is still somewhat supported, which is why
459    // this code block is conditional.  However, it's preferable to pass
460    // the source map URL to SourceMapConsumer, so that this function
461    // can implement the source URL resolution algorithm as outlined in
462    // the spec.  This block is basically the equivalent of:
463    //    new URL(sourceURL, sourceMapURL).toString()
464    // ... except it avoids using URL, which wasn't available in the
465    // older releases of node still supported by this library.
466    //
467    // The spec says:
468    //   If the sources are not absolute URLs after prepending of the
469    //   “sourceRoot”, the sources are resolved relative to the
470    //   SourceMap (like resolving script src in a html document).
471    if (sourceMapURL) {
472      var parsed = urlParse(sourceMapURL);
473      if (!parsed) {
474        throw new Error("sourceMapURL could not be parsed");
475      }
476      if (parsed.path) {
477        // Strip the last path component, but keep the "/".
478        var index = parsed.path.lastIndexOf('/');
479        if (index >= 0) {
480          parsed.path = parsed.path.substring(0, index + 1);
481        }
482      }
483      sourceURL = join(urlGenerate(parsed), sourceURL);
484    }
485  
486    return normalize(sourceURL);
487  }
488  exports.computeSourceURL = computeSourceURL;