simple-squiggle

A restricted subset of Squiggle
Log | Files | Refs | README

util.js (12950B)


      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;