├── .gitignore ├── .npmignore ├── .travis.yml ├── README.md ├── bower.json ├── package-lock.json ├── package.json ├── rlite.js ├── rlite.min.js ├── rlite.min.js.map └── test ├── rlite.spec.js └── rlite.test.html /.gitignore: -------------------------------------------------------------------------------- 1 | Web.config 2 | *.log 3 | node_modules 4 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | test 2 | example 3 | .gitignore 4 | node_modules 5 | bower_components 6 | README.md 7 | bower.json 8 | .travis.yml 9 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "7.2.0" 4 | before_script: 5 | - "npm i -g jasmine-node" 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # rlite 2 | 3 | Tiny, [fast](http://jsperf.com/rlite/2), light-weight JavaScript routing with zero dependencies. 4 | 5 | - Order of route declaration doesn't matter: the most specific route wins 6 | - Zero dependencies 7 | - No performance drop as you add routes 8 | - Less than 700 bytes minified and gzipped 9 | - Parses query strings 10 | - Wildcard support 11 | 12 | [![Build Status](https://travis-ci.org/chrisdavies/rlite.svg?branch=master)](https://travis-ci.org/chrisdavies/rlite) 13 | 14 | ## Usage 15 | 16 | Rlite does not come with any explicit tie into HTML5 push state or hash-change events, but these are easy enough to tie in based on your needs. Here's an example: 17 | 18 | ```javascript 19 | const route = rlite(notFound, { 20 | // Default route 21 | '': function () { 22 | return 'Home'; 23 | }, 24 | 25 | // #inbox 26 | 'inbox': function () { 27 | return 'Inbox'; 28 | }, 29 | 30 | // #sent?to=john -> r.params.to will equal 'john' 31 | 'sent': function ({to}) { 32 | return 'Sent to ' + to; 33 | }, 34 | 35 | // #users/chris -> r.params.name will equal 'chris' 36 | 'users/:name': function ({name}) { 37 | return 'User ' + name; 38 | }, 39 | 40 | // #users/foo/bar/baz -> r.params.path will equal 'foo/bar/baz' 41 | 'users/*path': function ({path}) { 42 | return 'Path = ' + path; 43 | }, 44 | 45 | // #logout 46 | 'logout': function () { 47 | return 'Logout'; 48 | } 49 | }); 50 | 51 | function notFound() { 52 | return '

404 Not found :/

'; 53 | } 54 | 55 | // Hash-based routing 56 | function processHash() { 57 | const hash = location.hash || '#'; 58 | 59 | // Do something useful with the result of the route 60 | document.body.textContent = route(hash.slice(1)); 61 | } 62 | 63 | window.addEventListener('hashchange', processHash); 64 | processHash(); 65 | ``` 66 | 67 | The previous examples should be relatively self-explantatory. Simple, parameterized routes are supported. Only relative URLs are supported. (So, instead of passing: `'http://example.com/users/1'`, pass `'/users/1'`). 68 | 69 | Routes are not case sensitive, so `'Users/:name'` will resolve to `'users/:name'` 70 | 71 | ## Possible surprises 72 | 73 | If there is a query parameter with the same name as a route parameter, it will override the route parameter. So given the following route definition: 74 | 75 | /users/:name 76 | 77 | If you pass the following URL: 78 | 79 | /users/chris?name=joe 80 | 81 | The value of params.name will be 'joe', not 'chris'. 82 | 83 | Keywords/patterns need to immediately follow a slash. So, routes like the following will not be matched: 84 | 85 | /users/user-:id 86 | 87 | In this case, you'll need to either use a wildcard route `/users/*prefixedId` or else, you'd want to modify the URL to be in a format like this: `/users/user/:id`. 88 | 89 | ## Route handlers 90 | 91 | Route handlers ara functions that take three arguments and return a result and/or produce a side-effect. 92 | 93 | Here's an example handler: 94 | 95 | ```javascript 96 | const route = rlite(notFound, { 97 | 'users/:id': function (params, state, url) { 98 | // Do interesting stuff here... 99 | } 100 | }); 101 | ``` 102 | 103 | The first argument is `params`. It is an object representing the route parameters. So, if you were to 104 | run `route('users/33')`, params would be `{id: '33'}`. 105 | 106 | The second argument is `state`. It is an optional value that was passed into the route function. So, 107 | if you were to run `route('users/22', 'Hello')`, params would be `{id: '22'}` and state would be `'Hello'`. 108 | 109 | The third argument is `url`. It is the URL which was matched to the route. So, if you were to run 110 | `route('users/25')`, params would be `{id: '25'}`, state would be `undefined` and url would be `'users/25'`. 111 | 112 | 113 | ## Modules 114 | 115 | If you're using ES6, import rlite like so: 116 | 117 | ```javascript 118 | import rlite from 'rlite-router'; 119 | 120 | const routes = rlite(notFound, { 121 | '': function () { } 122 | }); 123 | 124 | // etc 125 | ``` 126 | 127 | Or using [CommonJS](http://www.commonjs.org/) like so: 128 | 129 | ```javascript 130 | var Rlite = require('rlite-router'); 131 | var routes = rlite(notFound, { 132 | '': function () { } 133 | }); 134 | 135 | // etc 136 | ``` 137 | 138 | 139 | ## Handling 404s 140 | 141 | The first parameter to rlite is the 404 handler. This function will be invoked when rlite 142 | is called with a URL that has no matching routes. 143 | 144 | In the following example, the body will end up with `

404 NOT FOUND

`. 145 | 146 | ```javascript 147 | const route = rlite(() => '

404 NOT FOUND

', { 148 | 'hello': => '

WORLD

' 149 | }); 150 | 151 | document.body.innerHTML = route('/not/a/valid/url'); 152 | ``` 153 | 154 | ## Changes from 1.x 155 | 156 | - The parameters to route handlers have changed 157 | - The plugins have been dropped since 2.x is more functional in nature, it's easy to extend 158 | - rlite returns a function, rather than an object 159 | - You can pass optional state into your route handlers 160 | - The result of your route handler is returned by the router 161 | 162 | ## Installation 163 | 164 | Just download rlite.min.js, or use bower: 165 | 166 | bower install rlite 167 | 168 | Or use npm: https://www.npmjs.com/package/rlite-router 169 | 170 | npm install --save rlite-router 171 | 172 | ## Contributing 173 | 174 | Make your changes (and add tests), then run the tests: 175 | 176 | npm test 177 | 178 | If all is well, build your changes: 179 | 180 | npm run min 181 | 182 | This minifies rlite, and tells you the size. It's currently just under 700 183 | bytes, and I'd like to keep it that way! 184 | 185 | ## Status 186 | 187 | Rlite is being actively maintained, but is pretty much feature complete. Generally, I avoid repos that look stale (no recent activity), but in this case, the reason for inactivity is that library is stable and complete. 188 | 189 | ## Usage with React 190 | 191 | I've been using Rlite along with React and Redux. [Here's a write up on how that works.](https://github.com/chrisdavies/rlite/wiki/Using-with-React) 192 | 193 | ## License MIT 194 | 195 | Copyright (c) 2016 Chris Davies 196 | 197 | Permission is hereby granted, free of charge, to any person 198 | obtaining a copy of this software and associated documentation 199 | files (the "Software"), to deal in the Software without 200 | restriction, including without limitation the rights to use, 201 | copy, modify, merge, publish, distribute, sublicense, and/or sell 202 | copies of the Software, and to permit persons to whom the 203 | Software is furnished to do so, subject to the following 204 | conditions: 205 | 206 | The above copyright notice and this permission notice shall be 207 | included in all copies or substantial portions of the Software. 208 | 209 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 210 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 211 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 212 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 213 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 214 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 215 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 216 | OTHER DEALINGS IN THE SOFTWARE. 217 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rlite", 3 | "main": "rlite.js", 4 | "version": "2.0.3", 5 | "homepage": "https://github.com/chrisdavies/rlite", 6 | "authors": [ 7 | "Chris Davies " 8 | ], 9 | "description": "A tiny, fast client-side router", 10 | "keywords": [ 11 | "routing", "router" 12 | ], 13 | "license": "MIT", 14 | "ignore": [ 15 | ".gitignore", 16 | "node_modules", 17 | "bower_components", 18 | "test", 19 | "tests", 20 | "README.md" 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rlite-router", 3 | "version": "2.0.1", 4 | "lockfileVersion": 1, 5 | "dependencies": { 6 | "align-text": { 7 | "version": "0.1.4", 8 | "resolved": "https://registry.npmjs.org/align-text/-/align-text-0.1.4.tgz", 9 | "integrity": "sha1-DNkKVhCT810KmSVsIrcGlDP60Rc=", 10 | "dev": true 11 | }, 12 | "camelcase": { 13 | "version": "1.2.1", 14 | "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-1.2.1.tgz", 15 | "integrity": "sha1-m7UwTS4LVmmLLHWLCKPqqdqlijk=", 16 | "dev": true 17 | }, 18 | "center-align": { 19 | "version": "0.1.3", 20 | "resolved": "https://registry.npmjs.org/center-align/-/center-align-0.1.3.tgz", 21 | "integrity": "sha1-qg0yYptu6XIgBBHL1EYckHvCt60=", 22 | "dev": true 23 | }, 24 | "cliui": { 25 | "version": "2.1.0", 26 | "resolved": "https://registry.npmjs.org/cliui/-/cliui-2.1.0.tgz", 27 | "integrity": "sha1-S0dXYP+AJkx2LDoXGQMukcf+oNE=", 28 | "dev": true 29 | }, 30 | "coffee-script": { 31 | "version": "1.12.7", 32 | "resolved": "https://registry.npmjs.org/coffee-script/-/coffee-script-1.12.7.tgz", 33 | "integrity": "sha512-fLeEhqwymYat/MpTPUjSKHVYYl0ec2mOyALEMLmzr5i1isuG+6jfI2j2d5oBO3VIzgUXgBVIcOT9uH1TFxBckw==", 34 | "dev": true 35 | }, 36 | "decamelize": { 37 | "version": "1.2.0", 38 | "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", 39 | "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=", 40 | "dev": true 41 | }, 42 | "fileset": { 43 | "version": "0.1.8", 44 | "resolved": "https://registry.npmjs.org/fileset/-/fileset-0.1.8.tgz", 45 | "integrity": "sha1-UGuRqTluqn4y+0KoQHfHoMc2t0E=", 46 | "dev": true 47 | }, 48 | "gaze": { 49 | "version": "0.3.4", 50 | "resolved": "https://registry.npmjs.org/gaze/-/gaze-0.3.4.tgz", 51 | "integrity": "sha1-X5S92gr+U7xxCWm81vKCVI1gwnk=", 52 | "dev": true 53 | }, 54 | "glob": { 55 | "version": "3.2.11", 56 | "resolved": "https://registry.npmjs.org/glob/-/glob-3.2.11.tgz", 57 | "integrity": "sha1-Spc/Y1uRkPcV0QmH1cAP0oFevj0=", 58 | "dev": true, 59 | "dependencies": { 60 | "minimatch": { 61 | "version": "0.3.0", 62 | "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-0.3.0.tgz", 63 | "integrity": "sha1-J12O2qxPG7MyZHIInnlJyDlGmd0=", 64 | "dev": true 65 | } 66 | } 67 | }, 68 | "growl": { 69 | "version": "1.7.0", 70 | "resolved": "https://registry.npmjs.org/growl/-/growl-1.7.0.tgz", 71 | "integrity": "sha1-3i1mE20ALhErpw8/EMMc98NQsto=", 72 | "dev": true 73 | }, 74 | "inherits": { 75 | "version": "2.0.3", 76 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", 77 | "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", 78 | "dev": true 79 | }, 80 | "is-buffer": { 81 | "version": "1.1.5", 82 | "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.5.tgz", 83 | "integrity": "sha1-Hzsm72E7IUuIy8ojzGwB2Hlh7sw=", 84 | "dev": true 85 | }, 86 | "jasmine-growl-reporter": { 87 | "version": "0.0.3", 88 | "resolved": "https://registry.npmjs.org/jasmine-growl-reporter/-/jasmine-growl-reporter-0.0.3.tgz", 89 | "integrity": "sha1-uHrlUeNZ0orVIXdl6u9sB7dj9sg=", 90 | "dev": true 91 | }, 92 | "jasmine-node": { 93 | "version": "1.14.5", 94 | "resolved": "https://registry.npmjs.org/jasmine-node/-/jasmine-node-1.14.5.tgz", 95 | "integrity": "sha1-GOg5e4VpJO53ADZmw3MbWupQw50=", 96 | "dev": true 97 | }, 98 | "jasmine-reporters": { 99 | "version": "1.0.2", 100 | "resolved": "https://registry.npmjs.org/jasmine-reporters/-/jasmine-reporters-1.0.2.tgz", 101 | "integrity": "sha1-q2E+1Zd9x0h+hbPBL2qOqNsq3jE=", 102 | "dev": true 103 | }, 104 | "kind-of": { 105 | "version": "3.2.2", 106 | "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", 107 | "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", 108 | "dev": true 109 | }, 110 | "lazy-cache": { 111 | "version": "1.0.4", 112 | "resolved": "https://registry.npmjs.org/lazy-cache/-/lazy-cache-1.0.4.tgz", 113 | "integrity": "sha1-odePw6UEdMuAhF07O24dpJpEbo4=", 114 | "dev": true 115 | }, 116 | "longest": { 117 | "version": "1.0.1", 118 | "resolved": "https://registry.npmjs.org/longest/-/longest-1.0.1.tgz", 119 | "integrity": "sha1-MKCy2jj3N3DoKUoNIuZiXtd9AJc=", 120 | "dev": true 121 | }, 122 | "lru-cache": { 123 | "version": "2.7.3", 124 | "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-2.7.3.tgz", 125 | "integrity": "sha1-bUUk6LlV+V1PW1iFHOId1y+06VI=", 126 | "dev": true 127 | }, 128 | "minimatch": { 129 | "version": "0.2.14", 130 | "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-0.2.14.tgz", 131 | "integrity": "sha1-x054BXT2PG+aCQ6Q775u9TpqdWo=", 132 | "dev": true 133 | }, 134 | "mkdirp": { 135 | "version": "0.3.5", 136 | "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.3.5.tgz", 137 | "integrity": "sha1-3j5fiWHIjHh+4TaN+EmsRBPsqNc=", 138 | "dev": true 139 | }, 140 | "repeat-string": { 141 | "version": "1.6.1", 142 | "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", 143 | "integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc=", 144 | "dev": true 145 | }, 146 | "requirejs": { 147 | "version": "2.3.4", 148 | "resolved": "https://registry.npmjs.org/requirejs/-/requirejs-2.3.4.tgz", 149 | "integrity": "sha512-qPD5gRrj6kGQ0ZySAJgqbArkaDqPMbQIT0zSZDkIv1mfA17w/tR2Faq5l2qqRf2CdQx6FWln9nUrgd2UDzb19A==", 150 | "dev": true 151 | }, 152 | "right-align": { 153 | "version": "0.1.3", 154 | "resolved": "https://registry.npmjs.org/right-align/-/right-align-0.1.3.tgz", 155 | "integrity": "sha1-YTObci/mo1FWiSENJOFMlhSGE+8=", 156 | "dev": true 157 | }, 158 | "sigmund": { 159 | "version": "1.0.1", 160 | "resolved": "https://registry.npmjs.org/sigmund/-/sigmund-1.0.1.tgz", 161 | "integrity": "sha1-P/IfGYytIXX587eBhT/ZTQ0ZtZA=", 162 | "dev": true 163 | }, 164 | "source-map": { 165 | "version": "0.5.6", 166 | "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.6.tgz", 167 | "integrity": "sha1-dc449SvwczxafwwRjYEzSiu19BI=", 168 | "dev": true 169 | }, 170 | "uglify-js": { 171 | "version": "2.8.29", 172 | "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-2.8.29.tgz", 173 | "integrity": "sha1-KcVzMUgFe7Th913zW3qcty5qWd0=", 174 | "dev": true 175 | }, 176 | "uglify-to-browserify": { 177 | "version": "1.0.2", 178 | "resolved": "https://registry.npmjs.org/uglify-to-browserify/-/uglify-to-browserify-1.0.2.tgz", 179 | "integrity": "sha1-bgkk1r2mta/jSeOabWMoUKD4grc=", 180 | "dev": true, 181 | "optional": true 182 | }, 183 | "underscore": { 184 | "version": "1.8.3", 185 | "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.8.3.tgz", 186 | "integrity": "sha1-Tz+1OxBuYJf8+ctBCfKl6b36UCI=", 187 | "dev": true 188 | }, 189 | "walkdir": { 190 | "version": "0.0.11", 191 | "resolved": "https://registry.npmjs.org/walkdir/-/walkdir-0.0.11.tgz", 192 | "integrity": "sha1-oW0CXrkxvQO1LzCMrtD0D86+lTI=", 193 | "dev": true 194 | }, 195 | "window-size": { 196 | "version": "0.1.0", 197 | "resolved": "https://registry.npmjs.org/window-size/-/window-size-0.1.0.tgz", 198 | "integrity": "sha1-VDjNLqk7IC76Ohn+iIeu58lPnJ0=", 199 | "dev": true 200 | }, 201 | "wordwrap": { 202 | "version": "0.0.2", 203 | "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.2.tgz", 204 | "integrity": "sha1-t5Zpu0LstAn4PVg8rVLKF+qhZD8=", 205 | "dev": true 206 | }, 207 | "yargs": { 208 | "version": "3.10.0", 209 | "resolved": "https://registry.npmjs.org/yargs/-/yargs-3.10.0.tgz", 210 | "integrity": "sha1-9+572FfdfB0tOMDnTvvWgdFDH9E=", 211 | "dev": true 212 | } 213 | } 214 | } 215 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rlite-router", 3 | "version": "2.0.3", 4 | "description": "A tiny, fast client-side router", 5 | "main": "rlite.js", 6 | "directories": {}, 7 | "scripts": { 8 | "test": "jasmine-node test/*.spec.js", 9 | "min": "uglifyjs rlite.js --source-map rlite.min.js.map -m -c -o rlite.min.js && echo 'Minified size' && cat rlite.min.js | gzip -9f | wc -c" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "https://github.com/chrisdavies/rlite" 14 | }, 15 | "keywords": [ 16 | "routing", 17 | "router" 18 | ], 19 | "author": "Chris Davies ", 20 | "license": "MIT", 21 | "bugs": { 22 | "url": "https://github.com/chrisdavies/rlite/issues" 23 | }, 24 | "homepage": "https://github.com/chrisdavies/rlite", 25 | "devDependencies": { 26 | "jasmine-node": "^1.14.5", 27 | "uglify-js": "^2.7.5" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /rlite.js: -------------------------------------------------------------------------------- 1 | // This library started as an experiment to see how small I could make 2 | // a functional router. It has since been optimized (and thus grown). 3 | // The redundancy and inelegance here is for the sake of either size 4 | // or speed. 5 | // 6 | // That's why router params are marked with a single char: `~` and named params are denoted `:` 7 | (function (root, factory) { 8 | var define = root && root.define; 9 | 10 | if (define && define.amd) { 11 | define('rlite', [], factory); 12 | } else if (typeof module !== 'undefined' && module.exports) { 13 | module.exports = factory(); 14 | } else { 15 | root.Rlite = factory(); 16 | } 17 | }(this, function () { 18 | return function (notFound, routeDefinitions) { 19 | var routes = {}; 20 | var decode = decodeURIComponent; 21 | 22 | init(); 23 | 24 | return run; 25 | 26 | function init() { 27 | for (var key in routeDefinitions) { 28 | add(key, routeDefinitions[key]); 29 | } 30 | }; 31 | 32 | function noop(s) { return s; } 33 | 34 | function sanitize(url) { 35 | ~url.indexOf('/?') && (url = url.replace('/?', '?')); 36 | url[0] == '/' && (url = url.slice(1)); 37 | url[url.length - 1] == '/' && (url = url.slice(0, -1)); 38 | 39 | return url; 40 | } 41 | 42 | // Recursively searches the route tree for a matching route 43 | // pieces: an array of url parts, ['users', '1', 'edit'] 44 | // esc: the function used to url escape values 45 | // i: the index of the piece being processed 46 | // rules: the route tree 47 | // params: the computed route parameters (this is mutated), and is a stack since we don't have fast immutable datatypes 48 | // 49 | // This attempts to match the most specific route, but may end int a dead-end. We then attempt a less specific 50 | // route, following named route parameters. In searching this secondary branch, we need to make sure to clear 51 | // any route params that were generated during the search of the dead-end branch. 52 | function recurseUrl(pieces, esc, i, rules, params) { 53 | if (!rules) { 54 | return; 55 | } 56 | 57 | if (i >= pieces.length) { 58 | var cb = rules['@']; 59 | return cb && { 60 | cb: cb, 61 | params: params.reduce(function(h, kv) { h[kv[0]] = kv[1]; return h; }, {}), 62 | }; 63 | } 64 | 65 | var piece = esc(pieces[i]); 66 | var paramLen = params.length; 67 | return recurseUrl(pieces, esc, i + 1, rules[piece.toLowerCase()], params) 68 | || recurseNamedUrl(pieces, esc, i + 1, rules, ':', piece, params, paramLen) 69 | || recurseNamedUrl(pieces, esc, pieces.length, rules, '*', pieces.slice(i).join('/'), params, paramLen); 70 | } 71 | 72 | // Recurses for a named route, where the name is looked up via key and associated with val 73 | function recurseNamedUrl(pieces, esc, i, rules, key, val, params, paramLen) { 74 | params.length = paramLen; // Reset any params generated in the unsuccessful search branch 75 | var subRules = rules[key]; 76 | subRules && params.push([subRules['~'], val]); 77 | return recurseUrl(pieces, esc, i, subRules, params); 78 | } 79 | 80 | function processQuery(url, ctx, esc) { 81 | if (url && ctx.cb) { 82 | var hash = url.indexOf('#'), 83 | query = (hash < 0 ? url : url.slice(0, hash)).split('&'); 84 | 85 | for (var i = 0; i < query.length; ++i) { 86 | var nameValue = query[i].split('='); 87 | 88 | ctx.params[nameValue[0]] = esc(nameValue[1]); 89 | } 90 | } 91 | 92 | return ctx; 93 | } 94 | 95 | function lookup(url) { 96 | var querySplit = sanitize(url).split('?'); 97 | var esc = ~url.indexOf('%') ? decode : noop; 98 | 99 | return processQuery(querySplit[1], recurseUrl(querySplit[0].split('/'), esc, 0, routes, []) || {}, esc); 100 | } 101 | 102 | function add(route, handler) { 103 | var pieces = route.split('/'); 104 | var rules = routes; 105 | 106 | for (var i = +(route[0] === '/'); i < pieces.length; ++i) { 107 | var piece = pieces[i]; 108 | var name = piece[0] == ':' ? ':' : piece[0] == '*' ? '*' : piece.toLowerCase(); 109 | 110 | rules = rules[name] || (rules[name] = {}); 111 | 112 | (name == ':' || name == '*') && (rules['~'] = piece.slice(1)); 113 | } 114 | 115 | rules['@'] = handler; 116 | } 117 | 118 | function run(url, arg) { 119 | var result = lookup(url); 120 | 121 | return (result.cb || notFound)(result.params, arg, url); 122 | }; 123 | }; 124 | })); 125 | -------------------------------------------------------------------------------- /rlite.min.js: -------------------------------------------------------------------------------- 1 | !function(n,e){var r=n&&n.define;r&&r.amd?r("rlite",[],e):"undefined"!=typeof module&&module.exports?module.exports=e():n.Rlite=e()}(this,function(){return function(n,e){function r(n){return n}function t(n){return~n.indexOf("/?")&&(n=n.replace("/?","?")),"/"==n[0]&&(n=n.slice(1)),"/"==n[n.length-1]&&(n=n.slice(0,-1)),n}function i(n,e,r,t,u){if(t){if(r>=n.length){var f=t["@"];return f&&{cb:f,params:u.reduce(function(n,e){return n[e[0]]=e[1],n},{})}}var c=e(n[r]),l=u.length;return i(n,e,r+1,t[c.toLowerCase()],u)||o(n,e,r+1,t,":",c,u,l)||o(n,e,n.length,t,"*",n.slice(r).join("/"),u,l)}}function o(n,e,r,t,o,u,f,c){f.length=c;var l=t[o];return l&&f.push([l["~"],u]),i(n,e,r,l,f)}function u(n,e,r){if(n&&e.cb)for(var t=n.indexOf("#"),i=(t<0?n:n.slice(0,t)).split("&"),o=0;o expect(name).toEqual('value')}); 6 | 7 | route('stuff?name=value#baz'); 8 | }); 9 | 10 | it('Has empty params for parameterless routes', function () { 11 | const route = rlite(noop, { 12 | stuff: (params) => expect(Object.keys(params).length).toEqual(0) 13 | }); 14 | 15 | route('stuff'); 16 | }); 17 | 18 | it('Returns the result of the route', function () { 19 | const route = rlite(noop, { 20 | hi: () => 'Hello bob', 21 | bye: () => 'Bye bob', 22 | }); 23 | 24 | expect(route('hi')).toEqual('Hello bob'); 25 | expect(route('bye')).toEqual('Bye bob'); 26 | }); 27 | 28 | it('It handles leading and trailing slashes and 404s', function () { 29 | const route = rlite(() => 'Nope!', { 30 | stuff: () => 'Yep!' 31 | }); 32 | 33 | expect(route('/stuff/')).toEqual('Yep!'); 34 | expect(route('stuff/')).toEqual('Yep!'); 35 | expect(route('/stuff')).toEqual('Yep!'); 36 | expect(route('stuff')).toEqual('Yep!'); 37 | expect(route('nopes')).toEqual('Nope!'); 38 | }); 39 | 40 | it('It handles deep conflicting routes', function () { 41 | const route = rlite(() => '404', { 42 | 'foo/:bar/baz': ({bar}) => `Hi, ${bar}`, 43 | 'foo/:bar/:boo': ({bar, boo}) => `Bar=${bar}, Boo=${boo}`, 44 | 'foo/bar/bing': () => 'BING', 45 | }); 46 | 47 | expect(route('/foo/bar/baz/')).toEqual('Hi, bar'); 48 | expect(route('/foo/x/y/')).toEqual('Bar=x, Boo=y'); 49 | expect(route('/foo/bar/bing/')).toEqual('BING'); 50 | }); 51 | 52 | it('Handles route params', function() { 53 | const route = rlite(noop, { 54 | 'hey/:name': ({name}) => expect(name).toEqual('chris') 55 | }); 56 | 57 | route('hey/chris'); 58 | }); 59 | 60 | it('Handles different cases', function() { 61 | let count = 0; 62 | const route = rlite(noop, { 63 | 'Hey/:name': ({name}) => { 64 | expect(name).toEqual('chris'); 65 | ++count; 66 | }, 67 | 'hello/:firstName': ({firstName}) => { 68 | expect(firstName).toEqual('jane'); 69 | ++count; 70 | }, 71 | 'hoi/:FirstName/:LastName': ({FirstName, LastName}) => { 72 | expect(FirstName).toEqual('Joe'); 73 | expect(LastName).toEqual('Smith'); 74 | ++count; 75 | } 76 | }); 77 | 78 | route('hey/chris'); 79 | route('hello/jane'); 80 | route('hoi/Joe/Smith'); 81 | expect(count).toBe(3); 82 | }); 83 | 84 | it('Passes the argument and url through', function() { 85 | const route = rlite(noop, { 86 | 'hey/:name': ({name}, arg, url) => { 87 | expect(arg).toEqual('Wut'); 88 | expect(name).toEqual('You'); 89 | expect(url).toEqual('hey/You'); 90 | } 91 | }); 92 | 93 | route('hey/You', 'Wut'); 94 | }); 95 | 96 | it('Matches root routes correctly', function() { 97 | const route = rlite(noop, { 98 | 'hey/:name/new': () => {throw new Error('New called');}, 99 | 'hey/:name': ({name}) => expect(name).toEqual('chris'), 100 | 'hey/:name/edit': () => {throw new Error('Edit called');}, 101 | }); 102 | 103 | route('hey/chris'); 104 | }); 105 | 106 | it('Understands specificity', function() { 107 | const route = rlite(noop, { 108 | 'hey/joe': (_1, _2, url) => expect(url).toEqual('hey/joe'), 109 | 'hey/:name': () => {throw new Error('Name called')}, 110 | 'hey/jane': (_1, _2, url) => expect(url).toEqual('hey/jane'), 111 | }); 112 | 113 | route('hey/joe'); 114 | route('hey/jane'); 115 | }); 116 | 117 | it('Handles complex routes', function() { 118 | const route = rlite(noop, { 119 | 'hey/:name/new': () => {throw new Error('New called');}, 120 | 'hey/:name': () => {throw new Error('Name called');}, 121 | 'hey/:name/last/:last': () => ({name, last}) => { 122 | expect(name).toEqual('chris'); 123 | expect(last).toEqual('davies'); 124 | } 125 | }); 126 | 127 | route('hey/chris/last/davies'); 128 | }); 129 | 130 | it('Overrides params with query string values', function() { 131 | const route = rlite(noop, { 132 | 'hey/:name/new': () => {throw new Error('New called');}, 133 | 'hey/:name': () => {throw new Error('Name called');}, 134 | 'hey/:name/last/:last': function({name, last}) { 135 | expect(name).toEqual('ham'); 136 | expect(last).toEqual('mayo'); 137 | return name + ' ' + last; 138 | } 139 | }); 140 | 141 | expect(route('hey/chris/last/davies?last=mayo&name=ham')).toEqual('ham mayo'); 142 | }); 143 | 144 | it('Handles not founds', function() { 145 | const route = rlite(() => '404', { 146 | 'hey/:name': () => {throw new Error('Name called');} 147 | }); 148 | 149 | expect(route('hey?hi=there')).toEqual('404'); 150 | }); 151 | 152 | it('Handles default urls', function() { 153 | const route = rlite(noop, { 154 | '': () => 'HOME' 155 | }); 156 | 157 | expect(route('')).toEqual('HOME'); 158 | }); 159 | 160 | it('Handles multiple params in a row', function() { 161 | const route = rlite(noop, { 162 | 'hey/:hello/:world': ({hello, world}) => { 163 | expect(hello).toEqual('a'); 164 | expect(world).toEqual('b'); 165 | } 166 | }); 167 | 168 | route('hey/a/b'); 169 | }); 170 | 171 | it('Handles trailing slash with query', function() { 172 | const route = rlite(noop, { 173 | 'hoi': ({there}) => { 174 | expect(there).toEqual('yup'); 175 | return 'Yeppers'; 176 | } 177 | }); 178 | 179 | expect(route('hoi/?there=yup')).toEqual('Yeppers'); 180 | }); 181 | 182 | it('Handles leading slashes in defs', function() { 183 | const route = rlite(noop, { 184 | '/hoi': () => 'GOT IT' 185 | }); 186 | 187 | expect(route('hoi')).toEqual('GOT IT'); 188 | }); 189 | 190 | it('Handles wildcard routes', function() { 191 | const route = rlite(() => 'NOT FOUND', { 192 | '/users/:name/baz': ({name}) => `Hi, ${name}`, 193 | '/users/*name': ({name}) => `Wild, ${name}`, 194 | '/foo/:baz/qux': ({baz}) => `BAZ ${baz}`, 195 | '/foo/*bar': ({bar}) => `GOT ${bar}`, 196 | }); 197 | 198 | expect(route('hoi')).toEqual('NOT FOUND'); 199 | expect(route('users/chris/baz')).toEqual('Hi, chris'); 200 | expect(route('users/chris/bar')).toEqual('Wild, chris/bar'); 201 | expect(route('foo/something')).toEqual('GOT something'); 202 | expect(route('foo/something/qux')).toEqual('BAZ something'); 203 | }); 204 | 205 | it('Encodes params', function() { 206 | const route = rlite(noop, { 207 | '': ({hey}) => { 208 | expect(hey).toEqual('/what/now'); 209 | return 'HOME'; 210 | }, 211 | 212 | ':hey': ({hey}) => { 213 | expect(hey).toEqual('/hoi/hai?hui'); 214 | return ':hey'; 215 | }, 216 | 217 | 'more-complex/:hey': ({hey, hui}) => { 218 | expect(hey).toEqual('/hoi/hai?hui'); 219 | expect(hui).toEqual('/hoi/hai'); 220 | return 'LAST'; 221 | }, 222 | }); 223 | 224 | expect(route(encodeURIComponent('/hoi/hai?hui'))).toEqual(':hey'); 225 | expect(route('/?hey=' + encodeURIComponent('/what/now'))).toEqual('HOME'); 226 | expect(route('/more-complex/' + encodeURIComponent('/hoi/hai?hui') + '?hui=' + encodeURIComponent('/hoi/hai'))).toEqual('LAST'); 227 | }); 228 | }); 229 | 230 | function noop() { } 231 | 232 | })(this.Rlite || require('../rlite')); 233 | -------------------------------------------------------------------------------- /test/rlite.test.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Rlite tests 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | --------------------------------------------------------------------------------