Projekt

Obecné

Profil

Stáhnout (12.4 KB) Statistiky
| Větev: | Revize:
1
'use strict';
2

    
3
var required = require('requires-port')
4
  , qs = require('querystringify')
5
  , slashes = /^[A-Za-z][A-Za-z0-9+-.]*:\/\//
6
  , protocolre = /^([a-z][a-z0-9.+-]*:)?(\/\/)?([\S\s]*)/i
7
  , whitespace = '[\\x09\\x0A\\x0B\\x0C\\x0D\\x20\\xA0\\u1680\\u180E\\u2000\\u2001\\u2002\\u2003\\u2004\\u2005\\u2006\\u2007\\u2008\\u2009\\u200A\\u202F\\u205F\\u3000\\u2028\\u2029\\uFEFF]'
8
  , left = new RegExp('^'+ whitespace +'+');
9

    
10
/**
11
 * Trim a given string.
12
 *
13
 * @param {String} str String to trim.
14
 * @public
15
 */
16
function trimLeft(str) {
17
  return (str ? str : '').toString().replace(left, '');
18
}
19

    
20
/**
21
 * These are the parse rules for the URL parser, it informs the parser
22
 * about:
23
 *
24
 * 0. The char it Needs to parse, if it's a string it should be done using
25
 *    indexOf, RegExp using exec and NaN means set as current value.
26
 * 1. The property we should set when parsing this value.
27
 * 2. Indication if it's backwards or forward parsing, when set as number it's
28
 *    the value of extra chars that should be split off.
29
 * 3. Inherit from location if non existing in the parser.
30
 * 4. `toLowerCase` the resulting value.
31
 */
32
var rules = [
33
  ['#', 'hash'],                        // Extract from the back.
34
  ['?', 'query'],                       // Extract from the back.
35
  function sanitize(address) {          // Sanitize what is left of the address
36
    return address.replace('\\', '/');
37
  },
38
  ['/', 'pathname'],                    // Extract from the back.
39
  ['@', 'auth', 1],                     // Extract from the front.
40
  [NaN, 'host', undefined, 1, 1],       // Set left over value.
41
  [/:(\d+)$/, 'port', undefined, 1],    // RegExp the back.
42
  [NaN, 'hostname', undefined, 1, 1]    // Set left over.
43
];
44

    
45
/**
46
 * These properties should not be copied or inherited from. This is only needed
47
 * for all non blob URL's as a blob URL does not include a hash, only the
48
 * origin.
49
 *
50
 * @type {Object}
51
 * @private
52
 */
53
var ignore = { hash: 1, query: 1 };
54

    
55
/**
56
 * The location object differs when your code is loaded through a normal page,
57
 * Worker or through a worker using a blob. And with the blobble begins the
58
 * trouble as the location object will contain the URL of the blob, not the
59
 * location of the page where our code is loaded in. The actual origin is
60
 * encoded in the `pathname` so we can thankfully generate a good "default"
61
 * location from it so we can generate proper relative URL's again.
62
 *
63
 * @param {Object|String} loc Optional default location object.
64
 * @returns {Object} lolcation object.
65
 * @public
66
 */
67
function lolcation(loc) {
68
  var globalVar;
69

    
70
  if (typeof window !== 'undefined') globalVar = window;
71
  else if (typeof global !== 'undefined') globalVar = global;
72
  else if (typeof self !== 'undefined') globalVar = self;
73
  else globalVar = {};
74

    
75
  var location = globalVar.location || {};
76
  loc = loc || location;
77

    
78
  var finaldestination = {}
79
    , type = typeof loc
80
    , key;
81

    
82
  if ('blob:' === loc.protocol) {
83
    finaldestination = new Url(unescape(loc.pathname), {});
84
  } else if ('string' === type) {
85
    finaldestination = new Url(loc, {});
86
    for (key in ignore) delete finaldestination[key];
87
  } else if ('object' === type) {
88
    for (key in loc) {
89
      if (key in ignore) continue;
90
      finaldestination[key] = loc[key];
91
    }
92

    
93
    if (finaldestination.slashes === undefined) {
94
      finaldestination.slashes = slashes.test(loc.href);
95
    }
96
  }
97

    
98
  return finaldestination;
99
}
100

    
101
/**
102
 * @typedef ProtocolExtract
103
 * @type Object
104
 * @property {String} protocol Protocol matched in the URL, in lowercase.
105
 * @property {Boolean} slashes `true` if protocol is followed by "//", else `false`.
106
 * @property {String} rest Rest of the URL that is not part of the protocol.
107
 */
108

    
109
/**
110
 * Extract protocol information from a URL with/without double slash ("//").
111
 *
112
 * @param {String} address URL we want to extract from.
113
 * @return {ProtocolExtract} Extracted information.
114
 * @private
115
 */
116
function extractProtocol(address) {
117
  address = trimLeft(address);
118
  var match = protocolre.exec(address);
119

    
120
  return {
121
    protocol: match[1] ? match[1].toLowerCase() : '',
122
    slashes: !!match[2],
123
    rest: match[3]
124
  };
125
}
126

    
127
/**
128
 * Resolve a relative URL pathname against a base URL pathname.
129
 *
130
 * @param {String} relative Pathname of the relative URL.
131
 * @param {String} base Pathname of the base URL.
132
 * @return {String} Resolved pathname.
133
 * @private
134
 */
135
function resolve(relative, base) {
136
  if (relative === '') return base;
137

    
138
  var path = (base || '/').split('/').slice(0, -1).concat(relative.split('/'))
139
    , i = path.length
140
    , last = path[i - 1]
141
    , unshift = false
142
    , up = 0;
143

    
144
  while (i--) {
145
    if (path[i] === '.') {
146
      path.splice(i, 1);
147
    } else if (path[i] === '..') {
148
      path.splice(i, 1);
149
      up++;
150
    } else if (up) {
151
      if (i === 0) unshift = true;
152
      path.splice(i, 1);
153
      up--;
154
    }
155
  }
156

    
157
  if (unshift) path.unshift('');
158
  if (last === '.' || last === '..') path.push('');
159

    
160
  return path.join('/');
161
}
162

    
163
/**
164
 * The actual URL instance. Instead of returning an object we've opted-in to
165
 * create an actual constructor as it's much more memory efficient and
166
 * faster and it pleases my OCD.
167
 *
168
 * It is worth noting that we should not use `URL` as class name to prevent
169
 * clashes with the global URL instance that got introduced in browsers.
170
 *
171
 * @constructor
172
 * @param {String} address URL we want to parse.
173
 * @param {Object|String} [location] Location defaults for relative paths.
174
 * @param {Boolean|Function} [parser] Parser for the query string.
175
 * @private
176
 */
177
function Url(address, location, parser) {
178
  address = trimLeft(address);
179

    
180
  if (!(this instanceof Url)) {
181
    return new Url(address, location, parser);
182
  }
183

    
184
  var relative, extracted, parse, instruction, index, key
185
    , instructions = rules.slice()
186
    , type = typeof location
187
    , url = this
188
    , i = 0;
189

    
190
  //
191
  // The following if statements allows this module two have compatibility with
192
  // 2 different API:
193
  //
194
  // 1. Node.js's `url.parse` api which accepts a URL, boolean as arguments
195
  //    where the boolean indicates that the query string should also be parsed.
196
  //
197
  // 2. The `URL` interface of the browser which accepts a URL, object as
198
  //    arguments. The supplied object will be used as default values / fall-back
199
  //    for relative paths.
200
  //
201
  if ('object' !== type && 'string' !== type) {
202
    parser = location;
203
    location = null;
204
  }
205

    
206
  if (parser && 'function' !== typeof parser) parser = qs.parse;
207

    
208
  location = lolcation(location);
209

    
210
  //
211
  // Extract protocol information before running the instructions.
212
  //
213
  extracted = extractProtocol(address || '');
214
  relative = !extracted.protocol && !extracted.slashes;
215
  url.slashes = extracted.slashes || relative && location.slashes;
216
  url.protocol = extracted.protocol || location.protocol || '';
217
  address = extracted.rest;
218

    
219
  //
220
  // When the authority component is absent the URL starts with a path
221
  // component.
222
  //
223
  if (!extracted.slashes) instructions[3] = [/(.*)/, 'pathname'];
224

    
225
  for (; i < instructions.length; i++) {
226
    instruction = instructions[i];
227

    
228
    if (typeof instruction === 'function') {
229
      address = instruction(address);
230
      continue;
231
    }
232

    
233
    parse = instruction[0];
234
    key = instruction[1];
235

    
236
    if (parse !== parse) {
237
      url[key] = address;
238
    } else if ('string' === typeof parse) {
239
      if (~(index = address.indexOf(parse))) {
240
        if ('number' === typeof instruction[2]) {
241
          url[key] = address.slice(0, index);
242
          address = address.slice(index + instruction[2]);
243
        } else {
244
          url[key] = address.slice(index);
245
          address = address.slice(0, index);
246
        }
247
      }
248
    } else if ((index = parse.exec(address))) {
249
      url[key] = index[1];
250
      address = address.slice(0, index.index);
251
    }
252

    
253
    url[key] = url[key] || (
254
      relative && instruction[3] ? location[key] || '' : ''
255
    );
256

    
257
    //
258
    // Hostname, host and protocol should be lowercased so they can be used to
259
    // create a proper `origin`.
260
    //
261
    if (instruction[4]) url[key] = url[key].toLowerCase();
262
  }
263

    
264
  //
265
  // Also parse the supplied query string in to an object. If we're supplied
266
  // with a custom parser as function use that instead of the default build-in
267
  // parser.
268
  //
269
  if (parser) url.query = parser(url.query);
270

    
271
  //
272
  // If the URL is relative, resolve the pathname against the base URL.
273
  //
274
  if (
275
      relative
276
    && location.slashes
277
    && url.pathname.charAt(0) !== '/'
278
    && (url.pathname !== '' || location.pathname !== '')
279
  ) {
280
    url.pathname = resolve(url.pathname, location.pathname);
281
  }
282

    
283
  //
284
  // We should not add port numbers if they are already the default port number
285
  // for a given protocol. As the host also contains the port number we're going
286
  // override it with the hostname which contains no port number.
287
  //
288
  if (!required(url.port, url.protocol)) {
289
    url.host = url.hostname;
290
    url.port = '';
291
  }
292

    
293
  //
294
  // Parse down the `auth` for the username and password.
295
  //
296
  url.username = url.password = '';
297
  if (url.auth) {
298
    instruction = url.auth.split(':');
299
    url.username = instruction[0] || '';
300
    url.password = instruction[1] || '';
301
  }
302

    
303
  url.origin = url.protocol && url.host && url.protocol !== 'file:'
304
    ? url.protocol +'//'+ url.host
305
    : 'null';
306

    
307
  //
308
  // The href is just the compiled result.
309
  //
310
  url.href = url.toString();
311
}
312

    
313
/**
314
 * This is convenience method for changing properties in the URL instance to
315
 * insure that they all propagate correctly.
316
 *
317
 * @param {String} part          Property we need to adjust.
318
 * @param {Mixed} value          The newly assigned value.
319
 * @param {Boolean|Function} fn  When setting the query, it will be the function
320
 *                               used to parse the query.
321
 *                               When setting the protocol, double slash will be
322
 *                               removed from the final url if it is true.
323
 * @returns {URL} URL instance for chaining.
324
 * @public
325
 */
326
function set(part, value, fn) {
327
  var url = this;
328

    
329
  switch (part) {
330
    case 'query':
331
      if ('string' === typeof value && value.length) {
332
        value = (fn || qs.parse)(value);
333
      }
334

    
335
      url[part] = value;
336
      break;
337

    
338
    case 'port':
339
      url[part] = value;
340

    
341
      if (!required(value, url.protocol)) {
342
        url.host = url.hostname;
343
        url[part] = '';
344
      } else if (value) {
345
        url.host = url.hostname +':'+ value;
346
      }
347

    
348
      break;
349

    
350
    case 'hostname':
351
      url[part] = value;
352

    
353
      if (url.port) value += ':'+ url.port;
354
      url.host = value;
355
      break;
356

    
357
    case 'host':
358
      url[part] = value;
359

    
360
      if (/:\d+$/.test(value)) {
361
        value = value.split(':');
362
        url.port = value.pop();
363
        url.hostname = value.join(':');
364
      } else {
365
        url.hostname = value;
366
        url.port = '';
367
      }
368

    
369
      break;
370

    
371
    case 'protocol':
372
      url.protocol = value.toLowerCase();
373
      url.slashes = !fn;
374
      break;
375

    
376
    case 'pathname':
377
    case 'hash':
378
      if (value) {
379
        var char = part === 'pathname' ? '/' : '#';
380
        url[part] = value.charAt(0) !== char ? char + value : value;
381
      } else {
382
        url[part] = value;
383
      }
384
      break;
385

    
386
    default:
387
      url[part] = value;
388
  }
389

    
390
  for (var i = 0; i < rules.length; i++) {
391
    var ins = rules[i];
392

    
393
    if (ins[4]) url[ins[1]] = url[ins[1]].toLowerCase();
394
  }
395

    
396
  url.origin = url.protocol && url.host && url.protocol !== 'file:'
397
    ? url.protocol +'//'+ url.host
398
    : 'null';
399

    
400
  url.href = url.toString();
401

    
402
  return url;
403
}
404

    
405
/**
406
 * Transform the properties back in to a valid and full URL string.
407
 *
408
 * @param {Function} stringify Optional query stringify function.
409
 * @returns {String} Compiled version of the URL.
410
 * @public
411
 */
412
function toString(stringify) {
413
  if (!stringify || 'function' !== typeof stringify) stringify = qs.stringify;
414

    
415
  var query
416
    , url = this
417
    , protocol = url.protocol;
418

    
419
  if (protocol && protocol.charAt(protocol.length - 1) !== ':') protocol += ':';
420

    
421
  var result = protocol + (url.slashes ? '//' : '');
422

    
423
  if (url.username) {
424
    result += url.username;
425
    if (url.password) result += ':'+ url.password;
426
    result += '@';
427
  }
428

    
429
  result += url.host + url.pathname;
430

    
431
  query = 'object' === typeof url.query ? stringify(url.query) : url.query;
432
  if (query) result += '?' !== query.charAt(0) ? '?'+ query : query;
433

    
434
  if (url.hash) result += url.hash;
435

    
436
  return result;
437
}
438

    
439
Url.prototype = { set: set, toString: toString };
440

    
441
//
442
// Expose the URL parser and some additional properties that might be useful for
443
// others or testing.
444
//
445
Url.extractProtocol = extractProtocol;
446
Url.location = lolcation;
447
Url.trimLeft = trimLeft;
448
Url.qs = qs;
449

    
450
module.exports = Url;
(3-3/4)