fannypack 0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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
+ }));