fannypack 0.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (55) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +14 -0
  3. data/.ruby-version +1 -0
  4. data/Gemfile +8 -0
  5. data/Gemfile.lock +127 -0
  6. data/Guardfile +6 -0
  7. data/LICENSE +22 -0
  8. data/Procfile +1 -0
  9. data/README.md +166 -0
  10. data/Rakefile +50 -0
  11. data/config.rb +74 -0
  12. data/fannypack.gemspec +22 -0
  13. data/lib/assets/javascripts/fanny_pack.coffee +21 -0
  14. data/lib/assets/javascripts/fanny_pack/ajax.js.coffee +58 -0
  15. data/lib/assets/javascripts/fanny_pack/env.js.coffee +4 -0
  16. data/lib/assets/javascripts/fanny_pack/models/environment.js.coffee +15 -0
  17. data/lib/assets/javascripts/fanny_pack/namespace.coffee +10 -0
  18. data/lib/assets/javascripts/fanny_pack/routers/base.coffee +82 -0
  19. data/lib/assets/javascripts/fanny_pack/version.js.coffee +2 -0
  20. data/lib/assets/javascripts/fanny_pack/views/app.coffee +19 -0
  21. data/lib/assets/javascripts/fanny_pack/views/base.coffee +90 -0
  22. data/lib/assets/javascripts/fanny_pack/views/list.coffee +29 -0
  23. data/lib/fanny_pack.rb +4 -0
  24. data/lib/fanny_pack/assets.rb +24 -0
  25. data/lib/fanny_pack/version.rb +4 -0
  26. data/source/images/background.png +0 -0
  27. data/source/images/middleman.png +0 -0
  28. data/source/index.html.haml +9 -0
  29. data/source/javascripts/application.js.coffee +2 -0
  30. data/source/javascripts/lib/.gitkeep +0 -0
  31. data/source/javascripts/vendor/.gitkeep +0 -0
  32. data/source/layouts/layout.html.haml +49 -0
  33. data/source/stylesheets/application.css.sass +2 -0
  34. data/source/stylesheets/lib/_fonts.css.sass +0 -0
  35. data/source/stylesheets/lib/_mixins.css.sass +0 -0
  36. data/source/stylesheets/lib/_variables.css.sass +0 -0
  37. data/source/stylesheets/lib/common.css.sass +1 -0
  38. data/source/stylesheets/vendor/all.css +55 -0
  39. data/source/stylesheets/vendor/normalize.css +375 -0
  40. data/spec/javascripts/helpers/spec_helper.coffee +11 -0
  41. data/spec/javascripts/support/jasmine-jquery.js +812 -0
  42. data/spec/javascripts/support/jasmine-sinon.coffee +246 -0
  43. data/spec/javascripts/support/jasmine.yml +10 -0
  44. data/spec/javascripts/support/jasmine_helper.rb +3 -0
  45. data/spec/javascripts/support/sinon-1.7.3.js +4290 -0
  46. data/spec/javascripts/views/base_spec.coffee +130 -0
  47. data/spec/javascripts/views/list_spec.coffee +31 -0
  48. data/vendor/assets/javascripts/backbone-1.1.2.js +1608 -0
  49. data/vendor/assets/javascripts/jquery-1.11.0.js +10337 -0
  50. data/vendor/assets/javascripts/json2.js +487 -0
  51. data/vendor/assets/javascripts/ppx.js +391 -0
  52. data/vendor/assets/javascripts/underscore-1.5.1.js +1246 -0
  53. data/vendor/assets/javascripts/uri.js +1803 -0
  54. data/vendor/assets/javascripts/zepto-detect.js +66 -0
  55. metadata +150 -0
@@ -0,0 +1,1803 @@
1
+ /*!
2
+ * URI.js - Mutating URLs
3
+ *
4
+ * Version: 1.10.2
5
+ *
6
+ * Author: Rodney Rehm
7
+ * Web: http://medialize.github.com/URI.js/
8
+ *
9
+ * Licensed under
10
+ * MIT License http://www.opensource.org/licenses/mit-license
11
+ * GPL v3 http://opensource.org/licenses/GPL-3.0
12
+ *
13
+ */
14
+ (function (root, factory) {
15
+ // https://github.com/umdjs/umd/blob/master/returnExports.js
16
+ if (typeof exports === 'object') {
17
+ // Node
18
+ module.exports = factory(require('./punycode'), require('./IPv6'), require('./SecondLevelDomains'));
19
+ } else if (typeof define === 'function' && define.amd) {
20
+ // AMD. Register as an anonymous module.
21
+ define(['./punycode', './IPv6', './SecondLevelDomains'], factory);
22
+ } else {
23
+ // Browser globals (root is window)
24
+ root.URI = factory(root.punycode, root.IPv6, root.SecondLevelDomains);
25
+ }
26
+ }(this, function (punycode, IPv6, SLD) {
27
+ "use strict";
28
+
29
+ function URI(url, base) {
30
+ // Allow instantiation without the 'new' keyword
31
+ if (!(this instanceof URI)) {
32
+ return new URI(url, base);
33
+ }
34
+
35
+ if (url === undefined) {
36
+ if (typeof location !== 'undefined') {
37
+ url = location.href + "";
38
+ } else {
39
+ url = "";
40
+ }
41
+ }
42
+
43
+ this.href(url);
44
+
45
+ // resolve to base according to http://dvcs.w3.org/hg/url/raw-file/tip/Overview.html#constructor
46
+ if (base !== undefined) {
47
+ return this.absoluteTo(base);
48
+ }
49
+
50
+ return this;
51
+ };
52
+
53
+ var p = URI.prototype;
54
+ var hasOwn = Object.prototype.hasOwnProperty;
55
+
56
+ function escapeRegEx(string) {
57
+ // https://github.com/medialize/URI.js/commit/85ac21783c11f8ccab06106dba9735a31a86924d#commitcomment-821963
58
+ return string.replace(/([.*+?^=!:${}()|[\]\/\\])/g, '\\$1');
59
+ }
60
+
61
+ function getType(value) {
62
+ return String(Object.prototype.toString.call(value)).slice(8, -1);
63
+ }
64
+
65
+ function isArray(obj) {
66
+ return getType(obj) === "Array";
67
+ }
68
+
69
+ function filterArrayValues(data, value) {
70
+ var lookup = {};
71
+ var i, length;
72
+
73
+ if (isArray(value)) {
74
+ for (i = 0, length = value.length; i < length; i++) {
75
+ lookup[value[i]] = true;
76
+ }
77
+ } else {
78
+ lookup[value] = true;
79
+ }
80
+
81
+ for (i = 0, length = data.length; i < length; i++) {
82
+ if (lookup[data[i]] !== undefined) {
83
+ data.splice(i, 1);
84
+ length--;
85
+ i--;
86
+ }
87
+ }
88
+
89
+ return data;
90
+ }
91
+
92
+ function arrayContains(list, value) {
93
+ var i, length;
94
+
95
+ // value may be string, number, array, regexp
96
+ if (isArray(value)) {
97
+ // Note: this can be optimized to O(n) (instead of current O(m * n))
98
+ for (i = 0, length = value.length; i < length; i++) {
99
+ if (!arrayContains(list, value[i])) {
100
+ return false;
101
+ }
102
+ }
103
+
104
+ return true;
105
+ }
106
+
107
+ var _type = getType(value);
108
+ for (i = 0, length = list.length; i < length; i++) {
109
+ if (_type === 'RegExp') {
110
+ if (typeof list[i] === 'string' && list[i].match(value)) {
111
+ return true;
112
+ }
113
+ } else if (list[i] === value) {
114
+ return true;
115
+ }
116
+ }
117
+
118
+ return false;
119
+ }
120
+
121
+ function arraysEqual(one, two) {
122
+ if (!isArray(one) || !isArray(two)) {
123
+ return false;
124
+ }
125
+
126
+ // arrays can't be equal if they have different amount of content
127
+ if (one.length !== two.length) {
128
+ return false;
129
+ }
130
+
131
+ one.sort();
132
+ two.sort();
133
+
134
+ for (var i = 0, l = one.length; i < l; i++) {
135
+ if (one[i] !== two[i]) {
136
+ return false;
137
+ }
138
+ }
139
+
140
+ return true;
141
+ }
142
+
143
+ URI._parts = function() {
144
+ return {
145
+ protocol: null,
146
+ username: null,
147
+ password: null,
148
+ hostname: null,
149
+ urn: null,
150
+ port: null,
151
+ path: null,
152
+ query: null,
153
+ fragment: null,
154
+ // state
155
+ duplicateQueryParameters: URI.duplicateQueryParameters
156
+ };
157
+ };
158
+ // state: allow duplicate query parameters (a=1&a=1)
159
+ URI.duplicateQueryParameters = false;
160
+ // static properties
161
+ URI.protocol_expression = /^[a-z][a-z0-9-+-]*$/i;
162
+ URI.idn_expression = /[^a-z0-9\.-]/i;
163
+ URI.punycode_expression = /(xn--)/i;
164
+ // well, 333.444.555.666 matches, but it sure ain't no IPv4 - do we care?
165
+ URI.ip4_expression = /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/;
166
+ // credits to Rich Brown
167
+ // source: http://forums.intermapper.com/viewtopic.php?p=1096#1096
168
+ // specification: http://www.ietf.org/rfc/rfc4291.txt
169
+ URI.ip6_expression = /^\s*((([0-9A-Fa-f]{1,4}:){7}([0-9A-Fa-f]{1,4}|:))|(([0-9A-Fa-f]{1,4}:){6}(:[0-9A-Fa-f]{1,4}|((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){5}(((:[0-9A-Fa-f]{1,4}){1,2})|:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){4}(((:[0-9A-Fa-f]{1,4}){1,3})|((:[0-9A-Fa-f]{1,4})?:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){3}(((:[0-9A-Fa-f]{1,4}){1,4})|((:[0-9A-Fa-f]{1,4}){0,2}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){2}(((:[0-9A-Fa-f]{1,4}){1,5})|((:[0-9A-Fa-f]{1,4}){0,3}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){1}(((:[0-9A-Fa-f]{1,4}){1,6})|((:[0-9A-Fa-f]{1,4}){0,4}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(:(((:[0-9A-Fa-f]{1,4}){1,7})|((:[0-9A-Fa-f]{1,4}){0,5}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:)))(%.+)?\s*$/;
170
+ // gruber revised expression - http://rodneyrehm.de/t/url-regex.html
171
+ URI.find_uri_expression = /\b((?:[a-z][\w-]+:(?:\/{1,3}|[a-z0-9%])|www\d{0,3}[.]|[a-z0-9.\-]+[.][a-z]{2,4}\/)(?:[^\s()<>]+|\(([^\s()<>]+|(\([^\s()<>]+\)))*\))+(?:\(([^\s()<>]+|(\([^\s()<>]+\)))*\)|[^\s`!()\[\]{};:'".,<>?«»“”‘’]))/ig;
172
+ // http://www.iana.org/assignments/uri-schemes.html
173
+ // http://en.wikipedia.org/wiki/List_of_TCP_and_UDP_port_numbers#Well-known_ports
174
+ URI.defaultPorts = {
175
+ http: "80",
176
+ https: "443",
177
+ ftp: "21",
178
+ gopher: "70",
179
+ ws: "80",
180
+ wss: "443"
181
+ };
182
+ // allowed hostname characters according to RFC 3986
183
+ // ALPHA DIGIT "-" "." "_" "~" "!" "$" "&" "'" "(" ")" "*" "+" "," ";" "=" %encoded
184
+ // I've never seen a (non-IDN) hostname other than: ALPHA DIGIT . -
185
+ URI.invalid_hostname_characters = /[^a-zA-Z0-9\.-]/;
186
+ // encoding / decoding according to RFC3986
187
+ function strictEncodeURIComponent(string) {
188
+ // see https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/encodeURIComponent
189
+ return encodeURIComponent(string)
190
+ .replace(/[!'()*]/g, escape)
191
+ .replace(/\*/g, "%2A");
192
+ }
193
+ URI.encode = strictEncodeURIComponent;
194
+ URI.decode = decodeURIComponent;
195
+ URI.iso8859 = function() {
196
+ URI.encode = escape;
197
+ URI.decode = unescape;
198
+ };
199
+ URI.unicode = function() {
200
+ URI.encode = strictEncodeURIComponent;
201
+ URI.decode = decodeURIComponent;
202
+ };
203
+ URI.characters = {
204
+ pathname: {
205
+ encode: {
206
+ // RFC3986 2.1: For consistency, URI producers and normalizers should
207
+ // use uppercase hexadecimal digits for all percent-encodings.
208
+ expression: /%(24|26|2B|2C|3B|3D|3A|40)/ig,
209
+ map: {
210
+ // -._~!'()*
211
+ "%24": "$",
212
+ "%26": "&",
213
+ "%2B": "+",
214
+ "%2C": ",",
215
+ "%3B": ";",
216
+ "%3D": "=",
217
+ "%3A": ":",
218
+ "%40": "@"
219
+ }
220
+ },
221
+ decode: {
222
+ expression: /[\/\?#]/g,
223
+ map: {
224
+ "/": "%2F",
225
+ "?": "%3F",
226
+ "#": "%23"
227
+ }
228
+ }
229
+ },
230
+ reserved: {
231
+ encode: {
232
+ // RFC3986 2.1: For consistency, URI producers and normalizers should
233
+ // use uppercase hexadecimal digits for all percent-encodings.
234
+ expression: /%(21|23|24|26|27|28|29|2A|2B|2C|2F|3A|3B|3D|3F|40|5B|5D)/ig,
235
+ map: {
236
+ // gen-delims
237
+ "%3A": ":",
238
+ "%2F": "/",
239
+ "%3F": "?",
240
+ "%23": "#",
241
+ "%5B": "[",
242
+ "%5D": "]",
243
+ "%40": "@",
244
+ // sub-delims
245
+ "%21": "!",
246
+ "%24": "$",
247
+ "%26": "&",
248
+ "%27": "'",
249
+ "%28": "(",
250
+ "%29": ")",
251
+ "%2A": "*",
252
+ "%2B": "+",
253
+ "%2C": ",",
254
+ "%3B": ";",
255
+ "%3D": "="
256
+ }
257
+ }
258
+ }
259
+ };
260
+ URI.encodeQuery = function(string) {
261
+ return URI.encode(string + "").replace(/%20/g, '+');
262
+ };
263
+ URI.decodeQuery = function(string) {
264
+ return URI.decode((string + "").replace(/\+/g, '%20'));
265
+ };
266
+ URI.recodePath = function(string) {
267
+ var segments = (string + "").split('/');
268
+ for (var i = 0, length = segments.length; i < length; i++) {
269
+ segments[i] = URI.encodePathSegment(URI.decode(segments[i]));
270
+ }
271
+
272
+ return segments.join('/');
273
+ };
274
+ URI.decodePath = function(string) {
275
+ var segments = (string + "").split('/');
276
+ for (var i = 0, length = segments.length; i < length; i++) {
277
+ segments[i] = URI.decodePathSegment(segments[i]);
278
+ }
279
+
280
+ return segments.join('/');
281
+ };
282
+ // generate encode/decode path functions
283
+ var _parts = {'encode':'encode', 'decode':'decode'};
284
+ var _part;
285
+ var generateAccessor = function(_group, _part) {
286
+ return function(string) {
287
+ return URI[_part](string + "").replace(URI.characters[_group][_part].expression, function(c) {
288
+ return URI.characters[_group][_part].map[c];
289
+ });
290
+ };
291
+ };
292
+
293
+ for (_part in _parts) {
294
+ URI[_part + "PathSegment"] = generateAccessor("pathname", _parts[_part]);
295
+ }
296
+
297
+ URI.encodeReserved = generateAccessor("reserved", "encode");
298
+
299
+ URI.parse = function(string, parts) {
300
+ var pos, t;
301
+ if (!parts) {
302
+ parts = {};
303
+ }
304
+ // [protocol"://"[username[":"password]"@"]hostname[":"port]"/"?][path]["?"querystring]["#"fragment]
305
+
306
+ // extract fragment
307
+ pos = string.indexOf('#');
308
+ if (pos > -1) {
309
+ // escaping?
310
+ parts.fragment = string.substring(pos + 1) || null;
311
+ string = string.substring(0, pos);
312
+ }
313
+
314
+ // extract query
315
+ pos = string.indexOf('?');
316
+ if (pos > -1) {
317
+ // escaping?
318
+ parts.query = string.substring(pos + 1) || null;
319
+ string = string.substring(0, pos);
320
+ }
321
+
322
+ // extract protocol
323
+ if (string.substring(0, 2) === '//') {
324
+ // relative-scheme
325
+ parts.protocol = '';
326
+ string = string.substring(2);
327
+ // extract "user:pass@host:port"
328
+ string = URI.parseAuthority(string, parts);
329
+ } else {
330
+ pos = string.indexOf(':');
331
+ if (pos > -1) {
332
+ parts.protocol = string.substring(0, pos);
333
+ if (parts.protocol && !parts.protocol.match(URI.protocol_expression)) {
334
+ // : may be within the path
335
+ parts.protocol = undefined;
336
+ } else if (parts.protocol === 'file') {
337
+ // the file scheme: does not contain an authority
338
+ string = string.substring(pos + 3);
339
+ } else if (string.substring(pos + 1, pos + 3) === '//') {
340
+ string = string.substring(pos + 3);
341
+
342
+ // extract "user:pass@host:port"
343
+ string = URI.parseAuthority(string, parts);
344
+ } else {
345
+ string = string.substring(pos + 1);
346
+ parts.urn = true;
347
+ }
348
+ }
349
+ }
350
+
351
+ // what's left must be the path
352
+ parts.path = string;
353
+
354
+ // and we're done
355
+ return parts;
356
+ };
357
+ URI.parseHost = function(string, parts) {
358
+ // extract host:port
359
+ var pos = string.indexOf('/');
360
+ var bracketPos;
361
+ var t;
362
+
363
+ if (pos === -1) {
364
+ pos = string.length;
365
+ }
366
+
367
+ if (string.charAt(0) === "[") {
368
+ // IPv6 host - http://tools.ietf.org/html/draft-ietf-6man-text-addr-representation-04#section-6
369
+ // I claim most client software breaks on IPv6 anyways. To simplify things, URI only accepts
370
+ // IPv6+port in the format [2001:db8::1]:80 (for the time being)
371
+ bracketPos = string.indexOf(']');
372
+ parts.hostname = string.substring(1, bracketPos) || null;
373
+ parts.port = string.substring(bracketPos+2, pos) || null;
374
+ } else if (string.indexOf(':') !== string.lastIndexOf(':')) {
375
+ // IPv6 host contains multiple colons - but no port
376
+ // this notation is actually not allowed by RFC 3986, but we're a liberal parser
377
+ parts.hostname = string.substring(0, pos) || null;
378
+ parts.port = null;
379
+ } else {
380
+ t = string.substring(0, pos).split(':');
381
+ parts.hostname = t[0] || null;
382
+ parts.port = t[1] || null;
383
+ }
384
+
385
+ if (parts.hostname && string.substring(pos).charAt(0) !== '/') {
386
+ pos++;
387
+ string = "/" + string;
388
+ }
389
+
390
+ return string.substring(pos) || '/';
391
+ };
392
+ URI.parseAuthority = function(string, parts) {
393
+ string = URI.parseUserinfo(string, parts);
394
+ return URI.parseHost(string, parts);
395
+ };
396
+ URI.parseUserinfo = function(string, parts) {
397
+ // extract username:password
398
+ var pos = string.indexOf('@');
399
+ var firstSlash = string.indexOf('/');
400
+ var t;
401
+
402
+ // authority@ must come before /path
403
+ if (pos > -1 && (firstSlash === -1 || pos < firstSlash)) {
404
+ t = string.substring(0, pos).split(':');
405
+ parts.username = t[0] ? URI.decode(t[0]) : null;
406
+ t.shift();
407
+ parts.password = t[0] ? URI.decode(t.join(':')) : null;
408
+ string = string.substring(pos + 1);
409
+ } else {
410
+ parts.username = null;
411
+ parts.password = null;
412
+ }
413
+
414
+ return string;
415
+ };
416
+ URI.parseQuery = function(string) {
417
+ if (!string) {
418
+ return {};
419
+ }
420
+
421
+ // throw out the funky business - "?"[name"="value"&"]+
422
+ string = string.replace(/&+/g, '&').replace(/^\?*&*|&+$/g, '');
423
+
424
+ if (!string) {
425
+ return {};
426
+ }
427
+
428
+ var items = {};
429
+ var splits = string.split('&');
430
+ var length = splits.length;
431
+ var v, name, value;
432
+
433
+ for (var i = 0; i < length; i++) {
434
+ v = splits[i].split('=');
435
+ name = URI.decodeQuery(v.shift());
436
+ // no "=" is null according to http://dvcs.w3.org/hg/url/raw-file/tip/Overview.html#collect-url-parameters
437
+ value = v.length ? URI.decodeQuery(v.join('=')) : null;
438
+
439
+ if (items[name]) {
440
+ if (typeof items[name] === "string") {
441
+ items[name] = [items[name]];
442
+ }
443
+
444
+ items[name].push(value);
445
+ } else {
446
+ items[name] = value;
447
+ }
448
+ }
449
+
450
+ return items;
451
+ };
452
+
453
+ URI.build = function(parts) {
454
+ var t = "";
455
+
456
+ if (parts.protocol) {
457
+ t += parts.protocol + ":";
458
+ }
459
+
460
+ if (!parts.urn && (t || parts.hostname)) {
461
+ t += '//';
462
+ }
463
+
464
+ t += (URI.buildAuthority(parts) || '');
465
+
466
+ if (typeof parts.path === "string") {
467
+ if (parts.path.charAt(0) !== '/' && typeof parts.hostname === "string") {
468
+ t += '/';
469
+ }
470
+
471
+ t += parts.path;
472
+ }
473
+
474
+ if (typeof parts.query === "string" && parts.query) {
475
+ t += '?' + parts.query;
476
+ }
477
+
478
+ if (typeof parts.fragment === "string" && parts.fragment) {
479
+ t += '#' + parts.fragment;
480
+ }
481
+ return t;
482
+ };
483
+ URI.buildHost = function(parts) {
484
+ var t = "";
485
+
486
+ if (!parts.hostname) {
487
+ return "";
488
+ } else if (URI.ip6_expression.test(parts.hostname)) {
489
+ if (parts.port) {
490
+ t += "[" + parts.hostname + "]:" + parts.port;
491
+ } else {
492
+ // don't know if we should always wrap IPv6 in []
493
+ // the RFC explicitly says SHOULD, not MUST.
494
+ t += parts.hostname;
495
+ }
496
+ } else {
497
+ t += parts.hostname;
498
+ if (parts.port) {
499
+ t += ':' + parts.port;
500
+ }
501
+ }
502
+
503
+ return t;
504
+ };
505
+ URI.buildAuthority = function(parts) {
506
+ return URI.buildUserinfo(parts) + URI.buildHost(parts);
507
+ };
508
+ URI.buildUserinfo = function(parts) {
509
+ var t = "";
510
+
511
+ if (parts.username) {
512
+ t += URI.encode(parts.username);
513
+
514
+ if (parts.password) {
515
+ t += ':' + URI.encode(parts.password);
516
+ }
517
+
518
+ t += "@";
519
+ }
520
+
521
+ return t;
522
+ };
523
+ URI.buildQuery = function(data, duplicates) {
524
+ // according to http://tools.ietf.org/html/rfc3986 or http://labs.apache.org/webarch/uri/rfc/rfc3986.html
525
+ // being »-._~!$&'()*+,;=:@/?« %HEX and alnum are allowed
526
+ // the RFC explicitly states ?/foo being a valid use case, no mention of parameter syntax!
527
+ // URI.js treats the query string as being application/x-www-form-urlencoded
528
+ // see http://www.w3.org/TR/REC-html40/interact/forms.html#form-content-type
529
+
530
+ var t = "";
531
+ var unique, key, i, length;
532
+ for (key in data) {
533
+ if (hasOwn.call(data, key) && key) {
534
+ if (isArray(data[key])) {
535
+ unique = {};
536
+ for (i = 0, length = data[key].length; i < length; i++) {
537
+ if (data[key][i] !== undefined && unique[data[key][i] + ""] === undefined) {
538
+ t += "&" + URI.buildQueryParameter(key, data[key][i]);
539
+ if (duplicates !== true) {
540
+ unique[data[key][i] + ""] = true;
541
+ }
542
+ }
543
+ }
544
+ } else if (data[key] !== undefined) {
545
+ t += '&' + URI.buildQueryParameter(key, data[key]);
546
+ }
547
+ }
548
+ }
549
+
550
+ return t.substring(1);
551
+ };
552
+ URI.buildQueryParameter = function(name, value) {
553
+ // http://www.w3.org/TR/REC-html40/interact/forms.html#form-content-type -- application/x-www-form-urlencoded
554
+ // don't append "=" for null values, according to http://dvcs.w3.org/hg/url/raw-file/tip/Overview.html#url-parameter-serialization
555
+ return URI.encodeQuery(name) + (value !== null ? "=" + URI.encodeQuery(value) : "");
556
+ };
557
+
558
+ URI.addQuery = function(data, name, value) {
559
+ if (typeof name === "object") {
560
+ for (var key in name) {
561
+ if (hasOwn.call(name, key)) {
562
+ URI.addQuery(data, key, name[key]);
563
+ }
564
+ }
565
+ } else if (typeof name === "string") {
566
+ if (data[name] === undefined) {
567
+ data[name] = value;
568
+ return;
569
+ } else if (typeof data[name] === "string") {
570
+ data[name] = [data[name]];
571
+ }
572
+
573
+ if (!isArray(value)) {
574
+ value = [value];
575
+ }
576
+
577
+ data[name] = data[name].concat(value);
578
+ } else {
579
+ throw new TypeError("URI.addQuery() accepts an object, string as the name parameter");
580
+ }
581
+ };
582
+ URI.removeQuery = function(data, name, value) {
583
+ var i, length, key;
584
+
585
+ if (isArray(name)) {
586
+ for (i = 0, length = name.length; i < length; i++) {
587
+ data[name[i]] = undefined;
588
+ }
589
+ } else if (typeof name === "object") {
590
+ for (key in name) {
591
+ if (hasOwn.call(name, key)) {
592
+ URI.removeQuery(data, key, name[key]);
593
+ }
594
+ }
595
+ } else if (typeof name === "string") {
596
+ if (value !== undefined) {
597
+ if (data[name] === value) {
598
+ data[name] = undefined;
599
+ } else if (isArray(data[name])) {
600
+ data[name] = filterArrayValues(data[name], value);
601
+ }
602
+ } else {
603
+ data[name] = undefined;
604
+ }
605
+ } else {
606
+ throw new TypeError("URI.addQuery() accepts an object, string as the first parameter");
607
+ }
608
+ };
609
+ URI.hasQuery = function(data, name, value, withinArray) {
610
+ if (typeof name === "object") {
611
+ for (var key in name) {
612
+ if (hasOwn.call(name, key)) {
613
+ if (!URI.hasQuery(data, key, name[key])) {
614
+ return false;
615
+ }
616
+ }
617
+ }
618
+
619
+ return true;
620
+ } else if (typeof name !== "string") {
621
+ throw new TypeError("URI.hasQuery() accepts an object, string as the name parameter");
622
+ }
623
+
624
+ switch (getType(value)) {
625
+ case 'Undefined':
626
+ // true if exists (but may be empty)
627
+ return name in data; // data[name] !== undefined;
628
+
629
+ case 'Boolean':
630
+ // true if exists and non-empty
631
+ var _booly = Boolean(isArray(data[name]) ? data[name].length : data[name]);
632
+ return value === _booly;
633
+
634
+ case 'Function':
635
+ // allow complex comparison
636
+ return !!value(data[name], name, data);
637
+
638
+ case 'Array':
639
+ if (!isArray(data[name])) {
640
+ return false;
641
+ }
642
+
643
+ var op = withinArray ? arrayContains : arraysEqual;
644
+ return op(data[name], value);
645
+
646
+ case 'RegExp':
647
+ if (!isArray(data[name])) {
648
+ return Boolean(data[name] && data[name].match(value));
649
+ }
650
+
651
+ if (!withinArray) {
652
+ return false;
653
+ }
654
+
655
+ return arrayContains(data[name], value);
656
+
657
+ case 'Number':
658
+ value = String(value);
659
+ // omit break;
660
+ case 'String':
661
+ if (!isArray(data[name])) {
662
+ return data[name] === value;
663
+ }
664
+
665
+ if (!withinArray) {
666
+ return false;
667
+ }
668
+
669
+ return arrayContains(data[name], value);
670
+
671
+ default:
672
+ throw new TypeError("URI.hasQuery() accepts undefined, boolean, string, number, RegExp, Function as the value parameter");
673
+ }
674
+ };
675
+
676
+
677
+ URI.commonPath = function(one, two) {
678
+ var length = Math.min(one.length, two.length);
679
+ var pos;
680
+
681
+ // find first non-matching character
682
+ for (pos = 0; pos < length; pos++) {
683
+ if (one.charAt(pos) !== two.charAt(pos)) {
684
+ pos--;
685
+ break;
686
+ }
687
+ }
688
+
689
+ if (pos < 1) {
690
+ return one.charAt(0) === two.charAt(0) && one.charAt(0) === '/' ? '/' : '';
691
+ }
692
+
693
+ // revert to last /
694
+ if (one.charAt(pos) !== '/' || two.charAt(pos) !== '/') {
695
+ pos = one.substring(0, pos).lastIndexOf('/');
696
+ }
697
+
698
+ return one.substring(0, pos + 1);
699
+ };
700
+
701
+ URI.withinString = function(string, callback) {
702
+ // expression used is "gruber revised" (@gruber v2) determined to be the best solution in
703
+ // a regex sprint we did a couple of ages ago at
704
+ // * http://mathiasbynens.be/demo/url-regex
705
+ // * http://rodneyrehm.de/t/url-regex.html
706
+
707
+ return string.replace(URI.find_uri_expression, callback);
708
+ };
709
+
710
+ URI.ensureValidHostname = function(v) {
711
+ // Theoretically URIs allow percent-encoding in Hostnames (according to RFC 3986)
712
+ // they are not part of DNS and therefore ignored by URI.js
713
+
714
+ if (v.match(URI.invalid_hostname_characters)) {
715
+ // test punycode
716
+ if (!punycode) {
717
+ throw new TypeError("Hostname '" + v + "' contains characters other than [A-Z0-9.-] and Punycode.js is not available");
718
+ }
719
+
720
+ if (punycode.toASCII(v).match(URI.invalid_hostname_characters)) {
721
+ throw new TypeError("Hostname '" + v + "' contains characters other than [A-Z0-9.-]");
722
+ }
723
+ }
724
+ };
725
+
726
+ p.build = function(deferBuild) {
727
+ if (deferBuild === true) {
728
+ this._deferred_build = true;
729
+ } else if (deferBuild === undefined || this._deferred_build) {
730
+ this._string = URI.build(this._parts);
731
+ this._deferred_build = false;
732
+ }
733
+
734
+ return this;
735
+ };
736
+
737
+ p.clone = function() {
738
+ return new URI(this);
739
+ };
740
+
741
+ p.valueOf = p.toString = function() {
742
+ return this.build(false)._string;
743
+ };
744
+
745
+ // generate simple accessors
746
+ _parts = {protocol: 'protocol', username: 'username', password: 'password', hostname: 'hostname', port: 'port'};
747
+ generateAccessor = function(_part){
748
+ return function(v, build) {
749
+ if (v === undefined) {
750
+ return this._parts[_part] || "";
751
+ } else {
752
+ this._parts[_part] = v;
753
+ this.build(!build);
754
+ return this;
755
+ }
756
+ };
757
+ };
758
+
759
+ for (_part in _parts) {
760
+ p[_part] = generateAccessor(_parts[_part]);
761
+ }
762
+
763
+ // generate accessors with optionally prefixed input
764
+ _parts = {query: '?', fragment: '#'};
765
+ generateAccessor = function(_part, _key){
766
+ return function(v, build) {
767
+ if (v === undefined) {
768
+ return this._parts[_part] || "";
769
+ } else {
770
+ if (v !== null) {
771
+ v = v + "";
772
+ if (v.charAt(0) === _key) {
773
+ v = v.substring(1);
774
+ }
775
+ }
776
+
777
+ this._parts[_part] = v;
778
+ this.build(!build);
779
+ return this;
780
+ }
781
+ };
782
+ };
783
+
784
+ for (_part in _parts) {
785
+ p[_part] = generateAccessor(_part, _parts[_part]);
786
+ }
787
+
788
+ // generate accessors with prefixed output
789
+ _parts = {search: ['?', 'query'], hash: ['#', 'fragment']};
790
+ generateAccessor = function(_part, _key){
791
+ return function(v, build) {
792
+ var t = this[_part](v, build);
793
+ return typeof t === "string" && t.length ? (_key + t) : t;
794
+ };
795
+ };
796
+
797
+ for (_part in _parts) {
798
+ p[_part] = generateAccessor(_parts[_part][1], _parts[_part][0]);
799
+ }
800
+
801
+ p.pathname = function(v, build) {
802
+ if (v === undefined || v === true) {
803
+ var res = this._parts.path || (this._parts.urn ? '' : '/');
804
+ return v ? URI.decodePath(res) : res;
805
+ } else {
806
+ this._parts.path = v ? URI.recodePath(v) : "/";
807
+ this.build(!build);
808
+ return this;
809
+ }
810
+ };
811
+ p.path = p.pathname;
812
+ p.href = function(href, build) {
813
+ var key;
814
+
815
+ if (href === undefined) {
816
+ return this.toString();
817
+ }
818
+
819
+ this._string = "";
820
+ this._parts = URI._parts();
821
+
822
+ var _URI = href instanceof URI;
823
+ var _object = typeof href === "object" && (href.hostname || href.path);
824
+
825
+
826
+ // window.location is reported to be an object, but it's not the sort
827
+ // of object we're looking for:
828
+ // * location.protocol ends with a colon
829
+ // * location.query != object.search
830
+ // * location.hash != object.fragment
831
+ // simply serializing the unknown object should do the trick
832
+ // (for location, not for everything...)
833
+ if (!_URI && _object && href.pathname !== undefined) {
834
+ href = href.toString();
835
+ }
836
+
837
+ if (typeof href === "string") {
838
+ this._parts = URI.parse(href, this._parts);
839
+ } else if (_URI || _object) {
840
+ var src = _URI ? href._parts : href;
841
+ for (key in src) {
842
+ if (hasOwn.call(this._parts, key)) {
843
+ this._parts[key] = src[key];
844
+ }
845
+ }
846
+ } else {
847
+ throw new TypeError("invalid input");
848
+ }
849
+
850
+ this.build(!build);
851
+ return this;
852
+ };
853
+
854
+ // identification accessors
855
+ p.is = function(what) {
856
+ var ip = false;
857
+ var ip4 = false;
858
+ var ip6 = false;
859
+ var name = false;
860
+ var sld = false;
861
+ var idn = false;
862
+ var punycode = false;
863
+ var relative = !this._parts.urn;
864
+
865
+ if (this._parts.hostname) {
866
+ relative = false;
867
+ ip4 = URI.ip4_expression.test(this._parts.hostname);
868
+ ip6 = URI.ip6_expression.test(this._parts.hostname);
869
+ ip = ip4 || ip6;
870
+ name = !ip;
871
+ sld = name && SLD && SLD.has(this._parts.hostname);
872
+ idn = name && URI.idn_expression.test(this._parts.hostname);
873
+ punycode = name && URI.punycode_expression.test(this._parts.hostname);
874
+ }
875
+
876
+ switch (what.toLowerCase()) {
877
+ case 'relative':
878
+ return relative;
879
+
880
+ case 'absolute':
881
+ return !relative;
882
+
883
+ // hostname identification
884
+ case 'domain':
885
+ case 'name':
886
+ return name;
887
+
888
+ case 'sld':
889
+ return sld;
890
+
891
+ case 'ip':
892
+ return ip;
893
+
894
+ case 'ip4':
895
+ case 'ipv4':
896
+ case 'inet4':
897
+ return ip4;
898
+
899
+ case 'ip6':
900
+ case 'ipv6':
901
+ case 'inet6':
902
+ return ip6;
903
+
904
+ case 'idn':
905
+ return idn;
906
+
907
+ case 'url':
908
+ return !this._parts.urn;
909
+
910
+ case 'urn':
911
+ return !!this._parts.urn;
912
+
913
+ case 'punycode':
914
+ return punycode;
915
+ }
916
+
917
+ return null;
918
+ };
919
+
920
+ // component specific input validation
921
+ var _protocol = p.protocol;
922
+ var _port = p.port;
923
+ var _hostname = p.hostname;
924
+
925
+ p.protocol = function(v, build) {
926
+ if (v !== undefined) {
927
+ if (v) {
928
+ // accept trailing ://
929
+ v = v.replace(/:(\/\/)?$/, '');
930
+
931
+ if (v.match(/[^a-zA-z0-9\.+-]/)) {
932
+ throw new TypeError("Protocol '" + v + "' contains characters other than [A-Z0-9.+-]");
933
+ }
934
+ }
935
+ }
936
+ return _protocol.call(this, v, build);
937
+ };
938
+ p.scheme = p.protocol;
939
+ p.port = function(v, build) {
940
+ if (this._parts.urn) {
941
+ return v === undefined ? '' : this;
942
+ }
943
+
944
+ if (v !== undefined) {
945
+ if (v === 0) {
946
+ v = null;
947
+ }
948
+
949
+ if (v) {
950
+ v += "";
951
+ if (v.charAt(0) === ":") {
952
+ v = v.substring(1);
953
+ }
954
+
955
+ if (v.match(/[^0-9]/)) {
956
+ throw new TypeError("Port '" + v + "' contains characters other than [0-9]");
957
+ }
958
+ }
959
+ }
960
+ return _port.call(this, v, build);
961
+ };
962
+ p.hostname = function(v, build) {
963
+ if (this._parts.urn) {
964
+ return v === undefined ? '' : this;
965
+ }
966
+
967
+ if (v !== undefined) {
968
+ var x = {};
969
+ URI.parseHost(v, x);
970
+ v = x.hostname;
971
+ }
972
+ return _hostname.call(this, v, build);
973
+ };
974
+
975
+ // compound accessors
976
+ p.host = function(v, build) {
977
+ if (this._parts.urn) {
978
+ return v === undefined ? '' : this;
979
+ }
980
+
981
+ if (v === undefined) {
982
+ return this._parts.hostname ? URI.buildHost(this._parts) : "";
983
+ } else {
984
+ URI.parseHost(v, this._parts);
985
+ this.build(!build);
986
+ return this;
987
+ }
988
+ };
989
+ p.authority = function(v, build) {
990
+ if (this._parts.urn) {
991
+ return v === undefined ? '' : this;
992
+ }
993
+
994
+ if (v === undefined) {
995
+ return this._parts.hostname ? URI.buildAuthority(this._parts) : "";
996
+ } else {
997
+ URI.parseAuthority(v, this._parts);
998
+ this.build(!build);
999
+ return this;
1000
+ }
1001
+ };
1002
+ p.userinfo = function(v, build) {
1003
+ if (this._parts.urn) {
1004
+ return v === undefined ? '' : this;
1005
+ }
1006
+
1007
+ if (v === undefined) {
1008
+ if (!this._parts.username) {
1009
+ return "";
1010
+ }
1011
+
1012
+ var t = URI.buildUserinfo(this._parts);
1013
+ return t.substring(0, t.length -1);
1014
+ } else {
1015
+ if (v[v.length-1] !== '@') {
1016
+ v += '@';
1017
+ }
1018
+
1019
+ URI.parseUserinfo(v, this._parts);
1020
+ this.build(!build);
1021
+ return this;
1022
+ }
1023
+ };
1024
+ p.resource = function(v, build) {
1025
+ var parts;
1026
+
1027
+ if (v === undefined) {
1028
+ return this.path() + this.search() + this.hash();
1029
+ }
1030
+
1031
+ parts = URI.parse(v);
1032
+ this._parts.path = parts.path;
1033
+ this._parts.query = parts.query;
1034
+ this._parts.fragment = parts.fragment;
1035
+ this.build(!build);
1036
+ return this;
1037
+ };
1038
+
1039
+ // fraction accessors
1040
+ p.subdomain = function(v, build) {
1041
+ if (this._parts.urn) {
1042
+ return v === undefined ? '' : this;
1043
+ }
1044
+
1045
+ // convenience, return "www" from "www.example.org"
1046
+ if (v === undefined) {
1047
+ if (!this._parts.hostname || this.is('IP')) {
1048
+ return "";
1049
+ }
1050
+
1051
+ // grab domain and add another segment
1052
+ var end = this._parts.hostname.length - this.domain().length - 1;
1053
+ return this._parts.hostname.substring(0, end) || "";
1054
+ } else {
1055
+ var e = this._parts.hostname.length - this.domain().length;
1056
+ var sub = this._parts.hostname.substring(0, e);
1057
+ var replace = new RegExp('^' + escapeRegEx(sub));
1058
+
1059
+ if (v && v.charAt(v.length - 1) !== '.') {
1060
+ v += ".";
1061
+ }
1062
+
1063
+ if (v) {
1064
+ URI.ensureValidHostname(v);
1065
+ }
1066
+
1067
+ this._parts.hostname = this._parts.hostname.replace(replace, v);
1068
+ this.build(!build);
1069
+ return this;
1070
+ }
1071
+ };
1072
+ p.domain = function(v, build) {
1073
+ if (this._parts.urn) {
1074
+ return v === undefined ? '' : this;
1075
+ }
1076
+
1077
+ if (typeof v === 'boolean') {
1078
+ build = v;
1079
+ v = undefined;
1080
+ }
1081
+
1082
+ // convenience, return "example.org" from "www.example.org"
1083
+ if (v === undefined) {
1084
+ if (!this._parts.hostname || this.is('IP')) {
1085
+ return "";
1086
+ }
1087
+
1088
+ // if hostname consists of 1 or 2 segments, it must be the domain
1089
+ var t = this._parts.hostname.match(/\./g);
1090
+ if (t && t.length < 2) {
1091
+ return this._parts.hostname;
1092
+ }
1093
+
1094
+ // grab tld and add another segment
1095
+ var end = this._parts.hostname.length - this.tld(build).length - 1;
1096
+ end = this._parts.hostname.lastIndexOf('.', end -1) + 1;
1097
+ return this._parts.hostname.substring(end) || "";
1098
+ } else {
1099
+ if (!v) {
1100
+ throw new TypeError("cannot set domain empty");
1101
+ }
1102
+
1103
+ URI.ensureValidHostname(v);
1104
+
1105
+ if (!this._parts.hostname || this.is('IP')) {
1106
+ this._parts.hostname = v;
1107
+ } else {
1108
+ var replace = new RegExp(escapeRegEx(this.domain()) + "$");
1109
+ this._parts.hostname = this._parts.hostname.replace(replace, v);
1110
+ }
1111
+
1112
+ this.build(!build);
1113
+ return this;
1114
+ }
1115
+ };
1116
+ p.tld = function(v, build) {
1117
+ if (this._parts.urn) {
1118
+ return v === undefined ? '' : this;
1119
+ }
1120
+
1121
+ if (typeof v === 'boolean') {
1122
+ build = v;
1123
+ v = undefined;
1124
+ }
1125
+
1126
+ // return "org" from "www.example.org"
1127
+ if (v === undefined) {
1128
+ if (!this._parts.hostname || this.is('IP')) {
1129
+ return "";
1130
+ }
1131
+
1132
+ var pos = this._parts.hostname.lastIndexOf('.');
1133
+ var tld = this._parts.hostname.substring(pos + 1);
1134
+
1135
+ if (build !== true && SLD && SLD.list[tld.toLowerCase()]) {
1136
+ return SLD.get(this._parts.hostname) || tld;
1137
+ }
1138
+
1139
+ return tld;
1140
+ } else {
1141
+ var replace;
1142
+
1143
+ if (!v) {
1144
+ throw new TypeError("cannot set TLD empty");
1145
+ } else if (v.match(/[^a-zA-Z0-9-]/)) {
1146
+ if (SLD && SLD.is(v)) {
1147
+ replace = new RegExp(escapeRegEx(this.tld()) + "$");
1148
+ this._parts.hostname = this._parts.hostname.replace(replace, v);
1149
+ } else {
1150
+ throw new TypeError("TLD '" + v + "' contains characters other than [A-Z0-9]");
1151
+ }
1152
+ } else if (!this._parts.hostname || this.is('IP')) {
1153
+ throw new ReferenceError("cannot set TLD on non-domain host");
1154
+ } else {
1155
+ replace = new RegExp(escapeRegEx(this.tld()) + "$");
1156
+ this._parts.hostname = this._parts.hostname.replace(replace, v);
1157
+ }
1158
+
1159
+ this.build(!build);
1160
+ return this;
1161
+ }
1162
+ };
1163
+ p.directory = function(v, build) {
1164
+ if (this._parts.urn) {
1165
+ return v === undefined ? '' : this;
1166
+ }
1167
+
1168
+ if (v === undefined || v === true) {
1169
+ if (!this._parts.path && !this._parts.hostname) {
1170
+ return '';
1171
+ }
1172
+
1173
+ if (this._parts.path === '/') {
1174
+ return '/';
1175
+ }
1176
+
1177
+ var end = this._parts.path.length - this.filename().length - 1;
1178
+ var res = this._parts.path.substring(0, end) || (this._parts.hostname ? "/" : "");
1179
+
1180
+ return v ? URI.decodePath(res) : res;
1181
+
1182
+ } else {
1183
+ var e = this._parts.path.length - this.filename().length;
1184
+ var directory = this._parts.path.substring(0, e);
1185
+ var replace = new RegExp('^' + escapeRegEx(directory));
1186
+
1187
+ // fully qualifier directories begin with a slash
1188
+ if (!this.is('relative')) {
1189
+ if (!v) {
1190
+ v = '/';
1191
+ }
1192
+
1193
+ if (v.charAt(0) !== '/') {
1194
+ v = "/" + v;
1195
+ }
1196
+ }
1197
+
1198
+ // directories always end with a slash
1199
+ if (v && v.charAt(v.length - 1) !== '/') {
1200
+ v += '/';
1201
+ }
1202
+
1203
+ v = URI.recodePath(v);
1204
+ this._parts.path = this._parts.path.replace(replace, v);
1205
+ this.build(!build);
1206
+ return this;
1207
+ }
1208
+ };
1209
+ p.filename = function(v, build) {
1210
+ if (this._parts.urn) {
1211
+ return v === undefined ? '' : this;
1212
+ }
1213
+
1214
+ if (v === undefined || v === true) {
1215
+ if (!this._parts.path || this._parts.path === '/') {
1216
+ return "";
1217
+ }
1218
+
1219
+ var pos = this._parts.path.lastIndexOf('/');
1220
+ var res = this._parts.path.substring(pos+1);
1221
+
1222
+ return v ? URI.decodePathSegment(res) : res;
1223
+ } else {
1224
+ var mutatedDirectory = false;
1225
+
1226
+ if (v.charAt(0) === '/') {
1227
+ v = v.substring(1);
1228
+ }
1229
+
1230
+ if (v.match(/\.?\//)) {
1231
+ mutatedDirectory = true;
1232
+ }
1233
+
1234
+ var replace = new RegExp(escapeRegEx(this.filename()) + "$");
1235
+ v = URI.recodePath(v);
1236
+ this._parts.path = this._parts.path.replace(replace, v);
1237
+
1238
+ if (mutatedDirectory) {
1239
+ this.normalizePath(build);
1240
+ } else {
1241
+ this.build(!build);
1242
+ }
1243
+
1244
+ return this;
1245
+ }
1246
+ };
1247
+ p.suffix = function(v, build) {
1248
+ if (this._parts.urn) {
1249
+ return v === undefined ? '' : this;
1250
+ }
1251
+
1252
+ if (v === undefined || v === true) {
1253
+ if (!this._parts.path || this._parts.path === '/') {
1254
+ return "";
1255
+ }
1256
+
1257
+ var filename = this.filename();
1258
+ var pos = filename.lastIndexOf('.');
1259
+ var s, res;
1260
+
1261
+ if (pos === -1) {
1262
+ return "";
1263
+ }
1264
+
1265
+ // suffix may only contain alnum characters (yup, I made this up.)
1266
+ s = filename.substring(pos+1);
1267
+ res = (/^[a-z0-9%]+$/i).test(s) ? s : "";
1268
+ return v ? URI.decodePathSegment(res) : res;
1269
+ } else {
1270
+ if (v.charAt(0) === '.') {
1271
+ v = v.substring(1);
1272
+ }
1273
+
1274
+ var suffix = this.suffix();
1275
+ var replace;
1276
+
1277
+ if (!suffix) {
1278
+ if (!v) {
1279
+ return this;
1280
+ }
1281
+
1282
+ this._parts.path += '.' + URI.recodePath(v);
1283
+ } else if (!v) {
1284
+ replace = new RegExp(escapeRegEx("." + suffix) + "$");
1285
+ } else {
1286
+ replace = new RegExp(escapeRegEx(suffix) + "$");
1287
+ }
1288
+
1289
+ if (replace) {
1290
+ v = URI.recodePath(v);
1291
+ this._parts.path = this._parts.path.replace(replace, v);
1292
+ }
1293
+
1294
+ this.build(!build);
1295
+ return this;
1296
+ }
1297
+ };
1298
+ p.segment = function(segment, v, build) {
1299
+ var separator = this._parts.urn ? ':' : '/';
1300
+ var path = this.path();
1301
+ var absolute = path.substring(0, 1) === '/';
1302
+ var segments = path.split(separator);
1303
+
1304
+ if (typeof segment !== 'number') {
1305
+ build = v;
1306
+ v = segment;
1307
+ segment = undefined;
1308
+ }
1309
+
1310
+ if (segment !== undefined && typeof segment !== 'number') {
1311
+ throw new Error("Bad segment '" + segment + "', must be 0-based integer");
1312
+ }
1313
+
1314
+ if (absolute) {
1315
+ segments.shift();
1316
+ }
1317
+
1318
+ if (segment < 0) {
1319
+ // allow negative indexes to address from the end
1320
+ segment = Math.max(segments.length + segment, 0);
1321
+ }
1322
+
1323
+ if (v === undefined) {
1324
+ return segment === undefined
1325
+ ? segments
1326
+ : segments[segment];
1327
+ } else if (segment === null || segments[segment] === undefined) {
1328
+ if (isArray(v)) {
1329
+ segments = v;
1330
+ } else if (v || (typeof v === "string" && v.length)) {
1331
+ if (segments[segments.length -1] === "") {
1332
+ // empty trailing elements have to be overwritten
1333
+ // to prefent results such as /foo//bar
1334
+ segments[segments.length -1] = v;
1335
+ } else {
1336
+ segments.push(v);
1337
+ }
1338
+ }
1339
+ } else {
1340
+ if (v || (typeof v === "string" && v.length)) {
1341
+ segments[segment] = v;
1342
+ } else {
1343
+ segments.splice(segment, 1);
1344
+ }
1345
+ }
1346
+
1347
+ if (absolute) {
1348
+ segments.unshift("");
1349
+ }
1350
+
1351
+ return this.path(segments.join(separator), build);
1352
+ };
1353
+
1354
+ // mutating query string
1355
+ var q = p.query;
1356
+ p.query = function(v, build) {
1357
+ if (v === true) {
1358
+ return URI.parseQuery(this._parts.query);
1359
+ } else if (typeof v === "function") {
1360
+ var data = URI.parseQuery(this._parts.query);
1361
+ var result = v.call(this, data);
1362
+ this._parts.query = URI.buildQuery(result || data, this._parts.duplicateQueryParameters);
1363
+ this.build(!build);
1364
+ return this;
1365
+ } else if (v !== undefined && typeof v !== "string") {
1366
+ this._parts.query = URI.buildQuery(v, this._parts.duplicateQueryParameters);
1367
+ this.build(!build);
1368
+ return this;
1369
+ } else {
1370
+ return q.call(this, v, build);
1371
+ }
1372
+ };
1373
+ p.setQuery = function(name, value, build) {
1374
+ var data = URI.parseQuery(this._parts.query);
1375
+
1376
+ if (typeof name === "object") {
1377
+ for (var key in name) {
1378
+ if (hasOwn.call(name, key)) {
1379
+ data[key] = name[key];
1380
+ }
1381
+ }
1382
+ } else if (typeof name === "string") {
1383
+ data[name] = value !== undefined ? value : null;
1384
+ } else {
1385
+ throw new TypeError("URI.addQuery() accepts an object, string as the name parameter");
1386
+ }
1387
+
1388
+ this._parts.query = URI.buildQuery(data, this._parts.duplicateQueryParameters);
1389
+ if (typeof name !== "string") {
1390
+ build = value;
1391
+ }
1392
+
1393
+ this.build(!build);
1394
+ return this;
1395
+ };
1396
+ p.addQuery = function(name, value, build) {
1397
+ var data = URI.parseQuery(this._parts.query);
1398
+ URI.addQuery(data, name, value === undefined ? null : value);
1399
+ this._parts.query = URI.buildQuery(data, this._parts.duplicateQueryParameters);
1400
+ if (typeof name !== "string") {
1401
+ build = value;
1402
+ }
1403
+
1404
+ this.build(!build);
1405
+ return this;
1406
+ };
1407
+ p.removeQuery = function(name, value, build) {
1408
+ var data = URI.parseQuery(this._parts.query);
1409
+ URI.removeQuery(data, name, value);
1410
+ this._parts.query = URI.buildQuery(data, this._parts.duplicateQueryParameters);
1411
+ if (typeof name !== "string") {
1412
+ build = value;
1413
+ }
1414
+
1415
+ this.build(!build);
1416
+ return this;
1417
+ };
1418
+ p.hasQuery = function(name, value, withinArray) {
1419
+ var data = URI.parseQuery(this._parts.query);
1420
+ return URI.hasQuery(data, name, value, withinArray);
1421
+ };
1422
+ p.setSearch = p.setQuery;
1423
+ p.addSearch = p.addQuery;
1424
+ p.removeSearch = p.removeQuery;
1425
+ p.hasSearch = p.hasQuery;
1426
+
1427
+ // sanitizing URLs
1428
+ p.normalize = function() {
1429
+ if (this._parts.urn) {
1430
+ return this
1431
+ .normalizeProtocol(false)
1432
+ .normalizeQuery(false)
1433
+ .normalizeFragment(false)
1434
+ .build();
1435
+ }
1436
+
1437
+ return this
1438
+ .normalizeProtocol(false)
1439
+ .normalizeHostname(false)
1440
+ .normalizePort(false)
1441
+ .normalizePath(false)
1442
+ .normalizeQuery(false)
1443
+ .normalizeFragment(false)
1444
+ .build();
1445
+ };
1446
+ p.normalizeProtocol = function(build) {
1447
+ if (typeof this._parts.protocol === "string") {
1448
+ this._parts.protocol = this._parts.protocol.toLowerCase();
1449
+ this.build(!build);
1450
+ }
1451
+
1452
+ return this;
1453
+ };
1454
+ p.normalizeHostname = function(build) {
1455
+ if (this._parts.hostname) {
1456
+ if (this.is('IDN') && punycode) {
1457
+ this._parts.hostname = punycode.toASCII(this._parts.hostname);
1458
+ } else if (this.is('IPv6') && IPv6) {
1459
+ this._parts.hostname = IPv6.best(this._parts.hostname);
1460
+ }
1461
+
1462
+ this._parts.hostname = this._parts.hostname.toLowerCase();
1463
+ this.build(!build);
1464
+ }
1465
+
1466
+ return this;
1467
+ };
1468
+ p.normalizePort = function(build) {
1469
+ // remove port of it's the protocol's default
1470
+ if (typeof this._parts.protocol === "string" && this._parts.port === URI.defaultPorts[this._parts.protocol]) {
1471
+ this._parts.port = null;
1472
+ this.build(!build);
1473
+ }
1474
+
1475
+ return this;
1476
+ };
1477
+ p.normalizePath = function(build) {
1478
+ if (this._parts.urn) {
1479
+ return this;
1480
+ }
1481
+
1482
+ if (!this._parts.path || this._parts.path === '/') {
1483
+ return this;
1484
+ }
1485
+
1486
+ var _was_relative;
1487
+ var _was_relative_prefix;
1488
+ var _path = this._parts.path;
1489
+ var _parent, _pos;
1490
+
1491
+ // handle relative paths
1492
+ if (_path.charAt(0) !== '/') {
1493
+ if (_path.charAt(0) === '.') {
1494
+ _was_relative_prefix = _path.substring(0, _path.indexOf('/'));
1495
+ }
1496
+ _was_relative = true;
1497
+ _path = '/' + _path;
1498
+ }
1499
+ // resolve simples
1500
+ _path = _path.replace(/(\/(\.\/)+)|\/{2,}/g, '/');
1501
+ // resolve parents
1502
+ while (true) {
1503
+ _parent = _path.indexOf('/../');
1504
+ if (_parent === -1) {
1505
+ // no more ../ to resolve
1506
+ break;
1507
+ } else if (_parent === 0) {
1508
+ // top level cannot be relative...
1509
+ _path = _path.substring(3);
1510
+ break;
1511
+ }
1512
+
1513
+ _pos = _path.substring(0, _parent).lastIndexOf('/');
1514
+ if (_pos === -1) {
1515
+ _pos = _parent;
1516
+ }
1517
+ _path = _path.substring(0, _pos) + _path.substring(_parent + 3);
1518
+ }
1519
+ // revert to relative
1520
+ if (_was_relative && this.is('relative')) {
1521
+ _path = _path.substring(1);
1522
+ }
1523
+
1524
+ _path = URI.recodePath(_path);
1525
+ this._parts.path = _path;
1526
+ this.build(!build);
1527
+ return this;
1528
+ };
1529
+ p.normalizePathname = p.normalizePath;
1530
+ p.normalizeQuery = function(build) {
1531
+ if (typeof this._parts.query === "string") {
1532
+ if (!this._parts.query.length) {
1533
+ this._parts.query = null;
1534
+ } else {
1535
+ this.query(URI.parseQuery(this._parts.query));
1536
+ }
1537
+
1538
+ this.build(!build);
1539
+ }
1540
+
1541
+ return this;
1542
+ };
1543
+ p.normalizeFragment = function(build) {
1544
+ if (!this._parts.fragment) {
1545
+ this._parts.fragment = null;
1546
+ this.build(!build);
1547
+ }
1548
+
1549
+ return this;
1550
+ };
1551
+ p.normalizeSearch = p.normalizeQuery;
1552
+ p.normalizeHash = p.normalizeFragment;
1553
+
1554
+ p.iso8859 = function() {
1555
+ // expect unicode input, iso8859 output
1556
+ var e = URI.encode;
1557
+ var d = URI.decode;
1558
+
1559
+ URI.encode = escape;
1560
+ URI.decode = decodeURIComponent;
1561
+ this.normalize();
1562
+ URI.encode = e;
1563
+ URI.decode = d;
1564
+ return this;
1565
+ };
1566
+
1567
+ p.unicode = function() {
1568
+ // expect iso8859 input, unicode output
1569
+ var e = URI.encode;
1570
+ var d = URI.decode;
1571
+
1572
+ URI.encode = strictEncodeURIComponent;
1573
+ URI.decode = unescape;
1574
+ this.normalize();
1575
+ URI.encode = e;
1576
+ URI.decode = d;
1577
+ return this;
1578
+ };
1579
+
1580
+ p.readable = function() {
1581
+ var uri = this.clone();
1582
+ // removing username, password, because they shouldn't be displayed according to RFC 3986
1583
+ uri.username("").password("").normalize();
1584
+ var t = '';
1585
+ if (uri._parts.protocol) {
1586
+ t += uri._parts.protocol + '://';
1587
+ }
1588
+
1589
+ if (uri._parts.hostname) {
1590
+ if (uri.is('punycode') && punycode) {
1591
+ t += punycode.toUnicode(uri._parts.hostname);
1592
+ if (uri._parts.port) {
1593
+ t += ":" + uri._parts.port;
1594
+ }
1595
+ } else {
1596
+ t += uri.host();
1597
+ }
1598
+ }
1599
+
1600
+ if (uri._parts.hostname && uri._parts.path && uri._parts.path.charAt(0) !== '/') {
1601
+ t += '/';
1602
+ }
1603
+
1604
+ t += uri.path(true);
1605
+ if (uri._parts.query) {
1606
+ var q = '';
1607
+ for (var i = 0, qp = uri._parts.query.split('&'), l = qp.length; i < l; i++) {
1608
+ var kv = (qp[i] || "").split('=');
1609
+ q += '&' + URI.decodeQuery(kv[0])
1610
+ .replace(/&/g, '%26');
1611
+
1612
+ if (kv[1] !== undefined) {
1613
+ q += "=" + URI.decodeQuery(kv[1])
1614
+ .replace(/&/g, '%26');
1615
+ }
1616
+ }
1617
+ t += '?' + q.substring(1);
1618
+ }
1619
+
1620
+ t += uri.hash();
1621
+ return t;
1622
+ };
1623
+
1624
+ // resolving relative and absolute URLs
1625
+ p.absoluteTo = function(base) {
1626
+ var resolved = this.clone();
1627
+ var properties = ['protocol', 'username', 'password', 'hostname', 'port'];
1628
+ var basedir, i, p;
1629
+
1630
+ if (this._parts.urn) {
1631
+ throw new Error('URNs do not have any generally defined hierachical components');
1632
+ }
1633
+
1634
+ if (!(base instanceof URI)) {
1635
+ base = new URI(base);
1636
+ }
1637
+
1638
+ if (!resolved._parts.protocol) {
1639
+ resolved._parts.protocol = base._parts.protocol;
1640
+ }
1641
+
1642
+ if (this._parts.hostname) {
1643
+ return resolved;
1644
+ }
1645
+
1646
+ for (i = 0, p; p = properties[i]; i++) {
1647
+ resolved._parts[p] = base._parts[p];
1648
+ }
1649
+
1650
+ properties = ['query', 'path'];
1651
+ for (i = 0, p; p = properties[i]; i++) {
1652
+ if (!resolved._parts[p] && base._parts[p]) {
1653
+ resolved._parts[p] = base._parts[p];
1654
+ }
1655
+ }
1656
+
1657
+ if (resolved.path().charAt(0) !== '/') {
1658
+ basedir = base.directory();
1659
+ resolved._parts.path = (basedir ? (basedir + '/') : '') + resolved._parts.path;
1660
+ resolved.normalizePath();
1661
+ }
1662
+
1663
+ resolved.build();
1664
+ return resolved;
1665
+ };
1666
+ p.relativeTo = function(base) {
1667
+ var relative = this.clone();
1668
+ var properties = ['protocol', 'username', 'password', 'hostname', 'port'];
1669
+ var common, _base, _this, _base_diff, _this_diff;
1670
+
1671
+ if (relative._parts.urn) {
1672
+ throw new Error('URNs do not have any generally defined hierachical components');
1673
+ }
1674
+
1675
+ if (!(base instanceof URI)) {
1676
+ base = new URI(base);
1677
+ }
1678
+
1679
+ if (relative.path().charAt(0) !== '/' || base.path().charAt(0) !== '/') {
1680
+ throw new Error('Cannot calculate common path from non-relative URLs');
1681
+ }
1682
+
1683
+ // determine common sub path
1684
+ common = URI.commonPath(relative.path(), base.path());
1685
+
1686
+ // relative paths don't have authority
1687
+ for (var i = 0, p; p = properties[i]; i++) {
1688
+ relative._parts[p] = null;
1689
+ }
1690
+
1691
+ // no relation if there's nothing in common
1692
+ if (common === '/') {
1693
+ return relative;
1694
+ } else if (!common) {
1695
+ // there's absolutely nothing in common here
1696
+ return this.clone();
1697
+ }
1698
+
1699
+ _base = base.directory();
1700
+ _this = relative.directory();
1701
+
1702
+ // base and this are on the same level
1703
+ if (_base === _this) {
1704
+ relative._parts.path = relative.filename();
1705
+ return relative.build();
1706
+ }
1707
+
1708
+ _base_diff = _base.substring(common.length);
1709
+ _this_diff = _this.substring(common.length);
1710
+
1711
+ // this is a descendant of base
1712
+ if (_base + '/' === common) {
1713
+ if (_this_diff) {
1714
+ _this_diff += '/';
1715
+ }
1716
+
1717
+ relative._parts.path = _this_diff + relative.filename();
1718
+ return relative.build();
1719
+ }
1720
+
1721
+ // this is a descendant of base
1722
+ var parents = '../';
1723
+ var _common = new RegExp('^' + escapeRegEx(common));
1724
+ var _parents = _base.replace(_common, '/').match(/\//g).length -1;
1725
+
1726
+ while (_parents--) {
1727
+ parents += '../';
1728
+ }
1729
+
1730
+ relative._parts.path = relative._parts.path.replace(_common, parents);
1731
+ return relative.build();
1732
+ };
1733
+
1734
+ // comparing URIs
1735
+ p.equals = function(uri) {
1736
+ var one = this.clone();
1737
+ var two = new URI(uri);
1738
+ var one_map = {};
1739
+ var two_map = {};
1740
+ var checked = {};
1741
+ var one_query, two_query, key;
1742
+
1743
+ one.normalize();
1744
+ two.normalize();
1745
+
1746
+ // exact match
1747
+ if (one.toString() === two.toString()) {
1748
+ return true;
1749
+ }
1750
+
1751
+ // extract query string
1752
+ one_query = one.query();
1753
+ two_query = two.query();
1754
+ one.query("");
1755
+ two.query("");
1756
+
1757
+ // definitely not equal if not even non-query parts match
1758
+ if (one.toString() !== two.toString()) {
1759
+ return false;
1760
+ }
1761
+
1762
+ // query parameters have the same length, even if they're permutated
1763
+ if (one_query.length !== two_query.length) {
1764
+ return false;
1765
+ }
1766
+
1767
+ one_map = URI.parseQuery(one_query);
1768
+ two_map = URI.parseQuery(two_query);
1769
+
1770
+ for (key in one_map) {
1771
+ if (hasOwn.call(one_map, key)) {
1772
+ if (!isArray(one_map[key])) {
1773
+ if (one_map[key] !== two_map[key]) {
1774
+ return false;
1775
+ }
1776
+ } else if (!arraysEqual(one_map[key], two_map[key])) {
1777
+ return false;
1778
+ }
1779
+
1780
+ checked[key] = true;
1781
+ }
1782
+ }
1783
+
1784
+ for (key in two_map) {
1785
+ if (hasOwn.call(two_map, key)) {
1786
+ if (!checked[key]) {
1787
+ // two contains a parameter not present in one
1788
+ return false;
1789
+ }
1790
+ }
1791
+ }
1792
+
1793
+ return true;
1794
+ };
1795
+
1796
+ // state
1797
+ p.duplicateQueryParameters = function(v) {
1798
+ this._parts.duplicateQueryParameters = !!v;
1799
+ return this;
1800
+ };
1801
+
1802
+ return URI;
1803
+ }));