UNPKG

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