pretender-rails 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,367 @@
1
+ (function(window){
2
+
3
+ var isNode = typeof process !== 'undefined' && process.toString() === '[object process]';
4
+ var RouteRecognizer = isNode ? require('route-recognizer')['default'] : window.RouteRecognizer;
5
+ var FakeXMLHttpRequest = isNode ? require('./bower_components/FakeXMLHttpRequest/fake_xml_http_request') : window.FakeXMLHttpRequest;
6
+ var slice = [].slice;
7
+
8
+
9
+ /**
10
+ * parseURL - decompose a URL into its parts
11
+ * @param {String} url a URL
12
+ * @return {Object} parts of the URL, including the following
13
+ *
14
+ * 'https://www.yahoo.com:1234/mypage?test=yes#abc'
15
+ *
16
+ * {
17
+ * host: 'www.yahoo.com:1234',
18
+ * protocol: 'https:',
19
+ * search: '?test=yes',
20
+ * hash: '#abc',
21
+ * href: 'https://www.yahoo.com:1234/mypage?test=yes#abc',
22
+ * pathname: '/mypage',
23
+ * fullpath: '/mypage?test=yes'
24
+ * }
25
+ */
26
+ function parseURL(url) {
27
+ // TODO: something for when document isn't present... #yolo
28
+ var anchor = document.createElement('a');
29
+ anchor.href = url;
30
+ anchor.fullpath = anchor.pathname + (anchor.search || '') + (anchor.hash || '');
31
+ return anchor;
32
+ }
33
+
34
+
35
+ /**
36
+ * Registry
37
+ *
38
+ * A registry is a map of HTTP verbs to route recognizers.
39
+ */
40
+
41
+ function Registry(host) {
42
+ this.verbs = {
43
+ GET: new RouteRecognizer(),
44
+ PUT: new RouteRecognizer(),
45
+ POST: new RouteRecognizer(),
46
+ DELETE: new RouteRecognizer(),
47
+ PATCH: new RouteRecognizer(),
48
+ HEAD: new RouteRecognizer(),
49
+ OPTIONS: new RouteRecognizer()
50
+ };
51
+ }
52
+
53
+ /**
54
+ * Hosts
55
+ *
56
+ * a map of hosts to Registries, ultimately allowing
57
+ * a per-host-and-port, per HTTP verb lookup of RouteRecognizers
58
+ */
59
+ function Hosts() {
60
+ this._registries = {};
61
+ }
62
+
63
+ /**
64
+ * Hosts#forURL - retrieve a map of HTTP verbs to RouteRecognizers
65
+ * for a given URL
66
+ *
67
+ * @param {String} url a URL
68
+ * @return {Registry} a map of HTTP verbs to RouteRecognizers
69
+ * corresponding to the provided URL's
70
+ * hostname and port
71
+ */
72
+ Hosts.prototype.forURL = function(url) {
73
+ var host = parseURL(url).host;
74
+ var registry = this._registries[host];
75
+
76
+ if (registry === undefined) {
77
+ registry = (this._registries[host] = new Registry(host));
78
+ }
79
+
80
+ return registry.verbs;
81
+ }
82
+
83
+ function Pretender(/* routeMap1, routeMap2, ...*/){
84
+ maps = slice.call(arguments);
85
+ // Herein we keep track of RouteRecognizer instances
86
+ // keyed by HTTP method. Feel free to add more as needed.
87
+ this.hosts = new Hosts();
88
+
89
+ this.handlers = [];
90
+ this.handledRequests = [];
91
+ this.passthroughRequests = [];
92
+ this.unhandledRequests = [];
93
+ this.requestReferences = [];
94
+
95
+ // reference the native XMLHttpRequest object so
96
+ // it can be restored later
97
+ this._nativeXMLHttpRequest = window.XMLHttpRequest;
98
+
99
+ // capture xhr requests, channeling them into
100
+ // the route map.
101
+ window.XMLHttpRequest = interceptor(this);
102
+
103
+ // "start" the server
104
+ this.running = true;
105
+
106
+ // trigger the route map DSL.
107
+ for(var i=0; i < arguments.length; i++){
108
+ this.map(arguments[i]);
109
+ }
110
+ }
111
+
112
+ function interceptor(pretender) {
113
+ function FakeRequest(){
114
+ // super()
115
+ FakeXMLHttpRequest.call(this);
116
+ }
117
+ // extend
118
+ var proto = new FakeXMLHttpRequest();
119
+ proto.send = function send(){
120
+ if (!pretender.running) {
121
+ throw new Error('You shut down a Pretender instance while there was a pending request. '+
122
+ 'That request just tried to complete. Check to see if you accidentally shut down '+
123
+ 'a pretender earlier than you intended to');
124
+ }
125
+
126
+ FakeXMLHttpRequest.prototype.send.apply(this, arguments);
127
+ if (!pretender.checkPassthrough(this)) {
128
+ pretender.handleRequest(this);
129
+ }
130
+ else {
131
+ var xhr = createPassthrough(this);
132
+ xhr.send.apply(xhr, arguments);
133
+ }
134
+ };
135
+
136
+ // passthrough handling
137
+ var evts = ['error', 'timeout', 'progress', 'abort'];
138
+ var lifecycleProps = ['readyState', 'responseText', 'responseXML', 'status', 'statusText'];
139
+ function createPassthrough(fakeXHR) {
140
+ var xhr = fakeXHR._passthroughRequest = new pretender._nativeXMLHttpRequest();
141
+
142
+ // use onload instead of onreadystatechange if the browser supports it
143
+ if ('onload' in xhr) {
144
+ evts.push('load');
145
+ } else {
146
+ evts.push('readystatechange');
147
+ }
148
+
149
+ // listen to all events to update lifecycle properties
150
+ for (var i = 0; i < evts.length; i++) (function(evt) {
151
+ xhr['on' + evt] = function(e) {
152
+ // update lifecycle props on each event
153
+ for (var i = 0; i < lifecycleProps.length; i++) {
154
+ var prop = lifecycleProps[i];
155
+ if (xhr[prop]) {
156
+ fakeXHR[prop] = xhr[prop];
157
+ }
158
+ }
159
+ // fire fake events where applicable
160
+ fakeXHR.dispatchEvent(evt, e);
161
+ if (fakeXHR['on' + evt]) {
162
+ fakeXHR['on' + evt](e);
163
+ }
164
+ };
165
+ })(evts[i]);
166
+ xhr.open(fakeXHR.method, fakeXHR.url, fakeXHR.async, fakeXHR.username, fakeXHR.password);
167
+ xhr.timeout = fakeXHR.timeout;
168
+ xhr.withCredentials = fakeXHR.withCredentials;
169
+ for (var h in fakeXHR.requestHeaders) {
170
+ xhr.setRequestHeader(h, fakeXHR.requestHeaders[h]);
171
+ }
172
+ return xhr;
173
+ }
174
+ proto._passthroughCheck = function(method, arguments) {
175
+ if (this._passthroughRequest) {
176
+ return this._passthroughRequest[method].apply(this._passthroughRequest, arguments);
177
+ }
178
+ return FakeXMLHttpRequest.prototype[method].apply(this, arguments);
179
+ }
180
+ proto.abort = function abort(){
181
+ return this._passthroughCheck('abort', arguments);
182
+ }
183
+ proto.getResponseHeader = function getResponseHeader(){
184
+ return this._passthroughCheck('getResponseHeader', arguments);
185
+ }
186
+ proto.getAllResponseHeaders = function getAllResponseHeaders(){
187
+ return this._passthroughCheck('getAllResponseHeaders', arguments);
188
+ }
189
+
190
+ FakeRequest.prototype = proto;
191
+ return FakeRequest;
192
+ }
193
+
194
+ function verbify(verb){
195
+ return function(path, handler, async){
196
+ this.register(verb, path, handler, async);
197
+ };
198
+ }
199
+
200
+ function scheduleProgressEvent(request, startTime, totalTime) {
201
+ setTimeout(function() {
202
+ if (!request.aborted && !request.status) {
203
+ var ellapsedTime = new Date().getTime() - startTime.getTime();
204
+ request.upload._progress(true, ellapsedTime, totalTime);
205
+ request._progress(true, ellapsedTime, totalTime);
206
+ scheduleProgressEvent(request, startTime, totalTime);
207
+ }
208
+ }, 50);
209
+ }
210
+
211
+ var PASSTHROUGH = {};
212
+
213
+ Pretender.prototype = {
214
+ get: verbify('GET'),
215
+ post: verbify('POST'),
216
+ put: verbify('PUT'),
217
+ 'delete': verbify('DELETE'),
218
+ patch: verbify('PATCH'),
219
+ head: verbify('HEAD'),
220
+ map: function(maps){
221
+ maps.call(this);
222
+ },
223
+ register: function register(verb, url, handler, async){
224
+ if (!handler) {
225
+ throw new Error("The function you tried passing to Pretender to handle " + verb + " " + url + " is undefined or missing.");
226
+ }
227
+
228
+ handler.numberOfCalls = 0;
229
+ handler.async = async;
230
+ this.handlers.push(handler);
231
+
232
+ var registry = this.hosts.forURL(url)[verb];
233
+
234
+ registry.add([{
235
+ path: parseURL(url).fullpath,
236
+ handler: handler
237
+ }]);
238
+ },
239
+ passthrough: PASSTHROUGH,
240
+ checkPassthrough: function checkPassthrough(request) {
241
+ var verb = request.method.toUpperCase();
242
+
243
+ var path = parseURL(request.url).fullpath;
244
+
245
+ verb = verb.toUpperCase();
246
+
247
+ var recognized = this.hosts.forURL(request.url)[verb].recognize(path);
248
+ var match = recognized && recognized[0];
249
+ if (match && match.handler == PASSTHROUGH) {
250
+ this.passthroughRequests.push(request);
251
+ this.passthroughRequest(verb, path, request);
252
+ return true;
253
+ }
254
+
255
+ return false;
256
+ },
257
+ handleRequest: function handleRequest(request){
258
+ var verb = request.method.toUpperCase();
259
+ var path = request.url;
260
+
261
+ var handler = this._handlerFor(verb, path, request);
262
+
263
+ if (handler) {
264
+ handler.handler.numberOfCalls++;
265
+ var async = handler.handler.async;
266
+ this.handledRequests.push(request);
267
+
268
+ try {
269
+ var statusHeadersAndBody = handler.handler(request),
270
+ status = statusHeadersAndBody[0],
271
+ headers = this.prepareHeaders(statusHeadersAndBody[1]),
272
+ body = this.prepareBody(statusHeadersAndBody[2]),
273
+ pretender = this;
274
+
275
+ this.handleResponse(request, async, function() {
276
+ request.respond(status, headers, body);
277
+ pretender.handledRequest(verb, path, request);
278
+ });
279
+ } catch (error) {
280
+ this.erroredRequest(verb, path, request, error);
281
+ this.resolve(request);
282
+ }
283
+ } else {
284
+ this.unhandledRequests.push(request);
285
+ this.unhandledRequest(verb, path, request);
286
+ }
287
+ },
288
+ handleResponse: function handleResponse(request, strategy, callback) {
289
+ var delay = typeof strategy === 'function' ? strategy() : strategy;
290
+ delay = typeof delay === 'boolean' || typeof delay === 'number' ? delay : 0;
291
+
292
+ if (delay === false) {
293
+ callback();
294
+ } else {
295
+ var pretender = this;
296
+ pretender.requestReferences.push({
297
+ request: request,
298
+ callback: callback
299
+ });
300
+
301
+ if (delay !== true) {
302
+ scheduleProgressEvent(request, new Date(), delay);
303
+ setTimeout(function() {
304
+ pretender.resolve(request);
305
+ }, delay);
306
+ }
307
+ }
308
+ },
309
+ resolve: function resolve(request) {
310
+ for(var i = 0, len = this.requestReferences.length; i < len; i++) {
311
+ var res = this.requestReferences[i];
312
+ if (res.request === request) {
313
+ res.callback();
314
+ this.requestReferences.splice(i, 1);
315
+ break;
316
+ }
317
+ }
318
+ },
319
+ requiresManualResolution: function(verb, path) {
320
+ var handler = this._handlerFor(verb.toUpperCase(), path, {});
321
+ if (!handler) { return false; }
322
+
323
+ var async = handler.handler.async;
324
+ return typeof async === 'function' ? async() === true : async === true;
325
+ },
326
+ prepareBody: function(body) { return body; },
327
+ prepareHeaders: function(headers) { return headers; },
328
+ handledRequest: function(verb, path, request) { /* no-op */},
329
+ passthroughRequest: function(verb, path, request) { /* no-op */},
330
+ unhandledRequest: function(verb, path, request) {
331
+ throw new Error("Pretender intercepted "+verb+" "+path+" but no handler was defined for this type of request");
332
+ },
333
+ erroredRequest: function(verb, path, request, error){
334
+ error.message = "Pretender intercepted "+verb+" "+path+" but encountered an error: " + error.message;
335
+ throw error;
336
+ },
337
+ _handlerFor: function(verb, url, request){
338
+ var registry = this.hosts.forURL(url)[verb];
339
+ var matches = registry.recognize(parseURL(url).fullpath);
340
+
341
+ var match = matches ? matches[0] : null;
342
+ if (match) {
343
+ request.params = match.params;
344
+ request.queryParams = matches.queryParams;
345
+ }
346
+
347
+ return match;
348
+ },
349
+ shutdown: function shutdown(){
350
+ window.XMLHttpRequest = this._nativeXMLHttpRequest;
351
+
352
+ // "stop" the server
353
+ this.running = false;
354
+ }
355
+ };
356
+
357
+ Pretender.parseURL = parseURL;
358
+ Pretender.Hosts = Hosts;
359
+ Pretender.Registry = Registry;
360
+
361
+ if (isNode) {
362
+ module.exports = Pretender;
363
+ } else {
364
+ window.Pretender = Pretender;
365
+ }
366
+
367
+ })(window);
@@ -0,0 +1,653 @@
1
+ (function() {
2
+ "use strict";
3
+ function $$route$recognizer$dsl$$Target(path, matcher, delegate) {
4
+ this.path = path;
5
+ this.matcher = matcher;
6
+ this.delegate = delegate;
7
+ }
8
+
9
+ $$route$recognizer$dsl$$Target.prototype = {
10
+ to: function(target, callback) {
11
+ var delegate = this.delegate;
12
+
13
+ if (delegate && delegate.willAddRoute) {
14
+ target = delegate.willAddRoute(this.matcher.target, target);
15
+ }
16
+
17
+ this.matcher.add(this.path, target);
18
+
19
+ if (callback) {
20
+ if (callback.length === 0) { throw new Error("You must have an argument in the function passed to `to`"); }
21
+ this.matcher.addChild(this.path, target, callback, this.delegate);
22
+ }
23
+ return this;
24
+ }
25
+ };
26
+
27
+ function $$route$recognizer$dsl$$Matcher(target) {
28
+ this.routes = {};
29
+ this.children = {};
30
+ this.target = target;
31
+ }
32
+
33
+ $$route$recognizer$dsl$$Matcher.prototype = {
34
+ add: function(path, handler) {
35
+ this.routes[path] = handler;
36
+ },
37
+
38
+ addChild: function(path, target, callback, delegate) {
39
+ var matcher = new $$route$recognizer$dsl$$Matcher(target);
40
+ this.children[path] = matcher;
41
+
42
+ var match = $$route$recognizer$dsl$$generateMatch(path, matcher, delegate);
43
+
44
+ if (delegate && delegate.contextEntered) {
45
+ delegate.contextEntered(target, match);
46
+ }
47
+
48
+ callback(match);
49
+ }
50
+ };
51
+
52
+ function $$route$recognizer$dsl$$generateMatch(startingPath, matcher, delegate) {
53
+ return function(path, nestedCallback) {
54
+ var fullPath = startingPath + path;
55
+
56
+ if (nestedCallback) {
57
+ nestedCallback($$route$recognizer$dsl$$generateMatch(fullPath, matcher, delegate));
58
+ } else {
59
+ return new $$route$recognizer$dsl$$Target(startingPath + path, matcher, delegate);
60
+ }
61
+ };
62
+ }
63
+
64
+ function $$route$recognizer$dsl$$addRoute(routeArray, path, handler) {
65
+ var len = 0;
66
+ for (var i=0, l=routeArray.length; i<l; i++) {
67
+ len += routeArray[i].path.length;
68
+ }
69
+
70
+ path = path.substr(len);
71
+ var route = { path: path, handler: handler };
72
+ routeArray.push(route);
73
+ }
74
+
75
+ function $$route$recognizer$dsl$$eachRoute(baseRoute, matcher, callback, binding) {
76
+ var routes = matcher.routes;
77
+
78
+ for (var path in routes) {
79
+ if (routes.hasOwnProperty(path)) {
80
+ var routeArray = baseRoute.slice();
81
+ $$route$recognizer$dsl$$addRoute(routeArray, path, routes[path]);
82
+
83
+ if (matcher.children[path]) {
84
+ $$route$recognizer$dsl$$eachRoute(routeArray, matcher.children[path], callback, binding);
85
+ } else {
86
+ callback.call(binding, routeArray);
87
+ }
88
+ }
89
+ }
90
+ }
91
+
92
+ var $$route$recognizer$dsl$$default = function(callback, addRouteCallback) {
93
+ var matcher = new $$route$recognizer$dsl$$Matcher();
94
+
95
+ callback($$route$recognizer$dsl$$generateMatch("", matcher, this.delegate));
96
+
97
+ $$route$recognizer$dsl$$eachRoute([], matcher, function(route) {
98
+ if (addRouteCallback) { addRouteCallback(this, route); }
99
+ else { this.add(route); }
100
+ }, this);
101
+ };
102
+
103
+ var $$route$recognizer$$specials = [
104
+ '/', '.', '*', '+', '?', '|',
105
+ '(', ')', '[', ']', '{', '}', '\\'
106
+ ];
107
+
108
+ var $$route$recognizer$$escapeRegex = new RegExp('(\\' + $$route$recognizer$$specials.join('|\\') + ')', 'g');
109
+
110
+ function $$route$recognizer$$isArray(test) {
111
+ return Object.prototype.toString.call(test) === "[object Array]";
112
+ }
113
+
114
+ // A Segment represents a segment in the original route description.
115
+ // Each Segment type provides an `eachChar` and `regex` method.
116
+ //
117
+ // The `eachChar` method invokes the callback with one or more character
118
+ // specifications. A character specification consumes one or more input
119
+ // characters.
120
+ //
121
+ // The `regex` method returns a regex fragment for the segment. If the
122
+ // segment is a dynamic of star segment, the regex fragment also includes
123
+ // a capture.
124
+ //
125
+ // A character specification contains:
126
+ //
127
+ // * `validChars`: a String with a list of all valid characters, or
128
+ // * `invalidChars`: a String with a list of all invalid characters
129
+ // * `repeat`: true if the character specification can repeat
130
+
131
+ function $$route$recognizer$$StaticSegment(string) { this.string = string; }
132
+ $$route$recognizer$$StaticSegment.prototype = {
133
+ eachChar: function(callback) {
134
+ var string = this.string, ch;
135
+
136
+ for (var i=0, l=string.length; i<l; i++) {
137
+ ch = string.charAt(i);
138
+ callback({ validChars: ch });
139
+ }
140
+ },
141
+
142
+ regex: function() {
143
+ return this.string.replace($$route$recognizer$$escapeRegex, '\\$1');
144
+ },
145
+
146
+ generate: function() {
147
+ return this.string;
148
+ }
149
+ };
150
+
151
+ function $$route$recognizer$$DynamicSegment(name) { this.name = name; }
152
+ $$route$recognizer$$DynamicSegment.prototype = {
153
+ eachChar: function(callback) {
154
+ callback({ invalidChars: "/", repeat: true });
155
+ },
156
+
157
+ regex: function() {
158
+ return "([^/]+)";
159
+ },
160
+
161
+ generate: function(params) {
162
+ return params[this.name];
163
+ }
164
+ };
165
+
166
+ function $$route$recognizer$$StarSegment(name) { this.name = name; }
167
+ $$route$recognizer$$StarSegment.prototype = {
168
+ eachChar: function(callback) {
169
+ callback({ invalidChars: "", repeat: true });
170
+ },
171
+
172
+ regex: function() {
173
+ return "(.+)";
174
+ },
175
+
176
+ generate: function(params) {
177
+ return params[this.name];
178
+ }
179
+ };
180
+
181
+ function $$route$recognizer$$EpsilonSegment() {}
182
+ $$route$recognizer$$EpsilonSegment.prototype = {
183
+ eachChar: function() {},
184
+ regex: function() { return ""; },
185
+ generate: function() { return ""; }
186
+ };
187
+
188
+ function $$route$recognizer$$parse(route, names, specificity) {
189
+ // normalize route as not starting with a "/". Recognition will
190
+ // also normalize.
191
+ if (route.charAt(0) === "/") { route = route.substr(1); }
192
+
193
+ var segments = route.split("/"), results = [];
194
+
195
+ // A routes has specificity determined by the order that its different segments
196
+ // appear in. This system mirrors how the magnitude of numbers written as strings
197
+ // works.
198
+ // Consider a number written as: "abc". An example would be "200". Any other number written
199
+ // "xyz" will be smaller than "abc" so long as `a > z`. For instance, "199" is smaller
200
+ // then "200", even though "y" and "z" (which are both 9) are larger than "0" (the value
201
+ // of (`b` and `c`). This is because the leading symbol, "2", is larger than the other
202
+ // leading symbol, "1".
203
+ // The rule is that symbols to the left carry more weight than symbols to the right
204
+ // when a number is written out as a string. In the above strings, the leading digit
205
+ // represents how many 100's are in the number, and it carries more weight than the middle
206
+ // number which represents how many 10's are in the number.
207
+ // This system of number magnitude works well for route specificity, too. A route written as
208
+ // `a/b/c` will be more specific than `x/y/z` as long as `a` is more specific than
209
+ // `x`, irrespective of the other parts.
210
+ // Because of this similarity, we assign each type of segment a number value written as a
211
+ // string. We can find the specificity of compound routes by concatenating these strings
212
+ // together, from left to right. After we have looped through all of the segments,
213
+ // we convert the string to a number.
214
+ specificity.val = '';
215
+
216
+ for (var i=0, l=segments.length; i<l; i++) {
217
+ var segment = segments[i], match;
218
+
219
+ if (match = segment.match(/^:([^\/]+)$/)) {
220
+ results.push(new $$route$recognizer$$DynamicSegment(match[1]));
221
+ names.push(match[1]);
222
+ specificity.val += '3';
223
+ } else if (match = segment.match(/^\*([^\/]+)$/)) {
224
+ results.push(new $$route$recognizer$$StarSegment(match[1]));
225
+ specificity.val += '2';
226
+ names.push(match[1]);
227
+ } else if(segment === "") {
228
+ results.push(new $$route$recognizer$$EpsilonSegment());
229
+ specificity.val += '1';
230
+ } else {
231
+ results.push(new $$route$recognizer$$StaticSegment(segment));
232
+ specificity.val += '4';
233
+ }
234
+ }
235
+
236
+ specificity.val = +specificity.val;
237
+
238
+ return results;
239
+ }
240
+
241
+ // A State has a character specification and (`charSpec`) and a list of possible
242
+ // subsequent states (`nextStates`).
243
+ //
244
+ // If a State is an accepting state, it will also have several additional
245
+ // properties:
246
+ //
247
+ // * `regex`: A regular expression that is used to extract parameters from paths
248
+ // that reached this accepting state.
249
+ // * `handlers`: Information on how to convert the list of captures into calls
250
+ // to registered handlers with the specified parameters
251
+ // * `types`: How many static, dynamic or star segments in this route. Used to
252
+ // decide which route to use if multiple registered routes match a path.
253
+ //
254
+ // Currently, State is implemented naively by looping over `nextStates` and
255
+ // comparing a character specification against a character. A more efficient
256
+ // implementation would use a hash of keys pointing at one or more next states.
257
+
258
+ function $$route$recognizer$$State(charSpec) {
259
+ this.charSpec = charSpec;
260
+ this.nextStates = [];
261
+ }
262
+
263
+ $$route$recognizer$$State.prototype = {
264
+ get: function(charSpec) {
265
+ var nextStates = this.nextStates;
266
+
267
+ for (var i=0, l=nextStates.length; i<l; i++) {
268
+ var child = nextStates[i];
269
+
270
+ var isEqual = child.charSpec.validChars === charSpec.validChars;
271
+ isEqual = isEqual && child.charSpec.invalidChars === charSpec.invalidChars;
272
+
273
+ if (isEqual) { return child; }
274
+ }
275
+ },
276
+
277
+ put: function(charSpec) {
278
+ var state;
279
+
280
+ // If the character specification already exists in a child of the current
281
+ // state, just return that state.
282
+ if (state = this.get(charSpec)) { return state; }
283
+
284
+ // Make a new state for the character spec
285
+ state = new $$route$recognizer$$State(charSpec);
286
+
287
+ // Insert the new state as a child of the current state
288
+ this.nextStates.push(state);
289
+
290
+ // If this character specification repeats, insert the new state as a child
291
+ // of itself. Note that this will not trigger an infinite loop because each
292
+ // transition during recognition consumes a character.
293
+ if (charSpec.repeat) {
294
+ state.nextStates.push(state);
295
+ }
296
+
297
+ // Return the new state
298
+ return state;
299
+ },
300
+
301
+ // Find a list of child states matching the next character
302
+ match: function(ch) {
303
+ // DEBUG "Processing `" + ch + "`:"
304
+ var nextStates = this.nextStates,
305
+ child, charSpec, chars;
306
+
307
+ // DEBUG " " + debugState(this)
308
+ var returned = [];
309
+
310
+ for (var i=0, l=nextStates.length; i<l; i++) {
311
+ child = nextStates[i];
312
+
313
+ charSpec = child.charSpec;
314
+
315
+ if (typeof (chars = charSpec.validChars) !== 'undefined') {
316
+ if (chars.indexOf(ch) !== -1) { returned.push(child); }
317
+ } else if (typeof (chars = charSpec.invalidChars) !== 'undefined') {
318
+ if (chars.indexOf(ch) === -1) { returned.push(child); }
319
+ }
320
+ }
321
+
322
+ return returned;
323
+ }
324
+
325
+ /** IF DEBUG
326
+ , debug: function() {
327
+ var charSpec = this.charSpec,
328
+ debug = "[",
329
+ chars = charSpec.validChars || charSpec.invalidChars;
330
+
331
+ if (charSpec.invalidChars) { debug += "^"; }
332
+ debug += chars;
333
+ debug += "]";
334
+
335
+ if (charSpec.repeat) { debug += "+"; }
336
+
337
+ return debug;
338
+ }
339
+ END IF **/
340
+ };
341
+
342
+ /** IF DEBUG
343
+ function debug(log) {
344
+ console.log(log);
345
+ }
346
+
347
+ function debugState(state) {
348
+ return state.nextStates.map(function(n) {
349
+ if (n.nextStates.length === 0) { return "( " + n.debug() + " [accepting] )"; }
350
+ return "( " + n.debug() + " <then> " + n.nextStates.map(function(s) { return s.debug() }).join(" or ") + " )";
351
+ }).join(", ")
352
+ }
353
+ END IF **/
354
+
355
+ // Sort the routes by specificity
356
+ function $$route$recognizer$$sortSolutions(states) {
357
+ return states.sort(function(a, b) {
358
+ return b.specificity.val - a.specificity.val;
359
+ });
360
+ }
361
+
362
+ function $$route$recognizer$$recognizeChar(states, ch) {
363
+ var nextStates = [];
364
+
365
+ for (var i=0, l=states.length; i<l; i++) {
366
+ var state = states[i];
367
+
368
+ nextStates = nextStates.concat(state.match(ch));
369
+ }
370
+
371
+ return nextStates;
372
+ }
373
+
374
+ var $$route$recognizer$$oCreate = Object.create || function(proto) {
375
+ function F() {}
376
+ F.prototype = proto;
377
+ return new F();
378
+ };
379
+
380
+ function $$route$recognizer$$RecognizeResults(queryParams) {
381
+ this.queryParams = queryParams || {};
382
+ }
383
+ $$route$recognizer$$RecognizeResults.prototype = $$route$recognizer$$oCreate({
384
+ splice: Array.prototype.splice,
385
+ slice: Array.prototype.slice,
386
+ push: Array.prototype.push,
387
+ length: 0,
388
+ queryParams: null
389
+ });
390
+
391
+ function $$route$recognizer$$findHandler(state, path, queryParams) {
392
+ var handlers = state.handlers, regex = state.regex;
393
+ var captures = path.match(regex), currentCapture = 1;
394
+ var result = new $$route$recognizer$$RecognizeResults(queryParams);
395
+
396
+ for (var i=0, l=handlers.length; i<l; i++) {
397
+ var handler = handlers[i], names = handler.names, params = {};
398
+
399
+ for (var j=0, m=names.length; j<m; j++) {
400
+ params[names[j]] = captures[currentCapture++];
401
+ }
402
+
403
+ result.push({ handler: handler.handler, params: params, isDynamic: !!names.length });
404
+ }
405
+
406
+ return result;
407
+ }
408
+
409
+ function $$route$recognizer$$addSegment(currentState, segment) {
410
+ segment.eachChar(function(ch) {
411
+ var state;
412
+
413
+ currentState = currentState.put(ch);
414
+ });
415
+
416
+ return currentState;
417
+ }
418
+
419
+ function $$route$recognizer$$decodeQueryParamPart(part) {
420
+ // http://www.w3.org/TR/html401/interact/forms.html#h-17.13.4.1
421
+ part = part.replace(/\+/gm, '%20');
422
+ return decodeURIComponent(part);
423
+ }
424
+
425
+ // The main interface
426
+
427
+ var $$route$recognizer$$RouteRecognizer = function() {
428
+ this.rootState = new $$route$recognizer$$State();
429
+ this.names = {};
430
+ };
431
+
432
+
433
+ $$route$recognizer$$RouteRecognizer.prototype = {
434
+ add: function(routes, options) {
435
+ var currentState = this.rootState, regex = "^",
436
+ specificity = {},
437
+ handlers = [], allSegments = [], name;
438
+
439
+ var isEmpty = true;
440
+
441
+ for (var i=0, l=routes.length; i<l; i++) {
442
+ var route = routes[i], names = [];
443
+
444
+ var segments = $$route$recognizer$$parse(route.path, names, specificity);
445
+
446
+ allSegments = allSegments.concat(segments);
447
+
448
+ for (var j=0, m=segments.length; j<m; j++) {
449
+ var segment = segments[j];
450
+
451
+ if (segment instanceof $$route$recognizer$$EpsilonSegment) { continue; }
452
+
453
+ isEmpty = false;
454
+
455
+ // Add a "/" for the new segment
456
+ currentState = currentState.put({ validChars: "/" });
457
+ regex += "/";
458
+
459
+ // Add a representation of the segment to the NFA and regex
460
+ currentState = $$route$recognizer$$addSegment(currentState, segment);
461
+ regex += segment.regex();
462
+ }
463
+
464
+ var handler = { handler: route.handler, names: names };
465
+ handlers.push(handler);
466
+ }
467
+
468
+ if (isEmpty) {
469
+ currentState = currentState.put({ validChars: "/" });
470
+ regex += "/";
471
+ }
472
+
473
+ currentState.handlers = handlers;
474
+ currentState.regex = new RegExp(regex + "$");
475
+ currentState.specificity = specificity;
476
+
477
+ if (name = options && options.as) {
478
+ this.names[name] = {
479
+ segments: allSegments,
480
+ handlers: handlers
481
+ };
482
+ }
483
+ },
484
+
485
+ handlersFor: function(name) {
486
+ var route = this.names[name], result = [];
487
+ if (!route) { throw new Error("There is no route named " + name); }
488
+
489
+ for (var i=0, l=route.handlers.length; i<l; i++) {
490
+ result.push(route.handlers[i]);
491
+ }
492
+
493
+ return result;
494
+ },
495
+
496
+ hasRoute: function(name) {
497
+ return !!this.names[name];
498
+ },
499
+
500
+ generate: function(name, params) {
501
+ var route = this.names[name], output = "";
502
+ if (!route) { throw new Error("There is no route named " + name); }
503
+
504
+ var segments = route.segments;
505
+
506
+ for (var i=0, l=segments.length; i<l; i++) {
507
+ var segment = segments[i];
508
+
509
+ if (segment instanceof $$route$recognizer$$EpsilonSegment) { continue; }
510
+
511
+ output += "/";
512
+ output += segment.generate(params);
513
+ }
514
+
515
+ if (output.charAt(0) !== '/') { output = '/' + output; }
516
+
517
+ if (params && params.queryParams) {
518
+ output += this.generateQueryString(params.queryParams, route.handlers);
519
+ }
520
+
521
+ return output;
522
+ },
523
+
524
+ generateQueryString: function(params, handlers) {
525
+ var pairs = [];
526
+ var keys = [];
527
+ for(var key in params) {
528
+ if (params.hasOwnProperty(key)) {
529
+ keys.push(key);
530
+ }
531
+ }
532
+ keys.sort();
533
+ for (var i = 0, len = keys.length; i < len; i++) {
534
+ key = keys[i];
535
+ var value = params[key];
536
+ if (value == null) {
537
+ continue;
538
+ }
539
+ var pair = encodeURIComponent(key);
540
+ if ($$route$recognizer$$isArray(value)) {
541
+ for (var j = 0, l = value.length; j < l; j++) {
542
+ var arrayPair = key + '[]' + '=' + encodeURIComponent(value[j]);
543
+ pairs.push(arrayPair);
544
+ }
545
+ } else {
546
+ pair += "=" + encodeURIComponent(value);
547
+ pairs.push(pair);
548
+ }
549
+ }
550
+
551
+ if (pairs.length === 0) { return ''; }
552
+
553
+ return "?" + pairs.join("&");
554
+ },
555
+
556
+ parseQueryString: function(queryString) {
557
+ var pairs = queryString.split("&"), queryParams = {};
558
+ for(var i=0; i < pairs.length; i++) {
559
+ var pair = pairs[i].split('='),
560
+ key = $$route$recognizer$$decodeQueryParamPart(pair[0]),
561
+ keyLength = key.length,
562
+ isArray = false,
563
+ value;
564
+ if (pair.length === 1) {
565
+ value = 'true';
566
+ } else {
567
+ //Handle arrays
568
+ if (keyLength > 2 && key.slice(keyLength -2) === '[]') {
569
+ isArray = true;
570
+ key = key.slice(0, keyLength - 2);
571
+ if(!queryParams[key]) {
572
+ queryParams[key] = [];
573
+ }
574
+ }
575
+ value = pair[1] ? $$route$recognizer$$decodeQueryParamPart(pair[1]) : '';
576
+ }
577
+ if (isArray) {
578
+ queryParams[key].push(value);
579
+ } else {
580
+ queryParams[key] = value;
581
+ }
582
+ }
583
+ return queryParams;
584
+ },
585
+
586
+ recognize: function(path) {
587
+ var states = [ this.rootState ],
588
+ pathLen, i, l, queryStart, queryParams = {},
589
+ isSlashDropped = false;
590
+
591
+ queryStart = path.indexOf('?');
592
+ if (queryStart !== -1) {
593
+ var queryString = path.substr(queryStart + 1, path.length);
594
+ path = path.substr(0, queryStart);
595
+ queryParams = this.parseQueryString(queryString);
596
+ }
597
+
598
+ path = decodeURI(path);
599
+
600
+ // DEBUG GROUP path
601
+
602
+ if (path.charAt(0) !== "/") { path = "/" + path; }
603
+
604
+ pathLen = path.length;
605
+ if (pathLen > 1 && path.charAt(pathLen - 1) === "/") {
606
+ path = path.substr(0, pathLen - 1);
607
+ isSlashDropped = true;
608
+ }
609
+
610
+ for (i=0, l=path.length; i<l; i++) {
611
+ states = $$route$recognizer$$recognizeChar(states, path.charAt(i));
612
+ if (!states.length) { break; }
613
+ }
614
+
615
+ // END DEBUG GROUP
616
+
617
+ var solutions = [];
618
+ for (i=0, l=states.length; i<l; i++) {
619
+ if (states[i].handlers) { solutions.push(states[i]); }
620
+ }
621
+
622
+ states = $$route$recognizer$$sortSolutions(solutions);
623
+
624
+ var state = solutions[0];
625
+
626
+ if (state && state.handlers) {
627
+ // if a trailing slash was dropped and a star segment is the last segment
628
+ // specified, put the trailing slash back
629
+ if (isSlashDropped && state.regex.source.slice(-5) === "(.+)$") {
630
+ path = path + "/";
631
+ }
632
+ return $$route$recognizer$$findHandler(state, path, queryParams);
633
+ }
634
+ }
635
+ };
636
+
637
+ $$route$recognizer$$RouteRecognizer.prototype.map = $$route$recognizer$dsl$$default;
638
+
639
+ $$route$recognizer$$RouteRecognizer.VERSION = '0.1.9';
640
+
641
+ var $$route$recognizer$$default = $$route$recognizer$$RouteRecognizer;
642
+
643
+ /* global define:true module:true window: true */
644
+ if (typeof define === 'function' && define['amd']) {
645
+ define('route-recognizer', function() { return $$route$recognizer$$default; });
646
+ } else if (typeof module !== 'undefined' && module['exports']) {
647
+ module['exports'] = $$route$recognizer$$default;
648
+ } else if (typeof this !== 'undefined') {
649
+ this['RouteRecognizer'] = $$route$recognizer$$default;
650
+ }
651
+ }).call(this);
652
+
653
+ //# sourceMappingURL=route-recognizer.js.map