├── bower.json └── url.js /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "URL", 3 | "version": "1.0.0", 4 | "main": "./url.js" 5 | } 6 | -------------------------------------------------------------------------------- /url.js: -------------------------------------------------------------------------------- 1 | /* Any copyright is dedicated to the Public Domain. 2 | * http://creativecommons.org/publicdomain/zero/1.0/ */ 3 | 4 | (function(scope) { 5 | 'use strict'; 6 | 7 | // feature detect for URL constructor 8 | var hasWorkingUrl = false; 9 | if (!scope.forceJURL) { 10 | try { 11 | var u = new URL('b', 'http://a'); 12 | u.pathname = 'c%20d'; 13 | hasWorkingUrl = u.href === 'http://a/c%20d'; 14 | } catch(e) {} 15 | } 16 | 17 | if (hasWorkingUrl) 18 | return; 19 | 20 | var relative = Object.create(null); 21 | relative['ftp'] = 21; 22 | relative['file'] = 0; 23 | relative['gopher'] = 70; 24 | relative['http'] = 80; 25 | relative['https'] = 443; 26 | relative['ws'] = 80; 27 | relative['wss'] = 443; 28 | 29 | var relativePathDotMapping = Object.create(null); 30 | relativePathDotMapping['%2e'] = '.'; 31 | relativePathDotMapping['.%2e'] = '..'; 32 | relativePathDotMapping['%2e.'] = '..'; 33 | relativePathDotMapping['%2e%2e'] = '..'; 34 | 35 | function isRelativeScheme(scheme) { 36 | return relative[scheme] !== undefined; 37 | } 38 | 39 | function invalid() { 40 | clear.call(this); 41 | this._isInvalid = true; 42 | } 43 | 44 | function IDNAToASCII(h) { 45 | if ('' == h) { 46 | invalid.call(this) 47 | } 48 | // XXX 49 | return h.toLowerCase() 50 | } 51 | 52 | function percentEscape(c) { 53 | var unicode = c.charCodeAt(0); 54 | if (unicode > 0x20 && 55 | unicode < 0x7F && 56 | // " # < > ? ` 57 | [0x22, 0x23, 0x3C, 0x3E, 0x3F, 0x60].indexOf(unicode) == -1 58 | ) { 59 | return c; 60 | } 61 | return encodeURIComponent(c); 62 | } 63 | 64 | function percentEscapeQuery(c) { 65 | // XXX This actually needs to encode c using encoding and then 66 | // convert the bytes one-by-one. 67 | 68 | var unicode = c.charCodeAt(0); 69 | if (unicode > 0x20 && 70 | unicode < 0x7F && 71 | // " # < > ` (do not escape '?') 72 | [0x22, 0x23, 0x3C, 0x3E, 0x60].indexOf(unicode) == -1 73 | ) { 74 | return c; 75 | } 76 | return encodeURIComponent(c); 77 | } 78 | 79 | var EOF = undefined, 80 | ALPHA = /[a-zA-Z]/, 81 | ALPHANUMERIC = /[a-zA-Z0-9\+\-\.]/; 82 | 83 | function parse(input, stateOverride, base) { 84 | function err(message) { 85 | errors.push(message) 86 | } 87 | 88 | var state = stateOverride || 'scheme start', 89 | cursor = 0, 90 | buffer = '', 91 | seenAt = false, 92 | seenBracket = false, 93 | errors = []; 94 | 95 | loop: while ((input[cursor - 1] != EOF || cursor == 0) && !this._isInvalid) { 96 | var c = input[cursor]; 97 | switch (state) { 98 | case 'scheme start': 99 | if (c && ALPHA.test(c)) { 100 | buffer += c.toLowerCase(); // ASCII-safe 101 | state = 'scheme'; 102 | } else if (!stateOverride) { 103 | buffer = ''; 104 | state = 'no scheme'; 105 | continue; 106 | } else { 107 | err('Invalid scheme.'); 108 | break loop; 109 | } 110 | break; 111 | 112 | case 'scheme': 113 | if (c && ALPHANUMERIC.test(c)) { 114 | buffer += c.toLowerCase(); // ASCII-safe 115 | } else if (':' == c) { 116 | this._scheme = buffer; 117 | buffer = ''; 118 | if (stateOverride) { 119 | break loop; 120 | } 121 | if (isRelativeScheme(this._scheme)) { 122 | this._isRelative = true; 123 | } 124 | if ('file' == this._scheme) { 125 | state = 'relative'; 126 | } else if (this._isRelative && base && base._scheme == this._scheme) { 127 | state = 'relative or authority'; 128 | } else if (this._isRelative) { 129 | state = 'authority first slash'; 130 | } else { 131 | state = 'scheme data'; 132 | } 133 | } else if (!stateOverride) { 134 | buffer = ''; 135 | cursor = 0; 136 | state = 'no scheme'; 137 | continue; 138 | } else if (EOF == c) { 139 | break loop; 140 | } else { 141 | err('Code point not allowed in scheme: ' + c) 142 | break loop; 143 | } 144 | break; 145 | 146 | case 'scheme data': 147 | if ('?' == c) { 148 | query = '?'; 149 | state = 'query'; 150 | } else if ('#' == c) { 151 | this._fragment = '#'; 152 | state = 'fragment'; 153 | } else { 154 | // XXX error handling 155 | if (EOF != c && '\t' != c && '\n' != c && '\r' != c) { 156 | this._schemeData += percentEscape(c); 157 | } 158 | } 159 | break; 160 | 161 | case 'no scheme': 162 | if (!base || !(isRelativeScheme(base._scheme))) { 163 | err('Missing scheme.'); 164 | invalid.call(this); 165 | } else { 166 | state = 'relative'; 167 | continue; 168 | } 169 | break; 170 | 171 | case 'relative or authority': 172 | if ('/' == c && '/' == input[cursor+1]) { 173 | state = 'authority ignore slashes'; 174 | } else { 175 | err('Expected /, got: ' + c); 176 | state = 'relative'; 177 | continue 178 | } 179 | break; 180 | 181 | case 'relative': 182 | this._isRelative = true; 183 | if ('file' != this._scheme) 184 | this._scheme = base._scheme; 185 | if (EOF == c) { 186 | this._host = base._host; 187 | this._port = base._port; 188 | this._path = base._path.slice(); 189 | this._query = base._query; 190 | break loop; 191 | } else if ('/' == c || '\\' == c) { 192 | if ('\\' == c) 193 | err('\\ is an invalid code point.'); 194 | state = 'relative slash'; 195 | } else if ('?' == c) { 196 | this._host = base._host; 197 | this._port = base._port; 198 | this._path = base._path.slice(); 199 | this._query = '?'; 200 | state = 'query'; 201 | } else if ('#' == c) { 202 | this._host = base._host; 203 | this._port = base._port; 204 | this._path = base._path.slice(); 205 | this._query = base._query; 206 | this._fragment = '#'; 207 | state = 'fragment'; 208 | } else { 209 | var nextC = input[cursor+1] 210 | var nextNextC = input[cursor+2] 211 | if ( 212 | 'file' != this._scheme || !ALPHA.test(c) || 213 | (nextC != ':' && nextC != '|') || 214 | (EOF != nextNextC && '/' != nextNextC && '\\' != nextNextC && '?' != nextNextC && '#' != nextNextC)) { 215 | this._host = base._host; 216 | this._port = base._port; 217 | this._path = base._path.slice(); 218 | this._path.pop(); 219 | } 220 | state = 'relative path'; 221 | continue; 222 | } 223 | break; 224 | 225 | case 'relative slash': 226 | if ('/' == c || '\\' == c) { 227 | if ('\\' == c) { 228 | err('\\ is an invalid code point.'); 229 | } 230 | if ('file' == this._scheme) { 231 | state = 'file host'; 232 | } else { 233 | state = 'authority ignore slashes'; 234 | } 235 | } else { 236 | if ('file' != this._scheme) { 237 | this._host = base._host; 238 | this._port = base._port; 239 | } 240 | state = 'relative path'; 241 | continue; 242 | } 243 | break; 244 | 245 | case 'authority first slash': 246 | if ('/' == c) { 247 | state = 'authority second slash'; 248 | } else { 249 | err("Expected '/', got: " + c); 250 | state = 'authority ignore slashes'; 251 | continue; 252 | } 253 | break; 254 | 255 | case 'authority second slash': 256 | state = 'authority ignore slashes'; 257 | if ('/' != c) { 258 | err("Expected '/', got: " + c); 259 | continue; 260 | } 261 | break; 262 | 263 | case 'authority ignore slashes': 264 | if ('/' != c && '\\' != c) { 265 | state = 'authority'; 266 | continue; 267 | } else { 268 | err('Expected authority, got: ' + c); 269 | } 270 | break; 271 | 272 | case 'authority': 273 | if ('@' == c) { 274 | if (seenAt) { 275 | err('@ already seen.'); 276 | buffer += '%40'; 277 | } 278 | seenAt = true; 279 | for (var i = 0; i < buffer.length; i++) { 280 | var cp = buffer[i]; 281 | if ('\t' == cp || '\n' == cp || '\r' == cp) { 282 | err('Invalid whitespace in authority.'); 283 | continue; 284 | } 285 | // XXX check URL code points 286 | if (':' == cp && null === this._password) { 287 | this._password = ''; 288 | continue; 289 | } 290 | var tempC = percentEscape(cp); 291 | (null !== this._password) ? this._password += tempC : this._username += tempC; 292 | } 293 | buffer = ''; 294 | } else if (EOF == c || '/' == c || '\\' == c || '?' == c || '#' == c) { 295 | cursor -= buffer.length; 296 | buffer = ''; 297 | state = 'host'; 298 | continue; 299 | } else { 300 | buffer += c; 301 | } 302 | break; 303 | 304 | case 'file host': 305 | if (EOF == c || '/' == c || '\\' == c || '?' == c || '#' == c) { 306 | if (buffer.length == 2 && ALPHA.test(buffer[0]) && (buffer[1] == ':' || buffer[1] == '|')) { 307 | state = 'relative path'; 308 | } else if (buffer.length == 0) { 309 | state = 'relative path start'; 310 | } else { 311 | this._host = IDNAToASCII.call(this, buffer); 312 | buffer = ''; 313 | state = 'relative path start'; 314 | } 315 | continue; 316 | } else if ('\t' == c || '\n' == c || '\r' == c) { 317 | err('Invalid whitespace in file host.'); 318 | } else { 319 | buffer += c; 320 | } 321 | break; 322 | 323 | case 'host': 324 | case 'hostname': 325 | if (':' == c && !seenBracket) { 326 | // XXX host parsing 327 | this._host = IDNAToASCII.call(this, buffer); 328 | buffer = ''; 329 | state = 'port'; 330 | if ('hostname' == stateOverride) { 331 | break loop; 332 | } 333 | } else if (EOF == c || '/' == c || '\\' == c || '?' == c || '#' == c) { 334 | this._host = IDNAToASCII.call(this, buffer); 335 | buffer = ''; 336 | state = 'relative path start'; 337 | if (stateOverride) { 338 | break loop; 339 | } 340 | continue; 341 | } else if ('\t' != c && '\n' != c && '\r' != c) { 342 | if ('[' == c) { 343 | seenBracket = true; 344 | } else if (']' == c) { 345 | seenBracket = false; 346 | } 347 | buffer += c; 348 | } else { 349 | err('Invalid code point in host/hostname: ' + c); 350 | } 351 | break; 352 | 353 | case 'port': 354 | if (/[0-9]/.test(c)) { 355 | buffer += c; 356 | } else if (EOF == c || '/' == c || '\\' == c || '?' == c || '#' == c || stateOverride) { 357 | if ('' != buffer) { 358 | var temp = parseInt(buffer, 10); 359 | if (temp != relative[this._scheme]) { 360 | this._port = temp + ''; 361 | } 362 | buffer = ''; 363 | } 364 | if (stateOverride) { 365 | break loop; 366 | } 367 | state = 'relative path start'; 368 | continue; 369 | } else if ('\t' == c || '\n' == c || '\r' == c) { 370 | err('Invalid code point in port: ' + c); 371 | } else { 372 | invalid.call(this); 373 | } 374 | break; 375 | 376 | case 'relative path start': 377 | if ('\\' == c) 378 | err("'\\' not allowed in path."); 379 | state = 'relative path'; 380 | if ('/' != c && '\\' != c) { 381 | continue; 382 | } 383 | break; 384 | 385 | case 'relative path': 386 | if (EOF == c || '/' == c || '\\' == c || (!stateOverride && ('?' == c || '#' == c))) { 387 | if ('\\' == c) { 388 | err('\\ not allowed in relative path.'); 389 | } 390 | var tmp; 391 | if (tmp = relativePathDotMapping[buffer.toLowerCase()]) { 392 | buffer = tmp; 393 | } 394 | if ('..' == buffer) { 395 | this._path.pop(); 396 | if ('/' != c && '\\' != c) { 397 | this._path.push(''); 398 | } 399 | } else if ('.' == buffer && '/' != c && '\\' != c) { 400 | this._path.push(''); 401 | } else if ('.' != buffer) { 402 | if ('file' == this._scheme && this._path.length == 0 && buffer.length == 2 && ALPHA.test(buffer[0]) && buffer[1] == '|') { 403 | buffer = buffer[0] + ':'; 404 | } 405 | this._path.push(buffer); 406 | } 407 | buffer = ''; 408 | if ('?' == c) { 409 | this._query = '?'; 410 | state = 'query'; 411 | } else if ('#' == c) { 412 | this._fragment = '#'; 413 | state = 'fragment'; 414 | } 415 | } else if ('\t' != c && '\n' != c && '\r' != c) { 416 | buffer += percentEscape(c); 417 | } 418 | break; 419 | 420 | case 'query': 421 | if (!stateOverride && '#' == c) { 422 | this._fragment = '#'; 423 | state = 'fragment'; 424 | } else if (EOF != c && '\t' != c && '\n' != c && '\r' != c) { 425 | this._query += percentEscapeQuery(c); 426 | } 427 | break; 428 | 429 | case 'fragment': 430 | if (EOF != c && '\t' != c && '\n' != c && '\r' != c) { 431 | this._fragment += c; 432 | } 433 | break; 434 | } 435 | 436 | cursor++; 437 | } 438 | } 439 | 440 | function clear() { 441 | this._scheme = ''; 442 | this._schemeData = ''; 443 | this._username = ''; 444 | this._password = null; 445 | this._host = ''; 446 | this._port = ''; 447 | this._path = []; 448 | this._query = ''; 449 | this._fragment = ''; 450 | this._isInvalid = false; 451 | this._isRelative = false; 452 | } 453 | 454 | // Does not process domain names or IP addresses. 455 | // Does not handle encoding for the query parameter. 456 | function jURL(url, base /* , encoding */) { 457 | if (base !== undefined && !(base instanceof jURL)) 458 | base = new jURL(String(base)); 459 | 460 | this._url = url; 461 | clear.call(this); 462 | 463 | var input = url.replace(/^[ \t\r\n\f]+|[ \t\r\n\f]+$/g, ''); 464 | // encoding = encoding || 'utf-8' 465 | 466 | parse.call(this, input, null, base); 467 | } 468 | 469 | jURL.prototype = { 470 | get href() { 471 | if (this._isInvalid) 472 | return this._url; 473 | 474 | var authority = ''; 475 | if ('' != this._username || null != this._password) { 476 | authority = this._username + 477 | (null != this._password ? ':' + this._password : '') + '@'; 478 | } 479 | 480 | return this.protocol + 481 | (this._isRelative ? '//' + authority + this.host : '') + 482 | this.pathname + this._query + this._fragment; 483 | }, 484 | set href(href) { 485 | clear.call(this); 486 | parse.call(this, href); 487 | }, 488 | 489 | get protocol() { 490 | return this._scheme + ':'; 491 | }, 492 | set protocol(protocol) { 493 | if (this._isInvalid) 494 | return; 495 | parse.call(this, protocol + ':', 'scheme start'); 496 | }, 497 | 498 | get host() { 499 | return this._isInvalid ? '' : this._port ? 500 | this._host + ':' + this._port : this._host; 501 | }, 502 | set host(host) { 503 | if (this._isInvalid || !this._isRelative) 504 | return; 505 | parse.call(this, host, 'host'); 506 | }, 507 | 508 | get hostname() { 509 | return this._host; 510 | }, 511 | set hostname(hostname) { 512 | if (this._isInvalid || !this._isRelative) 513 | return; 514 | parse.call(this, hostname, 'hostname'); 515 | }, 516 | 517 | get port() { 518 | return this._port; 519 | }, 520 | set port(port) { 521 | if (this._isInvalid || !this._isRelative) 522 | return; 523 | parse.call(this, port, 'port'); 524 | }, 525 | 526 | get pathname() { 527 | return this._isInvalid ? '' : this._isRelative ? 528 | '/' + this._path.join('/') : this._schemeData; 529 | }, 530 | set pathname(pathname) { 531 | if (this._isInvalid || !this._isRelative) 532 | return; 533 | this._path = []; 534 | parse.call(this, pathname, 'relative path start'); 535 | }, 536 | 537 | get search() { 538 | return this._isInvalid || !this._query || '?' == this._query ? 539 | '' : this._query; 540 | }, 541 | set search(search) { 542 | if (this._isInvalid || !this._isRelative) 543 | return; 544 | this._query = '?'; 545 | if ('?' == search[0]) 546 | search = search.slice(1); 547 | parse.call(this, search, 'query'); 548 | }, 549 | 550 | get hash() { 551 | return this._isInvalid || !this._fragment || '#' == this._fragment ? 552 | '' : this._fragment; 553 | }, 554 | set hash(hash) { 555 | if (this._isInvalid) 556 | return; 557 | this._fragment = '#'; 558 | if ('#' == hash[0]) 559 | hash = hash.slice(1); 560 | parse.call(this, hash, 'fragment'); 561 | }, 562 | 563 | get origin() { 564 | var host; 565 | if (this._isInvalid || !this._scheme) { 566 | return ''; 567 | } 568 | // javascript: Gecko returns String(""), WebKit/Blink String("null") 569 | // Gecko throws error for "data://" 570 | // data: Gecko returns "", Blink returns "data://", WebKit returns "null" 571 | // Gecko returns String("") for file: mailto: 572 | // WebKit/Blink returns String("SCHEME://") for file: mailto: 573 | switch (this._scheme) { 574 | case 'data': 575 | case 'file': 576 | case 'javascript': 577 | case 'mailto': 578 | return 'null'; 579 | } 580 | host = this.host; 581 | if (!host) { 582 | return ''; 583 | } 584 | return this._scheme + '://' + host; 585 | } 586 | }; 587 | 588 | // Copy over the static methods 589 | var OriginalURL = scope.URL; 590 | if (OriginalURL) { 591 | jURL.createObjectURL = function(blob) { 592 | // IE extension allows a second optional options argument. 593 | // http://msdn.microsoft.com/en-us/library/ie/hh772302(v=vs.85).aspx 594 | return OriginalURL.createObjectURL.apply(OriginalURL, arguments); 595 | }; 596 | jURL.revokeObjectURL = function(url) { 597 | OriginalURL.revokeObjectURL(url); 598 | }; 599 | } 600 | 601 | scope.URL = jURL; 602 | 603 | })(this); 604 | --------------------------------------------------------------------------------