├── .travis.yml ├── .gitignore ├── bench ├── package.json └── index.js ├── .editorconfig ├── index.d.ts ├── package.json ├── license.md ├── src └── index.js ├── readme.md └── test └── index.js /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 8 4 | - 6 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | *-lock.* 4 | *.lock 5 | *.log 6 | lib 7 | -------------------------------------------------------------------------------- /bench/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "devDependencies": { 3 | "benchmark": "^2.1.4", 4 | "path-to-regexp": "^2.4.0", 5 | "regexparam": "^1.0.1" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_size = 2 5 | indent_style = tab 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [*.{json,yml,md}] 12 | indent_style = space 13 | -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | export interface Segment { 2 | old: string; 3 | type: number; 4 | val: string; 5 | } 6 | 7 | export type Route = Segment[]; 8 | 9 | export function exec(url: string, match: Route): Record; 10 | export function match(path: string, routes: Route[]): Route; 11 | export function parse(path: string): Route; 12 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "matchit", 3 | "version": "1.1.0", 4 | "repository": "lukeed/matchit", 5 | "description": "Quickly parse & match URLs", 6 | "module": "lib/matchit.mjs", 7 | "main": "lib/matchit.js", 8 | "types": "index.d.ts", 9 | "license": "MIT", 10 | "files": [ 11 | "*.d.ts", 12 | "lib" 13 | ], 14 | "author": { 15 | "name": "Luke Edwards", 16 | "email": "luke.edwards05@gmail.com", 17 | "url": "https://lukeed.com" 18 | }, 19 | "engines": { 20 | "node": ">=6" 21 | }, 22 | "scripts": { 23 | "build": "bundt", 24 | "bench": "node bench", 25 | "pretest": "npm run build", 26 | "prebench": "npm run build", 27 | "test": "tape test/*.js | tap-spec" 28 | }, 29 | "keywords": [ 30 | "route", 31 | "regexp", 32 | "routing", 33 | "pattern", 34 | "match", 35 | "parse", 36 | "url" 37 | ], 38 | "dependencies": { 39 | "@arr/every": "^1.0.0" 40 | }, 41 | "devDependencies": { 42 | "bundt": "^0.3.0", 43 | "tap-spec": "^4.1.1", 44 | "tape": "^4.6.3" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /license.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) Luke Edwards (lukeed.com) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import every from '@arr/every'; 4 | 5 | const SEP = '/'; 6 | // Types ~> static, param, any, optional 7 | const STYPE=0, PTYPE=1, ATYPE=2, OTYPE=3; 8 | // Char Codes ~> / : * ? 9 | const SLASH=47, COLON=58, ASTER=42, QMARK=63; 10 | 11 | function strip(str) { 12 | if (str === SEP) return str; 13 | (str.charCodeAt(0) === SLASH) && (str=str.substring(1)); 14 | var len = str.length - 1; 15 | return str.charCodeAt(len) === SLASH ? str.substring(0, len) : str; 16 | } 17 | 18 | function split(str) { 19 | return (str=strip(str)) === SEP ? [SEP] : str.split(SEP); 20 | } 21 | 22 | function isMatch(arr, obj, idx) { 23 | idx = arr[idx]; 24 | return (obj.val === idx && obj.type === STYPE) || (idx === SEP ? obj.type > PTYPE : obj.type !== STYPE && (idx || '').endsWith(obj.end)); 25 | } 26 | 27 | export function match(str, all) { 28 | var i=0, tmp, segs=split(str), len=segs.length, l; 29 | var fn = isMatch.bind(isMatch, segs); 30 | 31 | for (; i < all.length; i++) { 32 | tmp = all[i]; 33 | if ((l=tmp.length) === len || (l < len && tmp[l-1].type === ATYPE) || (l > len && tmp[l-1].type === OTYPE)) { 34 | if (every(tmp, fn)) return tmp; 35 | } 36 | } 37 | 38 | return []; 39 | } 40 | 41 | export function parse(str) { 42 | if (str === SEP) { 43 | return [{ old:str, type:STYPE, val:str, end:'' }]; 44 | } 45 | 46 | var c, x, t, sfx, nxt=strip(str), i=-1, j=0, len=nxt.length, out=[]; 47 | 48 | while (++i < len) { 49 | c = nxt.charCodeAt(i); 50 | 51 | if (c === COLON) { 52 | j = i + 1; // begining of param 53 | t = PTYPE; // set type 54 | x = 0; // reset mark 55 | sfx = ''; 56 | 57 | while (i < len && nxt.charCodeAt(i) !== SLASH) { 58 | c = nxt.charCodeAt(i); 59 | if (c === QMARK) { 60 | x=i; t=OTYPE; 61 | } else if (c === 46 && sfx.length === 0) { 62 | sfx = nxt.substring(x=i); 63 | } 64 | i++; // move on 65 | } 66 | 67 | out.push({ 68 | old: str, 69 | type: t, 70 | val: nxt.substring(j, x||i), 71 | end: sfx 72 | }); 73 | 74 | // shorten string & update pointers 75 | nxt=nxt.substring(i); len-=i; i=0; 76 | 77 | continue; // loop 78 | } else if (c === ASTER) { 79 | out.push({ 80 | old: str, 81 | type: ATYPE, 82 | val: nxt.substring(i), 83 | end: '' 84 | }); 85 | continue; // loop 86 | } else { 87 | j = i; 88 | while (i < len && nxt.charCodeAt(i) !== SLASH) { 89 | ++i; // skip to next slash 90 | } 91 | out.push({ 92 | old: str, 93 | type: STYPE, 94 | val: nxt.substring(j, i), 95 | end: '' 96 | }); 97 | // shorten string & update pointers 98 | nxt=nxt.substring(i); len-=i; i=j=0; 99 | } 100 | } 101 | 102 | return out; 103 | } 104 | 105 | export function exec(str, arr) { 106 | var i=0, x, y, segs=split(str), out={}; 107 | for (; i < arr.length; i++) { 108 | x=segs[i]; y=arr[i]; 109 | if (x === SEP) continue; 110 | if (x !== void 0 && y.type | 2 === OTYPE) { 111 | out[ y.val ] = x.replace(y.end, ''); 112 | } 113 | } 114 | return out; 115 | } 116 | -------------------------------------------------------------------------------- /bench/index.js: -------------------------------------------------------------------------------- 1 | const { Suite } = require('benchmark'); 2 | const pathRegex = require('path-to-regexp'); 3 | const regexparam = require('regexparam'); 4 | const curr = require('../lib/matchit'); 5 | 6 | const data = {}; 7 | const routes = ['/', '/about', 'books', '/books/:title', '/foo/*', '/bar/:baz/:bat?']; 8 | 9 | function bench(name) { 10 | console.log(`\n# ${name}`); 11 | const suite = new Suite(); 12 | suite.on('cycle', e => console.log(' ' + e.target)); 13 | return suite; 14 | } 15 | 16 | bench('Parsing') 17 | .add('matchit', _ => { 18 | data.matchit = routes.map(curr.parse); 19 | }) 20 | .add('regexparam', _ => { 21 | data.regexparam = routes.map(regexparam); 22 | }) 23 | .add('path-to-regexp', _ => { 24 | data.pregex = routes.map(x => pathRegex(x)); 25 | }) 26 | .add('path-to-regexp.parse', _ => { 27 | data.ptokens = routes.map(pathRegex.parse); 28 | }) 29 | .run(); 30 | 31 | bench('Match (index)') 32 | .add('matchit', _ => curr.match('/', data.matchit)) 33 | .add('regexparam', _ => { 34 | for (let i=0; i < data.regexparam.length; i++) { 35 | if (data.regexparam[i].pattern.test('/')) { 36 | return data.regexparam[i]; 37 | } 38 | } 39 | }) 40 | .add('path-to-regexp.exec', _ => data.pregex.filter(rgx => rgx.exec('/'))) 41 | .add('path-to-regexp.tokens', _ => { 42 | data.ptokens.map(x => pathRegex.tokensToRegExp(x)).filter(rgx => rgx.exec('/')); 43 | }) 44 | .run(); 45 | 46 | bench('Match (param)') 47 | .add('matchit', _ => curr.match('/bar/hello/world', data.matchit)) 48 | .add('regexparam', _ => { 49 | for (let i=0; i < data.regexparam.length; i++) { 50 | if (data.regexparam[i].pattern.test('/bar/hello/world')) { 51 | return data.regexparam[i]; 52 | } 53 | } 54 | }) 55 | .add('path-to-regexp.exec', _ => data.pregex.filter(rgx => rgx.exec('/bar/hello/world'))) 56 | .add('path-to-regexp.tokens', _ => { 57 | data.ptokens.map(x => pathRegex.tokensToRegExp(x)).filter(rgx => rgx.exec('/bar/hello/world')); 58 | }) 59 | .run(); 60 | 61 | bench('Match (optional)') 62 | .add('matchit', _ => curr.match('/bar/baz', data.matchit)) 63 | .add('regexparam', _ => { 64 | for (let i=0; i < data.regexparam.length; i++) { 65 | if (data.regexparam[i].pattern.test('/bar/baz')) { 66 | return data.regexparam[i]; 67 | } 68 | } 69 | }) 70 | .add('path-to-regexp.exec', _ => data.pregex.filter(rgx => rgx.exec('/bar/baz'))) 71 | .add('path-to-regexp.tokens', _ => { 72 | data.ptokens.map(x => pathRegex.tokensToRegExp(x)).filter(rgx => rgx.exec('/bar/baz')); 73 | }) 74 | .run(); 75 | 76 | bench('Match (wildcard)') 77 | .add('matchit', _ => curr.match('/foo/bar', data.matchit)) 78 | .add('regexparam', _ => { 79 | for (let i=0; i < data.regexparam.length; i++) { 80 | if (data.regexparam[i].pattern.test('/foo/bar')) { 81 | return data.regexparam[i]; 82 | } 83 | } 84 | }) 85 | .add('path-to-regexp.exec', _ => data.pregex.filter(rgx => rgx.exec('/foo/bar'))) 86 | .add('path-to-regexp.tokens', _ => { 87 | data.ptokens.map(x => pathRegex.tokensToRegExp(x)).filter(rgx => rgx.exec('/foo/bar')); 88 | }) 89 | .run(); 90 | 91 | function matchitParams(uri) { 92 | let arr = curr.match(uri, data.matchit); 93 | return curr.exec(uri, arr); 94 | } 95 | 96 | function toParam(uri) { 97 | let i=0, j=0, out={}, tmp, matches; 98 | for (; i < data.regexparam.length;) { 99 | tmp = data.regexparam[i++]; 100 | matches = tmp.pattern.exec(uri); 101 | if (matches == null) continue; 102 | while (j < tmp.keys.length) { 103 | out[ tmp.keys[j] ] = matches[++j] || null; 104 | } 105 | return out; 106 | } 107 | } 108 | 109 | function pathRegexParams(uri) { 110 | let i=0, j, tmp, tokens, obj={}; 111 | let regex = data.ptokens.map(x => pathRegex.tokensToRegExp(x)); 112 | 113 | for (; i < regex.length; i++) { 114 | tmp = regex[i].exec(uri); 115 | 116 | if (tmp && tmp.length > 0) { 117 | tokens = data.ptokens[i]; 118 | for (j=0; j < tokens.length; j++) { 119 | if (typeof tokens[j] === 'object') { 120 | obj[ tokens[j].name ] = tmp[j]; 121 | } 122 | } 123 | return obj; 124 | } 125 | } 126 | return obj; 127 | } 128 | 129 | bench('Exec') 130 | .add('matchit', _ => matchitParams('/books/foobar')) 131 | .add('regexparam', _ => toParam('/books/foobar')) 132 | .add('path-to-regexp', _ => pathRegexParams('/books/foobar')) 133 | .run(); 134 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # matchit [![Build Status](https://travis-ci.org/lukeed/matchit.svg?branch=master)](https://travis-ci.org/lukeed/matchit) 2 | 3 | > Quickly parse & match URLs 4 | 5 | ## Install 6 | 7 | ``` 8 | $ npm install --save matchit 9 | ``` 10 | 11 | 12 | ## Usage 13 | 14 | ```js 15 | const { exec, match, parse } = require('matchit'); 16 | 17 | parse('/foo/:bar/:baz?'); 18 | //=> [ 19 | //=> { old:'/foo/:bar', type:0, val:'foo' }, 20 | //=> { old:'/foo/:bar', type:1, val:'bar' }, 21 | //=> { old:'/foo/:bar', type:3, val:'baz' } 22 | //=> ] 23 | 24 | const routes = ['/', '/foo', 'bar', '/baz', '/baz/:title','/bat/*'].map(parse); 25 | 26 | match('/', routes); 27 | //=> [{ old:'/', type:0, val:'/' }] 28 | 29 | match('/foo', routes); 30 | //=> [{ old:'/foo', type:0, val:'foo' }] 31 | 32 | match('/bar', routes); 33 | //=> [{ old:'bar', type:0, val:'bar' }] 34 | 35 | match('/baz', routes); 36 | //=> [{ old:'/baz', type:0, val:'baz' }] 37 | 38 | let a = match('/baz/hello', routes); 39 | //=> [{...}, {...}] 40 | let b = exec('/baz/hello', a); 41 | //=> { title:'hello' } 42 | 43 | match('/bat/quz/qut', routes); 44 | //=> [ 45 | //=> { old:'/bat/*', type:0, val:'bat' }, 46 | //=> { old:'/bat/*', type:2, val:'*' } 47 | //=> ] 48 | ``` 49 | 50 | 51 | ## API 52 | 53 | ### matchit.parse(route) 54 | 55 | Returns: `Array` 56 | 57 | The `route` is `split` and parsed into a "definition" array of objects. Each object ("segment") contains a `val`, `type`, and `old` key: 58 | 59 | * `old` — The [`route`](#route)'s original value 60 | * `type` — An numerical representation of the segment type. 61 | * `0` - static 62 | * `1` - parameter 63 | * `2` - any/wildcard 64 | * `3` - optional param 65 | * `val` — The current segment's value. This is either a static value of the name of a parameter 66 | 67 | #### route 68 | 69 | Type: `String` 70 | 71 | A single URL pattern. 72 | 73 | > **Note:** Input will be stripped of all leading & trailing `/` characters, so there's no need to normalize your own URLs before passing it to `parse`! 74 | 75 | 76 | ### matchit.match(url, routes) 77 | 78 | Returns: `Array` 79 | 80 | Returns the [`route`](#route)'s encoded definition. See [`matchit.parse`](#matchitparseroute). 81 | 82 | #### url 83 | 84 | Type: `String` 85 | 86 | The true URL you want to be matched. 87 | 88 | #### routes 89 | 90 | Type: `Array` 91 | 92 | _All_ "parsed" route definitions, via [`matchit.parse`](#matchitparseroute). 93 | 94 | > **Important:** Multiple routes will require an Array of `matchit.parse` outputs. 95 | 96 | 97 | ### matchit.exec(url, match) 98 | 99 | Returns: `Object` 100 | 101 | Returns an object an object of `key:val` pairs, as defined by your [`route`](#route) pattern. 102 | 103 | #### url 104 | 105 | Type: `String` 106 | 107 | The URL (`pathname`) to evaluate. 108 | 109 | > **Important:** This should be `pathname`s only as any `querystring`s will be included the response. 110 | 111 | #### match 112 | 113 | Type: `Array` 114 | 115 | The route definition to use, via [`matchit.match`](#matchitmatchurl-routes). 116 | 117 | 118 | ## Benchmarks 119 | 120 | > Running Node v10.13.0 121 | 122 | ``` 123 | # Parsing 124 | matchit x 1,489,482 ops/sec ±2.89% (97 runs sampled) 125 | regexparam x 406,824 ops/sec ±1.38% (96 runs sampled) 126 | path-to-regexp x 83,439 ops/sec ±0.89% (96 runs sampled) 127 | path-to-regexp.parse x 421,266 ops/sec ±0.13% (97 runs sampled) 128 | 129 | # Match (index) 130 | matchit x 132,338,546 ops/sec ±0.14% (96 runs sampled) 131 | regexparam x 49,889,162 ops/sec ±0.21% (95 runs sampled) 132 | path-to-regexp.exec x 7,176,721 ops/sec ±1.23% (94 runs sampled) 133 | path-to-regexp.tokens x 102,021 ops/sec ±0.21% (96 runs sampled) 134 | 135 | # Match (param) 136 | matchit x 2,700,618 ops/sec ±0.92% (95 runs sampled) 137 | regexparam x 6,924,653 ops/sec ±0.33% (94 runs sampled) 138 | path-to-regexp.exec x 4,715,483 ops/sec ±0.28% (96 runs sampled) 139 | path-to-regexp.tokens x 98,182 ops/sec ±0.45% (93 runs sampled) 140 | 141 | # Match (optional) 142 | matchit x 2,816,313 ops/sec ±0.64% (93 runs sampled) 143 | regexparam x 8,437,064 ops/sec ±0.41% (93 runs sampled) 144 | path-to-regexp.exec x 5,909,510 ops/sec ±0.22% (97 runs sampled) 145 | path-to-regexp.tokens x 101,832 ops/sec ±0.43% (98 runs sampled) 146 | 147 | # Match (wildcard) 148 | matchit x 3,409,100 ops/sec ±0.34% (98 runs sampled) 149 | regexparam x 9,740,429 ops/sec ±0.49% (95 runs sampled) 150 | path-to-regexp.exec x 8,740,590 ops/sec ±0.43% (89 runs sampled) 151 | path-to-regexp.tokens x 102,109 ops/sec ±0.35% (96 runs sampled) 152 | 153 | # Exec 154 | matchit x 1,558,321 ops/sec ±0.33% (96 runs sampled) 155 | regexparam x 6,966,297 ops/sec ±0.21% (97 runs sampled) 156 | path-to-regexp x 102,250 ops/sec ±0.45% (95 runs sampled) 157 | ``` 158 | 159 | ## Related 160 | 161 | - [regexparam](https://github.com/lukeed/regexparam) - A similar (285B) utility, but relies on `RegExp` instead of String comparisons. 162 | 163 | 164 | ## License 165 | 166 | MIT © [Luke Edwards](https://lukeed.com) 167 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | const test = require('tape'); 2 | const $ = require('../lib/matchit'); 3 | 4 | const ALL = ['/', '/about', 'contact', '/books', '/books/:title', '/foo/*', 'bar/:baz/:bat?', '/videos/:title.mp4']; 5 | const PREP = ALL.map($.parse); 6 | 7 | function toMatch(t, url, idx) { 8 | const out = $.match(url, PREP); 9 | t.true(Array.isArray(out), 'returns an array'); 10 | if (idx !== -1) { 11 | t.deepEqual(out, PREP[idx], 'returns the expected definition'); 12 | } else { 13 | t.is(out.length, 0, 'returns an empty array'); 14 | } 15 | t.end(); 16 | } 17 | 18 | function isEntry(t, segs, expect) { 19 | t.true(Array.isArray(segs), '~> entry is an array of segments'); 20 | t.is(segs.length, expect.length, `~> entry has ${expect.length} segment(s)`) 21 | 22 | segs.forEach((obj, idx) => { 23 | t.is(Object.keys(obj).length, 4, '~~> segment has `old`, `type` & `val` keys'); 24 | t.is(typeof obj.type, 'number', '~~> segment.type is a number'); 25 | t.is(obj.type, expect[idx].type, '~~> segment.type returns expected value'); 26 | t.is(typeof obj.val, 'string', '~~> segment.val is a string'); 27 | t.is(obj.val, expect[idx].val, '~~> segment.val returns expected value'); 28 | t.is(typeof obj.end, 'string', '~~> segment.end is a string'); 29 | t.is(obj.end, expect[idx].end, '~~> segment.end returns expected value'); 30 | }); 31 | } 32 | 33 | function toParse(t, ins, outs) { 34 | const res = ins.map($.parse); 35 | 36 | t.true(Array.isArray(res), 'returns an array'); 37 | t.is(res.length, ins.length, `returns ${ins.length} item(s)`); 38 | 39 | res.forEach((val, idx) => { 40 | isEntry(t, val, outs[idx]); 41 | }); 42 | 43 | t.end(); 44 | } 45 | 46 | test('matchit', t => { 47 | t.is(typeof $, 'object', 'exports an object'); 48 | const keys = Object.keys($); 49 | t.is(keys.length, 3, 'exports two items'); 50 | keys.forEach(k => { 51 | t.is(typeof $[k], 'function', `exports.${k} is a function`); 52 | }); 53 | t.end(); 54 | }); 55 | 56 | test('parse empty', t => { 57 | const out = $.parse(''); 58 | t.true(Array.isArray(out), 'returns an array'); 59 | t.is(out.length, 0, 'returns an empty array'); 60 | t.end(); 61 | }); 62 | 63 | test('parse index', t => { 64 | const input = ['/']; 65 | toParse(t, input, [ 66 | [{ type:0, val:'/', end:'' }] 67 | ]); 68 | }); 69 | 70 | test('parse statics', t => { 71 | const input = ['/about', 'contact', '/foobar']; 72 | toParse(t, input, [ 73 | [{ type:0, val:'about', end:'' }], 74 | [{ type:0, val:'contact', end:'' }], 75 | [{ type:0, val:'foobar', end:'' }], 76 | ]); 77 | }); 78 | 79 | test('parse params', t => { 80 | const input = ['/:foo', 'books/:title', '/foo/:bar']; 81 | toParse(t, input, [ 82 | [{ type:1, val:'foo', end:'' }], 83 | [{ type:0, val:'books', end:'' }, { type:1, val:'title', end:'' }], 84 | [{ type:0, val:'foo', end:'' }, { type:1, val:'bar', end:'' }] 85 | ]); 86 | }); 87 | 88 | test('parse params (suffix)', t => { 89 | const input = ['/:foo.bar', 'books/:title.jpg', '/foo/:bar.html']; 90 | toParse(t, input, [ 91 | [{ type:1, val:'foo', end:'.bar' }], 92 | [{ type:0, val:'books', end:'' }, { type:1, val:'title', end:'.jpg' }], 93 | [{ type:0, val:'foo', end:'' }, { type:1, val:'bar', end:'.html' }] 94 | ]); 95 | }); 96 | 97 | test('parse params (multiple)', t => { 98 | const input = ['/foo/:bar/:baz', '/foo/bar/:baz', '/foo/bar/:baz/:bat']; 99 | toParse(t, input, [ 100 | [{ type:0, val:'foo', end:'' }, { type:1, val:'bar', end:'' }, { type:1, val:'baz', end:'' }], 101 | [{ type:0, val:'foo', end:'' }, { type:0, val:'bar', end:'' }, { type:1, val:'baz', end:'' }], 102 | [{ type:0, val:'foo', end:'' }, { type:0, val:'bar', end:'' }, { type:1, val:'baz', end:'' }, { type:1, val:'bat', end:'' }] 103 | ]); 104 | }); 105 | 106 | test('parse params (optional)', t => { 107 | const input = ['/:foo?', 'foo/:bar?', '/foo/:bar?/:baz?']; 108 | toParse(t, input, [ 109 | [{ type:3, val:'foo', end:'' }], 110 | [{ type:0, val:'foo', end:'' }, { type:3, val:'bar', end:'' }], 111 | [{ type:0, val:'foo', end:'' }, { type:3, val:'bar', end:'' }, { type:3, val:'baz', end:'' }], 112 | ]); 113 | }); 114 | 115 | test('parse wilds', t => { 116 | const input = ['*', '/*', 'foo/*', 'foo/bar/*']; 117 | toParse(t, input, [ 118 | [{ type:2, val:'*', end:'' }], 119 | [{ type:2, val:'*', end:'' }], 120 | [{ type:0, val:'foo', end:'' }, { type:2, val:'*', end:'' }], 121 | [{ type:0, val:'foo', end:'' }, { type:0, val:'bar', end:'' }, { type:2, val:'*', end:'' }] 122 | ]); 123 | }); 124 | 125 | 126 | test('match index', t => { 127 | toMatch(t, '/', 0); 128 | }); 129 | 130 | test('match static (exact)', t => { 131 | toMatch(t, '/about', 1); 132 | }); 133 | 134 | test('match static (exact, no-slash)', t => { 135 | toMatch(t, 'contact', 2); 136 | }); 137 | 138 | test('match static (bare-vs-slash)', t => { 139 | toMatch(t, 'about', 1); 140 | }); 141 | 142 | test('match static (slash-vs-bare)', t => { 143 | toMatch(t, '/contact', 2); 144 | }); 145 | 146 | test('match static (trailing slash)', t => { 147 | toMatch(t, '/books/', 3); 148 | }); 149 | 150 | test('match params (single)', t => { 151 | toMatch(t, '/books/foobar', 4); 152 | }); 153 | 154 | test('match params (no match, long)', t => { 155 | toMatch(t, '/books/foo/bar', -1); 156 | }); 157 | 158 | test('match params (no match, base)', t => { 159 | toMatch(t, '/hello/world', -1); 160 | }); 161 | 162 | test('match params (root index-vs-param)', t => { 163 | let foo = $.match('/', [$.parse('/')]); 164 | t.same(foo[0], { old:'/', type:0, val:'/', end:'' }, 'matches root-index route with index-static pattern'); 165 | 166 | let bar = $.match('/', [$.parse('/:title')]); 167 | t.is(bar[0], undefined, 'does not match root-index route with param-pattern'); 168 | 169 | let baz = $.match('/narnia', [$.parse('/:title')]); 170 | t.same(baz[0], { old:'/:title', type:1, val:'title', end:'' }, 'matches param-based route with param-pattern'); 171 | 172 | let bat = $.match('/', [$.parse('/:title?')]); 173 | t.same(bat[0], { old:'/:title?', type:3, val:'title', end:'' }, 'matches root-index route with optional-param pattern'); 174 | 175 | let quz = $.match('/', [$.parse('*')]); 176 | t.same(quz[0], { old:'*', type:2, val:'*', end:'' }, 'matches root-index route with root-wilcard pattern'); 177 | 178 | let qut = $.match('/', ['/x', '*'].map($.parse)); 179 | t.same(qut[0], { old:'*', type:2, val:'*', end:'' }, 'matches root-index with wildcard pattern'); 180 | 181 | let qar = $.match('/', ['*', '/x'].map($.parse)); 182 | t.same(qar[0], { old:'*', type:2, val:'*', end:'' }, 'matches root-index with wildcard pattern (reorder)'); 183 | 184 | t.end(); 185 | }); 186 | 187 | test('match params (index-vs-param)', t => { 188 | let foo = $.match('/books', [$.parse('/books/:title')]); 189 | t.same(foo, [], 'does not match index route with param-pattern'); 190 | let bar = $.match('/books/123', [$.parse('/books')]); 191 | t.same(bar, [], 'does not match param-based route with index-pattern'); 192 | t.end(); 193 | }); 194 | 195 | test('match params (suffix)', t => { 196 | toMatch(t, '/videos/buckbunny.mp4', 7); 197 | }); 198 | 199 | test('match params (suffix, nomatch)', t => { 200 | toMatch(t, '/videos/buckbunny', -1); 201 | }); 202 | 203 | // test('match params (suffix, nomatch)', t => { 204 | // let foo = $.match('/', [$.parse('/')]); 205 | // t.same(foo[0], { old:'/', type:0, val:'/', end:'' }, 'matches root-index route with index-static pattern'); 206 | // }); 207 | 208 | test('match params (optional)', t => { 209 | toMatch(t, '/bar/hello', 6); 210 | }); 211 | 212 | test('match params (optional, all)', t => { 213 | toMatch(t, '/bar/hello/world', 6); 214 | }); 215 | 216 | test('match params (querystring)', t => { 217 | toMatch(t, '/books/narnia?author=lukeed', 4); 218 | }); 219 | 220 | test('match wildcard (simple)', t => { 221 | toMatch(t, '/foo/bar', 5); 222 | }); 223 | 224 | test('match wildcard (multi-level)', t => { 225 | toMatch(t, '/foo/bar/baz', 5); 226 | }); 227 | 228 | 229 | 230 | test('exec index', t => { 231 | const arr = $.match('/', PREP); 232 | const out = $.exec('/', arr); 233 | t.is(typeof out, 'object', 'returns an object'); 234 | t.is(Object.keys(out).length, 0, 'returns an empty object'); 235 | t.end(); 236 | }); 237 | 238 | test('exec index (optional)', t => { 239 | const arr = $.parse('/:type?'); 240 | const foo = $.exec('/', arr); 241 | const bar = $.exec('/news', arr); 242 | 243 | t.is(typeof foo, 'object', 'returns an object'); 244 | t.same(foo, {}, 'returns empty object (no params)'); 245 | 246 | t.is(typeof bar, 'object', 'returns an object'); 247 | const keys = Object.keys(bar); 248 | t.is(keys.length, 1, 'returns object with 1 key'); 249 | t.is(keys[0], 'type', '~> contains `type` key'); 250 | t.is(bar.type, 'news', '~> adds `key:val` pair'); 251 | 252 | t.end(); 253 | }); 254 | 255 | test('exec statics', t => { 256 | const foo = $.match('/about', PREP); 257 | const bar = $.exec('/about', foo); 258 | t.is(typeof bar, 'object', 'returns an object'); 259 | t.is(Object.keys(bar).length, 0, 'returns an empty object'); 260 | 261 | const baz = $.match('/contact', PREP); 262 | const bat = $.exec('/contact', baz); 263 | t.is(typeof bat, 'object', 'returns an object'); 264 | t.is(Object.keys(bat).length, 0, 'returns an empty object'); 265 | t.end(); 266 | }); 267 | 268 | test('exec params', t => { 269 | const arr = $.match('/books/foo', PREP); 270 | const out = $.exec('/books/foo', arr); 271 | t.is(typeof out, 'object', 'returns an object'); 272 | const keys = Object.keys(out); 273 | t.is(keys.length, 1, 'returns object with 1 key'); 274 | t.is(keys[0], 'title', '~> contains `title` key'); 275 | t.is(out.title, 'foo', '~> adds `key:val` pair'); 276 | t.end(); 277 | }); 278 | 279 | test('exec params (suffix)', t => { 280 | const arr = $.match('/videos/foo.mp4', PREP); 281 | const out = $.exec('/videos/foo.mp4', arr); 282 | t.is(typeof out, 'object', 'returns an object'); 283 | const keys = Object.keys(out); 284 | t.is(keys.length, 1, 'returns object with 1 key'); 285 | t.is(keys[0], 'title', '~> contains `title` key'); 286 | t.is(out.title, 'foo', '~> adds `key:val` pair'); 287 | t.end(); 288 | }); 289 | 290 | test('exec params (multiple)', t => { 291 | const foo = $.parse('/foo/:bar/:baz'); 292 | const out = $.exec('/foo/hello/world', foo); 293 | t.is(typeof out, 'object', 'returns an object'); 294 | const keys = Object.keys(out); 295 | t.is(keys.length, 2, 'returns object with 2 keys'); 296 | t.deepEqual(keys, ['bar', 'baz'], '~> contains `bar` & `baz` keys'); 297 | t.is(out.bar, 'hello', '~> adds `key:val` pair'); 298 | t.is(out.baz, 'world', '~> adds `key:val` pair'); 299 | t.end(); 300 | }); 301 | 302 | test('exec params (optional)', t => { 303 | const out = $.exec('/bar/hello', PREP[6]); 304 | t.is(typeof out, 'object', 'returns an object'); 305 | const keys = Object.keys(out); 306 | t.is(keys.length, 1, 'returns object with 2 keys'); 307 | t.is(keys[0], 'baz', '~> contains `baz` key'); 308 | t.is(out.baz, 'hello', '~> adds `key:val` pair'); 309 | t.end(); 310 | }); 311 | 312 | test('exec params (optional, all)', t => { 313 | const out = $.exec('/bar/hello/world', PREP[6]); 314 | t.is(typeof out, 'object', 'returns an object'); 315 | const keys = Object.keys(out); 316 | t.is(keys.length, 2, 'returns object with 2 keys'); 317 | t.deepEqual(keys, ['baz', 'bat'], '~> contains `baz` & `bat` keys'); 318 | t.is(out.baz, 'hello', '~> adds `key:val` pair'); 319 | t.is(out.bat, 'world', '~> adds `key:val` pair'); 320 | t.end(); 321 | }); 322 | 323 | test('exec params (querystring)', t => { 324 | const url = '/books/foo?author=lukeed'; 325 | const arr = $.match(url, PREP); 326 | const out = $.exec(url, arr); 327 | t.is(typeof out, 'object', 'returns an object'); 328 | const keys = Object.keys(out); 329 | t.is(keys.length, 1, 'returns object with 1 key'); 330 | t.is(keys[0], 'title', '~> contains `title` key'); 331 | t.is(out.title, 'foo?author=lukeed', 'does NOT separate querystring from path'); 332 | t.end(); 333 | }); 334 | 335 | test('exec empty (no match)', t => { 336 | const out = $.exec('foo', PREP[0]); 337 | t.is(typeof out, 'object', 'returns an object'); 338 | t.is(Object.keys(out).length, 0, 'returns an empty object'); 339 | t.end(); 340 | }); 341 | --------------------------------------------------------------------------------