├── LICENSE ├── README.md ├── docs ├── index.html └── symbols │ └── src │ └── routemap.js.html ├── routemap.compressed.js ├── routemap.js └── tests ├── lib ├── qunit │ ├── qunit.css │ └── qunit.js └── tests.js ├── tests-browser.html └── tests-node.js /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2011 OpenGamma Inc. and the OpenGamma group of companies 2 | Licensed under the Apache License, Version 2.0 (the "License"); 3 | you may not use this file except in compliance with the License. 4 | You may obtain a copy of the License at 5 | 6 | http://www.apache.org/licenses/LICENSE-2.0 7 | 8 | Unless required by applicable law or agreed to in writing, software 9 | distributed under the License is distributed on an "AS IS" BASIS, 10 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | See the License for the specific language governing permissions and 12 | limitations under the License. 13 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | RouteMap.js 2 | =========== 3 | 4 | URL Mapping Library for client-side and server-side JS 5 | 6 | See [JSDoc documentation](http://opengamma.github.com/RouteMap "RouteMap.js Documentation"). 7 | 8 | `RouteMap` maps URL patterns to methods. It is written in "plain old" JavaScript and can be used in conjunction with any 9 | other libraries. It does, however, require JavaScript 1.8 Array methods (such as `map`, `filter`, `reduce`, etc.). If 10 | these methods do not exist, it will throw an error. 11 | [Reference implementations](https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/array/) of these errors 12 | can be added to any environment to back-port these functions if they don't already exist. 13 | 14 | In the browser, the typical use case is for mapping URL fragments (`window.location.hash`) to JavaScript methods. By 15 | default, `RouteMap.handler` is not associated with any event. If the environment it will be used in supports a window 16 | `onhashchange` event, then binding `RouteMap.handler` to it will work out of the box. If not, a simple URL polling 17 | function can be used instead. Similarly, if the environment supports the HTML5 `history` API, the `onpopstate` event can 18 | be bound. 19 | 20 | Hashbang (#!) 21 | --------------- 22 | 23 | The URL patterns `RouteMap` uses are based on a file-system path analogy, so all patterns must begin with a `'/'` 24 | character. In order to support the 25 | [hashbang convention](http://code.google.com/web/ajaxcrawling/docs/specification.html), even though all URL patterns 26 | must begin with `'/'`, a prefix can be specified. The default prefix value is `'#'` but if you want your site to be 27 | indexed, you can switch the prefix to be `'#!'`: 28 | 29 | RouteMap.prefix('#!'); 30 | 31 | Directives 32 | ---------- 33 | 34 | Routes added to `RouteMap` can be static URLs, or they can have dynamic components that get parsed and passed into their 35 | respective methods inside an arguments dictionary. There are three basic types of directives: 36 | 37 | ### (a) unnamed ### 38 | Consider the rule: 39 | 40 | RouteMap.add({route: '/users/:id', method: 'users.get'}); 41 | 42 | `:id` can be any scalar value as long as it does not contain a `'/'` character. So for example, the URL: `/users/45` 43 | would cause `users.get` to be invoked with one argument: `{id: '45'}` 44 | 45 | An unnamed token can be followed by a `'?'` character to indicate it is optional, but no other unnamed parameter can 46 | follow an optional unnamed parameter, because that would lead to ambiguous URLs: 47 | 48 | RouteMap.add({route: '/users/:id/:fave?', method: 'users.get'}); // works 49 | RouteMap.add({route: '/users/:id/:fave?/:other', method: 'users.get'}); // throws an error 50 | RouteMap.add({route: '/users/:id?/:fave?/:other', method: 'users.get'}); // also throws an error 51 | 52 | ### (b) named ### 53 | Named tokens of rule expressions are different from unnamed tokens in that they can appear anywhere in a URL. Because 54 | they are key/value pairs, their order can be arbitrary. Here is an example: 55 | 56 | RouteMap.add({route: '/users/id:', method: 'users.get'}); 57 | 58 | Notice that the colon comes *after* the token name `id`. A matching URL for this rule would look like: `/users/id=45` 59 | 60 | `RouteMap` will automatically URL encode/decode values when generating URLs and parsing them. 61 | 62 | ### (c) star ### 63 | Star directives act like a sieve. Normally, if a URL matches a pattern but has extraneous parameters, then it is not 64 | considered a match and `RouteMap` will not fire that pattern's handler. But if a star directive exists at the end of the 65 | rule, like in these examples: 66 | 67 | RouteMap.add({route: '/users/id:/*', method: 'users.get'}); 68 | RouteMap.add({route: '/users/id:/extras:*', method: 'users.get_two'}); 69 | 70 | Then URLs with extraneous information like `/users/45/something_else/goes=here` will still match. In the case of the 71 | rules above, the following function calls will fire: 72 | 73 | users.get({id: '45', '*': '/something_else/goes=here'}); 74 | users.get_two({id: '45', extras: '/something_else/goes=here'}); 75 | 76 | However, star directives are not exactly wildcards, they may not preserve the order of the extraneous items in a URL. 77 | They will always put all of the unnamed extra pieces of a URL *before* the named pieces. So if the URL 78 | `/users/45/goes=here/something_else` is accessed, the arguments will still be exactly as they are above. 79 | 80 | Client-Side Sample 81 | ------------------ 82 | In a browser environment `RouteMap` can be used as is. Here are some samples: 83 | 84 | 85 | 86 | // assumes jQuery exists and we are using a modern(ish) browser that supports onhashchange 87 | // but jQuery is not necessary to use RouteMap, just shown here for event handling 88 | $(function () { 89 | var routes = window.RouteMap, rules, rule; 90 | // add some rules 91 | rules = { 92 | load_main: {route: '/', method: 'load'}, 93 | load_foo_main: {route: '/foo', method: 'load_foo_main'}, 94 | load_foo: {route: '/foo/:id', method: 'load_foo'} 95 | }; 96 | for (rule in rules) if (rules.hasOwnProperty(rule)) routes.add(rules[rule]); 97 | // set up window listener and initial fire 98 | $(window).bind('hashchange', routes.handler); 99 | $(routes.handler); // in addition to binding hash change events to window, also fire it onload 100 | }); 101 | 102 | The previous example assumes that `load_main`, `load_foo_main`, and `load_foo` all exist in the global (`window`) 103 | object: 104 | 105 | window.load_main = function (args) { 106 | // do some work (args is an empty object) 107 | }; 108 | window.load_foo_main = function (args) { 109 | // do some work (args is an empty object) 110 | }; 111 | window.load_foo = function (args) { 112 | // do some work (args is an object that has 'id' in it) 113 | }; 114 | 115 | Typically, you may not want to pollute the global namespace, so `RouteMap` allows changing the context in which it looks 116 | for rules' methods. The above examples could, for example work like this: 117 | 118 | // assumes jQuery exists and we are using a modern(ish) browser that supports onhashchange 119 | // however, we could be using any other library (or no library!) and we could create a hash polling function, etc. 120 | $(function () { 121 | var routes = window.RouteMap, rules, rule; 122 | // add some rules 123 | rules = { 124 | load_main: {route: '/', method: 'load_main'}, 125 | load_foo_main: {route: '/foo', method: 'load_foo_main'}, 126 | load_foo: {route: '/foo/:id', method: 'load_foo'} 127 | }; 128 | routes.context({ 129 | load_main: function (args) {/* do some work (args is an empty object) */}, 130 | load_foo_main: function (args) {/* do some work (args is an empty object) */}, 131 | load_foo: function (args) {/* do some work (args is an object that has 'id' in it) */} 132 | }); 133 | for (rule in rules) if (rules.hasOwnProperty(rule)) routes.add(rules[rule]); 134 | // set up window listener and initial fire 135 | $(window).bind('hashchange', routes.handler); 136 | $(routes.handler); // in addition to binding hash change events to window, also fire it onload 137 | }); 138 | 139 | The `method` attribute of each rule can drill down arbitrarily deep (e.g., `'foo.bar.baz'`) into the `context` object 140 | and as long as that index exists, `RouteMap` will fire the correct function when a URL matching that pattern is called. 141 | 142 | Server-Side Sample 143 | ------------------ 144 | In a server-side setting like Node.js, `RouteMap` can be imported using `require`. Because the client-side functionality 145 | does not distinguish between different HTTP requests (`GET`, `POST`, `HEAD`, etc.), the server-side version will likely 146 | need a dispatcher function if you need to distinguish between different request types. The example below shows a server 147 | that will answer `GET` requests to `/` and `/bar/ + {an ID string}` and `POST` requests to `/foo`. It will return a 148 | not-found message to all other requests (by overwriting `RouteMap.default_handler`). Note that the `RouteMap.handler` 149 | function is passed the `request` and `response` objects, which means they get passed into each listener as additional 150 | parameters after the `args` object. 151 | 152 | var http = require('http'), routemap = require('./routemap').RouteMap, PORT = 8124; 153 | (function () { 154 | var listeners, dispatch, rules, rule; 155 | listeners = { 156 | main: { 157 | get: function (args, request, response) { 158 | response.writeHead(200, {'Content-Type': 'text/plain'}); 159 | response.write('GET / happened\n'); 160 | } 161 | }, 162 | foo: { 163 | post: function (args, request, response) { 164 | response.writeHead(200, {'Content-Type': 'text/plain'}); 165 | response.write('POST /foo happened\n'); 166 | } 167 | }, 168 | bar: { 169 | get: function (args, request, response) { 170 | response.writeHead(200, {'Content-Type': 'text/plain'}); 171 | response.write('here is bar[' + args.id + ']\n'); 172 | } 173 | } 174 | }; 175 | dispatch = function (listener) { 176 | return function (args, request, response) { 177 | var method = request.method.toLowerCase(); 178 | if (listeners[listener] && listeners[listener][method]) 179 | listeners[listener][method](args, request, response); 180 | else 181 | routemap.default_handler(request.url, request, response); 182 | }; 183 | }; 184 | rules = { 185 | main: {route: '/', method: 'main.handler', handler: dispatch('main')}, 186 | foo: {route: '/foo', method: 'foo.handler', handler: dispatch('foo')}, 187 | bar: {route: '/bar/:id', method: 'bar.handler', handler: dispatch('bar')} 188 | }; 189 | // set up routemap 190 | routemap.context(rules); // where routemap looks for the methods specified 191 | for (rule in rules) routemap.add(rules[rule]); 192 | routemap.default_handler = function (url, request, response) { 193 | response.writeHead(404, {'Content-Type': 'text/plain'}); 194 | response.write('Sorry!\n' + request.method + ' ' + request.url + ' does not work'); 195 | }; 196 | })(); 197 | http.createServer(function (request, response) { 198 | routemap.get = function () {return request.url;}; 199 | routemap.handler(request, response); 200 | response.end(); 201 | }).listen(PORT, '127.0.0.1'); 202 | console.log('HTTP listening on port ' + PORT + '\nCTRL-C to bail'); 203 | 204 | Tests 205 | ----- 206 | To run the tests in a browser, open: `./tests/tests-browser.html` 207 | 208 | To run the tests in node, run: `node ./tests/tests-node.js` 209 | 210 | © 2011 OpenGamma Inc. and the OpenGamma group of companies -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | 8 | JsDoc Reference - RouteMap 9 | 10 | 174 | 175 | 176 | 177 | 178 | 179 | 181 | 182 | 183 | 184 |
185 | 186 |

187 | 188 | Namespace RouteMap 189 |

190 | 191 | 192 |

193 | 194 | 195 | 196 | RouteMap 197 | 198 | 199 |
Defined in: routemap.js. 200 | 201 |

202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 223 | 224 | 225 |
Namespace Summary
Constructor AttributesConstructor Name and Description
  217 |
218 | RouteMap 219 |
220 |

RouteMap holds an internal table of route patterns and method names in addition to some 221 | adding/removing/utility methods and a handler for request routing.

222 |
226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 252 | 253 | 254 | 255 | 256 | 261 | 262 | 263 | 264 | 265 | 272 | 273 | 274 | 275 | 276 | 282 | 283 | 284 | 285 | 286 | 292 | 293 | 294 | 295 | 296 | 303 | 304 | 305 | 306 | 307 | 314 | 315 | 316 | 317 | 318 | 337 | 338 | 339 | 340 | 341 | 347 | 348 | 349 | 350 | 351 | 358 | 359 | 360 | 361 | 362 | 367 | 368 | 369 | 370 | 371 | 376 | 377 | 378 | 379 | 380 | 385 | 386 | 387 | 388 | 389 | 394 | 395 | 396 | 397 | 398 | 406 | 407 | 408 | 409 | 410 | 416 | 417 | 418 | 419 | 420 | 426 | 427 | 428 | 429 |
Method Summary
Method AttributesMethod Name and Description
<static>   248 |
RouteMap.add(rule) 249 |
250 |
adds a rule to the internal table of routes and methods
251 |
<private>   257 |
compile(route) 258 |
259 |
builds the internal representation of a rule based on the route definition
260 |
<static>   266 |
RouteMap.context(scope) 267 |
268 |
overrides the context where listener methods are sought, the default scope is window 269 | (in a browser setting), returns the current context, if no scope object is passed in, just 270 | returns current context without setting context
271 |
<static>   277 |
RouteMap.current() 278 |
279 |
returns the parsed (see #parse) currently accessed route; after listeners have finished 280 | firing, current and last are the same
281 |
<static>   287 |
RouteMap.default_handler() 288 |
289 |
this function is fired when no rule is matched by a URL, by default it does nothing, but it could be set up 290 | to handle things like 404 responses on the server-side or bad hash fragments in the browser
291 |
<static>   297 |
RouteMap.get() 298 |
299 |
URL grabber function, defaults to checking the URL fragment (hash); this function should be 300 | overwritten in a server-side environment; this method is called by RouteMap.handler; without 301 | window.location.hash it will return '/'
302 |
<static>   308 |
RouteMap.go(hash) 309 |
310 |
in a browser setting, it changes window.location.hash, in other settings, it should be 311 | overwritten to do something useful (if necessary); it will not throw an error if window does 312 | not exist
313 |
<static>   319 |
RouteMap.handler() 320 |
321 |
main handler function for routing, this should be bound to hashchange events in the browser, or 322 | (in conjunction with updating RouteMap.get) used with the HTML5 history API, it detects 323 | all the matching route patterns, parses the URL parameters and fires their methods with the arguments from 324 | the parsed URL; the timing of RouteMap.current and RouteMap.last being set is as follows 325 | (pseudo-code): 326 |
 327 | path: get_route             // RouteMap.get
 328 | parsed: parse path          // #parse
 329 | current: longest parsed     // RouteMap.current
 330 | parsed: pre_dispatch parsed // RouteMap.pre_dispatch
 331 | current: longest parsed     // reset current
 332 | fire matched rules in parsed
 333 | last: current               // RouteMap.last
 334 | 
335 | RouteMap.handler calls #parse and does not catch any errors that function throws
336 |
<static>   342 |
RouteMap.hash(rule, params) 343 |
344 |
returns a URL fragment by applying parameters to a rule; uses #compile and does not catch any errors 345 | thrown by that function
346 |
<static>   352 |
RouteMap.last() 353 |
354 |
returns the parsed (see #parse) last accessed route; when route listeners are being called, 355 | last is the previously accessed route, after listeners have finished firing, the current parsed 356 | route replaces last's value
357 |
<private>   363 |
merge() 364 |
365 |
merges one or more objects into a new object by value (nothing is a reference), useful for cloning
366 |
<static>   372 |
RouteMap.parse() 373 |
374 |
parses a URL fragment into a data structure only if there is a route whose pattern matches the fragment
375 |
<private>   381 |
parse(path) 382 |
383 |
parses a path and returns a list of objects that contain argument dictionaries, methods, and raw hash values
384 |
<static>   390 |
RouteMap.post_add(compiled) 391 |
392 |
this function is called by RouteMap.add, it receives a compiled rule object, e.g.
393 |
<static>   399 |
RouteMap.pre_dispatch(parsed) 400 |
401 |
like RouteMap.post_add this function can be overwritten to add application-specific code into 402 | route mapping, it is called before a route begins being dispatched to all matching rules; it receives the 403 | list of matching parsed route objects (#parse) and is expected to return it; one application of this 404 | function might be to set application-wide variables like debug flags
405 |
<static>   411 |
RouteMap.prefix(prefix) 412 |
413 |
if a string is passed in, it overwrites the prefix that is removed from each URL before parsing; primarily 414 | used for hashbang (#!); either way, it returns the current prefix
415 |
<static>   421 |
RouteMap.remove(rule) 422 |
423 |
counterpart to RouteMap.add, removes a rule specification; * remove uses 424 | #compile and does not catch any errors thrown by that function
425 |
430 | 431 | 432 | 433 | 434 | 435 | 436 | 437 | 438 | 439 |
440 |
441 | Namespace Detail 442 |
443 | 444 |
445 | RouteMap 446 |
447 | 448 |
449 |

RouteMap holds an internal table of route patterns and method names in addition to some 450 | adding/removing/utility methods and a handler for request routing.

451 |

It does not have any dependencies and is written in "plain old" JS, but it does require JS 1.8 array methods, so 452 | if the environment it will run in does not have those, the reference implementations from 453 | Mozilla should be 454 | supplied external to this library.

455 |

It is designed to be used in both a browser setting and a server-side context (for example in node.js).

456 | LICENSING INFORMATION: 457 |
 458 | Copyright 2011 OpenGamma Inc. and the OpenGamma group of companies
 459 | Licensed under the Apache License, Version 2.0 (the "License");
 460 | you may not use this file except in compliance with the License.
 461 | You may obtain a copy of the License at
 462 | 
 463 |     http://www.apache.org/licenses/LICENSE-2.0
 464 | 
 465 | Unless required by applicable law or agreed to in writing, software
 466 | distributed under the License is distributed on an "AS IS" BASIS,
 467 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 468 | See the License for the specific language governing permissions and
 469 | limitations under the License.
 470 | 
471 |
Author: Afshin Darian. 472 |
473 | 474 | 475 | 476 | 477 | 478 | 479 | 480 | 481 |
482 |
Throws:
483 | 484 |
485 | {Error} 486 |
487 |
if JS 1.8 Array.prototype methods don't exist
488 | 489 |
490 | 491 | 492 | 493 | 494 |
495 |
See:
496 | 497 |
OpenGamma
498 | 499 |
Apache License, Version 2.0
500 | 501 |
Mozilla Developer 502 | Network
503 | 504 |
505 | 506 | 507 |
508 | 509 | 510 | 511 | 512 | 513 | 514 | 515 |
516 | Method Detail 517 |
518 | 519 | 520 |
<static> 521 | 522 | {undefined} 523 | RouteMap.add(rule) 524 | 525 |
526 |
527 | adds a rule to the internal table of routes and methods 528 | 529 | 530 |
531 | 532 | 533 | 534 | 535 |
536 |
Parameters:
537 | 538 |
539 | {Object} rule 540 | 541 |
542 |
rule specification
543 | 544 |
545 | {String} rule.route 546 | 547 |
548 |
route pattern definition; there are three types of pattern arguments: scalars, 549 | keyvals, and stars; scalars are individual values in a URL (all URL values are separate by the 550 | '/' character), keyvals are named values, e.g. 'foo=bar', and star values are wildcards; so for 551 | example, the following pattern represents all the possible options:
552 | '/foo/:id/:sub?/attr:/subattr:?/rest:*'
the ? means that argument is 553 | optional, the star rule is named rest but it could have just simply been left as *, 554 | which means the resultant dictionary would have put the wildcard remainder into args['*'] 555 | instead of args.rest; so the following URL would match the pattern above:
556 | /foo/23/45/attr=something/subattr=something_else
557 | when its method is called, it will receive this arguments dictionary:
558 |
{
 559 |      id:'23',
 560 |      subid:'45',
 561 |      attr:'something',
 562 |      subattr:'something_else',
 563 |      rest:''
 564 | }
565 | add uses #compile and does not catch any errors thrown by that function
566 | 567 |
568 | {String} rule.method 569 | 570 |
571 |
listener method for this route
572 | 573 |
574 | 575 | 576 | 577 | 578 |
579 |
Throws:
580 | 581 |
582 | {TypeError} 583 |
584 |
if rule.route or rule.method are not strings or empty strings
585 | 586 |
587 | {Error} 588 |
589 |
if rule has already been added
590 | 591 |
592 | 593 | 594 | 595 | 596 |
597 |
See:
598 | 599 |
RouteMap.post_add
600 | 601 |
602 | 603 | 604 |
605 | 606 | 607 |
<private> 608 | 609 | 610 | compile(route) 611 | 612 |
613 |
614 | builds the internal representation of a rule based on the route definition 615 | 616 | 617 |
618 | 619 | 620 | 621 | 622 |
623 |
Parameters:
624 | 625 |
626 | {String} route 627 | 628 |
629 |
630 | 631 |
632 | 633 | 634 | 635 | 636 |
637 |
Throws:
638 | 639 |
640 | {SyntaxError} 641 |
642 |
if any portion of a rule definition follows a * directive
643 | 644 |
645 | {SyntaxError} 646 |
647 |
if a required scalar follows an optional scalar
648 | 649 |
650 | {SyntaxError} 651 |
652 |
if a rule cannot be parsed
653 | 654 |
655 | 656 | 657 |
658 |
Returns:
659 | 660 |
{Object} a compiled object, for example, the rule '/foo/:id/type:?/rest:*' would return 661 | an object of the form:
{
 662 |     page:'/foo',
 663 |     rules:{
 664 |         keyvals:[{name: 'type', required: false}],
 665 |         scalars:[{name: 'id', required: true}],
 666 |         star:'rest' // false if not defined
 667 |     }
 668 | }
669 | 670 |
671 | 672 | 673 | 674 |
675 |
See:
676 | 677 |
RouteMap.add
678 | 679 |
RouteMap.hash
680 | 681 |
RouteMap.remove
682 | 683 |
684 | 685 | 686 |
687 | 688 | 689 |
<static> 690 | 691 | 692 | RouteMap.context(scope) 693 | 694 |
695 |
696 | overrides the context where listener methods are sought, the default scope is window 697 | (in a browser setting), returns the current context, if no scope object is passed in, just 698 | returns current context without setting context 699 | 700 | 701 |
702 | 703 | 704 | 705 | 706 |
707 |
Parameters:
708 | 709 |
710 | {Object} scope 711 | 712 |
713 |
the scope within which methods for mapped routes will be looked for
714 | 715 |
716 | 717 | 718 | 719 | 720 | 721 |
722 |
Returns:
723 | 724 |
{Object} the current context within which RouteMap searches for handlers
725 | 726 |
727 | 728 | 729 | 730 | 731 |
732 | 733 | 734 |
<static> 735 | 736 | {Object} 737 | RouteMap.current() 738 | 739 |
740 |
741 | returns the parsed (see #parse) currently accessed route; after listeners have finished 742 | firing, current and last are the same 743 | 744 | 745 |
746 | 747 | 748 | 749 | 750 | 751 | 752 | 753 | 754 |
755 |
Returns:
756 | 757 |
{Object} the current parsed URL object
758 | 759 |
760 | 761 | 762 | 763 |
764 |
See:
765 | 766 |
RouteMap.last
767 | 768 |
769 | 770 | 771 |
772 | 773 | 774 |
<static> 775 | 776 | {undefined} 777 | RouteMap.default_handler() 778 | 779 |
780 |
781 | this function is fired when no rule is matched by a URL, by default it does nothing, but it could be set up 782 | to handle things like 404 responses on the server-side or bad hash fragments in the browser 783 | 784 | 785 |
786 | 787 | 788 | 789 | 790 | 791 | 792 | 793 | 794 | 795 | 796 | 797 |
798 | 799 | 800 |
<static> 801 | 802 | {String} 803 | RouteMap.get() 804 | 805 |
806 |
807 | URL grabber function, defaults to checking the URL fragment (hash); this function should be 808 | overwritten in a server-side environment; this method is called by RouteMap.handler; without 809 | window.location.hash it will return '/' 810 | 811 | 812 |
813 | 814 | 815 | 816 | 817 | 818 | 819 | 820 | 821 |
822 |
Returns:
823 | 824 |
{String} by default, this returns a subset of the URL hash (everything after the first 825 | '/' character ... if nothing follows a slash, it returns '/'); if overwritten, it 826 | must be a function that returns URL path strings (beginning with '/') to match added rules
827 | 828 |
829 | 830 | 831 | 832 | 833 |
834 | 835 | 836 |
<static> 837 | 838 | {undefined} 839 | RouteMap.go(hash) 840 | 841 |
842 |
843 | in a browser setting, it changes window.location.hash, in other settings, it should be 844 | overwritten to do something useful (if necessary); it will not throw an error if window does 845 | not exist 846 | 847 | 848 |
849 | 850 | 851 | 852 | 853 |
854 |
Parameters:
855 | 856 |
857 | {String} hash 858 | 859 |
860 |
the hash fragment to go to
861 | 862 |
863 | 864 | 865 | 866 | 867 | 868 | 869 | 870 | 871 |
872 | 873 | 874 |
<static> 875 | 876 | {undefined} 877 | RouteMap.handler() 878 | 879 |
880 |
881 | main handler function for routing, this should be bound to hashchange events in the browser, or 882 | (in conjunction with updating RouteMap.get) used with the HTML5 history API, it detects 883 | all the matching route patterns, parses the URL parameters and fires their methods with the arguments from 884 | the parsed URL; the timing of RouteMap.current and RouteMap.last being set is as follows 885 | (pseudo-code): 886 |
 887 | path: get_route             // RouteMap.get
 888 | parsed: parse path          // #parse
 889 | current: longest parsed     // RouteMap.current
 890 | parsed: pre_dispatch parsed // RouteMap.pre_dispatch
 891 | current: longest parsed     // reset current
 892 | fire matched rules in parsed
 893 | last: current               // RouteMap.last
 894 | 
895 | RouteMap.handler calls #parse and does not catch any errors that function throws 896 | 897 | 898 |
899 | 900 | 901 | 902 | 903 | 904 | 905 | 906 | 907 | 908 | 909 |
910 |
See:
911 | 912 |
RouteMap.pre_dispatch
913 | 914 |
915 | 916 | 917 |
918 | 919 | 920 |
<static> 921 | 922 | {String} 923 | RouteMap.hash(rule, params) 924 | 925 |
926 |
927 | returns a URL fragment by applying parameters to a rule; uses #compile and does not catch any errors 928 | thrown by that function 929 | 930 | 931 |
932 | 933 | 934 | 935 | 936 |
937 |
Parameters:
938 | 939 |
940 | {Object} rule 941 | 942 |
943 |
the rule specification; it typically looks like:
944 | {route:'/foo', method:'bar'}
but only route is strictly necessary
945 | 946 |
947 | {Object} params 948 | 949 |
950 |
a dictionary of argument key/value pairs required by the rule
951 | 952 |
953 | 954 | 955 | 956 | 957 |
958 |
Throws:
959 | 960 |
961 | {TypeError} 962 |
963 |
if a required parameter is not present
964 | 965 |
966 | 967 | 968 |
969 |
Returns:
970 | 971 |
{String} URL fragment resulting from applying arguments to rule pattern
972 | 973 |
974 | 975 | 976 | 977 | 978 |
979 | 980 | 981 |
<static> 982 | 983 | {Object} 984 | RouteMap.last() 985 | 986 |
987 |
988 | returns the parsed (see #parse) last accessed route; when route listeners are being called, 989 | last is the previously accessed route, after listeners have finished firing, the current parsed 990 | route replaces last's value 991 | 992 | 993 |
994 | 995 | 996 | 997 | 998 | 999 | 1000 | 1001 | 1002 |
1003 |
Returns:
1004 | 1005 |
{Object} the last parsed URL object, will be null on first load
1006 | 1007 |
1008 | 1009 | 1010 | 1011 |
1012 |
See:
1013 | 1014 |
RouteMap.current
1015 | 1016 |
1017 | 1018 | 1019 |
1020 | 1021 | 1022 |
<private> 1023 | 1024 | {Object} 1025 | merge() 1026 | 1027 |
1028 |
1029 | merges one or more objects into a new object by value (nothing is a reference), useful for cloning 1030 | 1031 | 1032 |
1033 | 1034 | 1035 | 1036 | 1037 | 1038 | 1039 | 1040 |
1041 |
Throws:
1042 | 1043 |
1044 | {TypeError} 1045 |
1046 |
if one of the arguments is not a mergeable object (i.e. a primitive, null or array)
1047 | 1048 |
1049 | 1050 | 1051 |
1052 |
Returns:
1053 | 1054 |
{Object} a merged object
1055 | 1056 |
1057 | 1058 | 1059 | 1060 | 1061 |
1062 | 1063 | 1064 |
<static> 1065 | 1066 | {Object} 1067 | RouteMap.parse() 1068 | 1069 |
1070 |
1071 | parses a URL fragment into a data structure only if there is a route whose pattern matches the fragment 1072 | 1073 | 1074 |
1075 | 1076 | 1077 | 1078 | 1079 | 1080 | 1081 | 1082 |
1083 |
Throws:
1084 | 1085 |
1086 | {TypeError} 1087 |
1088 |
if hash is not a string, is empty, or does not contain a '/' character
1089 | 1090 |
1091 | {SyntaxError} 1092 |
1093 |
if hash cannot be parsed by #parse
1094 | 1095 |
1096 | 1097 | 1098 |
1099 |
Returns:
1100 | 1101 |
{Object} of the form:
{page:'/foo', args:{bar:'some_value'}}
1102 | only if a rule with the route: '/foo/:bar' has already been added
1103 | 1104 |
1105 | 1106 | 1107 | 1108 | 1109 |
1110 | 1111 | 1112 |
<private> 1113 | 1114 | {Array} 1115 | parse(path) 1116 | 1117 |
1118 |
1119 | parses a path and returns a list of objects that contain argument dictionaries, methods, and raw hash values 1120 | 1121 | 1122 |
1123 | 1124 | 1125 | 1126 | 1127 |
1128 |
Parameters:
1129 | 1130 |
1131 | {String} path 1132 | 1133 |
1134 |
1135 | 1136 |
1137 | 1138 | 1139 | 1140 | 1141 |
1142 |
Throws:
1143 | 1144 |
1145 | {TypeError} 1146 |
1147 |
if the method specified by a rule specification does not exist during parse time
1148 | 1149 |
1150 | 1151 | 1152 |
1153 |
Returns:
1154 | 1155 |
{Array} a list of parsed objects in descending order of matched hash length
1156 | 1157 |
1158 | 1159 | 1160 | 1161 | 1162 |
1163 | 1164 | 1165 |
<static> 1166 | 1167 | {Object} 1168 | RouteMap.post_add(compiled) 1169 | 1170 |
1171 |
1172 | this function is called by RouteMap.add, it receives a compiled rule object, e.g. for the rule: 1173 |
{route:'/foo/:id/:sub?/attr:/subattr:?/rest:*', method:'console.log'}
1174 | post_add would receive the following object: 1175 |
{
1176 |     method:'console.log',
1177 |     rules:{
1178 |         scalars:[{name:'id',required:true},{name:'sub',required:false}],
1179 |         keyvals:[{name:'attr',required:true},{name:'subattr',required:false}],
1180 |         star:'rest'
1181 |     },
1182 |     raw:'/foo/:id/:sub?/attr:/subattr:?/rest:*'
1183 | }
1184 | and it is expected to pass back an object of the same format; it can be overwritten to post-process added 1185 | rules e.g. to add extra default application-wide parameters; by default, it simply returns what was passed 1186 | into it 1187 | 1188 | 1189 |
1190 | 1191 | 1192 | 1193 | 1194 |
1195 |
Parameters:
1196 | 1197 |
1198 | {Object} compiled 1199 | 1200 |
1201 |
the compiled rule
1202 | 1203 |
1204 | 1205 | 1206 | 1207 | 1208 | 1209 |
1210 |
Returns:
1211 | 1212 |
{Object} the default function returns the exact object it received; a custom function needs to 1213 | an object that is of the same form (but could possibly have more or fewer parameters, etc.)
1214 | 1215 |
1216 | 1217 | 1218 | 1219 | 1220 |
1221 | 1222 | 1223 |
<static> 1224 | 1225 | {Array} 1226 | RouteMap.pre_dispatch(parsed) 1227 | 1228 |
1229 |
1230 | like RouteMap.post_add this function can be overwritten to add application-specific code into 1231 | route mapping, it is called before a route begins being dispatched to all matching rules; it receives the 1232 | list of matching parsed route objects (#parse) and is expected to return it; one application of this 1233 | function might be to set application-wide variables like debug flags 1234 | 1235 | 1236 |
1237 | 1238 | 1239 | 1240 | 1241 |
1242 |
Parameters:
1243 | 1244 |
1245 | {Array} parsed 1246 | 1247 |
1248 |
the parsed request
1249 | 1250 |
1251 | 1252 | 1253 | 1254 | 1255 | 1256 |
1257 |
Returns:
1258 | 1259 |
{Array} a list of the same form as the one it receives
1260 | 1261 |
1262 | 1263 | 1264 | 1265 | 1266 |
1267 | 1268 | 1269 |
<static> 1270 | 1271 | {undefined} 1272 | RouteMap.prefix(prefix) 1273 | 1274 |
1275 |
1276 | if a string is passed in, it overwrites the prefix that is removed from each URL before parsing; primarily 1277 | used for hashbang (#!); either way, it returns the current prefix 1278 | 1279 | 1280 |
1281 | 1282 | 1283 | 1284 | 1285 |
1286 |
Parameters:
1287 | 1288 |
1289 | {String} prefix 1290 | 1291 |
1292 |
(optional) the prefix string
1293 | 1294 |
1295 | 1296 | 1297 | 1298 | 1299 | 1300 | 1301 | 1302 | 1303 |
1304 | 1305 | 1306 |
<static> 1307 | 1308 | {undefined} 1309 | RouteMap.remove(rule) 1310 | 1311 |
1312 |
1313 | counterpart to RouteMap.add, removes a rule specification; * remove uses 1314 | #compile and does not catch any errors thrown by that function 1315 | 1316 | 1317 |
1318 | 1319 | 1320 | 1321 | 1322 |
1323 |
Parameters:
1324 | 1325 |
1326 | {Object} rule 1327 | 1328 |
1329 |
the rule specification that was used in RouteMap.add
1330 | 1331 |
1332 | 1333 | 1334 | 1335 | 1336 |
1337 |
Throws:
1338 | 1339 |
1340 | {TypeError} 1341 |
1342 |
if rule.route or rule.method are not strings or empty strings
1343 | 1344 |
1345 | 1346 | 1347 | 1348 | 1349 | 1350 | 1351 | 1352 | 1353 | 1354 | 1355 | 1356 | 1357 |
1358 |
1359 | 1360 | 1361 | 1362 |
1363 | 1364 | Documentation generated by JsDoc Toolkit 2.4.0 on Wed Apr 18 2012 13:03:13 GMT+0100 (BST) 1365 |
1366 | 1367 | 1368 | -------------------------------------------------------------------------------- /routemap.compressed.js: -------------------------------------------------------------------------------- 1 | (function(x,y){(function(a,b){if(!a.every||!a.filter||!a.indexOf||!a.map||!a.reduce||!a.some||!a.forEach)throw Error("See "+b+" for reference versions of Array.prototype methods available in JS 1.8");})([],"https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/array/");var k,f={},l={},i=[],p=0,n=0,u=encodeURIComponent,v=decodeURIComponent,o="#",z=/\*|:|\?/,A=/(^([^\*:\?]+):\*)|(^\*$)/,B=/^:([^\*:\?]+)(\??)$/,C=/^([^\*:\?]+):(\??)$/,D=/([^/])$/,q="undefined"!==typeof window?window:{}, 2 | h=function(a){return"string"!==typeof a||!a.length},r=function(){var a=Object.prototype.toString,b=function(c){return"object"!==typeof c||null===c?c:"[object Array]"===a.call(c)?c.map(b):r(c)};return Array.prototype.reduce.call(arguments,function(c,d){if(!d||"object"!==typeof d||"[object Array]"===a.call(d))throw new TypeError("merge: "+a.call(d)+" is not mergeable");for(var e in d)d.hasOwnProperty(e)&&(c[e]=b(d[e]));return c},{})},w=function(a){var b=i.filter(function(b){return 0===a.replace(D,"$1/").indexOf(b)}).filter(function(a, 3 | b){return!b||f[a].some(function(a){return!!a.rules.star})});return!b.length?[]:b.reduce(function(b,d){var e=f[d].map(function(b){var c={},e=b.rules.scalars,m=b.rules.keyvals,f,g=a.replace(d,"").split("/").reduce(function(a,b){var c=b.split("="),d=c[0],c=c.slice(1).join("=");return!b.length||(c?a.keyvals[d]=c:a.scalars.push(b)),a},{keyvals:{},scalars:[]}),j,h=m.reduce(function(a,b){return(a[b.name]=0)||a},{}),i=e.filter(function(a){return a.required}).length,l=m.filter(function(a){return a.required}).every(function(a){return g.keyvals.hasOwnProperty(a.name)}); 4 | if(i>g.scalars.length||!l)return 0;if(!b.rules.star){if(g.scalars.length>e.length)return 0;for(j in g.keyvals)if(g.keyvals.hasOwnProperty(j)&&!h.hasOwnProperty(j))return 0}g.scalars.slice(0,e.length).forEach(function(a,b){c[e[b].name]=v(a)});m.forEach(function(a){g.keyvals[a.name]&&(c[a.name]=v(g.keyvals[a.name]));delete g.keyvals[a.name]});if(b.rules.star){m=g.scalars.slice(e.length,g.scalars.length);for(j in g.keyvals)g.keyvals.hasOwnProperty(j)&&m.push([j,g.keyvals[j]].join("="));c[b.rules.star]= 5 | m.join("/")}try{if(f=b.method.split(".").reduce(function(a,b){return a[b]},q),"function"!==typeof f)throw Error();}catch(n){throw new TypeError("parse: "+b.method+" is not a function in current context");}return{page:d,hash:k.hash({route:b.raw},c),method:f,args:c}});return b.concat(e).filter(Boolean)},[]).sort(function(a,b){return b.hash.length-a.hash.length})},t=function(a){return function(b){var c,d={},e="/"===b[0]?b:~(c=b.indexOf("/"))?b.slice(c):0,s=function(a){if(d.hasOwnProperty(a)||(d[a]=0))throw new SyntaxError('compile: "'+ 6 | a+'" is repeated in: '+b);};if(!e)throw new SyntaxError("compile: the route "+b+" was not understood");if(a[e])return a[e];c=e.split("/").reduce(function(a,c){var d=a.rules,e=d.scalars,g=d.keyvals;if(d.star)throw new SyntaxError("compile: no rules can follow a * directive in: "+b);if(!~c.search(z)&&!e.length&&!g.length)return a.page.push(c),a;if(c.match(A))return d.star=RegExp.$2||RegExp.$3,s(d.star),a;if(c.match(B)){if(a.has_optional_scalar)throw new SyntaxError('compile: "'+c+'" cannot follow an optional rule in: '+ 7 | b);RegExp.$2&&(a.has_optional_scalar=c);return e.push({name:RegExp.$1,required:!RegExp.$2}),s(RegExp.$1),a}if(c.match(C))return g.push({name:RegExp.$1,required:!RegExp.$2}),s(RegExp.$1),a;throw new SyntaxError('compile: the rule "'+c+'" was not understood in: '+b);},{page:[],rules:{scalars:[],keyvals:[],star:!1},has_optional_scalar:""});delete c.has_optional_scalar;c.page=c.page.join("/").replace(/\/$/,"")||"/";return a[e]=c}}({});x[y]=k={add:function(a){var b=a.method,c=a.route,d=[a.method,a.route].join("|"); 8 | if([c,b].some(h))throw new TypeError("add: rule.route and rule.method must both be non-empty strings");if(l[d])throw Error("add: "+c+" to "+b+" already exists");a=t(c);l[d]=!0;if(!f[a.page]&&(f[a.page]=[]))i=i.concat(a.page).sort(function(a,b){return b.length-a.length});f[a.page].push(k.post_add({method:b,rules:a.rules,raw:c}))},context:function(a){return q="object"===typeof a?a:q},current:function(){return n?r(n):null},default_handler:function(){},get:function(){if("undefined"===typeof window)return"/"; 9 | var a=window.location.hash,b=a.indexOf("/");return~b?a.slice(b):"/"},go:function(a){"undefined"!==typeof window&&(window.location.hash=(0===a.indexOf(o)?"":o)+a)},handler:function(){var a=k.get(),b=w(a),c=Array.prototype.slice.call(arguments);if(!b.length)return k.default_handler.apply(null,[a].concat(c));n=b[0];b=k.pre_dispatch(b);n=b[0];b.forEach(function(a){a.method.apply(null,[a.args].concat(c))});p=b[0]},hash:function(a,b){var c,d,b=b||{};if(h(a.route))throw new TypeError("hash: rule.route must be a non-empty string"); 10 | d=t(a.route);c=d.page+("/"===d.page?"":"/")+d.rules.scalars.map(function(c){var d=u(b[c.name]),f=void 0===b[c.name]||h(d);if(c.required&&f)throw new TypeError("hash: params."+c.name+" is undefined, route: "+a.route);return f?0:d}).concat(d.rules.keyvals.map(function(c){var d=u(b[c.name]),f=void 0===b[c.name]||h(d);if(c.required&&f)throw new TypeError("hash: params."+c.name+" is undefined, route: "+a.route);return f?0:c.name+"="+d})).filter(Boolean).join("/");d.rules.star&&b[d.rules.star]&&(c+=("/"=== 11 | c[c.length-1]?"":"/")+b[d.rules.star]);return c},last:function(){return p?r(p):null},parse:function(a){var b;b=a.indexOf("/");a=~b?a.slice(b):"";if(h(a))throw new TypeError("parse: hash must be a string with a / character");if(!(b=w(a)).length)throw new SyntaxError("parse: "+a+" cannot be parsed");return{page:b[0].page,args:b[0].args}},post_add:function(a){return a},pre_dispatch:function(a){return a},prefix:function(a){return o="undefined"!==typeof a?a+"":o},remove:function(a){var b=a.method,c=a.route, 12 | d=[a.method,a.route].join("|"),e;if([c,b].some(h))throw new TypeError("remove: rule.route and rule.method must both be non-empty strings");l[d]&&(a=t(c),delete l[d],f[a.page]=f[a.page].filter(function(a){return a.raw!==c||a.method!==b}),!f[a.page].length&&delete f[a.page]&&~(e=i.indexOf(a.page))&&i.splice(e,1))}}})("undefined"===typeof exports?window:exports,"RouteMap"); 13 | -------------------------------------------------------------------------------- /routemap.js: -------------------------------------------------------------------------------- 1 | /** 2 | *

RouteMap holds an internal table of route patterns and method names in addition to some 3 | * adding/removing/utility methods and a handler for request routing.

4 | *

It does not have any dependencies and is written in "plain old" JS, but it does require JS 1.8 array methods, so 5 | * if the environment it will run in does not have those, the reference implementations from 6 | * Mozilla should be 7 | * supplied external to this library.

8 | *

It is designed to be used in both a browser setting and a server-side context (for example in node.js).

9 | * LICENSING INFORMATION: 10 | *
 11 |  * Copyright 2011 OpenGamma Inc. and the OpenGamma group of companies
 12 |  * Licensed under the Apache License, Version 2.0 (the "License");
 13 |  * you may not use this file except in compliance with the License.
 14 |  * You may obtain a copy of the License at
 15 |  *
 16 |  *     http://www.apache.org/licenses/LICENSE-2.0
 17 |  *
 18 |  * Unless required by applicable law or agreed to in writing, software
 19 |  * distributed under the License is distributed on an "AS IS" BASIS,
 20 |  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 21 |  * See the License for the specific language governing permissions and
 22 |  * limitations under the License.
 23 |  * 
24 | * @see OpenGamma 25 | * @see Apache License, Version 2.0 26 | * @see Mozilla Developer 27 | * Network 28 | * @name RouteMap 29 | * @namespace RouteMap 30 | * @author Afshin Darian 31 | * @static 32 | * @throws {Error} if JS 1.8 Array.prototype methods don't exist 33 | */ 34 | (function (pub, namespace) { // defaults to exports, uses window if exports does not exist 35 | (function (arr, url) { // plain old JS, but needs some JS 1.8 array methods 36 | if (!arr.every || !arr.filter || !arr.indexOf || !arr.map || !arr.reduce || !arr.some || !arr.forEach) 37 | throw new Error('See ' + url + ' for reference versions of Array.prototype methods available in JS 1.8'); 38 | })([], 'https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/array/'); 39 | var routes /* internal reference to RouteMap */, active_routes = {}, added_routes = {}, flat_pages = [], 40 | last = 0, current = 0, encode = encodeURIComponent, decode = decodeURIComponent, has = 'hasOwnProperty', 41 | EQ = '=' /* equal string */, SL = '/' /* slash string */, PR = '#' /* default prefix string */, 42 | token_exp = /\*|:|\?/, star_exp = /(^([^\*:\?]+):\*)|(^\*$)/, scalar_exp = /^:([^\*:\?]+)(\??)$/, 43 | keyval_exp = /^([^\*:\?]+):(\??)$/, slash_exp = new RegExp('([^' + SL + '])$'), 44 | context = typeof window !== 'undefined' ? window : {}, // where listeners reside, routes.context() overwrites it 45 | /** @ignore */ 46 | invalid_str = function (str) {return typeof str !== 'string' || !str.length;}, 47 | /** @ignore */ 48 | fingerprint = function (rule) {return [rule.method, rule.route].join('|');}, 49 | /** 50 | * merges one or more objects into a new object by value (nothing is a reference), useful for cloning 51 | * @name RouteMap#merge 52 | * @inner 53 | * @function 54 | * @type Object 55 | * @returns {Object} a merged object 56 | * @throws {TypeError} if one of the arguments is not a mergeable object (i.e. a primitive, null or array) 57 | */ 58 | merge = function () { 59 | var self = 'merge', to_string = Object.prototype.toString, clone = function (obj) { 60 | return typeof obj !== 'object' || obj === null ? obj // primitives 61 | : to_string.call(obj) === '[object Array]' ? obj.map(clone) // arrays 62 | : merge(obj); // objects 63 | }; 64 | return Array.prototype.reduce.call(arguments, function (acc, obj) { 65 | if (!obj || typeof obj !== 'object' || to_string.call(obj) === '[object Array]') 66 | throw new TypeError(self + ': ' + to_string.call(obj) + ' is not mergeable'); 67 | for (var key in obj) if (obj[has](key)) acc[key] = clone(obj[key]); 68 | return acc; 69 | }, {}); 70 | }, 71 | /** 72 | * parses a path and returns a list of objects that contain argument dictionaries, methods, and raw hash values 73 | * @name RouteMap#parse 74 | * @inner 75 | * @function 76 | * @param {String} path 77 | * @type Array 78 | * @returns {Array} a list of parsed objects in descending order of matched hash length 79 | * @throws {TypeError} if the method specified by a rule specification does not exist during parse time 80 | */ 81 | parse = function (path) { 82 | // go with the first matching page (longest) or any pages with * rules 83 | var self = 'parse', pages = flat_pages.filter(function (val) { // add slash to paths so all vals match 84 | return path.replace(slash_exp, '$1' + SL).indexOf(val) === 0; 85 | }) 86 | .filter(function (page, index) { 87 | return !index || active_routes[page].some(function (val) {return !!val.rules.star;}); 88 | }); 89 | return !pages.length ? [] : pages.reduce(function (acc, page) { // flatten parsed rules for all pages 90 | var current_page = active_routes[page].map(function (rule_set) { 91 | var args = {}, scalars = rule_set.rules.scalars, keyvals = rule_set.rules.keyvals, method, 92 | // populate the current request object as a collection of keys/values and scalars 93 | request = path.replace(page, '').split(SL).reduce(function (acc, val) { 94 | var split = val.split(EQ), key = split[0], value = split.slice(1).join(EQ); 95 | return !val.length ? acc // discard empty values, separate rest into scalars or keyvals 96 | : (value ? acc.keyvals[key] = value : acc.scalars.push(val)), acc; 97 | }, {keyvals: {}, scalars: []}), star, keyval, 98 | keyval_keys = keyvals.reduce(function (acc, val) {return (acc[val.name] = 0) || acc;}, {}), 99 | required_scalars_length = scalars.filter(function (val) {return val.required;}).length, 100 | required_keyvals = keyvals.filter(function (val) {return val.required;}) 101 | .every(function (val) {return request.keyvals[has](val.name);}); 102 | // not enough parameters are supplied in the request for this rule 103 | if (required_scalars_length > request.scalars.length || !required_keyvals) return 0; 104 | if (!rule_set.rules.star) { // too many params are only a problem if the rule isn't a wildcard 105 | if (request.scalars.length > scalars.length) return 0; // if too many scalars are supplied 106 | for (keyval in request.keyvals) // if too many keyvals are supplied 107 | if (request.keyvals[has](keyval) && !keyval_keys[has](keyval)) return 0; 108 | } 109 | request.scalars.slice(0, scalars.length) // populate args scalars 110 | .forEach(function (scalar, index) {args[scalars[index].name] = decode(scalar);}); 111 | keyvals.forEach(function (keyval) { // populate args keyvals 112 | if (request.keyvals[keyval.name]) args[keyval.name] = decode(request.keyvals[keyval.name]); 113 | delete request.keyvals[keyval.name]; // remove so that * can be constructed 114 | }); 115 | if (rule_set.rules.star) { // all unused scalars and keyvals go into the * argument (still encoded) 116 | star = request.scalars.slice(scalars.length, request.scalars.length); 117 | for (keyval in request.keyvals) if (request.keyvals[has](keyval)) 118 | star.push([keyval, request.keyvals[keyval]].join(EQ)); 119 | args[rule_set.rules.star] = star.join(SL); 120 | } 121 | try { // make sure the rule's method actually exists and can be accessed 122 | method = rule_set.method.split('.').reduce(function (acc, val) {return acc[val];}, context); 123 | if (typeof method !== 'function') throw new Error; 124 | } catch (error) { 125 | throw new TypeError(self + ': ' + rule_set.method + ' is not a function in current context'); 126 | } 127 | return {page: page, hash: routes.hash({route: rule_set.raw}, args), method: method, args: args}; 128 | }); 129 | return acc.concat(current_page).filter(Boolean); // only return the parsed rules that matched 130 | }, []).sort(function (a, b) {return b.hash.length - a.hash.length;}); // order in descending hash length 131 | }, 132 | /** 133 | * builds the internal representation of a rule based on the route definition 134 | * @inner 135 | * @name RouteMap#compile 136 | * @function 137 | * @param {String} route 138 | * @throws {SyntaxError} if any portion of a rule definition follows a * directive 139 | * @throws {SyntaxError} if a required scalar follows an optional scalar 140 | * @throws {SyntaxError} if a rule cannot be parsed 141 | * @type {Object} 142 | * @returns {Object} a compiled object, for example, the rule '/foo/:id/type:?/rest:*' would return 143 | * an object of the form:
{
144 |          *     page:'/foo',
145 |          *     rules:{
146 |          *         keyvals:[{name: 'type', required: false}],
147 |          *         scalars:[{name: 'id', required: true}],
148 |          *         star:'rest' // false if not defined
149 |          *     }
150 |          * }
151 |          * @see RouteMap.add
152 |          * @see RouteMap.hash
153 |          * @see RouteMap.remove
154 |          */
155 |         compile = (function (memo) { // compile is slow so cache compiled objects in a memo
156 |             return function (orig) {
157 |                 var self = 'compile', compiled, index, names = {},
158 |                     route = orig[0] === SL ? orig : ~(index = orig.indexOf(SL)) ? orig.slice(index) : 0,
159 |                     /** @ignore */
160 |                     valid_name = function (name) {
161 |                         if (names[has](name) || (names[name] = 0))
162 |                             throw new SyntaxError(self + ': "' + name + '" is repeated in: ' + orig);
163 |                     };
164 |                 if (!route) throw new SyntaxError(self + ': the route ' + orig + ' was not understood');
165 |                 if (memo[route]) return memo[route];
166 |                 compiled = route.split(SL).reduce(function (acc, val) {
167 |                     var rules = acc.rules, scalars = rules.scalars, keyvals = rules.keyvals;
168 |                     if (rules.star) throw new SyntaxError(self + ': no rules can follow a * directive in: ' + orig);
169 |                     // construct the name of the page
170 |                     if (!~val.search(token_exp) && !scalars.length && !keyvals.length) return acc.page.push(val), acc;
171 |                     // construct the parameters
172 |                     if (val.match(star_exp)) return (rules.star = RegExp.$2 || RegExp.$3), valid_name(rules.star), acc;
173 |                     if (val.match(scalar_exp)) {
174 |                         if (acc.has_optional_scalar) // no scalars can follow optional scalars
175 |                             throw new SyntaxError(self + ': "' + val + '" cannot follow an optional rule in: ' + orig);
176 |                         if (!!RegExp.$2) acc.has_optional_scalar = val;
177 |                         return scalars.push({name: RegExp.$1, required: !RegExp.$2}), valid_name(RegExp.$1), acc;
178 |                     }
179 |                     if (val.match(keyval_exp))
180 |                         return keyvals.push({name: RegExp.$1, required: !RegExp.$2}), valid_name(RegExp.$1), acc;
181 |                     throw new SyntaxError(self + ': the rule "' + val + '" was not understood in: ' + orig);
182 |                 }, {page: [], rules: {scalars: [], keyvals: [], star: false}, has_optional_scalar: ''});
183 |                 delete compiled.has_optional_scalar; // this is just a temporary value and should not be exposed
184 |                 compiled.page = compiled.page.join(SL).replace(new RegExp(SL + '$'), '') || SL;
185 |                 return memo[route] = compiled;
186 |             };
187 |         })({});
188 |     pub[namespace] = (routes) = { // parens around routes to satisfy JSDoc's caprice
189 |         /**
190 |          * adds a rule to the internal table of routes and methods
191 |          * @name RouteMap.add
192 |          * @function
193 |          * @type undefined
194 |          * @param {Object} rule rule specification
195 |          * @param {String} rule.route route pattern definition; there are three types of pattern arguments: scalars,
196 |          * keyvals, and stars; scalars are individual values in a URL (all URL values are separate by the
197 |          * '/' character), keyvals are named values, e.g. 'foo=bar', and star values are wildcards; so for
198 |          * example, the following pattern represents all the possible options:
199 | * '/foo/:id/:sub?/attr:/subattr:?/rest:*'
the ? means that argument is 200 | * optional, the star rule is named rest but it could have just simply been left as *, 201 | * which means the resultant dictionary would have put the wildcard remainder into args['*'] 202 | * instead of args.rest; so the following URL would match the pattern above:
203 | * /foo/23/45/attr=something/subattr=something_else
204 | * when its method is called, it will receive this arguments dictionary:
205 | *
{
206 |          *      id:'23',
207 |          *      subid:'45',
208 |          *      attr:'something',
209 |          *      subattr:'something_else',
210 |          *      rest:''
211 |          * }
212 | * add uses {@link #compile} and does not catch any errors thrown by that function 213 | * @param {String} rule.method listener method for this route 214 | * @throws {TypeError} if rule.route or rule.method are not strings or empty strings 215 | * @throws {Error} if rule has already been added 216 | * @see RouteMap.post_add 217 | */ 218 | add: function (rule) { 219 | var self = 'add', method = rule.method, route = rule.route, compiled, id = fingerprint(rule); 220 | if ([route, method].some(invalid_str)) 221 | throw new TypeError(self + ': rule.route and rule.method must both be non-empty strings'); 222 | if (added_routes[id]) throw new Error(self + ': ' + route + ' to ' + method + ' already exists'); 223 | compiled = compile(route); 224 | added_routes[id] = true; 225 | if (!active_routes[compiled.page] && (active_routes[compiled.page] = [])) // add route to list and sort 226 | flat_pages = flat_pages.concat(compiled.page).sort(function (a, b) {return b.length - a.length;}); 227 | active_routes[compiled.page].push(routes.post_add({method: method, rules: compiled.rules, raw: route})); 228 | }, 229 | /** 230 | * overrides the context where listener methods are sought, the default scope is window 231 | * (in a browser setting), returns the current context, if no scope object is passed in, just 232 | * returns current context without setting context 233 | * @name RouteMap.context 234 | * @function 235 | * @type {Object} 236 | * @returns {Object} the current context within which RouteMap searches for handlers 237 | * @param {Object} scope the scope within which methods for mapped routes will be looked for 238 | */ 239 | context: function (scope) {return context = typeof scope === 'object' ? scope : context;}, 240 | /** 241 | * returns the parsed (see {@link #parse}) currently accessed route; after listeners have finished 242 | * firing, current and last are the same 243 | * @name RouteMap.current 244 | * @function 245 | * @type Object 246 | * @returns {Object} the current parsed URL object 247 | * @see RouteMap.last 248 | */ 249 | current: function () {return current ? merge(current) : null;}, 250 | /** 251 | * this function is fired when no rule is matched by a URL, by default it does nothing, but it could be set up 252 | * to handle things like 404 responses on the server-side or bad hash fragments in the browser 253 | * @name RouteMap.default_handler 254 | * @function 255 | * @type undefined 256 | */ 257 | default_handler: function () {}, 258 | /** 259 | * URL grabber function, defaults to checking the URL fragment (hash); this function should be 260 | * overwritten in a server-side environment; this method is called by {@link RouteMap.handler}; without 261 | * window.location.hash it will return '/' 262 | * @name RouteMap.get 263 | * @function 264 | * @returns {String} by default, this returns a subset of the URL hash (everything after the first 265 | * '/' character ... if nothing follows a slash, it returns '/'); if overwritten, it 266 | * must be a function that returns URL path strings (beginning with '/') to match added rules 267 | * @type String 268 | */ 269 | get: function () { 270 | if (typeof window === 'undefined') return SL; 271 | var hash = window.location.hash, index = hash.indexOf(SL); 272 | return ~index ? hash.slice(index) : SL; 273 | }, 274 | /** 275 | * in a browser setting, it changes window.location.hash, in other settings, it should be 276 | * overwritten to do something useful (if necessary); it will not throw an error if window does 277 | * not exist 278 | * @name RouteMap.go 279 | * @function 280 | * @type undefined 281 | * @param {String} hash the hash fragment to go to 282 | */ 283 | go: function (hash) { 284 | if (typeof window !== 'undefined') window.location.hash = (hash.indexOf(PR) === 0 ? '' : PR) + hash; 285 | }, 286 | /** 287 | * main handler function for routing, this should be bound to hashchange events in the browser, or 288 | * (in conjunction with updating {@link RouteMap.get}) used with the HTML5 history API, it detects 289 | * all the matching route patterns, parses the URL parameters and fires their methods with the arguments from 290 | * the parsed URL; the timing of {@link RouteMap.current} and {@link RouteMap.last} being set is as follows 291 | * (pseudo-code): 292 | *
293 |          * path: get_route             // {@link RouteMap.get}
294 |          * parsed: parse path          // {@link #parse}
295 |          * current: longest parsed     // {@link RouteMap.current}
296 |          * parsed: pre_dispatch parsed // {@link RouteMap.pre_dispatch}
297 |          * current: longest parsed     // reset current
298 |          * fire matched rules in parsed
299 |          * last: current               // {@link RouteMap.last}
300 |          * 
301 | * RouteMap.handler calls {@link #parse} and does not catch any errors that function throws 302 | * @name RouteMap.handler 303 | * @function 304 | * @type undefined 305 | * @see RouteMap.pre_dispatch 306 | */ 307 | handler: function () { 308 | var url = routes.get(), parsed = parse(url), args = Array.prototype.slice.call(arguments); 309 | if (!parsed.length) return routes.default_handler.apply(null, [url].concat(args)); 310 | current = parsed[0]; // set current to the longest hash before pre_dispatch touches it 311 | parsed = routes.pre_dispatch(parsed); // pre_dispatch might change the contents of parsed 312 | current = parsed[0]; // set current to the longest hash again after pre_dispatch 313 | parsed.forEach(function (val) {val.method.apply(null, [val.args].concat(args));}); // fire requested methods 314 | last = parsed[0]; 315 | }, 316 | /** 317 | * returns a URL fragment by applying parameters to a rule; uses {@link #compile} and does not catch any errors 318 | * thrown by that function 319 | * @name RouteMap.hash 320 | * @function 321 | * @type String 322 | * @param {Object} rule the rule specification; it typically looks like:
323 | * {route:'/foo', method:'bar'}
but only route is strictly necessary 324 | * @param {Object} params a dictionary of argument key/value pairs required by the rule 325 | * @returns {String} URL fragment resulting from applying arguments to rule pattern 326 | * @throws {TypeError} if a required parameter is not present 327 | */ 328 | hash: function (rule, params) { 329 | var self = 'hash', hash, compiled, params = params || {}; 330 | if (invalid_str(rule.route)) throw new TypeError(self + ': rule.route must be a non-empty string'); 331 | compiled = compile(rule.route); 332 | hash = compiled.page + (compiled.page === SL ? '' : SL) + // 1. start with page, then add params 333 | compiled.rules.scalars.map(function (val) { // 2. add scalar values next 334 | var value = encode(params[val.name]), bad_param = params[val.name] === void 0 || invalid_str(value); 335 | if (val.required && bad_param) 336 | throw new TypeError(self + ': params.' + val.name + ' is undefined, route: ' + rule.route); 337 | return bad_param ? 0 : value; 338 | }) 339 | .concat(compiled.rules.keyvals.map(function (val) { // 3. then concat keyval values 340 | var value = encode(params[val.name]), bad_param = params[val.name] === void 0 || invalid_str(value); 341 | if (val.required && bad_param) 342 | throw new TypeError(self + ': params.' + val.name + ' is undefined, route: ' + rule.route); 343 | return bad_param ? 0 : val.name + EQ + value; 344 | })) 345 | .filter(Boolean).join(SL); // remove empty (0) values 346 | if (compiled.rules.star && params[compiled.rules.star]) // 4. add star value if it exists 347 | hash += (hash[hash.length - 1] === SL ? '' : SL) + params[compiled.rules.star]; 348 | return hash; 349 | }, 350 | /** 351 | * returns the parsed (see {@link #parse}) last accessed route; when route listeners are being called, 352 | * last is the previously accessed route, after listeners have finished firing, the current parsed 353 | * route replaces last's value 354 | * @name RouteMap.last 355 | * @function 356 | * @type Object 357 | * @returns {Object} the last parsed URL object, will be null on first load 358 | * @see RouteMap.current 359 | */ 360 | last: function () {return last ? merge(last) : null;}, 361 | /** 362 | * parses a URL fragment into a data structure only if there is a route whose pattern matches the fragment 363 | * @name RouteMap.parse 364 | * @function 365 | * @type Object 366 | * @returns {Object} of the form:
{page:'/foo', args:{bar:'some_value'}}
367 | * only if a rule with the route: '/foo/:bar' has already been added 368 | * @throws {TypeError} if hash is not a string, is empty, or does not contain a '/' character 369 | * @throws {SyntaxError} if hash cannot be parsed by {@link #parse} 370 | */ 371 | parse: function (hash) { 372 | var self = 'parse', parsed, index = hash.indexOf(SL); 373 | hash = ~index ? hash.slice(index) : ''; 374 | if (invalid_str(hash)) throw new TypeError(self + ': hash must be a string with a ' + SL + ' character'); 375 | if (!(parsed = parse(hash)).length) throw new SyntaxError(self + ': ' + hash + ' cannot be parsed'); 376 | return {page: parsed[0].page, args: parsed[0].args}; 377 | }, 378 | /** 379 | * this function is called by {@link RouteMap.add}, it receives a compiled rule object, e.g. for the rule: 380 | *
{route:'/foo/:id/:sub?/attr:/subattr:?/rest:*', method:'console.log'}
381 | * post_add would receive the following object: 382 | *
{
383 |          *     method:'console.log',
384 |          *     rules:{
385 |          *         scalars:[{name:'id',required:true},{name:'sub',required:false}],
386 |          *         keyvals:[{name:'attr',required:true},{name:'subattr',required:false}],
387 |          *         star:'rest'
388 |          *     },
389 |          *     raw:'/foo/:id/:sub?/attr:/subattr:?/rest:*'
390 |          * }
391 | * and it is expected to pass back an object of the same format; it can be overwritten to post-process added 392 | * rules e.g. to add extra default application-wide parameters; by default, it simply returns what was passed 393 | * into it 394 | * @name RouteMap.post_add 395 | * @function 396 | * @type Object 397 | * @returns {Object} the default function returns the exact object it received; a custom function needs to 398 | * an object that is of the same form (but could possibly have more or fewer parameters, etc.) 399 | * @param {Object} compiled the compiled rule 400 | */ 401 | post_add: function (compiled) {return compiled;}, 402 | /** 403 | * like {@link RouteMap.post_add} this function can be overwritten to add application-specific code into 404 | * route mapping, it is called before a route begins being dispatched to all matching rules; it receives the 405 | * list of matching parsed route objects ({@link #parse}) and is expected to return it; one application of this 406 | * function might be to set application-wide variables like debug flags 407 | * @name RouteMap.pre_dispatch 408 | * @function 409 | * @type Array 410 | * @returns {Array} a list of the same form as the one it receives 411 | * @param {Array} parsed the parsed request 412 | */ 413 | pre_dispatch: function (parsed) {return parsed;}, 414 | /** 415 | * if a string is passed in, it overwrites the prefix that is removed from each URL before parsing; primarily 416 | * used for hashbang (#!); either way, it returns the current prefix 417 | * @name RouteMap.prefix 418 | * @function 419 | * @type undefined 420 | * @param {String} prefix (optional) the prefix string 421 | */ 422 | prefix: function (prefix) {return PR = typeof prefix !== 'undefined' ? prefix + '' : PR;}, 423 | /** 424 | * counterpart to {@link RouteMap.add}, removes a rule specification; * remove uses 425 | * {@link #compile} and does not catch any errors thrown by that function 426 | * @name RouteMap.remove 427 | * @function 428 | * @type undefined 429 | * @param {Object} rule the rule specification that was used in {@link RouteMap.add} 430 | * @throws {TypeError} if rule.route or rule.method are not strings or empty strings 431 | */ 432 | remove: function (rule) { 433 | var self = 'remove', method = rule.method, route = rule.route, compiled, id = fingerprint(rule), index; 434 | if ([route, method].some(invalid_str)) 435 | throw new TypeError(self + ': rule.route and rule.method must both be non-empty strings'); 436 | if (!added_routes[id]) return; 437 | compiled = compile(route); 438 | delete added_routes[id]; 439 | active_routes[compiled.page] = active_routes[compiled.page] 440 | .filter(function (rule) {return (rule.raw !== route) || (rule.method !== method);}); 441 | if (!active_routes[compiled.page].length && (delete active_routes[compiled.page])) // delete active route 442 | if (~(index = flat_pages.indexOf(compiled.page))) flat_pages.splice(index, 1); // then flat page 443 | } 444 | }; 445 | })(typeof exports === 'undefined' ? window : exports, 'RouteMap'); -------------------------------------------------------------------------------- /tests/lib/qunit/qunit.css: -------------------------------------------------------------------------------- 1 | /** 2 | * QUnit - A JavaScript Unit Testing Framework 3 | * 4 | * http://docs.jquery.com/QUnit 5 | * 6 | * Copyright (c) 2011 John Resig, Jörn Zaefferer 7 | * Dual licensed under the MIT (MIT-LICENSE.txt) 8 | * or GPL (GPL-LICENSE.txt) licenses. 9 | * Pulled Live from Git Thu Aug 4 13:45:01 UTC 2011 10 | * Last Commit: 244c198ee73c1992ab005e0ac5f20fdcd50f14d9 11 | */ 12 | 13 | /** Font Family and Sizes */ 14 | 15 | #qunit-tests, #qunit-header, #qunit-banner, #qunit-testrunner-toolbar, #qunit-userAgent, #qunit-testresult { 16 | font-family: "Helvetica Neue Light", "HelveticaNeue-Light", "Helvetica Neue", Calibri, Helvetica, Arial, sans-serif; 17 | } 18 | 19 | #qunit-testrunner-toolbar, #qunit-userAgent, #qunit-testresult, #qunit-tests li { font-size: small; } 20 | #qunit-tests { font-size: smaller; } 21 | 22 | 23 | /** Resets */ 24 | 25 | #qunit-tests, #qunit-tests ol, #qunit-header, #qunit-banner, #qunit-userAgent, #qunit-testresult { 26 | margin: 0; 27 | padding: 0; 28 | } 29 | 30 | 31 | /** Header */ 32 | 33 | #qunit-header { 34 | padding: 0.5em 0 0.5em 1em; 35 | 36 | color: #8699a4; 37 | background-color: #0d3349; 38 | 39 | font-size: 1.5em; 40 | line-height: 1em; 41 | font-weight: normal; 42 | 43 | border-radius: 15px 15px 0 0; 44 | -moz-border-radius: 15px 15px 0 0; 45 | -webkit-border-top-right-radius: 15px; 46 | -webkit-border-top-left-radius: 15px; 47 | } 48 | 49 | #qunit-header a { 50 | text-decoration: none; 51 | color: #c2ccd1; 52 | } 53 | 54 | #qunit-header a:hover, 55 | #qunit-header a:focus { 56 | color: #fff; 57 | } 58 | 59 | #qunit-banner { 60 | height: 5px; 61 | } 62 | 63 | #qunit-testrunner-toolbar { 64 | padding: 0.5em 0 0.5em 2em; 65 | color: #5E740B; 66 | background-color: #eee; 67 | } 68 | 69 | #qunit-userAgent { 70 | padding: 0.5em 0 0.5em 2.5em; 71 | background-color: #2b81af; 72 | color: #fff; 73 | text-shadow: rgba(0, 0, 0, 0.5) 2px 2px 1px; 74 | } 75 | 76 | 77 | /** Tests: Pass/Fail */ 78 | 79 | #qunit-tests { 80 | list-style-position: inside; 81 | } 82 | 83 | #qunit-tests li { 84 | padding: 0.4em 0.5em 0.4em 2.5em; 85 | border-bottom: 1px solid #fff; 86 | list-style-position: inside; 87 | } 88 | 89 | #qunit-tests.hidepass li.pass, #qunit-tests.hidepass li.running { 90 | display: none; 91 | } 92 | 93 | #qunit-tests li strong { 94 | cursor: pointer; 95 | } 96 | 97 | #qunit-tests li a { 98 | padding: 0.5em; 99 | color: #c2ccd1; 100 | text-decoration: none; 101 | } 102 | #qunit-tests li a:hover, 103 | #qunit-tests li a:focus { 104 | color: #000; 105 | } 106 | 107 | #qunit-tests ol { 108 | margin-top: 0.5em; 109 | padding: 0.5em; 110 | 111 | background-color: #fff; 112 | 113 | border-radius: 15px; 114 | -moz-border-radius: 15px; 115 | -webkit-border-radius: 15px; 116 | 117 | box-shadow: inset 0px 2px 13px #999; 118 | -moz-box-shadow: inset 0px 2px 13px #999; 119 | -webkit-box-shadow: inset 0px 2px 13px #999; 120 | } 121 | 122 | #qunit-tests table { 123 | border-collapse: collapse; 124 | margin-top: .2em; 125 | } 126 | 127 | #qunit-tests th { 128 | text-align: right; 129 | vertical-align: top; 130 | padding: 0 .5em 0 0; 131 | } 132 | 133 | #qunit-tests td { 134 | vertical-align: top; 135 | } 136 | 137 | #qunit-tests pre { 138 | margin: 0; 139 | white-space: pre-wrap; 140 | word-wrap: break-word; 141 | } 142 | 143 | #qunit-tests del { 144 | background-color: #e0f2be; 145 | color: #374e0c; 146 | text-decoration: none; 147 | } 148 | 149 | #qunit-tests ins { 150 | background-color: #ffcaca; 151 | color: #500; 152 | text-decoration: none; 153 | } 154 | 155 | /*** Test Counts */ 156 | 157 | #qunit-tests b.counts { color: black; } 158 | #qunit-tests b.passed { color: #5E740B; } 159 | #qunit-tests b.failed { color: #710909; } 160 | 161 | #qunit-tests li li { 162 | margin: 0.5em; 163 | padding: 0.4em 0.5em 0.4em 0.5em; 164 | background-color: #fff; 165 | border-bottom: none; 166 | list-style-position: inside; 167 | } 168 | 169 | /*** Passing Styles */ 170 | 171 | #qunit-tests li li.pass { 172 | color: #5E740B; 173 | background-color: #fff; 174 | border-left: 26px solid #C6E746; 175 | } 176 | 177 | #qunit-tests .pass { color: #528CE0; background-color: #D2E0E6; } 178 | #qunit-tests .pass .test-name { color: #366097; } 179 | 180 | #qunit-tests .pass .test-actual, 181 | #qunit-tests .pass .test-expected { color: #999999; } 182 | 183 | #qunit-banner.qunit-pass { background-color: #C6E746; } 184 | 185 | /*** Failing Styles */ 186 | 187 | #qunit-tests li li.fail { 188 | color: #710909; 189 | background-color: #fff; 190 | border-left: 26px solid #EE5757; 191 | } 192 | 193 | #qunit-tests > li:last-child { 194 | border-radius: 0 0 15px 15px; 195 | -moz-border-radius: 0 0 15px 15px; 196 | -webkit-border-bottom-right-radius: 15px; 197 | -webkit-border-bottom-left-radius: 15px; 198 | } 199 | 200 | #qunit-tests .fail { color: #000000; background-color: #EE5757; } 201 | #qunit-tests .fail .test-name, 202 | #qunit-tests .fail .module-name { color: #000000; } 203 | 204 | #qunit-tests .fail .test-actual { color: #EE5757; } 205 | #qunit-tests .fail .test-expected { color: green; } 206 | 207 | #qunit-banner.qunit-fail { background-color: #EE5757; } 208 | 209 | 210 | /** Result */ 211 | 212 | #qunit-testresult { 213 | padding: 0.5em 0.5em 0.5em 2.5em; 214 | 215 | color: #2b81af; 216 | background-color: #D2E0E6; 217 | 218 | border-bottom: 1px solid white; 219 | } 220 | 221 | /** Fixture */ 222 | 223 | #qunit-fixture { 224 | position: absolute; 225 | top: -10000px; 226 | left: -10000px; 227 | } -------------------------------------------------------------------------------- /tests/lib/qunit/qunit.js: -------------------------------------------------------------------------------- 1 | /** 2 | * QUnit - A JavaScript Unit Testing Framework 3 | * 4 | * http://docs.jquery.com/QUnit 5 | * 6 | * Copyright (c) 2011 John Resig, Jörn Zaefferer 7 | * Dual licensed under the MIT (MIT-LICENSE.txt) 8 | * or GPL (GPL-LICENSE.txt) licenses. 9 | * Pulled Live from Git Thu Aug 4 13:45:01 UTC 2011 10 | * Last Commit: 244c198ee73c1992ab005e0ac5f20fdcd50f14d9 11 | */ 12 | 13 | (function(window) { 14 | 15 | var defined = { 16 | setTimeout: typeof window.setTimeout !== "undefined", 17 | sessionStorage: (function() { 18 | try { 19 | return !!sessionStorage.getItem; 20 | } catch(e){ 21 | return false; 22 | } 23 | })() 24 | }; 25 | 26 | var testId = 0; 27 | 28 | var Test = function(name, testName, expected, testEnvironmentArg, async, callback) { 29 | this.name = name; 30 | this.testName = testName; 31 | this.expected = expected; 32 | this.testEnvironmentArg = testEnvironmentArg; 33 | this.async = async; 34 | this.callback = callback; 35 | this.assertions = []; 36 | }; 37 | Test.prototype = { 38 | init: function() { 39 | var tests = id("qunit-tests"); 40 | if (tests) { 41 | var b = document.createElement("strong"); 42 | b.innerHTML = "Running " + this.name; 43 | var li = document.createElement("li"); 44 | li.appendChild( b ); 45 | li.className = "running"; 46 | li.id = this.id = "test-output" + testId++; 47 | tests.appendChild( li ); 48 | } 49 | }, 50 | setup: function() { 51 | if (this.module != config.previousModule) { 52 | if ( config.previousModule ) { 53 | QUnit.moduleDone( { 54 | name: config.previousModule, 55 | failed: config.moduleStats.bad, 56 | passed: config.moduleStats.all - config.moduleStats.bad, 57 | total: config.moduleStats.all 58 | } ); 59 | } 60 | config.previousModule = this.module; 61 | config.moduleStats = { all: 0, bad: 0 }; 62 | QUnit.moduleStart( { 63 | name: this.module 64 | } ); 65 | } 66 | 67 | config.current = this; 68 | this.testEnvironment = extend({ 69 | setup: function() {}, 70 | teardown: function() {} 71 | }, this.moduleTestEnvironment); 72 | if (this.testEnvironmentArg) { 73 | extend(this.testEnvironment, this.testEnvironmentArg); 74 | } 75 | 76 | QUnit.testStart( { 77 | name: this.testName 78 | } ); 79 | 80 | // allow utility functions to access the current test environment 81 | // TODO why?? 82 | QUnit.current_testEnvironment = this.testEnvironment; 83 | 84 | try { 85 | if ( !config.pollution ) { 86 | saveGlobal(); 87 | } 88 | 89 | this.testEnvironment.setup.call(this.testEnvironment); 90 | } catch(e) { 91 | QUnit.ok( false, "Setup failed on " + this.testName + ": " + e.message ); 92 | } 93 | }, 94 | run: function() { 95 | if ( this.async ) { 96 | QUnit.stop(); 97 | } 98 | 99 | if ( config.notrycatch ) { 100 | this.callback.call(this.testEnvironment); 101 | return; 102 | } 103 | try { 104 | this.callback.call(this.testEnvironment); 105 | } catch(e) { 106 | fail("Test " + this.testName + " died, exception and test follows", e, this.callback); 107 | QUnit.ok( false, "Died on test #" + (this.assertions.length + 1) + ": " + e.message + " - " + QUnit.jsDump.parse(e) ); 108 | // else next test will carry the responsibility 109 | saveGlobal(); 110 | 111 | // Restart the tests if they're blocking 112 | if ( config.blocking ) { 113 | start(); 114 | } 115 | } 116 | }, 117 | teardown: function() { 118 | try { 119 | this.testEnvironment.teardown.call(this.testEnvironment); 120 | checkPollution(); 121 | } catch(e) { 122 | QUnit.ok( false, "Teardown failed on " + this.testName + ": " + e.message ); 123 | } 124 | }, 125 | finish: function() { 126 | if ( this.expected && this.expected != this.assertions.length ) { 127 | QUnit.ok( false, "Expected " + this.expected + " assertions, but " + this.assertions.length + " were run" ); 128 | } 129 | 130 | var good = 0, bad = 0, 131 | tests = id("qunit-tests"); 132 | 133 | config.stats.all += this.assertions.length; 134 | config.moduleStats.all += this.assertions.length; 135 | 136 | if ( tests ) { 137 | var ol = document.createElement("ol"); 138 | 139 | for ( var i = 0; i < this.assertions.length; i++ ) { 140 | var assertion = this.assertions[i]; 141 | 142 | var li = document.createElement("li"); 143 | li.className = assertion.result ? "pass" : "fail"; 144 | li.innerHTML = assertion.message || (assertion.result ? "okay" : "failed"); 145 | ol.appendChild( li ); 146 | 147 | if ( assertion.result ) { 148 | good++; 149 | } else { 150 | bad++; 151 | config.stats.bad++; 152 | config.moduleStats.bad++; 153 | } 154 | } 155 | 156 | // store result when possible 157 | if ( QUnit.config.reorder && defined.sessionStorage ) { 158 | if (bad) { 159 | sessionStorage.setItem("qunit-" + this.module + "-" + this.testName, bad); 160 | } else { 161 | sessionStorage.removeItem("qunit-" + this.module + "-" + this.testName); 162 | } 163 | } 164 | 165 | if (bad == 0) { 166 | ol.style.display = "none"; 167 | } 168 | 169 | var b = document.createElement("strong"); 170 | b.innerHTML = this.name + " (" + bad + ", " + good + ", " + this.assertions.length + ")"; 171 | 172 | var a = document.createElement("a"); 173 | a.innerHTML = "Rerun"; 174 | a.href = QUnit.url({ filter: getText([b]).replace(/\([^)]+\)$/, "").replace(/(^\s*|\s*$)/g, "") }); 175 | 176 | addEvent(b, "click", function() { 177 | var next = b.nextSibling.nextSibling, 178 | display = next.style.display; 179 | next.style.display = display === "none" ? "block" : "none"; 180 | }); 181 | 182 | addEvent(b, "dblclick", function(e) { 183 | var target = e && e.target ? e.target : window.event.srcElement; 184 | if ( target.nodeName.toLowerCase() == "span" || target.nodeName.toLowerCase() == "b" ) { 185 | target = target.parentNode; 186 | } 187 | if ( window.location && target.nodeName.toLowerCase() === "strong" ) { 188 | window.location = QUnit.url({ filter: getText([target]).replace(/\([^)]+\)$/, "").replace(/(^\s*|\s*$)/g, "") }); 189 | } 190 | }); 191 | 192 | var li = id(this.id); 193 | li.className = bad ? "fail" : "pass"; 194 | li.removeChild( li.firstChild ); 195 | li.appendChild( b ); 196 | li.appendChild( a ); 197 | li.appendChild( ol ); 198 | 199 | } else { 200 | for ( var i = 0; i < this.assertions.length; i++ ) { 201 | if ( !this.assertions[i].result ) { 202 | bad++; 203 | config.stats.bad++; 204 | config.moduleStats.bad++; 205 | } 206 | } 207 | } 208 | 209 | try { 210 | QUnit.reset(); 211 | } catch(e) { 212 | fail("reset() failed, following Test " + this.testName + ", exception and reset fn follows", e, QUnit.reset); 213 | } 214 | 215 | QUnit.testDone( { 216 | name: this.testName, 217 | failed: bad, 218 | passed: this.assertions.length - bad, 219 | total: this.assertions.length 220 | } ); 221 | }, 222 | 223 | queue: function() { 224 | var test = this; 225 | synchronize(function() { 226 | test.init(); 227 | }); 228 | function run() { 229 | // each of these can by async 230 | synchronize(function() { 231 | test.setup(); 232 | }); 233 | synchronize(function() { 234 | test.run(); 235 | }); 236 | synchronize(function() { 237 | test.teardown(); 238 | }); 239 | synchronize(function() { 240 | test.finish(); 241 | }); 242 | } 243 | // defer when previous test run passed, if storage is available 244 | var bad = QUnit.config.reorder && defined.sessionStorage && +sessionStorage.getItem("qunit-" + this.module + "-" + this.testName); 245 | if (bad) { 246 | run(); 247 | } else { 248 | synchronize(run); 249 | }; 250 | } 251 | 252 | }; 253 | 254 | var QUnit = { 255 | 256 | // call on start of module test to prepend name to all tests 257 | module: function(name, testEnvironment) { 258 | config.currentModule = name; 259 | config.currentModuleTestEnviroment = testEnvironment; 260 | }, 261 | 262 | asyncTest: function(testName, expected, callback) { 263 | if ( arguments.length === 2 ) { 264 | callback = expected; 265 | expected = 0; 266 | } 267 | 268 | QUnit.test(testName, expected, callback, true); 269 | }, 270 | 271 | test: function(testName, expected, callback, async) { 272 | var name = '' + testName + '', testEnvironmentArg; 273 | 274 | if ( arguments.length === 2 ) { 275 | callback = expected; 276 | expected = null; 277 | } 278 | // is 2nd argument a testEnvironment? 279 | if ( expected && typeof expected === 'object') { 280 | testEnvironmentArg = expected; 281 | expected = null; 282 | } 283 | 284 | if ( config.currentModule ) { 285 | name = '' + config.currentModule + ": " + name; 286 | } 287 | 288 | if ( !validTest(config.currentModule + ": " + testName) ) { 289 | return; 290 | } 291 | 292 | var test = new Test(name, testName, expected, testEnvironmentArg, async, callback); 293 | test.module = config.currentModule; 294 | test.moduleTestEnvironment = config.currentModuleTestEnviroment; 295 | test.queue(); 296 | }, 297 | 298 | /** 299 | * Specify the number of expected assertions to gurantee that failed test (no assertions are run at all) don't slip through. 300 | */ 301 | expect: function(asserts) { 302 | config.current.expected = asserts; 303 | }, 304 | 305 | /** 306 | * Asserts true. 307 | * @example ok( "asdfasdf".length > 5, "There must be at least 5 chars" ); 308 | */ 309 | ok: function(a, msg) { 310 | a = !!a; 311 | var details = { 312 | result: a, 313 | message: msg 314 | }; 315 | msg = escapeHtml(msg); 316 | QUnit.log(details); 317 | config.current.assertions.push({ 318 | result: a, 319 | message: msg 320 | }); 321 | }, 322 | 323 | /** 324 | * Checks that the first two arguments are equal, with an optional message. 325 | * Prints out both actual and expected values. 326 | * 327 | * Prefered to ok( actual == expected, message ) 328 | * 329 | * @example equal( format("Received {0} bytes.", 2), "Received 2 bytes." ); 330 | * 331 | * @param Object actual 332 | * @param Object expected 333 | * @param String message (optional) 334 | */ 335 | equal: function(actual, expected, message) { 336 | QUnit.push(expected == actual, actual, expected, message); 337 | }, 338 | 339 | notEqual: function(actual, expected, message) { 340 | QUnit.push(expected != actual, actual, expected, message); 341 | }, 342 | 343 | deepEqual: function(actual, expected, message) { 344 | QUnit.push(QUnit.equiv(actual, expected), actual, expected, message); 345 | }, 346 | 347 | notDeepEqual: function(actual, expected, message) { 348 | QUnit.push(!QUnit.equiv(actual, expected), actual, expected, message); 349 | }, 350 | 351 | strictEqual: function(actual, expected, message) { 352 | QUnit.push(expected === actual, actual, expected, message); 353 | }, 354 | 355 | notStrictEqual: function(actual, expected, message) { 356 | QUnit.push(expected !== actual, actual, expected, message); 357 | }, 358 | 359 | raises: function(block, expected, message) { 360 | var actual, ok = false; 361 | 362 | if (typeof expected === 'string') { 363 | message = expected; 364 | expected = null; 365 | } 366 | 367 | try { 368 | block(); 369 | } catch (e) { 370 | actual = e; 371 | } 372 | 373 | if (actual) { 374 | // we don't want to validate thrown error 375 | if (!expected) { 376 | ok = true; 377 | // expected is a regexp 378 | } else if (QUnit.objectType(expected) === "regexp") { 379 | ok = expected.test(actual); 380 | // expected is a constructor 381 | } else if (actual instanceof expected) { 382 | ok = true; 383 | // expected is a validation function which returns true is validation passed 384 | } else if (expected.call({}, actual) === true) { 385 | ok = true; 386 | } 387 | } 388 | 389 | QUnit.ok(ok, message); 390 | }, 391 | 392 | start: function() { 393 | config.semaphore--; 394 | if (config.semaphore > 0) { 395 | // don't start until equal number of stop-calls 396 | return; 397 | } 398 | if (config.semaphore < 0) { 399 | // ignore if start is called more often then stop 400 | config.semaphore = 0; 401 | } 402 | // A slight delay, to avoid any current callbacks 403 | if ( defined.setTimeout ) { 404 | window.setTimeout(function() { 405 | if ( config.timeout ) { 406 | clearTimeout(config.timeout); 407 | } 408 | 409 | config.blocking = false; 410 | process(); 411 | }, 13); 412 | } else { 413 | config.blocking = false; 414 | process(); 415 | } 416 | }, 417 | 418 | stop: function(timeout) { 419 | config.semaphore++; 420 | config.blocking = true; 421 | 422 | if ( timeout && defined.setTimeout ) { 423 | clearTimeout(config.timeout); 424 | config.timeout = window.setTimeout(function() { 425 | QUnit.ok( false, "Test timed out" ); 426 | QUnit.start(); 427 | }, timeout); 428 | } 429 | } 430 | }; 431 | 432 | // Backwards compatibility, deprecated 433 | QUnit.equals = QUnit.equal; 434 | QUnit.same = QUnit.deepEqual; 435 | 436 | // Maintain internal state 437 | var config = { 438 | // The queue of tests to run 439 | queue: [], 440 | 441 | // block until document ready 442 | blocking: true, 443 | 444 | // by default, run previously failed tests first 445 | // very useful in combination with "Hide passed tests" checked 446 | reorder: true, 447 | 448 | noglobals: false, 449 | notrycatch: false 450 | }; 451 | 452 | // Load paramaters 453 | (function() { 454 | var location = window.location || { search: "", protocol: "file:" }, 455 | params = location.search.slice( 1 ).split( "&" ), 456 | length = params.length, 457 | urlParams = {}, 458 | current; 459 | 460 | if ( params[ 0 ] ) { 461 | for ( var i = 0; i < length; i++ ) { 462 | current = params[ i ].split( "=" ); 463 | current[ 0 ] = decodeURIComponent( current[ 0 ] ); 464 | // allow just a key to turn on a flag, e.g., test.html?noglobals 465 | current[ 1 ] = current[ 1 ] ? decodeURIComponent( current[ 1 ] ) : true; 466 | urlParams[ current[ 0 ] ] = current[ 1 ]; 467 | if ( current[ 0 ] in config ) { 468 | config[ current[ 0 ] ] = current[ 1 ]; 469 | } 470 | } 471 | } 472 | 473 | QUnit.urlParams = urlParams; 474 | config.filter = urlParams.filter; 475 | 476 | // Figure out if we're running the tests from a server or not 477 | QUnit.isLocal = !!(location.protocol === 'file:'); 478 | })(); 479 | 480 | // Expose the API as global variables, unless an 'exports' 481 | // object exists, in that case we assume we're in CommonJS 482 | if ( typeof exports === "undefined" || typeof require === "undefined" ) { 483 | extend(window, QUnit); 484 | window.QUnit = QUnit; 485 | } else { 486 | extend(exports, QUnit); 487 | exports.QUnit = QUnit; 488 | } 489 | 490 | // define these after exposing globals to keep them in these QUnit namespace only 491 | extend(QUnit, { 492 | config: config, 493 | 494 | // Initialize the configuration options 495 | init: function() { 496 | extend(config, { 497 | stats: { all: 0, bad: 0 }, 498 | moduleStats: { all: 0, bad: 0 }, 499 | started: +new Date, 500 | updateRate: 1000, 501 | blocking: false, 502 | autostart: true, 503 | autorun: false, 504 | filter: "", 505 | queue: [], 506 | semaphore: 0 507 | }); 508 | 509 | var tests = id( "qunit-tests" ), 510 | banner = id( "qunit-banner" ), 511 | result = id( "qunit-testresult" ); 512 | 513 | if ( tests ) { 514 | tests.innerHTML = ""; 515 | } 516 | 517 | if ( banner ) { 518 | banner.className = ""; 519 | } 520 | 521 | if ( result ) { 522 | result.parentNode.removeChild( result ); 523 | } 524 | 525 | if ( tests ) { 526 | result = document.createElement( "p" ); 527 | result.id = "qunit-testresult"; 528 | result.className = "result"; 529 | tests.parentNode.insertBefore( result, tests ); 530 | result.innerHTML = 'Running...
 '; 531 | } 532 | }, 533 | 534 | /** 535 | * Resets the test setup. Useful for tests that modify the DOM. 536 | * 537 | * If jQuery is available, uses jQuery's html(), otherwise just innerHTML. 538 | */ 539 | reset: function() { 540 | if ( window.jQuery ) { 541 | jQuery( "#qunit-fixture" ).html( config.fixture ); 542 | } else { 543 | var main = id( 'qunit-fixture' ); 544 | if ( main ) { 545 | main.innerHTML = config.fixture; 546 | } 547 | } 548 | }, 549 | 550 | /** 551 | * Trigger an event on an element. 552 | * 553 | * @example triggerEvent( document.body, "click" ); 554 | * 555 | * @param DOMElement elem 556 | * @param String type 557 | */ 558 | triggerEvent: function( elem, type, event ) { 559 | if ( document.createEvent ) { 560 | event = document.createEvent("MouseEvents"); 561 | event.initMouseEvent(type, true, true, elem.ownerDocument.defaultView, 562 | 0, 0, 0, 0, 0, false, false, false, false, 0, null); 563 | elem.dispatchEvent( event ); 564 | 565 | } else if ( elem.fireEvent ) { 566 | elem.fireEvent("on"+type); 567 | } 568 | }, 569 | 570 | // Safe object type checking 571 | is: function( type, obj ) { 572 | return QUnit.objectType( obj ) == type; 573 | }, 574 | 575 | objectType: function( obj ) { 576 | if (typeof obj === "undefined") { 577 | return "undefined"; 578 | 579 | // consider: typeof null === object 580 | } 581 | if (obj === null) { 582 | return "null"; 583 | } 584 | 585 | var type = Object.prototype.toString.call( obj ) 586 | .match(/^\[object\s(.*)\]$/)[1] || ''; 587 | 588 | switch (type) { 589 | case 'Number': 590 | if (isNaN(obj)) { 591 | return "nan"; 592 | } else { 593 | return "number"; 594 | } 595 | case 'String': 596 | case 'Boolean': 597 | case 'Array': 598 | case 'Date': 599 | case 'RegExp': 600 | case 'Function': 601 | return type.toLowerCase(); 602 | } 603 | if (typeof obj === "object") { 604 | return "object"; 605 | } 606 | return undefined; 607 | }, 608 | 609 | push: function(result, actual, expected, message) { 610 | var details = { 611 | result: result, 612 | message: message, 613 | actual: actual, 614 | expected: expected 615 | }; 616 | 617 | message = escapeHtml(message) || (result ? "okay" : "failed"); 618 | message = '' + message + ""; 619 | expected = escapeHtml(QUnit.jsDump.parse(expected)); 620 | actual = escapeHtml(QUnit.jsDump.parse(actual)); 621 | var output = message + ''; 622 | if (actual != expected) { 623 | output += ''; 624 | output += ''; 625 | } 626 | if (!result) { 627 | var source = sourceFromStacktrace(); 628 | if (source) { 629 | details.source = source; 630 | output += ''; 631 | } 632 | } 633 | output += "
Expected:
' + expected + '
Result:
' + actual + '
Diff:
' + QUnit.diff(expected, actual) +'
Source:
' + escapeHtml(source) + '
"; 634 | 635 | QUnit.log(details); 636 | 637 | config.current.assertions.push({ 638 | result: !!result, 639 | message: output 640 | }); 641 | }, 642 | 643 | url: function( params ) { 644 | params = extend( extend( {}, QUnit.urlParams ), params ); 645 | var querystring = "?", 646 | key; 647 | for ( key in params ) { 648 | querystring += encodeURIComponent( key ) + "=" + 649 | encodeURIComponent( params[ key ] ) + "&"; 650 | } 651 | return window.location.pathname + querystring.slice( 0, -1 ); 652 | }, 653 | 654 | extend: extend, 655 | id: id, 656 | addEvent: addEvent, 657 | 658 | // Logging callbacks; all receive a single argument with the listed properties 659 | // run test/logs.html for any related changes 660 | begin: function() {}, 661 | // done: { failed, passed, total, runtime } 662 | done: function() {}, 663 | // log: { result, actual, expected, message } 664 | log: function() {}, 665 | // testStart: { name } 666 | testStart: function() {}, 667 | // testDone: { name, failed, passed, total } 668 | testDone: function() {}, 669 | // moduleStart: { name } 670 | moduleStart: function() {}, 671 | // moduleDone: { name, failed, passed, total } 672 | moduleDone: function() {} 673 | }); 674 | 675 | if ( typeof document === "undefined" || document.readyState === "complete" ) { 676 | config.autorun = true; 677 | } 678 | 679 | addEvent(window, "load", function() { 680 | QUnit.begin({}); 681 | 682 | // Initialize the config, saving the execution queue 683 | var oldconfig = extend({}, config); 684 | QUnit.init(); 685 | extend(config, oldconfig); 686 | 687 | config.blocking = false; 688 | 689 | var userAgent = id("qunit-userAgent"); 690 | if ( userAgent ) { 691 | userAgent.innerHTML = navigator.userAgent; 692 | } 693 | var banner = id("qunit-header"); 694 | if ( banner ) { 695 | banner.innerHTML = ' ' + banner.innerHTML + ' ' + 696 | '' + 697 | ''; 698 | addEvent( banner, "change", function( event ) { 699 | var params = {}; 700 | params[ event.target.name ] = event.target.checked ? true : undefined; 701 | window.location = QUnit.url( params ); 702 | }); 703 | } 704 | 705 | var toolbar = id("qunit-testrunner-toolbar"); 706 | if ( toolbar ) { 707 | var filter = document.createElement("input"); 708 | filter.type = "checkbox"; 709 | filter.id = "qunit-filter-pass"; 710 | addEvent( filter, "click", function() { 711 | var ol = document.getElementById("qunit-tests"); 712 | if ( filter.checked ) { 713 | ol.className = ol.className + " hidepass"; 714 | } else { 715 | var tmp = " " + ol.className.replace( /[\n\t\r]/g, " " ) + " "; 716 | ol.className = tmp.replace(/ hidepass /, " "); 717 | } 718 | if ( defined.sessionStorage ) { 719 | if (filter.checked) { 720 | sessionStorage.setItem("qunit-filter-passed-tests", "true"); 721 | } else { 722 | sessionStorage.removeItem("qunit-filter-passed-tests"); 723 | } 724 | } 725 | }); 726 | if ( defined.sessionStorage && sessionStorage.getItem("qunit-filter-passed-tests") ) { 727 | filter.checked = true; 728 | var ol = document.getElementById("qunit-tests"); 729 | ol.className = ol.className + " hidepass"; 730 | } 731 | toolbar.appendChild( filter ); 732 | 733 | var label = document.createElement("label"); 734 | label.setAttribute("for", "qunit-filter-pass"); 735 | label.innerHTML = "Hide passed tests"; 736 | toolbar.appendChild( label ); 737 | } 738 | 739 | var main = id('qunit-fixture'); 740 | if ( main ) { 741 | config.fixture = main.innerHTML; 742 | } 743 | 744 | if (config.autostart) { 745 | QUnit.start(); 746 | } 747 | }); 748 | 749 | function done() { 750 | config.autorun = true; 751 | 752 | // Log the last module results 753 | if ( config.currentModule ) { 754 | QUnit.moduleDone( { 755 | name: config.currentModule, 756 | failed: config.moduleStats.bad, 757 | passed: config.moduleStats.all - config.moduleStats.bad, 758 | total: config.moduleStats.all 759 | } ); 760 | } 761 | 762 | var banner = id("qunit-banner"), 763 | tests = id("qunit-tests"), 764 | runtime = +new Date - config.started, 765 | passed = config.stats.all - config.stats.bad, 766 | html = [ 767 | 'Tests completed in ', 768 | runtime, 769 | ' milliseconds.
', 770 | '', 771 | passed, 772 | ' tests of ', 773 | config.stats.all, 774 | ' passed, ', 775 | config.stats.bad, 776 | ' failed.' 777 | ].join(''); 778 | 779 | if ( banner ) { 780 | banner.className = (config.stats.bad ? "qunit-fail" : "qunit-pass"); 781 | } 782 | 783 | if ( tests ) { 784 | id( "qunit-testresult" ).innerHTML = html; 785 | } 786 | 787 | if ( typeof document !== "undefined" && document.title ) { 788 | // show ✖ for bad, ✔ for good suite result in title 789 | // use escape sequences in case file gets loaded with non-utf-8-charset 790 | document.title = (config.stats.bad ? "\u2716" : "\u2714") + " " + document.title; 791 | } 792 | 793 | QUnit.done( { 794 | failed: config.stats.bad, 795 | passed: passed, 796 | total: config.stats.all, 797 | runtime: runtime 798 | } ); 799 | } 800 | 801 | function validTest( name ) { 802 | var filter = config.filter, 803 | run = false; 804 | 805 | if ( !filter ) { 806 | return true; 807 | } 808 | 809 | var not = filter.charAt( 0 ) === "!"; 810 | if ( not ) { 811 | filter = filter.slice( 1 ); 812 | } 813 | 814 | if ( name.indexOf( filter ) !== -1 ) { 815 | return !not; 816 | } 817 | 818 | if ( not ) { 819 | run = true; 820 | } 821 | 822 | return run; 823 | } 824 | 825 | // so far supports only Firefox, Chrome and Opera (buggy) 826 | // could be extended in the future to use something like https://github.com/csnover/TraceKit 827 | function sourceFromStacktrace() { 828 | try { 829 | throw new Error(); 830 | } catch ( e ) { 831 | if (e.stacktrace) { 832 | // Opera 833 | return e.stacktrace.split("\n")[6]; 834 | } else if (e.stack) { 835 | // Firefox, Chrome 836 | return e.stack.split("\n")[4]; 837 | } 838 | } 839 | } 840 | 841 | function escapeHtml(s) { 842 | if (!s) { 843 | return ""; 844 | } 845 | s = s + ""; 846 | return s.replace(/[\&"<>\\]/g, function(s) { 847 | switch(s) { 848 | case "&": return "&"; 849 | case "\\": return "\\\\"; 850 | case '"': return '\"'; 851 | case "<": return "<"; 852 | case ">": return ">"; 853 | default: return s; 854 | } 855 | }); 856 | } 857 | 858 | function synchronize( callback ) { 859 | config.queue.push( callback ); 860 | 861 | if ( config.autorun && !config.blocking ) { 862 | process(); 863 | } 864 | } 865 | 866 | function process() { 867 | var start = (new Date()).getTime(); 868 | 869 | while ( config.queue.length && !config.blocking ) { 870 | if ( config.updateRate <= 0 || (((new Date()).getTime() - start) < config.updateRate) ) { 871 | config.queue.shift()(); 872 | } else { 873 | window.setTimeout( process, 13 ); 874 | break; 875 | } 876 | } 877 | if (!config.blocking && !config.queue.length) { 878 | done(); 879 | } 880 | } 881 | 882 | function saveGlobal() { 883 | config.pollution = []; 884 | 885 | if ( config.noglobals ) { 886 | for ( var key in window ) { 887 | config.pollution.push( key ); 888 | } 889 | } 890 | } 891 | 892 | function checkPollution( name ) { 893 | var old = config.pollution; 894 | saveGlobal(); 895 | 896 | var newGlobals = diff( config.pollution, old ); 897 | if ( newGlobals.length > 0 ) { 898 | ok( false, "Introduced global variable(s): " + newGlobals.join(", ") ); 899 | } 900 | 901 | var deletedGlobals = diff( old, config.pollution ); 902 | if ( deletedGlobals.length > 0 ) { 903 | ok( false, "Deleted global variable(s): " + deletedGlobals.join(", ") ); 904 | } 905 | } 906 | 907 | // returns a new Array with the elements that are in a but not in b 908 | function diff( a, b ) { 909 | var result = a.slice(); 910 | for ( var i = 0; i < result.length; i++ ) { 911 | for ( var j = 0; j < b.length; j++ ) { 912 | if ( result[i] === b[j] ) { 913 | result.splice(i, 1); 914 | i--; 915 | break; 916 | } 917 | } 918 | } 919 | return result; 920 | } 921 | 922 | function fail(message, exception, callback) { 923 | if ( typeof console !== "undefined" && console.error && console.warn ) { 924 | console.error(message); 925 | console.error(exception); 926 | console.warn(callback.toString()); 927 | 928 | } else if ( window.opera && opera.postError ) { 929 | opera.postError(message, exception, callback.toString); 930 | } 931 | } 932 | 933 | function extend(a, b) { 934 | for ( var prop in b ) { 935 | if ( b[prop] === undefined ) { 936 | delete a[prop]; 937 | } else { 938 | a[prop] = b[prop]; 939 | } 940 | } 941 | 942 | return a; 943 | } 944 | 945 | function addEvent(elem, type, fn) { 946 | if ( elem.addEventListener ) { 947 | elem.addEventListener( type, fn, false ); 948 | } else if ( elem.attachEvent ) { 949 | elem.attachEvent( "on" + type, fn ); 950 | } else { 951 | fn(); 952 | } 953 | } 954 | 955 | function id(name) { 956 | return !!(typeof document !== "undefined" && document && document.getElementById) && 957 | document.getElementById( name ); 958 | } 959 | 960 | // Test for equality any JavaScript type. 961 | // Discussions and reference: http://philrathe.com/articles/equiv 962 | // Test suites: http://philrathe.com/tests/equiv 963 | // Author: Philippe Rathé 964 | QUnit.equiv = function () { 965 | 966 | var innerEquiv; // the real equiv function 967 | var callers = []; // stack to decide between skip/abort functions 968 | var parents = []; // stack to avoiding loops from circular referencing 969 | 970 | // Call the o related callback with the given arguments. 971 | function bindCallbacks(o, callbacks, args) { 972 | var prop = QUnit.objectType(o); 973 | if (prop) { 974 | if (QUnit.objectType(callbacks[prop]) === "function") { 975 | return callbacks[prop].apply(callbacks, args); 976 | } else { 977 | return callbacks[prop]; // or undefined 978 | } 979 | } 980 | } 981 | 982 | var callbacks = function () { 983 | 984 | // for string, boolean, number and null 985 | function useStrictEquality(b, a) { 986 | if (b instanceof a.constructor || a instanceof b.constructor) { 987 | // to catch short annotaion VS 'new' annotation of a declaration 988 | // e.g. var i = 1; 989 | // var j = new Number(1); 990 | return a == b; 991 | } else { 992 | return a === b; 993 | } 994 | } 995 | 996 | return { 997 | "string": useStrictEquality, 998 | "boolean": useStrictEquality, 999 | "number": useStrictEquality, 1000 | "null": useStrictEquality, 1001 | "undefined": useStrictEquality, 1002 | 1003 | "nan": function (b) { 1004 | return isNaN(b); 1005 | }, 1006 | 1007 | "date": function (b, a) { 1008 | return QUnit.objectType(b) === "date" && a.valueOf() === b.valueOf(); 1009 | }, 1010 | 1011 | "regexp": function (b, a) { 1012 | return QUnit.objectType(b) === "regexp" && 1013 | a.source === b.source && // the regex itself 1014 | a.global === b.global && // and its modifers (gmi) ... 1015 | a.ignoreCase === b.ignoreCase && 1016 | a.multiline === b.multiline; 1017 | }, 1018 | 1019 | // - skip when the property is a method of an instance (OOP) 1020 | // - abort otherwise, 1021 | // initial === would have catch identical references anyway 1022 | "function": function () { 1023 | var caller = callers[callers.length - 1]; 1024 | return caller !== Object && 1025 | typeof caller !== "undefined"; 1026 | }, 1027 | 1028 | "array": function (b, a) { 1029 | var i, j, loop; 1030 | var len; 1031 | 1032 | // b could be an object literal here 1033 | if ( ! (QUnit.objectType(b) === "array")) { 1034 | return false; 1035 | } 1036 | 1037 | len = a.length; 1038 | if (len !== b.length) { // safe and faster 1039 | return false; 1040 | } 1041 | 1042 | //track reference to avoid circular references 1043 | parents.push(a); 1044 | for (i = 0; i < len; i++) { 1045 | loop = false; 1046 | for(j=0;j= 0) { 1191 | type = "array"; 1192 | } else { 1193 | type = typeof obj; 1194 | } 1195 | return type; 1196 | }, 1197 | separator:function() { 1198 | return this.multiline ? this.HTML ? '
' : '\n' : this.HTML ? ' ' : ' '; 1199 | }, 1200 | indent:function( extra ) {// extra can be a number, shortcut for increasing-calling-decreasing 1201 | if ( !this.multiline ) 1202 | return ''; 1203 | var chr = this.indentChar; 1204 | if ( this.HTML ) 1205 | chr = chr.replace(/\t/g,' ').replace(/ /g,' '); 1206 | return Array( this._depth_ + (extra||0) ).join(chr); 1207 | }, 1208 | up:function( a ) { 1209 | this._depth_ += a || 1; 1210 | }, 1211 | down:function( a ) { 1212 | this._depth_ -= a || 1; 1213 | }, 1214 | setParser:function( name, parser ) { 1215 | this.parsers[name] = parser; 1216 | }, 1217 | // The next 3 are exposed so you can use them 1218 | quote:quote, 1219 | literal:literal, 1220 | join:join, 1221 | // 1222 | _depth_: 1, 1223 | // This is the list of parsers, to modify them, use jsDump.setParser 1224 | parsers:{ 1225 | window: '[Window]', 1226 | document: '[Document]', 1227 | error:'[ERROR]', //when no parser is found, shouldn't happen 1228 | unknown: '[Unknown]', 1229 | 'null':'null', 1230 | 'undefined':'undefined', 1231 | 'function':function( fn ) { 1232 | var ret = 'function', 1233 | name = 'name' in fn ? fn.name : (reName.exec(fn)||[])[1];//functions never have name in IE 1234 | if ( name ) 1235 | ret += ' ' + name; 1236 | ret += '('; 1237 | 1238 | ret = [ ret, QUnit.jsDump.parse( fn, 'functionArgs' ), '){'].join(''); 1239 | return join( ret, QUnit.jsDump.parse(fn,'functionCode'), '}' ); 1240 | }, 1241 | array: array, 1242 | nodelist: array, 1243 | arguments: array, 1244 | object:function( map ) { 1245 | var ret = [ ]; 1246 | QUnit.jsDump.up(); 1247 | for ( var key in map ) 1248 | ret.push( QUnit.jsDump.parse(key,'key') + ': ' + QUnit.jsDump.parse(map[key]) ); 1249 | QUnit.jsDump.down(); 1250 | return join( '{', ret, '}' ); 1251 | }, 1252 | node:function( node ) { 1253 | var open = QUnit.jsDump.HTML ? '<' : '<', 1254 | close = QUnit.jsDump.HTML ? '>' : '>'; 1255 | 1256 | var tag = node.nodeName.toLowerCase(), 1257 | ret = open + tag; 1258 | 1259 | for ( var a in QUnit.jsDump.DOMAttrs ) { 1260 | var val = node[QUnit.jsDump.DOMAttrs[a]]; 1261 | if ( val ) 1262 | ret += ' ' + a + '=' + QUnit.jsDump.parse( val, 'attribute' ); 1263 | } 1264 | return ret + close + open + '/' + tag + close; 1265 | }, 1266 | functionArgs:function( fn ) {//function calls it internally, it's the arguments part of the function 1267 | var l = fn.length; 1268 | if ( !l ) return ''; 1269 | 1270 | var args = Array(l); 1271 | while ( l-- ) 1272 | args[l] = String.fromCharCode(97+l);//97 is 'a' 1273 | return ' ' + args.join(', ') + ' '; 1274 | }, 1275 | key:quote, //object calls it internally, the key part of an item in a map 1276 | functionCode:'[code]', //function calls it internally, it's the content of the function 1277 | attribute:quote, //node calls it internally, it's an html attribute value 1278 | string:quote, 1279 | date:quote, 1280 | regexp:literal, //regex 1281 | number:literal, 1282 | 'boolean':literal 1283 | }, 1284 | DOMAttrs:{//attributes to dump from nodes, name=>realName 1285 | id:'id', 1286 | name:'name', 1287 | 'class':'className' 1288 | }, 1289 | HTML:false,//if true, entities are escaped ( <, >, \t, space and \n ) 1290 | indentChar:' ',//indentation unit 1291 | multiline:true //if true, items in a collection, are separated by a \n, else just a space. 1292 | }; 1293 | 1294 | return jsDump; 1295 | })(); 1296 | 1297 | // from Sizzle.js 1298 | function getText( elems ) { 1299 | var ret = "", elem; 1300 | 1301 | for ( var i = 0; elems[i]; i++ ) { 1302 | elem = elems[i]; 1303 | 1304 | // Get the text from text nodes and CDATA nodes 1305 | if ( elem.nodeType === 3 || elem.nodeType === 4 ) { 1306 | ret += elem.nodeValue; 1307 | 1308 | // Traverse everything else, except comment nodes 1309 | } else if ( elem.nodeType !== 8 ) { 1310 | ret += getText( elem.childNodes ); 1311 | } 1312 | } 1313 | 1314 | return ret; 1315 | }; 1316 | 1317 | /* 1318 | * Javascript Diff Algorithm 1319 | * By John Resig (http://ejohn.org/) 1320 | * Modified by Chu Alan "sprite" 1321 | * 1322 | * Released under the MIT license. 1323 | * 1324 | * More Info: 1325 | * http://ejohn.org/projects/javascript-diff-algorithm/ 1326 | * 1327 | * Usage: QUnit.diff(expected, actual) 1328 | * 1329 | * QUnit.diff("the quick brown fox jumped over", "the quick fox jumps over") == "the quick brown fox jumped jumps over" 1330 | */ 1331 | QUnit.diff = (function() { 1332 | function diff(o, n){ 1333 | var ns = new Object(); 1334 | var os = new Object(); 1335 | 1336 | for (var i = 0; i < n.length; i++) { 1337 | if (ns[n[i]] == null) 1338 | ns[n[i]] = { 1339 | rows: new Array(), 1340 | o: null 1341 | }; 1342 | ns[n[i]].rows.push(i); 1343 | } 1344 | 1345 | for (var i = 0; i < o.length; i++) { 1346 | if (os[o[i]] == null) 1347 | os[o[i]] = { 1348 | rows: new Array(), 1349 | n: null 1350 | }; 1351 | os[o[i]].rows.push(i); 1352 | } 1353 | 1354 | for (var i in ns) { 1355 | if (ns[i].rows.length == 1 && typeof(os[i]) != "undefined" && os[i].rows.length == 1) { 1356 | n[ns[i].rows[0]] = { 1357 | text: n[ns[i].rows[0]], 1358 | row: os[i].rows[0] 1359 | }; 1360 | o[os[i].rows[0]] = { 1361 | text: o[os[i].rows[0]], 1362 | row: ns[i].rows[0] 1363 | }; 1364 | } 1365 | } 1366 | 1367 | for (var i = 0; i < n.length - 1; i++) { 1368 | if (n[i].text != null && n[i + 1].text == null && n[i].row + 1 < o.length && o[n[i].row + 1].text == null && 1369 | n[i + 1] == o[n[i].row + 1]) { 1370 | n[i + 1] = { 1371 | text: n[i + 1], 1372 | row: n[i].row + 1 1373 | }; 1374 | o[n[i].row + 1] = { 1375 | text: o[n[i].row + 1], 1376 | row: i + 1 1377 | }; 1378 | } 1379 | } 1380 | 1381 | for (var i = n.length - 1; i > 0; i--) { 1382 | if (n[i].text != null && n[i - 1].text == null && n[i].row > 0 && o[n[i].row - 1].text == null && 1383 | n[i - 1] == o[n[i].row - 1]) { 1384 | n[i - 1] = { 1385 | text: n[i - 1], 1386 | row: n[i].row - 1 1387 | }; 1388 | o[n[i].row - 1] = { 1389 | text: o[n[i].row - 1], 1390 | row: i - 1 1391 | }; 1392 | } 1393 | } 1394 | 1395 | return { 1396 | o: o, 1397 | n: n 1398 | }; 1399 | } 1400 | 1401 | return function(o, n){ 1402 | o = o.replace(/\s+$/, ''); 1403 | n = n.replace(/\s+$/, ''); 1404 | var out = diff(o == "" ? [] : o.split(/\s+/), n == "" ? [] : n.split(/\s+/)); 1405 | 1406 | var str = ""; 1407 | 1408 | var oSpace = o.match(/\s+/g); 1409 | if (oSpace == null) { 1410 | oSpace = [" "]; 1411 | } 1412 | else { 1413 | oSpace.push(" "); 1414 | } 1415 | var nSpace = n.match(/\s+/g); 1416 | if (nSpace == null) { 1417 | nSpace = [" "]; 1418 | } 1419 | else { 1420 | nSpace.push(" "); 1421 | } 1422 | 1423 | if (out.n.length == 0) { 1424 | for (var i = 0; i < out.o.length; i++) { 1425 | str += '' + out.o[i] + oSpace[i] + ""; 1426 | } 1427 | } 1428 | else { 1429 | if (out.n[0].text == null) { 1430 | for (n = 0; n < out.o.length && out.o[n].text == null; n++) { 1431 | str += '' + out.o[n] + oSpace[n] + ""; 1432 | } 1433 | } 1434 | 1435 | for (var i = 0; i < out.n.length; i++) { 1436 | if (out.n[i].text == null) { 1437 | str += '' + out.n[i] + nSpace[i] + ""; 1438 | } 1439 | else { 1440 | var pre = ""; 1441 | 1442 | for (n = out.n[i].row + 1; n < out.o.length && out.o[n].text == null; n++) { 1443 | pre += '' + out.o[n] + oSpace[n] + ""; 1444 | } 1445 | str += " " + out.n[i].text + nSpace[i] + pre; 1446 | } 1447 | } 1448 | } 1449 | 1450 | return str; 1451 | }; 1452 | })(); 1453 | 1454 | })(this); -------------------------------------------------------------------------------- /tests/lib/tests.js: -------------------------------------------------------------------------------- 1 | (function (pub, namespace) { 2 | pub[namespace] = function (qunit, routes) { 3 | var is_syntax_error = function (error) {return error instanceof SyntaxError;}, has = 'hasOwnProperty'; 4 | qunit.module('RouteMap'); 5 | qunit.test('initialization', function () { 6 | qunit.ok(routes, 'RouteMap exists'); 7 | }); 8 | qunit.test('#add / #remove', function () { 9 | var rule, str, add_rule = function () {routes.add(rule);}; 10 | rule = {route: '/foo/:bar/baz:', method: 'foo'}, str = JSON.stringify(rule); 11 | // add a rule (returns nothing, just checking to make sure no errors thrown) 12 | qunit.ok(typeof routes.add(rule) === 'undefined', 'addition of rule: ' + str); 13 | // no duplicate rules 14 | qunit.raises(add_rule, null, 'duplicate rules fail: ' + str); 15 | // remove a rule 16 | qunit.ok(typeof routes.remove(rule) === 'undefined', 'removal of rule: ' + str); 17 | // bad rule removal fails [1] 18 | qunit.raises(function () {routes.remove({});}, null, 'non-rule param to remove fails [1]'); 19 | // bad rule removal fails [2] 20 | qunit.raises(function () {routes.remove('foo');}, null, 'non-rule param to remove fails [2]'); 21 | // bad rule removal fails [3] 22 | qunit.raises(function () {routes.remove({route: '/foo'});}, null, 'non-rule param to remove fails [3]'); 23 | // bad rule removal fails [4] 24 | qunit.raises(function () {routes.remove({method: 'foo'});}, null, 'non-rule param to remove fails [4]'); 25 | // no duplicate params [1] 26 | rule = {route: '/foo/:bar/:bar', method: 'foo'}, str = JSON.stringify(rule);; 27 | qunit.raises(add_rule, is_syntax_error, 'duplicate param throws SyntaxError [1]: ' + str); 28 | // no duplicate params [2] 29 | rule = {route: '/foo/:bar/bar:', method: 'foo'}, str = JSON.stringify(rule);; 30 | qunit.raises(add_rule, is_syntax_error, 'duplicate param throws SyntaxError [2]: ' + str); 31 | // no required unnamed after optional unnamed 32 | rule = {route: '/foo/:bar?/:baz', method: 'foo'}, str = JSON.stringify(rule);; 33 | qunit.raises(add_rule, is_syntax_error, 'required unnamed + optional unnamed throws SyntaxError: ' + str); 34 | // no params after * [1] 35 | rule = {route: '/foo/*/:bar', method: 'foo'}, str = JSON.stringify(rule);; 36 | qunit.raises(add_rule, is_syntax_error, 'any param after * throws SyntaxError [1]: ' + str); 37 | // no params after * [2] 38 | rule = {route: '/*/:foo', method: 'foo'}, str = JSON.stringify(rule);; 39 | qunit.raises(add_rule, is_syntax_error, 'any param after * throws SyntaxError [2]:' + str); 40 | // no params after * [3] 41 | rule = {route: '/foo:*/:bar', method: 'foo'}, str = JSON.stringify(rule);; 42 | qunit.raises(add_rule, is_syntax_error, 'any param after * throws SyntaxError [3]: ' + str); 43 | }); 44 | qunit.test('#context', function () { 45 | var methods = {foo: function () {}}; 46 | qunit.deepEqual(methods, routes.context(methods), 'RouteMap context overwrite works [set]'); 47 | qunit.deepEqual(methods, routes.context(), 'RouteMap context overwrite works [get]'); 48 | routes.context(pub); 49 | }); 50 | qunit.test('#hash', function () { 51 | var rule, hash, params; 52 | routes.context({foo: function () {}}); 53 | // hash with one required param: unnamed 54 | rule = {route: '/foo/:bar', method: 'foo'}, params = {bar: 'abc'}, hash = '/foo/abc'; 55 | qunit.equal(hash, routes.hash(rule, params), 'hash test for: ' + hash); 56 | // hash with one required param: named 57 | rule = {route: '/foo/bar:', method: 'foo'}, params = {bar: 'abc'}, hash = '/foo/bar=abc'; 58 | qunit.equal(hash, routes.hash(rule, params), 'hash test for: ' + hash); 59 | // hash with one optional param: unnamed 60 | rule = {route: '/foo/:bar?', method: 'foo'}, params = {bar: 'abc'}, hash = '/foo/abc'; 61 | qunit.equal(hash, routes.hash(rule, params), 'hash test for: ' + hash); 62 | // hash with one optional param: named 63 | rule = {route: '/foo/bar:?', method: 'foo'}, params = {bar: 'abc'}, hash = '/foo/bar=abc'; 64 | qunit.equal(hash, routes.hash(rule, params), 'hash test for: ' + hash); 65 | // hash with one optional param: unnamed 66 | rule = {route: '/foo/:bar?', method: 'foo'}, params = {}, hash = '/foo/'; 67 | qunit.equal(hash, routes.hash(rule, params), 'hash test for: ' + hash); 68 | // hash with one optional param: named 69 | rule = {route: '/foo/bar:?', method: 'foo'}, params = {}, hash = '/foo/'; 70 | qunit.equal(hash, routes.hash(rule, params), 'hash test for: ' + hash); 71 | // hash with multiple params: required unnamed 72 | rule = {route: '/foo/:bar/:baz', method: 'foo'}, params = {bar: 'abc', baz: 'def'}, hash = '/foo/abc/def'; 73 | qunit.equal(hash, routes.hash(rule, params), 'hash test for: ' + hash); 74 | // hash with multiple params: required named 75 | rule = {route: '/foo/bar:/baz:', method: 'foo'}; 76 | params = {bar: 'abc', baz: 'def'}, hash = '/foo/bar=abc/baz=def'; 77 | qunit.equal(hash, routes.hash(rule, params), 'hash test for: ' + hash); 78 | // hash with multiple params: optional unnamed 79 | rule = {route: '/foo/:bar/:baz?', method: 'foo'}, params = {bar: 'abc', baz: 'def'}, hash = '/foo/abc/def'; 80 | qunit.equal(hash, routes.hash(rule, params), 'hash test for: ' + hash); 81 | // hash with multiple params: optional named 82 | rule = {route: '/foo/:bar/baz:?', method: 'foo'}; 83 | params = {bar: 'abc', baz: 'def'}, hash = '/foo/abc/baz=def'; 84 | qunit.equal(hash, routes.hash(rule, params), 'hash test for: ' + hash); 85 | // hash with multiple params: optional unnamed 86 | rule = {route: '/foo/:bar/:baz?', method: 'foo'}, params = {bar: 'abc'}, hash = '/foo/abc'; 87 | qunit.equal(hash, routes.hash(rule, params), 'hash test for: ' + hash); 88 | // hash with multiple params: optional named 89 | rule = {route: '/foo/:bar/baz:?', method: 'foo'}, params = {bar: 'abc'}, hash = '/foo/abc'; 90 | qunit.equal(hash, routes.hash(rule, params), 'hash test for: ' + hash); 91 | routes.context(pub); 92 | }); 93 | qunit.test('#merge', function () { 94 | var hash = '/foo/abc/def', rule = {route: '/foo/:bar/:baz/*', method: 'foo'}, current, last; 95 | routes.add(rule); 96 | routes.context({foo: function (args) { 97 | qunit.notDeepEqual(routes.current(), routes.last(), 'within handler, last !== current'); 98 | }}); 99 | routes.get = function () {return hash;}; 100 | routes.handler(); 101 | current = routes.current(); 102 | current.args.bar = 'ghi'; 103 | qunit.notDeepEqual(current, routes.current(), 'current is cloned so that it is not a reference'); 104 | last = routes.last(); 105 | last.args.bar = 'ghi'; 106 | qunit.notDeepEqual(last, routes.last(), 'last is cloned so that it is not a reference'); 107 | qunit.deepEqual(routes.last(), routes.current(), 'after handler has finished, last and current are same'); 108 | routes.remove(rule); 109 | }); 110 | qunit.test('#parse [1]', function () { 111 | var rule, hash, params, str; 112 | routes.context({foo: function () {}}); 113 | // calling parse on a hash that does not match any added routes fails 114 | hash = '/foo/'; 115 | qunit.raises(function () {routes.parse(hash);}, is_syntax_error, 'route must be added for parse to work'); 116 | // hash with one required param: unnamed 117 | rule = {route: '/foo/:bar', method: 'foo'}, params = {bar: 'abc'}; 118 | hash = '/foo/abc'; 119 | str = 'parse for hash: ' + hash + ' rule: ' + JSON.stringify(rule) + ' returns: ' + JSON.stringify(params); 120 | routes.add(rule); 121 | qunit.deepEqual(params, routes.parse(hash).args, str); 122 | routes.remove(rule); 123 | // hash with one required param: named 124 | rule = {route: '/foo/bar:', method: 'foo'}, params = {bar: 'abc'}; 125 | hash = '/foo/bar=abc'; 126 | str = 'parse for hash: ' + hash + ' rule: ' + JSON.stringify(rule) + ' returns: ' + JSON.stringify(params); 127 | routes.add(rule); 128 | qunit.deepEqual(params, routes.parse(hash).args, str); 129 | routes.remove(rule); 130 | // hash with one optional param: unnamed [1] 131 | rule = {route: '/foo/:bar?', method: 'foo'}, params = {bar: 'abc'}; 132 | hash = '/foo/abc'; 133 | str = 'parse for hash: ' + hash + ' rule: ' + JSON.stringify(rule) + ' returns: ' + JSON.stringify(params); 134 | routes.add(rule); 135 | qunit.deepEqual(params, routes.parse(hash).args, str); 136 | routes.remove(rule); 137 | // hash with one optional param: named [1] 138 | rule = {route: '/foo/bar:?', method: 'foo'}, params = {bar: 'abc'}; 139 | hash = '/foo/bar=abc'; 140 | str = 'parse for hash: ' + hash + ' rule: ' + JSON.stringify(rule) + ' returns: ' + JSON.stringify(params); 141 | routes.add(rule); 142 | qunit.deepEqual(params, routes.parse(hash).args, str); 143 | routes.remove(rule); 144 | // hash with one optional param: unnamed [2] 145 | rule = {route: '/foo/:bar?', method: 'foo'}, params = {}; 146 | hash = '/foo/'; 147 | str = 'parse for hash: ' + hash + ' rule: ' + JSON.stringify(rule) + ' returns: ' + JSON.stringify(params); 148 | routes.add(rule); 149 | qunit.deepEqual(params, routes.parse(hash).args, str); 150 | routes.remove(rule); 151 | // hash with multiple params: [1] 152 | rule = {route: '/foo/:bar/:baz', method: 'foo'}, params = {bar: 'abc', baz: 'def'}; 153 | hash = '/foo/abc/def'; 154 | str = 'parse for hash: ' + hash + ' rule: ' + JSON.stringify(rule) + ' returns: ' + JSON.stringify(params); 155 | routes.add(rule); 156 | qunit.deepEqual(params, routes.parse(hash).args, str); 157 | routes.remove(rule); 158 | // hash with multiple params: [2] 159 | rule = {route: '/foo/bar:/:baz', method: 'foo'}, params = {bar: 'abc', baz: 'def'}; 160 | hash = '/foo/def/bar=abc'; 161 | str = 'parse for hash: ' + hash + ' rule: ' + JSON.stringify(rule) + ' returns: ' + JSON.stringify(params); 162 | routes.add(rule); 163 | qunit.deepEqual(params, routes.parse(hash).args, str); 164 | routes.remove(rule); 165 | // hash with multiple params: [3] 166 | rule = {route: '/foo/:bar/baz:/qux:', method: 'foo'}, params = {bar: 'abc', baz: 'def', qux: 'ghi'}; 167 | hash = '/foo/abc/baz=def/qux=ghi'; 168 | str = 'parse for hash: ' + hash + ' rule: ' + JSON.stringify(rule) + ' returns: ' + JSON.stringify(params); 169 | routes.add(rule); 170 | qunit.deepEqual(params, routes.parse(hash).args, str); 171 | routes.remove(rule); 172 | // hash with multiple params: [4] 173 | rule = {route: '/foo/:bar?/baz:/qux:?', method: 'foo'}, params = {bar: 'abc', baz: 'def', qux: 'ghi'}; 174 | hash = '/foo/abc/baz=def/qux=ghi'; 175 | str = 'parse for hash: ' + hash + ' rule: ' + JSON.stringify(rule) + ' returns: ' + JSON.stringify(params); 176 | routes.add(rule); 177 | qunit.deepEqual(params, routes.parse(hash).args, str); 178 | routes.remove(rule); 179 | // hash with *: [1] 180 | rule = {route: '/foo/*', method: 'foo'}, params = {'*': 'abc/def/ghi'}; 181 | hash = '/foo/abc/def/ghi'; 182 | str = 'parse for hash: ' + hash + ' rule: ' + JSON.stringify(rule) + ' returns: ' + JSON.stringify(params); 183 | routes.add(rule); 184 | qunit.deepEqual(params, routes.parse(hash).args, str); 185 | routes.remove(rule); 186 | // hash with *: [2] 187 | rule = {route: '/foo/bar:*', method: 'foo'}, params = {bar: 'abc/def/ghi'}; 188 | hash = '/foo/abc/def/ghi'; 189 | str = 'parse for hash: ' + hash + ' rule: ' + JSON.stringify(rule) + ' returns: ' + JSON.stringify(params); 190 | routes.add(rule); 191 | qunit.deepEqual(params, routes.parse(hash).args, str); 192 | routes.remove(rule); 193 | // hash with *: [3] 194 | rule = {route: '/foo/:bar/baz:*', method: 'foo'}, params = {bar: 'abc', baz: 'def/ghi'}; 195 | hash = '/foo/abc/def/ghi'; 196 | str = 'parse for hash: ' + hash + ' rule: ' + JSON.stringify(rule) + ' returns: ' + JSON.stringify(params); 197 | routes.add(rule); 198 | qunit.deepEqual(params, routes.parse(hash).args, str); 199 | routes.remove(rule); 200 | routes.context(pub); 201 | }); 202 | qunit.test('#parse [2]', function () { 203 | // reuse of the same tokens in multiple parts of a hash should not confuse parser 204 | var rule_one, rule_two, hash, fired = false; 205 | rule_one = {route: '/foo/:bar/:baz/*', method: 'foo'}; 206 | rule_two = {route: '/bar/:baz/:qux/*', method: 'bar'}; 207 | hash = '/bar/foo/abc'; 208 | routes.add(rule_one); 209 | routes.add(rule_two); 210 | routes.context({ 211 | foo: function (args) {qunit.ok(false, 'foo() should have never fired');}, 212 | bar: function (args) {fired = true, qunit.ok(true, 'bar() should fire');} 213 | }); 214 | routes.get = function () {return hash;}; 215 | routes.handler(); 216 | if (!fired) qunit.ok(false, 'bar() should have fired'); 217 | routes.remove(rule_one); 218 | routes.remove(rule_two); 219 | routes.context(pub); 220 | }); 221 | qunit.test('#prefix', function () { 222 | var old_prefix = routes.prefix(), new_prefix = 'FOO'; 223 | qunit.equal(new_prefix, routes.prefix(new_prefix), 'RouteMap prefix overwrite works [set]'); 224 | qunit.equal(new_prefix, routes.prefix(), 'RouteMap prefix overwrite works [get]'); 225 | routes.prefix(old_prefix); 226 | }); 227 | }; 228 | })(typeof exports === 'undefined' ? window : exports, 'RouteMapTests'); -------------------------------------------------------------------------------- /tests/tests-browser.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | RouteMap Browser Tests 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |

RouteMap Tests

13 |

14 |
15 |

16 |
    17 |
    18 | 19 | -------------------------------------------------------------------------------- /tests/tests-node.js: -------------------------------------------------------------------------------- 1 | var qunit = require('./lib/qunit/qunit').QUnit, all_passed, global_passed; 2 | qunit.log = function (output) { 3 | var message = (output.result ? 'PASS: ' : 'FAIL: ') + output.message + '\n'; 4 | if (!output.result) all_passed = false; 5 | if (output.expected) message += ' expected: ' + output.expected + '\n'; 6 | if (output.actual) message += ' actual: ' + output.actual + '\n'; 7 | if (!output.result) message = '\033[31m' + message + '\033[39m'; // red! 8 | console[output.result ? 'log' : 'error'](message); 9 | }; 10 | console.log('Tests for routemap.js'); 11 | global_passed = all_passed = true; 12 | require('./lib/tests').RouteMapTests(qunit, require('../routemap').RouteMap); 13 | console[all_passed ? 'log' : 'error']('Tests for routemap.js => ' + 14 | (all_passed ? 'ALL TESTS PASSED' : '\033[31mSOME TESTS FAILED\033[39m')); 15 | console.log('Tests for routemap.compressed.js'); 16 | global_passed = all_passed; 17 | all_passed = true; 18 | require('./lib/tests').RouteMapTests(qunit, require('../routemap.compressed').RouteMap); 19 | console[all_passed ? 'log' : 'error']('Tests for routemap.compressed.js => ' + 20 | (all_passed ? 'ALL TESTS PASSED' : '\033[31mSOME TESTS FAILED\033[39m')); 21 | global_passed = global_passed && all_passed; 22 | if (!global_passed) console.error('\n\n\n\033[31mONE OR BOTH FILES FAILED\033[39m'); 23 | process.exit(global_passed ? 0 : 1); --------------------------------------------------------------------------------