79d992e827b5a683401be923eb63665b7ea45ba8
[platform/framework/web/crosswalk-tizen.git] /
1 /** @license
2  * crossroads <http://millermedeiros.github.com/crossroads.js/>
3  * License: MIT
4  * Author: Miller Medeiros
5  * Version: 0.11.0 (2012/10/31 21:44)
6  */
7
8 (function (define) {
9 define(['signals'], function (signals) {
10
11     var crossroads,
12         _hasOptionalGroupBug,
13         UNDEF;
14
15     // Helpers -----------
16     //====================
17
18     // IE 7-8 capture optional groups as empty strings while other browsers
19     // capture as `undefined`
20     _hasOptionalGroupBug = (/t(.+)?/).exec('t')[1] === '';
21
22     function arrayIndexOf(arr, val) {
23         if (arr.indexOf) {
24             return arr.indexOf(val);
25         } else {
26             //Array.indexOf doesn't work on IE 6-7
27             var n = arr.length;
28             while (n--) {
29                 if (arr[n] === val) {
30                     return n;
31                 }
32             }
33             return -1;
34         }
35     }
36
37     function arrayRemove(arr, item) {
38         var i = arrayIndexOf(arr, item);
39         if (i !== -1) {
40             arr.splice(i, 1);
41         }
42     }
43
44     function isKind(val, kind) {
45         return '[object '+ kind +']' === Object.prototype.toString.call(val);
46     }
47
48     function isRegExp(val) {
49         return isKind(val, 'RegExp');
50     }
51
52     function isArray(val) {
53         return isKind(val, 'Array');
54     }
55
56     function isFunction(val) {
57         return typeof val === 'function';
58     }
59
60     //borrowed from AMD-utils
61     function typecastValue(val) {
62         var r;
63         if (val === null || val === 'null') {
64             r = null;
65         } else if (val === 'true') {
66             r = true;
67         } else if (val === 'false') {
68             r = false;
69         } else if (val === UNDEF || val === 'undefined') {
70             r = UNDEF;
71         } else if (val === '' || isNaN(val)) {
72             //isNaN('') returns false
73             r = val;
74         } else {
75             //parseFloat(null || '') returns NaN
76             r = parseFloat(val);
77         }
78         return r;
79     }
80
81     function typecastArrayValues(values) {
82         var n = values.length,
83             result = [];
84         while (n--) {
85             result[n] = typecastValue(values[n]);
86         }
87         return result;
88     }
89
90     //borrowed from AMD-Utils
91     function decodeQueryString(str, shouldTypecast) {
92         var queryArr = (str || '').replace('?', '').split('&'),
93             n = queryArr.length,
94             obj = {},
95             item, val;
96         while (n--) {
97             item = queryArr[n].split('=');
98             val = shouldTypecast ? typecastValue(item[1]) : item[1];
99             obj[item[0]] = (typeof val === 'string')? decodeURIComponent(val) : val;
100         }
101         return obj;
102     }
103
104
105     // Crossroads --------
106     //====================
107
108     /**
109      * @constructor
110      */
111     function Crossroads() {
112         this.bypassed = new signals.Signal();
113         this.routed = new signals.Signal();
114         this._routes = [];
115         this._prevRoutes = [];
116         this._piped = [];
117         this.resetState();
118     }
119
120     Crossroads.prototype = {
121
122         greedy : false,
123
124         greedyEnabled : true,
125
126         ignoreCase : true,
127
128         ignoreState : false,
129
130         shouldTypecast : false,
131
132         normalizeFn : null,
133
134         resetState : function(){
135             this._prevRoutes.length = 0;
136             this._prevMatchedRequest = null;
137             this._prevBypassedRequest = null;
138         },
139
140         create : function () {
141             return new Crossroads();
142         },
143
144         addRoute : function (pattern, callback, priority) {
145             var route = new Route(pattern, callback, priority, this);
146             this._sortedInsert(route);
147             return route;
148         },
149
150         removeRoute : function (route) {
151             arrayRemove(this._routes, route);
152             route._destroy();
153         },
154
155         removeAllRoutes : function () {
156             var n = this.getNumRoutes();
157             while (n--) {
158                 this._routes[n]._destroy();
159             }
160             this._routes.length = 0;
161         },
162
163         parse : function (request, defaultArgs) {
164             request = request || '';
165             defaultArgs = defaultArgs || [];
166
167             // should only care about different requests if ignoreState isn't true
168             if ( !this.ignoreState &&
169                 (request === this._prevMatchedRequest ||
170                  request === this._prevBypassedRequest) ) {
171                 return;
172             }
173
174             var routes = this._getMatchedRoutes(request),
175                 i = 0,
176                 n = routes.length,
177                 cur;
178
179             if (n) {
180                 this._prevMatchedRequest = request;
181
182                 this._notifyPrevRoutes(routes, request);
183                 this._prevRoutes = routes;
184                 //should be incremental loop, execute routes in order
185                 while (i < n) {
186                     cur = routes[i];
187                     cur.route.matched.dispatch.apply(cur.route.matched, defaultArgs.concat(cur.params));
188                     cur.isFirst = !i;
189                     this.routed.dispatch.apply(this.routed, defaultArgs.concat([request, cur]));
190                     i += 1;
191                 }
192             } else {
193                 this._prevBypassedRequest = request;
194                 this.bypassed.dispatch.apply(this.bypassed, defaultArgs.concat([request]));
195             }
196
197             this._pipeParse(request, defaultArgs);
198         },
199
200         _notifyPrevRoutes : function(matchedRoutes, request) {
201             var i = 0, prev;
202             while (prev = this._prevRoutes[i++]) {
203                 //check if switched exist since route may be disposed
204                 if(prev.route.switched && this._didSwitch(prev.route, matchedRoutes)) {
205                     prev.route.switched.dispatch(request);
206                 }
207             }
208         },
209
210         _didSwitch : function (route, matchedRoutes){
211             var matched,
212                 i = 0;
213             while (matched = matchedRoutes[i++]) {
214                 // only dispatch switched if it is going to a different route
215                 if (matched.route === route) {
216                     return false;
217                 }
218             }
219             return true;
220         },
221
222         _pipeParse : function(request, defaultArgs) {
223             var i = 0, route;
224             while (route = this._piped[i++]) {
225                 route.parse(request, defaultArgs);
226             }
227         },
228
229         getNumRoutes : function () {
230             return this._routes.length;
231         },
232
233         _sortedInsert : function (route) {
234             //simplified insertion sort
235             var routes = this._routes,
236                 n = routes.length;
237             do { --n; } while (routes[n] && route._priority <= routes[n]._priority);
238             routes.splice(n+1, 0, route);
239         },
240
241         _getMatchedRoutes : function (request) {
242             var res = [],
243                 routes = this._routes,
244                 n = routes.length,
245                 route;
246             //should be decrement loop since higher priorities are added at the end of array
247             while (route = routes[--n]) {
248                 if ((!res.length || this.greedy || route.greedy) && route.match(request)) {
249                     res.push({
250                         route : route,
251                         params : route._getParamsArray(request)
252                     });
253                 }
254                 if (!this.greedyEnabled && res.length) {
255                     break;
256                 }
257             }
258             return res;
259         },
260
261         pipe : function (otherRouter) {
262             this._piped.push(otherRouter);
263         },
264
265         unpipe : function (otherRouter) {
266             arrayRemove(this._piped, otherRouter);
267         },
268
269         toString : function () {
270             return '[crossroads numRoutes:'+ this.getNumRoutes() +']';
271         }
272     };
273
274     //"static" instance
275     crossroads = new Crossroads();
276     crossroads.VERSION = '0.11.0';
277
278     crossroads.NORM_AS_ARRAY = function (req, vals) {
279         return [vals.vals_];
280     };
281
282     crossroads.NORM_AS_OBJECT = function (req, vals) {
283         return [vals];
284     };
285
286
287     // Route --------------
288     //=====================
289
290     /**
291      * @constructor
292      */
293     function Route(pattern, callback, priority, router) {
294         var isRegexPattern = isRegExp(pattern),
295             patternLexer = crossroads.patternLexer;
296         this._router = router;
297         this._pattern = pattern;
298         this._paramsIds = isRegexPattern? null : patternLexer.getParamIds(pattern);
299         this._optionalParamsIds = isRegexPattern? null : patternLexer.getOptionalParamsIds(pattern);
300         this._matchRegexp = isRegexPattern? pattern : patternLexer.compilePattern(pattern, router.ignoreCase);
301         this.matched = new signals.Signal();
302         this.switched = new signals.Signal();
303         if (callback) {
304             this.matched.add(callback);
305         }
306         this._priority = priority || 0;
307     }
308
309     Route.prototype = {
310
311         greedy : false,
312
313         rules : void(0),
314
315         match : function (request) {
316             request = request || '';
317             return this._matchRegexp.test(request) && this._validateParams(request); //validate params even if regexp because of `request_` rule.
318         },
319
320         _validateParams : function (request) {
321             var rules = this.rules,
322                 values = this._getParamsObject(request),
323                 key;
324             for (key in rules) {
325                 // normalize_ isn't a validation rule... (#39)
326                 if(key !== 'normalize_' && rules.hasOwnProperty(key) && ! this._isValidParam(request, key, values)){
327                     return false;
328                 }
329             }
330             return true;
331         },
332
333         _isValidParam : function (request, prop, values) {
334             var validationRule = this.rules[prop],
335                 val = values[prop],
336                 isValid = false,
337                 isQuery = (prop.indexOf('?') === 0);
338
339             if (val == null && this._optionalParamsIds && arrayIndexOf(this._optionalParamsIds, prop) !== -1) {
340                 isValid = true;
341             }
342             else if (isRegExp(validationRule)) {
343                 if (isQuery) {
344                     val = values[prop +'_']; //use raw string
345                 }
346                 isValid = validationRule.test(val);
347             }
348             else if (isArray(validationRule)) {
349                 if (isQuery) {
350                     val = values[prop +'_']; //use raw string
351                 }
352                 isValid = this._isValidArrayRule(validationRule, val);
353             }
354             else if (isFunction(validationRule)) {
355                 isValid = validationRule(val, request, values);
356             }
357
358             return isValid; //fail silently if validationRule is from an unsupported type
359         },
360
361         _isValidArrayRule : function (arr, val) {
362             if (! this._router.ignoreCase) {
363                 return arrayIndexOf(arr, val) !== -1;
364             }
365
366             if (typeof val === 'string') {
367                 val = val.toLowerCase();
368             }
369
370             var n = arr.length,
371                 item,
372                 compareVal;
373
374             while (n--) {
375                 item = arr[n];
376                 compareVal = (typeof item === 'string')? item.toLowerCase() : item;
377                 if (compareVal === val) {
378                     return true;
379                 }
380             }
381             return false;
382         },
383
384         _getParamsObject : function (request) {
385             var shouldTypecast = this._router.shouldTypecast,
386                 values = crossroads.patternLexer.getParamValues(request, this._matchRegexp, shouldTypecast),
387                 o = {},
388                 n = values.length,
389                 param, val;
390             while (n--) {
391                 val = values[n];
392                 if (this._paramsIds) {
393                     param = this._paramsIds[n];
394                     if (param.indexOf('?') === 0 && val) {
395                         //make a copy of the original string so array and
396                         //RegExp validation can be applied properly
397                         o[param +'_'] = val;
398                         //update vals_ array as well since it will be used
399                         //during dispatch
400                         val = decodeQueryString(val, shouldTypecast);
401                         values[n] = val;
402                     }
403                     // IE will capture optional groups as empty strings while other
404                     // browsers will capture `undefined` so normalize behavior.
405                     // see: #gh-58, #gh-59, #gh-60
406                     if ( _hasOptionalGroupBug && val === '' && arrayIndexOf(this._optionalParamsIds, param) !== -1 ) {
407                         val = void(0);
408                         values[n] = val;
409                     }
410                     o[param] = val;
411                 }
412                 //alias to paths and for RegExp pattern
413                 o[n] = val;
414             }
415             o.request_ = shouldTypecast? typecastValue(request) : request;
416             o.vals_ = values;
417             return o;
418         },
419
420         _getParamsArray : function (request) {
421             var norm = this.rules? this.rules.normalize_ : null,
422                 params;
423             norm = norm || this._router.normalizeFn; // default normalize
424             if (norm && isFunction(norm)) {
425                 params = norm(request, this._getParamsObject(request));
426             } else {
427                 params = this._getParamsObject(request).vals_;
428             }
429             return params;
430         },
431
432         interpolate : function(replacements) {
433             var str = crossroads.patternLexer.interpolate(this._pattern, replacements);
434             if (! this._validateParams(str) ) {
435                 throw new Error('Generated string doesn\'t validate against `Route.rules`.');
436             }
437             return str;
438         },
439
440         dispose : function () {
441             this._router.removeRoute(this);
442         },
443
444         _destroy : function () {
445             this.matched.dispose();
446             this.switched.dispose();
447             this.matched = this.switched = this._pattern = this._matchRegexp = null;
448         },
449
450         toString : function () {
451             return '[Route pattern:"'+ this._pattern +'", numListeners:'+ this.matched.getNumListeners() +']';
452         }
453
454     };
455
456
457
458     // Pattern Lexer ------
459     //=====================
460
461     crossroads.patternLexer = (function () {
462
463         var
464             //match chars that should be escaped on string regexp
465             ESCAPE_CHARS_REGEXP = /[\\.+*?\^$\[\](){}\/'#]/g,
466
467             //trailing slashes (begin/end of string)
468             LOOSE_SLASHES_REGEXP = /^\/|\/$/g,
469             LEGACY_SLASHES_REGEXP = /\/$/g,
470
471             //params - everything between `{ }` or `: :`
472             PARAMS_REGEXP = /(?:\{|:)([^}:]+)(?:\}|:)/g,
473
474             //used to save params during compile (avoid escaping things that
475             //shouldn't be escaped).
476             TOKENS = {
477                 'OS' : {
478                     //optional slashes
479                     //slash between `::` or `}:` or `\w:` or `:{?` or `}{?` or `\w{?`
480                     rgx : /([:}]|\w(?=\/))\/?(:|(?:\{\?))/g,
481                     save : '$1{{id}}$2',
482                     res : '\\/?'
483                 },
484                 'RS' : {
485                     //required slashes
486                     //used to insert slash between `:{` and `}{`
487                     rgx : /([:}])\/?(\{)/g,
488                     save : '$1{{id}}$2',
489                     res : '\\/'
490                 },
491                 'RQ' : {
492                     //required query string - everything in between `{? }`
493                     rgx : /\{\?([^}]+)\}/g,
494                     //everything from `?` till `#` or end of string
495                     res : '\\?([^#]+)'
496                 },
497                 'OQ' : {
498                     //optional query string - everything in between `:? :`
499                     rgx : /:\?([^:]+):/g,
500                     //everything from `?` till `#` or end of string
501                     res : '(?:\\?([^#]*))?'
502                 },
503                 'OR' : {
504                     //optional rest - everything in between `: *:`
505                     rgx : /:([^:]+)\*:/g,
506                     res : '(.*)?' // optional group to avoid passing empty string as captured
507                 },
508                 'RR' : {
509                     //rest param - everything in between `{ *}`
510                     rgx : /\{([^}]+)\*\}/g,
511                     res : '(.+)'
512                 },
513                 // required/optional params should come after rest segments
514                 'RP' : {
515                     //required params - everything between `{ }`
516                     rgx : /\{([^}]+)\}/g,
517                     res : '([^\\/?]+)'
518                 },
519                 'OP' : {
520                     //optional params - everything between `: :`
521                     rgx : /:([^:]+):/g,
522                     res : '([^\\/?]+)?\/?'
523                 }
524             },
525
526             LOOSE_SLASH = 1,
527             STRICT_SLASH = 2,
528             LEGACY_SLASH = 3,
529
530             _slashMode = LOOSE_SLASH;
531
532
533         function precompileTokens(){
534             var key, cur;
535             for (key in TOKENS) {
536                 if (TOKENS.hasOwnProperty(key)) {
537                     cur = TOKENS[key];
538                     cur.id = '__CR_'+ key +'__';
539                     cur.save = ('save' in cur)? cur.save.replace('{{id}}', cur.id) : cur.id;
540                     cur.rRestore = new RegExp(cur.id, 'g');
541                 }
542             }
543         }
544         precompileTokens();
545
546
547         function captureVals(regex, pattern) {
548             var vals = [], match;
549             // very important to reset lastIndex since RegExp can have "g" flag
550             // and multiple runs might affect the result, specially if matching
551             // same string multiple times on IE 7-8
552             regex.lastIndex = 0;
553             while (match = regex.exec(pattern)) {
554                 vals.push(match[1]);
555             }
556             return vals;
557         }
558
559         function getParamIds(pattern) {
560             return captureVals(PARAMS_REGEXP, pattern);
561         }
562
563         function getOptionalParamsIds(pattern) {
564             return captureVals(TOKENS.OP.rgx, pattern);
565         }
566
567         function compilePattern(pattern, ignoreCase) {
568             pattern = pattern || '';
569
570             if(pattern){
571                 if (_slashMode === LOOSE_SLASH) {
572                     pattern = pattern.replace(LOOSE_SLASHES_REGEXP, '');
573                 }
574                 else if (_slashMode === LEGACY_SLASH) {
575                     pattern = pattern.replace(LEGACY_SLASHES_REGEXP, '');
576                 }
577
578                 //save tokens
579                 pattern = replaceTokens(pattern, 'rgx', 'save');
580                 //regexp escape
581                 pattern = pattern.replace(ESCAPE_CHARS_REGEXP, '\\$&');
582                 //restore tokens
583                 pattern = replaceTokens(pattern, 'rRestore', 'res');
584
585                 if (_slashMode === LOOSE_SLASH) {
586                     pattern = '\\/?'+ pattern;
587                 }
588             }
589
590             if (_slashMode !== STRICT_SLASH) {
591                 //single slash is treated as empty and end slash is optional
592                 pattern += '\\/?';
593             }
594             return new RegExp('^'+ pattern + '$', ignoreCase? 'i' : '');
595         }
596
597         function replaceTokens(pattern, regexpName, replaceName) {
598             var cur, key;
599             for (key in TOKENS) {
600                 if (TOKENS.hasOwnProperty(key)) {
601                     cur = TOKENS[key];
602                     pattern = pattern.replace(cur[regexpName], cur[replaceName]);
603                 }
604             }
605             return pattern;
606         }
607
608         function getParamValues(request, regexp, shouldTypecast) {
609             var vals = regexp.exec(request);
610             if (vals) {
611                 vals.shift();
612                 if (shouldTypecast) {
613                     vals = typecastArrayValues(vals);
614                 }
615             }
616             return vals;
617         }
618
619         function interpolate(pattern, replacements) {
620             if (typeof pattern !== 'string') {
621                 throw new Error('Route pattern should be a string.');
622             }
623
624             var replaceFn = function(match, prop){
625                     var val;
626                     if (prop in replacements) {
627                         // make sure value is a string see #gh-54
628                         val = String(replacements[prop]);
629                         if (match.indexOf('*') === -1 && val.indexOf('/') !== -1) {
630                             throw new Error('Invalid value "'+ val +'" for segment "'+ match +'".');
631                         }
632                     }
633                     else if (match.indexOf('{') !== -1) {
634                         throw new Error('The segment '+ match +' is required.');
635                     }
636                     else {
637                         val = '';
638                     }
639                     return val;
640                 };
641
642             if (! TOKENS.OS.trail) {
643                 TOKENS.OS.trail = new RegExp('(?:'+ TOKENS.OS.id +')+$');
644             }
645
646             return pattern
647                         .replace(TOKENS.OS.rgx, TOKENS.OS.save)
648                         .replace(PARAMS_REGEXP, replaceFn)
649                         .replace(TOKENS.OS.trail, '') // remove trailing
650                         .replace(TOKENS.OS.rRestore, '/'); // add slash between segments
651         }
652
653         //API
654         return {
655             strict : function(){
656                 _slashMode = STRICT_SLASH;
657             },
658             loose : function(){
659                 _slashMode = LOOSE_SLASH;
660             },
661             legacy : function(){
662                 _slashMode = LEGACY_SLASH;
663             },
664             getParamIds : getParamIds,
665             getOptionalParamsIds : getOptionalParamsIds,
666             getParamValues : getParamValues,
667             compilePattern : compilePattern,
668             interpolate : interpolate
669         };
670
671     }());
672
673
674     return crossroads;
675 });
676 }(typeof define === 'function' && define.amd ? define : function (deps, factory) {
677     if (typeof module !== 'undefined' && module.exports) { //Node
678         module.exports = factory(require(deps[0]));
679     } else {
680         /*jshint sub:true */
681         window['crossroads'] = factory(window[deps[0]]);
682     }
683 }));