├── .gitignore ├── README.md ├── feather-route-matcher.js ├── feather-route-matcher.js.map ├── feather-route-matcher.min.js ├── feather-route-matcher.min.js.map ├── index.js ├── package-lock.json ├── package.json ├── rollup.config.js └── test.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | npm-debug.log -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # feather-route-matcher 2 | 3 | ![](https://img.shields.io/npm/dm/feather-route-matcher.svg)![](https://img.shields.io/npm/v/feather-route-matcher.svg)![](https://img.shields.io/npm/l/feather-route-matcher.svg) 4 | 5 | This tiny module exports a single function that takes an object of url patterns and returns a function that can be called to get the matching object, based on the url. 6 | 7 | This is in support of experiments I'm doing for building lightweight clientside apps [here](https://github.com/henrikjoreteg/feather-app). 8 | 9 | You call `createMatcher` and pass it an object of routes where the key is the url pattern (using same matching logic as Backbone.js) and the result is whatever you want to return as a match to that url. Usually this would be a component representing the page you want to show for that url pattern, but it could be anything. 10 | 11 | You could, for example, create a module that exports that function like this: 12 | 13 | **routes.js** 14 | 15 | ```js 16 | import createMatcher from 'feather-route-matcher' 17 | import homePage from './pages/home' 18 | import courseListingPage from './pages/course-listing' 19 | import courseDetailPage from './pages/course-detail' 20 | import notFoundPage from './pages/not-found' 21 | 22 | export default createMatcher({ 23 | '/': homePage, 24 | '/courses': courseListingPage, 25 | '/courses/:id': courseDetailPage, 26 | '/*': notFoundPage 27 | }) 28 | ``` 29 | 30 | This returns a function that can be called to retrieve the value, along with extracted parameters: 31 | 32 | **other.js** 33 | 34 | ```js 35 | import routeMatcher from './routes' 36 | 37 | // call it with a pathname you want to match 38 | routeMatcher('/') 39 | // => 40 | // { 41 | // value: homePage, 42 | // url: '/', 43 | // params: null 44 | // } 45 | 46 | routeMatcher('/courses/47') 47 | // => 48 | // { 49 | // value: courseDetailPage, 50 | // url: '/', 51 | // params: { 52 | // id: '47' 53 | // } 54 | // } 55 | 56 | routeMatcher('/some-garbage') 57 | // => 58 | // { 59 | // value: notFoundPage, 60 | // url: '/some/garbage', 61 | // params: { 62 | // // anything matched by a wildcard `*` 63 | // // will return a param named `path` 64 | // // with whatever existed in the location 65 | // // where the `*` was 66 | // path: 'some/garbage' 67 | // } 68 | // } 69 | 70 | ``` 71 | 72 | ## why is this useful? 73 | 74 | If you treat the `url` in a clientside app as just another piece of application state in a frontend app you'll likely need some sort of `switch` statement or set of `if`/`else` blocks to match the current url with the component you want to show. 75 | 76 | That's easy with urls that are known ahead of time, such as `/home` but becomes a bit more arduous when you want to see whether it matches a given pattern and want to extract values such as: `/user/42`. That's where this module helps with. 77 | 78 | The result of the matcher is a great candidate for going into a state store, like [redux](http://redux.js.org/). 79 | 80 | This module could be used as a really lightweight routing system for a react/redux app without the need for React Router. 81 | 82 | ## how parameter extraction works 83 | 84 | pattern: `'/users/:id'` 85 | url: `'/something-else'` 86 | extracted params: nothing, because it won't match 87 | 88 | pattern: `'/users/:id'` 89 | url: `'/users/scrooge-mc-duck'` 90 | extracted params: `{id: 'scrooge-mc-duck'}` 91 | 92 | pattern: `'/users/:id'` 93 | url: `'/users/47'` 94 | extracted params: `{id: '47'}` 95 | 96 | pattern: `'/schools/:schoolId/teachers/:teacherId'` 97 | url: `'/schools/richland/teachers/47'` 98 | extracted params: `{schoolId: 'richland', teacherId: '47'}` 99 | 100 | pattern: `*` 101 | url: `'/asdfas'` 102 | extracted params: `{path: '/asdfas'}` 103 | **note:** extracted param always called `path` 104 | 105 | pattern: `'/schools/*` 106 | url: `'/schools/richland/teachers/47'` 107 | extracted params: `{path: 'richland/teachers/47'}` 108 | 109 | 110 | ## Other notes 111 | 112 | This module borrows a few extremely well-tested regexes from Backbone.js to do its pattern matching. Thanks for the generous licensing! 113 | 114 | Things to be aware of... 115 | 116 | 1. Order is important, first match wins 117 | 2. If you re-use parameter names in the url pattern they'll be overwritten in the result. 118 | 3. If you need to parse query string values, match the base url first with this module, then use [`query-string`](http://npmjs.com/package/query-string) to parse query values. 119 | 120 | 121 | ## install 122 | 123 | ``` 124 | npm install feather-route-matcher --save 125 | ``` 126 | 127 | ## credits 128 | 129 | If you like this follow [@HenrikJoreteg](http://twitter.com/henrikjoreteg) on twitter. The regex patterns were borrowed from Backbone.js because they're extremely well tested and I want this to Just Work™. 130 | 131 | ## tests 132 | 133 | ``` 134 | npm run test 135 | ``` 136 | 137 | ## changelog 138 | 139 | * `4.0.0` - Breaking changes: In the matched response, the property called `page` was renamed `value`. Removed `fallback` option because it's easy to do in app code using this. Fixed bug where passed in route object was being mutated. 140 | 141 | * `3.1.0` - Non-breaking conversion of `./index.js` into an esm module. umd version remains available as `./feather-route-matcher.js` and `./feather-route-matcher.min.js` 142 | 143 | * `3.0.0` - Changed result to now include the pattern that was matched as well. 144 | 145 | * `2.0.1` - Remove accidentally left ES6 usage. 146 | 147 | * `2.0.0` - Instead of expecting the values of object passed to `createMatcher` to be function of a certain structure, it now always just returns an object of a pre-determined structure, including the passed url, any extracted params, and a `page` key that contains whatever the original value of that key was. 148 | 149 | * `1.0.0` - initial release 150 | 151 | ## license 152 | 153 | [MIT](http://mit.joreteg.com/) 154 | -------------------------------------------------------------------------------- /feather-route-matcher.js: -------------------------------------------------------------------------------- 1 | (function (global, factory) { 2 | typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : 3 | typeof define === 'function' && define.amd ? define(factory) : 4 | (global = global || self, global.createMatcher = factory()); 5 | }(this, (function () { 'use strict'; 6 | 7 | // regexes borrowed from backbone 8 | var optionalParam = /\((.*?)\)/g; 9 | var namedParam = /(\(\?)?:\w+/g; 10 | // eslint-disable-next-line no-useless-escape 11 | var escapeRegExp = /[\-{}\[\]+?.,\\\^$|#\s]/g; 12 | var splatParam = /\*/g; 13 | 14 | // Parses a URL pattern such as `/users/:id` 15 | // and builds and returns a regex that can be used to 16 | // match said pattern. Credit for these 17 | // regexes belongs to Jeremy Ashkenas and the 18 | // other maintainers of Backbone.js 19 | // 20 | // It has been modified for extraction of 21 | // named parameters from the URL 22 | var parsePattern = function (pattern) { 23 | var names = []; 24 | pattern = pattern 25 | .replace(escapeRegExp, '\\$&') 26 | .replace(optionalParam, '(?:$1)?') 27 | .replace(namedParam, function (match, optional) { 28 | names.push(match.slice(1)); 29 | return optional ? match : '([^/?]+)' 30 | }) 31 | .replace(splatParam, function () { 32 | names.push('path'); 33 | return '([^?]*?)' 34 | }); 35 | 36 | return { 37 | regExp: new RegExp('^' + pattern + '(?:\\?([\\s\\S]*))?$'), 38 | namedParams: names 39 | } 40 | }; 41 | 42 | function index (routes) { 43 | var keys = Object.keys(routes); 44 | var routeCache = {}; 45 | 46 | // loop through each route we're 47 | // and build the shell of our 48 | // route cache. 49 | for (var item in routes) { 50 | routeCache[item] = { 51 | value: routes[item] 52 | }; 53 | } 54 | 55 | // main result is a function that can be called 56 | // with the url 57 | return function (url) { 58 | var params; 59 | var route; 60 | 61 | // start looking for matches 62 | var matchFound = keys.some(function (key) { 63 | var parsed; 64 | 65 | // fetch the route pattern from the cache 66 | // there will always be one 67 | route = routeCache[key]; 68 | 69 | // if the route doesn't already have 70 | // a regex we never generated one 71 | // so we do that here lazily. 72 | // Parse the pattern to generate the 73 | // regex once, and store the result 74 | // for next time. 75 | if (!route.regExp) { 76 | parsed = parsePattern(key); 77 | route.regExp = parsed.regExp; 78 | route.namedParams = parsed.namedParams; 79 | route.pattern = key; 80 | } 81 | 82 | // run our cached regex 83 | var result = route.regExp.exec(url); 84 | 85 | // if null there was no match 86 | // returning falsy here continues 87 | // the `Array.prototype.some` loop 88 | if (!result) { 89 | return 90 | } 91 | 92 | // remove other cruft from result 93 | result = result.slice(1, -1); 94 | 95 | // reduce our match to an object of named parameters 96 | // we've extracted from the url 97 | params = result.reduce(function (obj, val, index) { 98 | if (val) { 99 | obj[route.namedParams[index]] = val; 100 | } 101 | return obj 102 | }, {}); 103 | 104 | // stops the loop 105 | return true 106 | }); 107 | 108 | // no routes matched 109 | if (!matchFound) { 110 | return null 111 | } 112 | 113 | return { 114 | value: route.value, 115 | params: params, 116 | url: url, 117 | pattern: route.pattern 118 | } 119 | } 120 | } 121 | 122 | return index; 123 | 124 | }))); 125 | //# sourceMappingURL=feather-route-matcher.js.map 126 | -------------------------------------------------------------------------------- /feather-route-matcher.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"feather-route-matcher.js","sources":["index.js"],"sourcesContent":["// regexes borrowed from backbone\nvar optionalParam = /\\((.*?)\\)/g\nvar namedParam = /(\\(\\?)?:\\w+/g\n// eslint-disable-next-line no-useless-escape\nvar escapeRegExp = /[\\-{}\\[\\]+?.,\\\\\\^$|#\\s]/g\nvar splatParam = /\\*/g\n\n// Parses a URL pattern such as `/users/:id`\n// and builds and returns a regex that can be used to\n// match said pattern. Credit for these\n// regexes belongs to Jeremy Ashkenas and the\n// other maintainers of Backbone.js\n//\n// It has been modified for extraction of\n// named parameters from the URL\nvar parsePattern = function (pattern) {\n var names = []\n pattern = pattern\n .replace(escapeRegExp, '\\\\$&')\n .replace(optionalParam, '(?:$1)?')\n .replace(namedParam, function (match, optional) {\n names.push(match.slice(1))\n return optional ? match : '([^/?]+)'\n })\n .replace(splatParam, function () {\n names.push('path')\n return '([^?]*?)'\n })\n\n return {\n regExp: new RegExp('^' + pattern + '(?:\\\\?([\\\\s\\\\S]*))?$'),\n namedParams: names\n }\n}\n\nexport default function (routes) {\n var keys = Object.keys(routes)\n var routeCache = {}\n\n // loop through each route we're\n // and build the shell of our\n // route cache.\n for (var item in routes) {\n routeCache[item] = {\n value: routes[item]\n }\n }\n\n // main result is a function that can be called\n // with the url\n return function (url) {\n var params\n var route\n\n // start looking for matches\n var matchFound = keys.some(function (key) {\n var parsed\n\n // fetch the route pattern from the cache\n // there will always be one\n route = routeCache[key]\n\n // if the route doesn't already have\n // a regex we never generated one\n // so we do that here lazily.\n // Parse the pattern to generate the\n // regex once, and store the result\n // for next time.\n if (!route.regExp) {\n parsed = parsePattern(key)\n route.regExp = parsed.regExp\n route.namedParams = parsed.namedParams\n route.pattern = key\n }\n\n // run our cached regex\n var result = route.regExp.exec(url)\n\n // if null there was no match\n // returning falsy here continues\n // the `Array.prototype.some` loop\n if (!result) {\n return\n }\n\n // remove other cruft from result\n result = result.slice(1, -1)\n\n // reduce our match to an object of named parameters\n // we've extracted from the url\n params = result.reduce(function (obj, val, index) {\n if (val) {\n obj[route.namedParams[index]] = val\n }\n return obj\n }, {})\n\n // stops the loop\n return true\n })\n\n // no routes matched\n if (!matchFound) {\n return null\n }\n\n return {\n value: route.value,\n params: params,\n url: url,\n pattern: route.pattern\n }\n }\n}\n"],"names":[],"mappings":";;;;;;EAAA;EACA,IAAI,aAAa,GAAG,aAAY;EAChC,IAAI,UAAU,GAAG,eAAc;EAC/B;EACA,IAAI,YAAY,GAAG,2BAA0B;EAC7C,IAAI,UAAU,GAAG,MAAK;AACtB;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA,IAAI,YAAY,GAAG,UAAU,OAAO,EAAE;EACtC,EAAE,IAAI,KAAK,GAAG,GAAE;EAChB,EAAE,OAAO,GAAG,OAAO;EACnB,KAAK,OAAO,CAAC,YAAY,EAAE,MAAM,CAAC;EAClC,KAAK,OAAO,CAAC,aAAa,EAAE,SAAS,CAAC;EACtC,KAAK,OAAO,CAAC,UAAU,EAAE,UAAU,KAAK,EAAE,QAAQ,EAAE;EACpD,MAAM,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,EAAC;EAChC,MAAM,OAAO,QAAQ,GAAG,KAAK,GAAG,UAAU;EAC1C,KAAK,CAAC;EACN,KAAK,OAAO,CAAC,UAAU,EAAE,YAAY;EACrC,MAAM,KAAK,CAAC,IAAI,CAAC,MAAM,EAAC;EACxB,MAAM,OAAO,UAAU;EACvB,KAAK,EAAC;AACN;EACA,EAAE,OAAO;EACT,IAAI,MAAM,EAAE,IAAI,MAAM,CAAC,GAAG,GAAG,OAAO,GAAG,sBAAsB,CAAC;EAC9D,IAAI,WAAW,EAAE,KAAK;EACtB,GAAG;EACH,EAAC;AACD;EACe,cAAQ,EAAE,MAAM,EAAE;EACjC,EAAE,IAAI,IAAI,GAAG,MAAM,CAAC,IAAI,CAAC,MAAM,EAAC;EAChC,EAAE,IAAI,UAAU,GAAG,GAAE;AACrB;EACA;EACA;EACA;EACA,EAAE,KAAK,IAAI,IAAI,IAAI,MAAM,EAAE;EAC3B,IAAI,UAAU,CAAC,IAAI,CAAC,GAAG;EACvB,MAAM,KAAK,EAAE,MAAM,CAAC,IAAI,CAAC;EACzB,MAAK;EACL,GAAG;AACH;EACA;EACA;EACA,EAAE,OAAO,UAAU,GAAG,EAAE;EACxB,IAAI,IAAI,OAAM;EACd,IAAI,IAAI,MAAK;AACb;EACA;EACA,IAAI,IAAI,UAAU,GAAG,IAAI,CAAC,IAAI,CAAC,UAAU,GAAG,EAAE;EAC9C,MAAM,IAAI,OAAM;AAChB;EACA;EACA;EACA,MAAM,KAAK,GAAG,UAAU,CAAC,GAAG,EAAC;AAC7B;EACA;EACA;EACA;EACA;EACA;EACA;EACA,MAAM,IAAI,CAAC,KAAK,CAAC,MAAM,EAAE;EACzB,QAAQ,MAAM,GAAG,YAAY,CAAC,GAAG,EAAC;EAClC,QAAQ,KAAK,CAAC,MAAM,GAAG,MAAM,CAAC,OAAM;EACpC,QAAQ,KAAK,CAAC,WAAW,GAAG,MAAM,CAAC,YAAW;EAC9C,QAAQ,KAAK,CAAC,OAAO,GAAG,IAAG;EAC3B,OAAO;AACP;EACA;EACA,MAAM,IAAI,MAAM,GAAG,KAAK,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,EAAC;AACzC;EACA;EACA;EACA;EACA,MAAM,IAAI,CAAC,MAAM,EAAE;EACnB,QAAQ,MAAM;EACd,OAAO;AACP;EACA;EACA,MAAM,MAAM,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,EAAC;AAClC;EACA;EACA;EACA,MAAM,MAAM,GAAG,MAAM,CAAC,MAAM,CAAC,UAAU,GAAG,EAAE,GAAG,EAAE,KAAK,EAAE;EACxD,QAAQ,IAAI,GAAG,EAAE;EACjB,UAAU,GAAG,CAAC,KAAK,CAAC,WAAW,CAAC,KAAK,CAAC,CAAC,GAAG,IAAG;EAC7C,SAAS;EACT,QAAQ,OAAO,GAAG;EAClB,OAAO,EAAE,EAAE,EAAC;AACZ;EACA;EACA,MAAM,OAAO,IAAI;EACjB,KAAK,EAAC;AACN;EACA;EACA,IAAI,IAAI,CAAC,UAAU,EAAE;EACrB,MAAM,OAAO,IAAI;EACjB,KAAK;AACL;EACA,IAAI,OAAO;EACX,MAAM,KAAK,EAAE,KAAK,CAAC,KAAK;EACxB,MAAM,MAAM,EAAE,MAAM;EACpB,MAAM,GAAG,EAAE,GAAG;EACd,MAAM,OAAO,EAAE,KAAK,CAAC,OAAO;EAC5B,KAAK;EACL,GAAG;EACH;;;;;;;;"} -------------------------------------------------------------------------------- /feather-route-matcher.min.js: -------------------------------------------------------------------------------- 1 | !function(e,r){"object"==typeof exports&&"undefined"!=typeof module?module.exports=r():"function"==typeof define&&define.amd?define(r):(e=e||self).createMatcher=r()}(this,function(){"use strict";var i=/\((.*?)\)/g,o=/(\(\?)?:\w+/g,s=/[\-{}\[\]+?.,\\\^$|#\s]/g,l=/\*/g;return function(e){var r=Object.keys(e),f={};for(var n in e)f[n]={value:e[n]};return function(u){var c,p;return r.some(function(e){var r,n,t;(p=f[e]).regExp||(t=[],n=(n=e).replace(s,"\\$&").replace(i,"(?:$1)?").replace(o,function(e,r){return t.push(e.slice(1)),r?e:"([^/?]+)"}).replace(l,function(){return t.push("path"),"([^?]*?)"}),r={regExp:new RegExp("^"+n+"(?:\\?([\\s\\S]*))?$"),namedParams:t},p.regExp=r.regExp,p.namedParams=r.namedParams,p.pattern=e);var a=p.regExp.exec(u);if(a)return a=a.slice(1,-1),c=a.reduce(function(e,r,n){return r&&(e[p.namedParams[n]]=r),e},{}),!0})?{value:p.value,params:c,url:u,pattern:p.pattern}:null}}}); 2 | //# sourceMappingURL=feather-route-matcher.min.js.map 3 | -------------------------------------------------------------------------------- /feather-route-matcher.min.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"feather-route-matcher.min.js","sources":["index.js"],"sourcesContent":["// regexes borrowed from backbone\nvar optionalParam = /\\((.*?)\\)/g\nvar namedParam = /(\\(\\?)?:\\w+/g\n// eslint-disable-next-line no-useless-escape\nvar escapeRegExp = /[\\-{}\\[\\]+?.,\\\\\\^$|#\\s]/g\nvar splatParam = /\\*/g\n\n// Parses a URL pattern such as `/users/:id`\n// and builds and returns a regex that can be used to\n// match said pattern. Credit for these\n// regexes belongs to Jeremy Ashkenas and the\n// other maintainers of Backbone.js\n//\n// It has been modified for extraction of\n// named parameters from the URL\nvar parsePattern = function (pattern) {\n var names = []\n pattern = pattern\n .replace(escapeRegExp, '\\\\$&')\n .replace(optionalParam, '(?:$1)?')\n .replace(namedParam, function (match, optional) {\n names.push(match.slice(1))\n return optional ? match : '([^/?]+)'\n })\n .replace(splatParam, function () {\n names.push('path')\n return '([^?]*?)'\n })\n\n return {\n regExp: new RegExp('^' + pattern + '(?:\\\\?([\\\\s\\\\S]*))?$'),\n namedParams: names\n }\n}\n\nexport default function (routes) {\n var keys = Object.keys(routes)\n var routeCache = {}\n\n // loop through each route we're\n // and build the shell of our\n // route cache.\n for (var item in routes) {\n routeCache[item] = {\n value: routes[item]\n }\n }\n\n // main result is a function that can be called\n // with the url\n return function (url) {\n var params\n var route\n\n // start looking for matches\n var matchFound = keys.some(function (key) {\n var parsed\n\n // fetch the route pattern from the cache\n // there will always be one\n route = routeCache[key]\n\n // if the route doesn't already have\n // a regex we never generated one\n // so we do that here lazily.\n // Parse the pattern to generate the\n // regex once, and store the result\n // for next time.\n if (!route.regExp) {\n parsed = parsePattern(key)\n route.regExp = parsed.regExp\n route.namedParams = parsed.namedParams\n route.pattern = key\n }\n\n // run our cached regex\n var result = route.regExp.exec(url)\n\n // if null there was no match\n // returning falsy here continues\n // the `Array.prototype.some` loop\n if (!result) {\n return\n }\n\n // remove other cruft from result\n result = result.slice(1, -1)\n\n // reduce our match to an object of named parameters\n // we've extracted from the url\n params = result.reduce(function (obj, val, index) {\n if (val) {\n obj[route.namedParams[index]] = val\n }\n return obj\n }, {})\n\n // stops the loop\n return true\n })\n\n // no routes matched\n if (!matchFound) {\n return null\n }\n\n return {\n value: route.value,\n params: params,\n url: url,\n pattern: route.pattern\n }\n }\n}\n"],"names":["optionalParam","namedParam","escapeRegExp","splatParam","routes","keys","Object","routeCache","item","value","url","params","route","some","key","parsed","pattern","names","regExp","replace","match","optional","push","slice","RegExp","namedParams","result","exec","reduce","obj","val","index"],"mappings":"mMACA,IAAIA,EAAgB,aAChBC,EAAa,eAEbC,EAAe,2BACfC,EAAa,aA8BF,SAAUC,GACvB,IAAIC,EAAOC,OAAOD,KAAKD,GACnBG,EAAa,GAKjB,IAAK,IAAIC,KAAQJ,EACfG,EAAWC,GAAQ,CACjBC,MAAOL,EAAOI,IAMlB,OAAO,SAAUE,GACf,IAAIC,EACAC,EAkDJ,OA/CiBP,EAAKQ,KAAK,SAAUC,GACnC,IAAIC,EAzCmBC,EACvBC,GA4CAL,EAAQL,EAAWO,IAQRI,SApDXD,EAAQ,GACZD,GAF2BA,EAsDCF,GAnDzBK,QAAQjB,EAAc,QACtBiB,QAAQnB,EAAe,WACvBmB,QAAQlB,EAAY,SAAUmB,EAAOC,GAEpC,OADAJ,EAAMK,KAAKF,EAAMG,MAAM,IAChBF,EAAWD,EAAQ,aAE3BD,QAAQhB,EAAY,WAEnB,OADAc,EAAMK,KAAK,QACJ,aA2CLP,EAxCC,CACLG,OAAQ,IAAIM,OAAO,IAAMR,EAAU,wBACnCS,YAAaR,GAuCTL,EAAMM,OAASH,EAAOG,OACtBN,EAAMa,YAAcV,EAAOU,YAC3Bb,EAAMI,QAAUF,GAIlB,IAAIY,EAASd,EAAMM,OAAOS,KAAKjB,GAK/B,GAAKgB,EAiBL,OAZAA,EAASA,EAAOH,MAAM,GAAI,GAI1BZ,EAASe,EAAOE,OAAO,SAAUC,EAAKC,EAAKC,GAIzC,OAHID,IACFD,EAAIjB,EAAMa,YAAYM,IAAUD,GAE3BD,GACN,KAGI,IAQF,CACLpB,MAAOG,EAAMH,MACbE,OAAQA,EACRD,IAAKA,EACLM,QAASJ,EAAMI,SAPR"} -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | // regexes borrowed from backbone 2 | var optionalParam = /\((.*?)\)/g 3 | var namedParam = /(\(\?)?:\w+/g 4 | // eslint-disable-next-line no-useless-escape 5 | var escapeRegExp = /[\-{}\[\]+?.,\\\^$|#\s]/g 6 | var splatParam = /\*/g 7 | 8 | // Parses a URL pattern such as `/users/:id` 9 | // and builds and returns a regex that can be used to 10 | // match said pattern. Credit for these 11 | // regexes belongs to Jeremy Ashkenas and the 12 | // other maintainers of Backbone.js 13 | // 14 | // It has been modified for extraction of 15 | // named parameters from the URL 16 | var parsePattern = function (pattern) { 17 | var names = [] 18 | pattern = pattern 19 | .replace(escapeRegExp, '\\$&') 20 | .replace(optionalParam, '(?:$1)?') 21 | .replace(namedParam, function (match, optional) { 22 | names.push(match.slice(1)) 23 | return optional ? match : '([^/?]+)' 24 | }) 25 | .replace(splatParam, function () { 26 | names.push('path') 27 | return '([^?]*?)' 28 | }) 29 | 30 | return { 31 | regExp: new RegExp('^' + pattern + '(?:\\?([\\s\\S]*))?$'), 32 | namedParams: names 33 | } 34 | } 35 | 36 | export default function (routes) { 37 | var keys = Object.keys(routes) 38 | var routeCache = {} 39 | 40 | // loop through each route we're 41 | // and build the shell of our 42 | // route cache. 43 | for (var item in routes) { 44 | routeCache[item] = { 45 | value: routes[item] 46 | } 47 | } 48 | 49 | // main result is a function that can be called 50 | // with the url 51 | return function (url) { 52 | var params 53 | var route 54 | 55 | // start looking for matches 56 | var matchFound = keys.some(function (key) { 57 | var parsed 58 | 59 | // fetch the route pattern from the cache 60 | // there will always be one 61 | route = routeCache[key] 62 | 63 | // if the route doesn't already have 64 | // a regex we never generated one 65 | // so we do that here lazily. 66 | // Parse the pattern to generate the 67 | // regex once, and store the result 68 | // for next time. 69 | if (!route.regExp) { 70 | parsed = parsePattern(key) 71 | route.regExp = parsed.regExp 72 | route.namedParams = parsed.namedParams 73 | route.pattern = key 74 | } 75 | 76 | // run our cached regex 77 | var result = route.regExp.exec(url) 78 | 79 | // if null there was no match 80 | // returning falsy here continues 81 | // the `Array.prototype.some` loop 82 | if (!result) { 83 | return 84 | } 85 | 86 | // remove other cruft from result 87 | result = result.slice(1, -1) 88 | 89 | // reduce our match to an object of named parameters 90 | // we've extracted from the url 91 | params = result.reduce(function (obj, val, index) { 92 | if (val) { 93 | obj[route.namedParams[index]] = val 94 | } 95 | return obj 96 | }, {}) 97 | 98 | // stops the loop 99 | return true 100 | }) 101 | 102 | // no routes matched 103 | if (!matchFound) { 104 | return null 105 | } 106 | 107 | return { 108 | value: route.value, 109 | params: params, 110 | url: url, 111 | pattern: route.pattern 112 | } 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "feather-route-matcher", 3 | "description": "featherweight url to handler matching", 4 | "version": "4.0.0", 5 | "author": "Henrik Joreteg ", 6 | "bugs": { 7 | "url": "https://github.com/henrikjoreteg/feather-route-matcher/issues" 8 | }, 9 | "devDependencies": { 10 | "rollup": "2.10.5", 11 | "rollup-plugin-commonjs": "10.1.0", 12 | "rollup-plugin-filesize": "9.0.0", 13 | "rollup-plugin-node-resolve": "5.2.0", 14 | "rollup-plugin-uglify": "6.0.4", 15 | "standard": "14.3.4", 16 | "tap": "14.10.7", 17 | "tap-spec": "5.0.0", 18 | "tape": "5.0.0" 19 | }, 20 | "homepage": "https://github.com/henrikjoreteg/feather-route-matcher", 21 | "keywords": [ 22 | "feather", 23 | "matcher", 24 | "router", 25 | "routing", 26 | "url" 27 | ], 28 | "license": "MIT", 29 | "main": "feather-route-matcher.js", 30 | "module": "index.js", 31 | "repository": { 32 | "type": "git", 33 | "url": "git://github.com/henrikjoreteg/feather-route-matcher" 34 | }, 35 | "scripts": { 36 | "build": "rollup -c", 37 | "pretest": "npm run build", 38 | "test": "node test.js | tap-spec && standard" 39 | }, 40 | "standard": { 41 | "ignore": [ 42 | "feather-route-matcher.js", 43 | "feather-route-matcher.min.js" 44 | ] 45 | }, 46 | "prettier": { 47 | "semi": false, 48 | "singleQuote": true, 49 | "trailingComma": "none" 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | const commonJs = require('rollup-plugin-commonjs') 2 | const filesize = require('rollup-plugin-filesize') 3 | const nodeResolve = require('rollup-plugin-node-resolve') 4 | const uglify = require('rollup-plugin-uglify') 5 | 6 | module.exports = [ 7 | { 8 | input: 'index.js', 9 | output: { 10 | file: 'feather-route-matcher.js', 11 | exports: 'default', 12 | format: 'umd', 13 | name: 'createMatcher', 14 | sourcemap: true 15 | }, 16 | plugins: [ 17 | nodeResolve(), 18 | commonJs(), 19 | filesize() 20 | ] 21 | }, 22 | { 23 | input: 'index.js', 24 | output: { 25 | file: 'feather-route-matcher.min.js', 26 | exports: 'default', 27 | format: 'umd', 28 | name: 'createMatcher', 29 | sourcemap: true 30 | }, 31 | plugins: [ 32 | nodeResolve(), 33 | commonJs(), 34 | uglify.uglify({ mangle: true, compress: true }), 35 | filesize() 36 | ] 37 | } 38 | ] 39 | -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | var test = require('tape') 2 | var createMatcher = require('./feather-route-matcher') 3 | 4 | test('passes all matching cases', function (t) { 5 | var cases = [ 6 | [ 7 | '/:first/:second', 8 | '/ok', 9 | null, 10 | 'should not match if missing required params' 11 | ], 12 | ['/:first', '/ok', { first: 'ok' }, 'should extract simple, named params'], 13 | ['/:first/', '/ok', null, 'should not tolerate missing trailing slashes'], 14 | [ 15 | '/:first/', 16 | '/ok/', 17 | { first: 'ok' }, 18 | 'should tolerate trailing slashes when explicit' 19 | ], 20 | [ 21 | '/:first/:second', 22 | '/ok/', 23 | null, 24 | 'should not match if has slash but no value' 25 | ], 26 | [ 27 | '/:first/:second', 28 | '/ok/second', 29 | { first: 'ok', second: 'second' }, 30 | 'can extract two values' 31 | ], 32 | [ 33 | '/:first(/:second)', 34 | '/ok/second', 35 | { first: 'ok', second: 'second' }, 36 | 'second value optional and is supplied' 37 | ], 38 | [ 39 | '/:first(/:second)', 40 | '/ok', 41 | { first: 'ok' }, 42 | 'second value optional and not supplied' 43 | ], 44 | [ 45 | '/users/:id', 46 | '/something-else', 47 | null, 48 | 'make sure example works as written in readme' 49 | ], 50 | [ 51 | '/users/:id', 52 | '/users/scrooge-mc-duck', 53 | { id: 'scrooge-mc-duck' }, 54 | 'make sure examples works as written in readme' 55 | ], 56 | [ 57 | '/users/:id', 58 | '/users/47', 59 | { id: '47' }, 60 | 'make sure example works as written in readme' 61 | ], 62 | [ 63 | '/schools/:schoolId/teachers/:teacherId', 64 | '/schools/richland/teachers/47', 65 | { schoolId: 'richland', teacherId: '47' }, 66 | 'example from readme' 67 | ], 68 | ['/random/*', '/random/something/stuff', { path: 'something/stuff' }], 69 | ['/*', '/sdfasfas', { path: 'sdfasfas' }, 'matches wildcards'], 70 | ['/', '/blah', null, 'returns null if not matching'] 71 | ] 72 | 73 | cases.forEach(function (testCase) { 74 | var routes = {} 75 | var [pattern, url, expectedResult, description] = testCase 76 | var SOME_PAGE = {} 77 | 78 | routes[pattern] = SOME_PAGE 79 | var matchUrl = createMatcher(routes) 80 | 81 | var result = matchUrl(url) 82 | 83 | if (result) { 84 | if (expectedResult) { 85 | t.pass('got a match as expected') 86 | } else { 87 | t.fail('should not have gotten a match') 88 | } 89 | t.deepEqual( 90 | result, 91 | { 92 | value: SOME_PAGE, 93 | url: url, 94 | params: expectedResult, 95 | pattern: pattern 96 | }, 97 | description 98 | ) 99 | } else { 100 | if (expectedResult) { 101 | t.fail( 102 | 'should have gotten a match, pattern: ' + pattern + ' url: ' + url 103 | ) 104 | } else { 105 | t.pass('got null as expected') 106 | } 107 | } 108 | }) 109 | 110 | t.end() 111 | }) 112 | 113 | test('does not modify route object passed in', (t) => { 114 | const startingRouteObject = { 115 | '/': 'something', 116 | '/else': 'somethingelse' 117 | } 118 | const serialized = JSON.stringify(startingRouteObject) 119 | createMatcher(startingRouteObject) 120 | t.equal(serialized, JSON.stringify(startingRouteObject)) 121 | 122 | t.end() 123 | }) 124 | --------------------------------------------------------------------------------