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;