├── .editorconfig ├── .github ├── FUNDING.yml └── workflows │ └── ci.yml ├── .gitignore ├── index.d.ts ├── license ├── package.json ├── readme.md ├── src └── index.js └── test ├── inject.js └── parse.js /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_size = 2 6 | indent_style = tab 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.{json,yml,md}] 13 | indent_style = space 14 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: lukeed 2 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | test: 7 | name: Node.js v${{ matrix.nodejs }} 8 | runs-on: ubuntu-latest 9 | strategy: 10 | matrix: 11 | nodejs: [8, 10, 12, 14, 16] 12 | steps: 13 | - uses: actions/checkout@v2 14 | - uses: actions/setup-node@v2 15 | with: 16 | node-version: ${{ matrix.nodejs }} 17 | 18 | - name: (cache) restore 19 | uses: actions/cache@master 20 | with: 21 | path: node_modules 22 | key: ${{ runner.os }}-${{ hashFiles('**/package.json') }} 23 | 24 | - name: Install 25 | run: npm install 26 | 27 | - name: (coverage) Install 28 | if: matrix.nodejs >= 14 29 | run: npm install -g c8 30 | 31 | - name: Test 32 | if: matrix.nodejs < 14 33 | run: npm test 34 | 35 | - name: (coverage) Test 36 | if: matrix.nodejs >= 14 37 | run: c8 --include=src npm test 38 | 39 | - name: (coverage) Report 40 | if: matrix.nodejs >= 14 41 | run: | 42 | c8 report --reporter=text-lcov > coverage.lcov 43 | bash <(curl -s https://codecov.io/bash) 44 | env: 45 | CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} 46 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | *-lock.* 4 | *.lock 5 | *.log 6 | 7 | /dist 8 | -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | export function parse(route: string, loose?: boolean): { 2 | keys: string[]; 3 | pattern: RegExp; 4 | } 5 | 6 | export function parse(route: RegExp): { 7 | keys: false; 8 | pattern: RegExp; 9 | } 10 | 11 | export type RouteParams = 12 | T extends `${infer Prev}/*/${infer Rest}` 13 | ? RouteParams & { wild: string } & RouteParams 14 | : T extends `${string}:${infer P}?/${infer Rest}` 15 | ? { [K in P]?: string } & RouteParams 16 | : T extends `${string}:${infer P}/${infer Rest}` 17 | ? { [K in P]: string } & RouteParams 18 | : T extends `${string}:${infer P}?` 19 | ? { [K in P]?: string } 20 | : T extends `${string}:${infer P}` 21 | ? { [K in P]: string } 22 | : T extends `${string}*` 23 | ? { "*": string } 24 | : T extends `${string}*?` 25 | ? { "*"?: string } 26 | : {}; 27 | 28 | export function inject(route: T, values: RouteParams): string; 29 | -------------------------------------------------------------------------------- /license: -------------------------------------------------------------------------------- 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 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "regexparam", 3 | "version": "3.0.0", 4 | "repository": "lukeed/regexparam", 5 | "description": "A tiny (399B) utility that converts route patterns into RegExp. Limited alternative to `path-to-regexp` 🙇‍", 6 | "unpkg": "dist/index.min.js", 7 | "module": "dist/index.mjs", 8 | "main": "dist/index.js", 9 | "types": "index.d.ts", 10 | "license": "MIT", 11 | "author": { 12 | "name": "Luke Edwards", 13 | "email": "luke.edwards05@gmail.com", 14 | "url": "https://lukeed.com" 15 | }, 16 | "engines": { 17 | "node": ">=8" 18 | }, 19 | "scripts": { 20 | "build": "bundt", 21 | "test": "uvu -r esm test" 22 | }, 23 | "exports": { 24 | ".": { 25 | "types": "./index.d.ts", 26 | "import": "./dist/index.mjs", 27 | "require": "./dist/index.js", 28 | "default": "./dist/index.js" 29 | }, 30 | "./package.json": "./package.json" 31 | }, 32 | "files": [ 33 | "*.d.ts", 34 | "dist" 35 | ], 36 | "keywords": [ 37 | "regexp", 38 | "route", 39 | "routing", 40 | "inject", 41 | "parse" 42 | ], 43 | "devDependencies": { 44 | "bundt": "1.1.2", 45 | "esm": "3.2.25", 46 | "uvu": "0.5.1" 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # regexparam [![CI](https://github.com/lukeed/regexparam/actions/workflows/ci.yml/badge.svg)](https://github.com/lukeed/regexparam/actions/workflows/ci.yml) 2 | 3 | > A tiny (399B) utility that converts route patterns into RegExp. Limited alternative to [`path-to-regexp`](https://github.com/pillarjs/path-to-regexp) 🙇 4 | 5 | With `regexparam`, you may turn a pathing string (eg, `/users/:id`) into a regular expression. 6 | 7 | An object with shape of `{ keys, pattern }` is returned, where `pattern` is the `RegExp` and `keys` is an array of your parameter name(s) in the order that they appeared. 8 | 9 | Unlike [`path-to-regexp`](https://github.com/pillarjs/path-to-regexp), this module does not create a `keys` dictionary, nor mutate an existing variable. Also, this only ships a parser, which only accept strings. Similarly, and most importantly, `regexparam` **only** handles basic pathing operators: 10 | 11 | * Static (`/foo`, `/foo/bar`) 12 | * Parameter (`/:title`, `/books/:title`, `/books/:genre/:title`) 13 | * Parameter w/ Suffix (`/movies/:title.mp4`, `/movies/:title.(mp4|mov)`) 14 | * Optional Parameters (`/:title?`, `/books/:title?`, `/books/:genre/:title?`) 15 | * Wildcards (`*`, `/books/*`, `/books/:genre/*`) 16 | * Optional Wildcard (`/books/*?`) 17 | 18 | This module exposes three module definitions: 19 | 20 | * **CommonJS**: [`dist/index.js`](https://unpkg.com/regexparam/dist/index.js) 21 | * **ESModule**: [`dist/index.mjs`](https://unpkg.com/regexparam/dist/index.mjs) 22 | * **UMD**: [`dist/index.min.js`](https://unpkg.com/regexparam/dist/index.min.js) 23 | 24 | ## Install 25 | 26 | ``` 27 | $ npm install --save regexparam 28 | ``` 29 | 30 | 31 | ## Usage 32 | 33 | ```js 34 | import { parse, inject } from 'regexparam'; 35 | 36 | // Example param-assignment 37 | function exec(path, result) { 38 | let i=0, out={}; 39 | let matches = result.pattern.exec(path); 40 | while (i < result.keys.length) { 41 | out[ result.keys[i] ] = matches[++i] || null; 42 | } 43 | return out; 44 | } 45 | 46 | 47 | // Parameter, with Optional Parameter 48 | // --- 49 | let foo = parse('/books/:genre/:title?') 50 | // foo.pattern => /^\/books\/([^\/]+?)(?:\/([^\/]+?))?\/?$/i 51 | // foo.keys => ['genre', 'title'] 52 | 53 | foo.pattern.test('/books/horror'); //=> true 54 | foo.pattern.test('/books/horror/goosebumps'); //=> true 55 | 56 | exec('/books/horror', foo); 57 | //=> { genre: 'horror', title: null } 58 | 59 | exec('/books/horror/goosebumps', foo); 60 | //=> { genre: 'horror', title: 'goosebumps' } 61 | 62 | 63 | // Parameter, with suffix 64 | // --- 65 | let bar = parse('/movies/:title.(mp4|mov)'); 66 | // bar.pattern => /^\/movies\/([^\/]+?)\.(mp4|mov)\/?$/i 67 | // bar.keys => ['title'] 68 | 69 | bar.pattern.test('/movies/narnia'); //=> false 70 | bar.pattern.test('/movies/narnia.mp3'); //=> false 71 | bar.pattern.test('/movies/narnia.mp4'); //=> true 72 | 73 | exec('/movies/narnia.mp4', bar); 74 | //=> { title: 'narnia' } 75 | 76 | 77 | // Wildcard 78 | // --- 79 | let baz = parse('users/*'); 80 | // baz.pattern => /^\/users\/(.*)\/?$/i 81 | // baz.keys => ['*'] 82 | 83 | baz.pattern.test('/users'); //=> false 84 | baz.pattern.test('/users/lukeed'); //=> true 85 | baz.pattern.test('/users/'); //=> true 86 | 87 | 88 | // Optional Wildcard 89 | // --- 90 | let baz = parse('/users/*?'); 91 | // baz.pattern => /^\/users(?:\/(.*))?(?=$|\/)/i 92 | // baz.keys => ['*'] 93 | 94 | baz.pattern.test('/users'); //=> true 95 | baz.pattern.test('/users/lukeed'); //=> true 96 | baz.pattern.test('/users/'); //=> true 97 | 98 | 99 | // Injecting 100 | // --- 101 | 102 | inject('/users/:id', { 103 | id: 'lukeed' 104 | }); //=> '/users/lukeed' 105 | 106 | inject('/movies/:title.mp4', { 107 | title: 'narnia' 108 | }); //=> '/movies/narnia.mp4' 109 | 110 | inject('/:foo/:bar?/:baz?', { 111 | foo: 'aaa' 112 | }); //=> '/aaa' 113 | 114 | inject('/:foo/:bar?/:baz?', { 115 | foo: 'aaa', 116 | baz: 'ccc' 117 | }); //=> '/aaa/ccc' 118 | 119 | inject('/posts/:slug/*', { 120 | slug: 'hello', 121 | }); //=> '/posts/hello' 122 | 123 | inject('/posts/:slug/*', { 124 | slug: 'hello', 125 | '*': 'x/y/z', 126 | }); //=> '/posts/hello/x/y/z' 127 | 128 | // Missing non-optional value 129 | // ~> keeps the pattern in output 130 | inject('/hello/:world', { 131 | abc: 123 132 | }); //=> '/hello/:world' 133 | ``` 134 | 135 | > **Important:** When matching/testing against a generated RegExp, your path **must** begin with a leading slash (`"/"`)! 136 | 137 | ## Regular Expressions 138 | 139 | For fine-tuned control, you may pass a `RegExp` value directly to `regexparam` as its only parameter. 140 | 141 | In these situations, `regexparam` **does not** parse nor manipulate your pattern in any way! Because of this, `regexparam` has no "insight" on your route, and instead trusts your input fully. In code, this means that the return value's `keys` is always equal to `false` and the `pattern` is identical to your input value. 142 | 143 | This also means that you must manage and parse your own `keys`~!
144 | You may use [named capture groups](https://javascript.info/regexp-groups#named-groups) or traverse the matched segments manually the "old-fashioned" way: 145 | 146 | > **Important:** Please check your target browsers' and target [Node.js runtimes' support](https://node.green/#ES2018-features--RegExp-named-capture-groups)! 147 | 148 | ```js 149 | // Named capture group 150 | const named = regexparam.parse(/^\/posts[/](?[0-9]{4})[/](?[0-9]{2})[/](?[^\/]+)/i); 151 | const { groups } = named.pattern.exec('/posts/2019/05/hello-world'); 152 | console.log(groups); 153 | //=> { year: '2019', month: '05', title: 'hello-world' } 154 | 155 | // Widely supported / "Old-fashioned" 156 | const named = regexparam.parse(/^\/posts[/]([0-9]{4})[/]([0-9]{2})[/]([^\/]+)/i); 157 | const [url, year, month, title] = named.pattern.exec('/posts/2019/05/hello-world'); 158 | console.log(year, month, title); 159 | //=> 2019 05 hello-world 160 | ``` 161 | 162 | 163 | ## API 164 | 165 | ### regexparam.parse(input: RegExp) 166 | ### regexparam.parse(input: string, loose?: boolean) 167 | Returns: `Object` 168 | 169 | Parse a route pattern into an equivalent RegExp pattern. Also collects the names of pattern's parameters as a `keys` array. An `input` that's already a RegExp is kept as is, and `regexparam` makes no additional insights. 170 | 171 | Returns a `{ keys, pattern }` object, where `pattern` is always a `RegExp` instance and `keys` is either `false` or a list of extracted parameter names. 172 | 173 | > **Important:** The `keys` will _always_ be `false` when `input` is a RegExp and it will _always_ be an Array when `input` is a string. 174 | 175 | #### input 176 | Type: `string` or `RegExp` 177 | 178 | When `input` is a string, it's treated as a route pattern and an equivalent RegExp is generated. 179 | 180 | > **Note:** It does not matter if `input` strings begin with a `/` — it will be added if missing. 181 | 182 | When `input` is a RegExp, it will be used **as is** – no modifications will be made. 183 | 184 | #### loose 185 | Type: `boolean`<br> 186 | Default: `false` 187 | 188 | Should the `RegExp` match URLs that are longer than the [`str`](#str) pattern itself?<br> 189 | By default, the generated `RegExp` will test that the URL begins and _ends with_ the pattern. 190 | 191 | > **Important:** When `input` is a RegExp, the `loose` argument is ignored! 192 | 193 | ```js 194 | const { parse } = require('regexparam'); 195 | 196 | parse('/users').pattern.test('/users/lukeed'); //=> false 197 | parse('/users', true).pattern.test('/users/lukeed'); //=> true 198 | 199 | parse('/users/:name').pattern.test('/users/lukeed/repos'); //=> false 200 | parse('/users/:name', true).pattern.test('/users/lukeed/repos'); //=> true 201 | ``` 202 | 203 | 204 | ### regexparam.inject(pattern: string, values: object) 205 | Returns: `string` 206 | 207 | Returns a new string by replacing the `pattern` segments/parameters with their matching values. 208 | 209 | > **Important:** Named segments (eg, `/:name`) that _do not_ have a `values` match will be kept in the output. This is true _except for_ optional segments (eg, `/:name?`) and wildcard segments (eg, `/*`). 210 | 211 | #### pattern 212 | Type: `string` 213 | 214 | The route pattern that to receive injections. 215 | 216 | #### values 217 | Type: `Record<string, string>` 218 | 219 | The values to be injected. The keys within `values` must match the `pattern`'s segments in order to be replaced. 220 | 221 | > **Note:** To replace a wildcard segment (eg, `/*`), define a `values['*']` key. 222 | 223 | 224 | ## Deno 225 | 226 | As of version `1.3.0`, you may use `regexparam` with Deno. These options are all valid: 227 | 228 | ```ts 229 | // The official Deno registry: 230 | import regexparam from 'https://deno.land/x/regexparam/src/index.js'; 231 | // Third-party CDNs with ESM support: 232 | import regexparam from 'https://cdn.skypack.dev/regexparam'; 233 | import regexparam from 'https://esm.sh/regexparam'; 234 | ``` 235 | 236 | > **Note:** All registries support versioned URLs, if desired. <br>The above examples always resolve to the latest published version. 237 | 238 | 239 | ## Related 240 | 241 | - [trouter](https://github.com/lukeed/trouter) - A server-side HTTP router that extends from this module. 242 | - [matchit](https://github.com/lukeed/matchit) - Similar (650B) library, but relies on String comparison instead of `RegExp`s. 243 | 244 | 245 | ## License 246 | 247 | MIT © [Luke Edwards](https://lukeed.com) 248 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @param {string|RegExp} input The route pattern 3 | * @param {boolean} [loose] Allow open-ended matching. Ignored with `RegExp` input. 4 | */ 5 | export function parse(input, loose) { 6 | if (input instanceof RegExp) return { keys:false, pattern:input }; 7 | var c, o, tmp, ext, keys=[], pattern='', arr = input.split('/'); 8 | arr[0] || arr.shift(); 9 | 10 | while (tmp = arr.shift()) { 11 | c = tmp[0]; 12 | if (c === '*') { 13 | keys.push(c); 14 | pattern += tmp[1] === '?' ? '(?:/(.*))?' : '/(.*)'; 15 | } else if (c === ':') { 16 | o = tmp.indexOf('?', 1); 17 | ext = tmp.indexOf('.', 1); 18 | keys.push( tmp.substring(1, !!~o ? o : !!~ext ? ext : tmp.length) ); 19 | pattern += !!~o && !~ext ? '(?:/([^/]+?))?' : '/([^/]+?)'; 20 | if (!!~ext) pattern += (!!~o ? '?' : '') + '\\' + tmp.substring(ext); 21 | } else { 22 | pattern += '/' + tmp; 23 | } 24 | } 25 | 26 | return { 27 | keys: keys, 28 | pattern: new RegExp('^' + pattern + (loose ? '(?=$|\/)' : '\/?$'), 'i') 29 | }; 30 | } 31 | 32 | var RGX = /(\/|^)([:*][^/]*?)(\?)?(?=[/.]|$)/g; 33 | 34 | // error if key missing? 35 | export function inject(route, values) { 36 | return route.replace(RGX, (x, lead, key, optional) => { 37 | x = values[key=='*' ? key : key.substring(1)]; 38 | return x ? '/'+x : (optional || key=='*') ? '' : '/' + key; 39 | }); 40 | } 41 | -------------------------------------------------------------------------------- /test/inject.js: -------------------------------------------------------------------------------- 1 | import { test } from 'uvu'; 2 | import * as assert from 'uvu/assert'; 3 | import { inject } from '../src'; 4 | 5 | /** 6 | * @param {string} pattern 7 | * @param {string} expected 8 | * @param {Record<string, any>} values 9 | */ 10 | function run(pattern, expected, values) { 11 | let keys = Object.keys(values); 12 | test(`inject "${pattern}" with [${keys}]`, () => { 13 | let output = inject(pattern, values); 14 | assert.type(output, 'string'); 15 | assert.is(output, expected); 16 | }); 17 | } 18 | 19 | test('exports', () => { 20 | assert.type(inject, 'function'); 21 | }); 22 | 23 | test('returns', () => { 24 | let output = inject('/', {}); 25 | assert.type(output, 'string'); 26 | }); 27 | 28 | test('throws', () => { 29 | try { 30 | inject('/:foo/:bar'); 31 | assert.unreachable('should throw'); 32 | } catch (err) { 33 | assert.instance(err, TypeError); 34 | assert.match(err.message, /Cannot read propert(ies|y 'foo') of undefined/); 35 | } 36 | }); 37 | 38 | // Test Outputs 39 | // --- 40 | 41 | run('/foo/:id', '/foo/123', { id: 123 }); 42 | run('/foo/:id/', '/foo/123/', { id: 123 }); 43 | 44 | run('/:a/:b/:c', '/1/2/3', { a: 1, b: 2, c: 3 }); 45 | run('/:a/:b/:c/', '/1/2/3/', { a: 1, b: 2, c: 3 }); 46 | 47 | run('/assets/:video.mp4', '/assets/demo.mp4', { video: 'demo' }); 48 | run('/assets/:video.mp4/extra', '/assets/demo.mp4/extra', { video: 'demo' }); 49 | run('/assets/:video.mp4?foo=bar', '/assets/demo.mp4?foo=bar', { video: 'demo' }); 50 | run('/assets/:video/.hidden', '/assets/demo/.hidden', { video: 'demo' }); 51 | 52 | run('/foo/:id/:bar?', '/foo/123', { id: 123 }); 53 | run('/foo/:id/:bar?/', '/foo/123/', { id: 123 }); 54 | 55 | run('/foo/:id/:bar?', '/foo/123/xxx', { id: 123, bar: 'xxx' }); 56 | run('/foo/:id/:bar?/', '/foo/123/xxx/', { id: 123, bar: 'xxx' }); 57 | 58 | run('/foo/:id/:bar?/extra', '/foo/123/extra', { id: 123 }); 59 | run('/foo/:id/:bar?/extra', '/foo/123/xxx/extra', { id: 123, bar: 'xxx' }); 60 | 61 | run('/foo/:id/:a?/:b?/:bar?', '/foo/123', { id: 123 }); 62 | run('/foo/:id/:a?/:b?/:bar?', '/foo/123/bb', { id: 123, b: 'bb' }); 63 | run('/foo/:id/:a?/:b?/:bar?', '/foo/123/xxx', { id: 123, bar: 'xxx' }); 64 | run('/foo/:id/:a?/:b?/:bar?', '/foo/123/aa/xxx', { id: 123, a: 'aa', bar: 'xxx' }); 65 | 66 | run('/foo/:bar/*', '/foo/123', { bar: '123' }); 67 | run('/foo/:bar/*?', '/foo/123', { bar: '123' }); 68 | 69 | run('/foo/:bar/*', '/foo/123/aa/bb/cc', { bar: '123', '*': 'aa/bb/cc' }); 70 | run('/foo/:bar/*?', '/foo/123/aa/bb/cc', { bar: '123', '*': 'aa/bb/cc' }); 71 | 72 | // NOTE: Missing non-optional values 73 | // --- 74 | run('/foo/:id', '/foo/:id', { /* empty */ }); 75 | run('/foo/:id/', '/foo/:id/', { /* empty */ }); 76 | 77 | run('/:a/:b/:c', '/1/:b/:c', { a: 1 }); 78 | run('/:a/:b/:c', '/1/:b/3', { a: 1, c: 3 }); 79 | 80 | test.run(); 81 | -------------------------------------------------------------------------------- /test/parse.js: -------------------------------------------------------------------------------- 1 | import { test } from 'uvu'; 2 | import * as assert from 'uvu/assert'; 3 | import { parse } from '../src'; 4 | 5 | const hasNamedGroups = 'groups' in /x/.exec('x'); 6 | 7 | function run(route, url, loose) { 8 | let i=0, out={}, result=parse(route, !!loose); 9 | let matches = result.pattern.exec(url); 10 | if (matches === null) return false; 11 | if (matches.groups) return matches.groups; 12 | while (i < result.keys.length) { 13 | out[ result.keys[i] ] = matches[++i] || null; 14 | } 15 | return out; 16 | } 17 | 18 | function raw(route, url, loose) { 19 | return parse(route, !!loose).pattern.exec(url); 20 | } 21 | 22 | function toExec(route, url, params) { 23 | let out = run(route, url); 24 | if (out && params) out = { ...out }; // convert null proto 25 | assert.equal(out, params, out ? `~> parsed "${url}" into correct params` : `~> route and "${url}" did not match`); 26 | } 27 | 28 | function toLooseExec(route, url, params) { 29 | let out = run(route, url, true); 30 | if (out && params) out = { ...out }; // convert null proto 31 | assert.equal(out, params, out ? `~> parsed "${url}" into correct params` : `~> route and "${url}" did not match`); 32 | } 33 | 34 | // --- 35 | 36 | test('exports', () => { 37 | assert.type(parse, 'function'); 38 | }); 39 | 40 | test('returns :: string', () => { 41 | let output = parse('/'); 42 | assert.type(output, 'object'); 43 | assert.instance(output.pattern, RegExp); 44 | assert.instance(output.keys, Array); 45 | }); 46 | 47 | test('returns :: RegExp', () => { 48 | let pattern = /foobar/; 49 | let output = parse(pattern); 50 | assert.type(output, 'object'); 51 | assert.is(output.pattern, pattern, 'referential'); 52 | assert.is(output.keys, false); 53 | }); 54 | 55 | test('ensure lead slash', () => { 56 | assert.equal(parse('/'), parse(''), '~> root'); 57 | assert.equal(parse('/books'), parse('books'), '~> static'); 58 | assert.equal(parse('/books/:title'), parse('books/:title'), '~> param'); 59 | assert.equal(parse('/books/:title?'), parse('books/:title?'), '~> optional'); 60 | assert.equal(parse('/books/*'), parse('books/*'), '~> wildcard'); 61 | }); 62 | 63 | test('static', () => { 64 | let { keys, pattern } = parse('/books'); 65 | assert.equal(keys, [], '~> empty keys'); 66 | assert.ok(pattern.test('/books'), '~> matches route'); 67 | assert.ok(pattern.test('/books/'), '~> matches trailing slash'); 68 | assert.not.ok(pattern.test('/books/author'), '~> does not match extra bits'); 69 | assert.not.ok(pattern.test('books'), '~> does not match path without lead slash'); 70 | }); 71 | 72 | test('static :: multiple', () => { 73 | let { keys, pattern } = parse('/foo/bar'); 74 | assert.equal(keys, [], '~> empty keys'); 75 | assert.ok(pattern.test('/foo/bar'), '~> matches route'); 76 | assert.ok(pattern.test('/foo/bar/'), '~> matches trailing slash'); 77 | assert.not.ok(pattern.test('/foo/bar/baz'), '~> does not match extra bits'); 78 | assert.not.ok(pattern.test('foo/bar'), '~> does not match path without lead slash'); 79 | }); 80 | 81 | test('param', () => { 82 | let { keys, pattern } = parse('/books/:title'); 83 | assert.equal(keys, ['title'], '~> keys has "title" value'); 84 | assert.not.ok(pattern.test('/books'), '~> does not match naked base'); 85 | assert.not.ok(pattern.test('/books/'), '~> does not match naked base w/ trailing slash'); 86 | assert.ok(pattern.test('/books/narnia'), '~> matches definition'); 87 | assert.ok(pattern.test('/books/narnia/'), '~> matches definition w/ trailing slash'); 88 | assert.not.ok(pattern.test('/books/narnia/hello'), '~> does not match extra bits'); 89 | assert.not.ok(pattern.test('books/narnia'), '~> does not match path without lead slash'); 90 | let [url, value] = pattern.exec('/books/narnia'); 91 | assert.is(url, '/books/narnia', '~> executing pattern on correct trimming'); 92 | assert.is(value, 'narnia', '~> executing pattern gives correct value'); 93 | }); 94 | 95 | test('param :: static :: none', () => { 96 | let { keys, pattern } = parse('/:title'); 97 | assert.equal(keys, ['title'], '~> keys has "title" value'); 98 | assert.not.ok(pattern.test('/'), '~> does not match naked base w/ trailing slash'); 99 | assert.ok(pattern.test('/narnia'), '~> matches definition'); 100 | assert.ok(pattern.test('/narnia/'), '~> matches definition w/ trailing slash'); 101 | assert.not.ok(pattern.test('/narnia/reviews'), '~> does not match extra bits'); 102 | assert.not.ok(pattern.test('narnia'), '~> does not match path without lead slash'); 103 | let [url, value] = pattern.exec('/narnia/'); 104 | assert.is(url, '/narnia/', '~> executing pattern on correct trimming'); 105 | assert.is(value, 'narnia', '~> executing pattern gives correct value'); 106 | }); 107 | 108 | test('param :: static :: multiple', () => { 109 | let { keys, pattern } = parse('/foo/bar/:title'); 110 | assert.equal(keys, ['title'], '~> keys has "title" value'); 111 | assert.not.ok(pattern.test('/foo/bar'), '~> does not match naked base'); 112 | assert.not.ok(pattern.test('/foo/bar/'), '~> does not match naked base w/ trailing slash'); 113 | assert.ok(pattern.test('/foo/bar/narnia'), '~> matches definition'); 114 | assert.ok(pattern.test('/foo/bar/narnia/'), '~> matches definition w/ trailing slash'); 115 | assert.not.ok(pattern.test('/foo/bar/narnia/hello'), '~> does not match extra bits'); 116 | assert.not.ok(pattern.test('foo/bar/narnia'), '~> does not match path without lead slash'); 117 | assert.not.ok(pattern.test('/foo/narnia'), '~> does not match if statics are different'); 118 | assert.not.ok(pattern.test('/bar/narnia'), '~> does not match if statics are different'); 119 | let [url, value] = pattern.exec('/foo/bar/narnia'); 120 | assert.is(url, '/foo/bar/narnia', '~> executing pattern on correct trimming'); 121 | assert.is(value, 'narnia', '~> executing pattern gives correct value'); 122 | }); 123 | 124 | test('param :: multiple', () => { 125 | let { keys, pattern } = parse('/books/:author/:title'); 126 | assert.equal(keys, ['author', 'title'], '~> keys has "author" & "title" values'); 127 | assert.not.ok(pattern.test('/books'), '~> does not match naked base'); 128 | assert.not.ok(pattern.test('/books/'), '~> does not match naked base w/ trailing slash'); 129 | assert.not.ok(pattern.test('/books/smith'), '~> does not match insufficient parameter counts'); 130 | assert.not.ok(pattern.test('/books/smith/'), '~> does not match insufficient paramters w/ trailing slash'); 131 | assert.ok(pattern.test('/books/smith/narnia'), '~> matches definition'); 132 | assert.ok(pattern.test('/books/smith/narnia/'), '~> matches definition w/ trailing slash'); 133 | assert.not.ok(pattern.test('/books/smith/narnia/reviews'), '~> does not match extra bits'); 134 | assert.not.ok(pattern.test('books/smith/narnia'), '~> does not match path without lead slash'); 135 | let [url, author, title] = pattern.exec('/books/smith/narnia'); 136 | assert.is(url, '/books/smith/narnia', '~> executing pattern on correct trimming'); 137 | assert.is(author, 'smith', '~> executing pattern gives correct value'); 138 | assert.is(title, 'narnia', '~> executing pattern gives correct value'); 139 | }); 140 | 141 | test('param :: suffix', () => { 142 | let { keys, pattern } = parse('/movies/:title.mp4'); 143 | assert.equal(keys, ['title'], '~> keys has "title" only (no suffix)'); 144 | assert.not.ok(pattern.test('/movies'), '~> does not match naked base'); 145 | assert.not.ok(pattern.test('/movies/'), '~> does not match naked base w/ trailing slash'); 146 | assert.not.ok(pattern.test('/movies/foo'), '~> does not match without suffix'); 147 | assert.not.ok(pattern.test('/movies/foo.mp3'), '~> does not match with wrong suffix'); 148 | assert.ok(pattern.test('/movies/foo.mp4'), '~> does match with correct suffix'); 149 | assert.ok(pattern.test('/movies/foo.mp4/'), '~> does match with trailing slash'); 150 | }); 151 | 152 | test('param :: suffices', () => { 153 | let { keys, pattern } = parse('/movies/:title.(mp4|mov)'); 154 | assert.equal(keys, ['title'], '~> keys has "title" only (no suffix)'); 155 | assert.not.ok(pattern.test('/movies'), '~> does not match naked base'); 156 | assert.not.ok(pattern.test('/movies/'), '~> does not match naked base w/ trailing slash'); 157 | assert.not.ok(pattern.test('/movies/foo'), '~> does not match without suffix'); 158 | assert.not.ok(pattern.test('/movies/foo.mp3'), '~> does not match with wrong suffix'); 159 | assert.ok(pattern.test('/movies/foo.mp4'), '~> does match with correct suffix (mp4)'); 160 | assert.ok(pattern.test('/movies/foo.mp4/'), '~> does match with trailing slash (mp4)'); 161 | assert.ok(pattern.test('/movies/foo.mov'), '~> does match with correct suffix (mov)'); 162 | assert.ok(pattern.test('/movies/foo.mov/'), '~> does match with trailing slash (mov)'); 163 | }); 164 | 165 | test('param :: optional', () => { 166 | let { keys, pattern } = parse('/books/:author/:title?'); 167 | assert.equal(keys, ['author', 'title'], '~> keys has "author" & "title" values'); 168 | assert.not.ok(pattern.test('/books'), '~> does not match naked base'); 169 | assert.not.ok(pattern.test('/books/'), '~> does not match naked base w/ trailing slash'); 170 | assert.ok(pattern.test('/books/smith'), '~> matches when optional parameter is missing counts'); 171 | assert.ok(pattern.test('/books/smith/'), '~> matches when optional paramter is missing w/ trailing slash'); 172 | assert.ok(pattern.test('/books/smith/narnia'), '~> matches when fully populated'); 173 | assert.ok(pattern.test('/books/smith/narnia/'), '~> matches when fully populated w/ trailing slash'); 174 | assert.not.ok(pattern.test('/books/smith/narnia/reviews'), '~> does not match extra bits'); 175 | assert.not.ok(pattern.test('books/smith/narnia'), '~> does not match path without lead slash'); 176 | let [_, author, title] = pattern.exec('/books/smith/narnia'); 177 | assert.is(author, 'smith', '~> executing pattern gives correct value'); 178 | assert.is(title, 'narnia', '~> executing pattern gives correct value'); 179 | }); 180 | 181 | test('param :: optional :: static :: none', () => { 182 | let { keys, pattern } = parse('/:title?'); 183 | assert.equal(keys, ['title'], '~> keys has "title" value'); 184 | assert.ok(pattern.test('/'), '~> matches root w/ trailing slash'); 185 | assert.ok(pattern.test('/narnia'), '~> matches definition'); 186 | assert.ok(pattern.test('/narnia/'), '~> matches definition w/ trailing slash'); 187 | assert.not.ok(pattern.test('/narnia/reviews'), '~> does not match extra bits'); 188 | assert.not.ok(pattern.test('narnia'), '~> does not match path without lead slash'); 189 | let [_, value] = pattern.exec('/narnia'); 190 | assert.is(value, 'narnia', '~> executing pattern gives correct value'); 191 | }); 192 | 193 | test('param :: optional :: multiple', () => { 194 | let { keys, pattern } = parse('/books/:genre/:author?/:title?'); 195 | assert.equal(keys, ['genre', 'author', 'title'], '~> keys has "genre", "author" & "title" values'); 196 | assert.not.ok(pattern.test('/books'), '~> does not match naked base'); 197 | assert.not.ok(pattern.test('/books/'), '~> does not match naked base w/ trailing slash'); 198 | assert.ok(pattern.test('/books/horror'), '~> matches when optional parameter is missing counts'); 199 | assert.ok(pattern.test('/books/horror/'), '~> matches when optional paramter is missing w/ trailing slash'); 200 | assert.ok(pattern.test('/books/horror/smith'), '~> matches when optional parameter is missing counts'); 201 | assert.ok(pattern.test('/books/horror/smith/'), '~> matches when optional paramter is missing w/ trailing slash'); 202 | assert.ok(pattern.test('/books/horror/smith/narnia'), '~> matches when fully populated'); 203 | assert.ok(pattern.test('/books/horror/smith/narnia/'), '~> matches when fully populated w/ trailing slash'); 204 | assert.not.ok(pattern.test('/books/horror/smith/narnia/reviews'), '~> does not match extra bits'); 205 | assert.not.ok(pattern.test('books/horror/smith/narnia'), '~> does not match path without lead slash'); 206 | let [_, genre, author, title] = pattern.exec('/books/horror/smith/narnia'); 207 | assert.is(genre, 'horror', '~> executing pattern gives correct value'); 208 | assert.is(author, 'smith', '~> executing pattern gives correct value'); 209 | assert.is(title, 'narnia', '~> executing pattern gives correct value'); 210 | }); 211 | 212 | test('wildcard', () => { 213 | let { keys, pattern } = parse('/books/*'); 214 | assert.equal(keys, ['*'], '~> keys has "*" value'); 215 | assert.not.ok(pattern.test('/books'), '~> does not match naked base'); 216 | assert.ok(pattern.test('/books/'), '~> does not match naked base w/ trailing slash'); 217 | assert.ok(pattern.test('/books/narnia'), '~> matches definition'); 218 | assert.ok(pattern.test('/books/narnia/'), '~> matches definition w/ trailing slash'); 219 | assert.ok(pattern.test('/books/narnia/reviews'), '~> does not match extra bits'); 220 | assert.not.ok(pattern.test('books/narnia'), '~> does not match path without lead slash'); 221 | let [_, value] = pattern.exec('/books/narnia/reviews'); 222 | assert.is(value, 'narnia/reviews', '~> executing pattern gives ALL values after base'); 223 | }); 224 | 225 | test('wildcard :: root', () => { 226 | let { keys, pattern } = parse('*'); 227 | assert.equal(keys, ['*'], '~> keys has "*" value'); 228 | assert.ok(pattern.test('/'), '~> matches root path'); 229 | assert.ok(pattern.test('/narnia'), '~> matches definition'); 230 | assert.ok(pattern.test('/narnia/'), '~> matches definition w/ trailing slash'); 231 | assert.ok(pattern.test('/narnia/reviews'), '~> does not match extra bits'); 232 | assert.not.ok(pattern.test('narnia'), '~> does not match path without lead slash'); 233 | let [_, value] = pattern.exec('/foo/bar/baz'); 234 | assert.is(value, 'foo/bar/baz', '~> executing pattern gives ALL values together'); 235 | }); 236 | 237 | test('optional wildcard', () => { 238 | let { keys, pattern } = parse('/books/*?'); 239 | assert.equal(keys, ['*'], '~> keys has "*" value'); 240 | assert.ok(pattern.test('/books'), '~> matches naked base'); 241 | assert.ok(pattern.test('/books/'), '~> matches naked base w/ trailing slash'); 242 | assert.ok(pattern.test('/books/narnia'), '~> matches definition'); 243 | assert.ok(pattern.test('/books/narnia/'), '~> matches definition w/ trailing slash'); 244 | assert.ok(pattern.test('/books/narnia/reviews'), '~> does not match extra bits'); 245 | assert.not.ok(pattern.test('books/narnia'), '~> does not match path without lead slash'); 246 | let [_, value] = pattern.exec('/books/narnia/reviews'); 247 | assert.is(value, 'narnia/reviews', '~> executing pattern gives ALL values after base'); 248 | }); 249 | 250 | test('optional wildcard :: root', () => { 251 | let { keys, pattern } = parse('*?'); 252 | assert.equal(keys, ['*'], '~> keys has "*" value'); 253 | assert.ok(pattern.test('/'), '~> matches root path'); 254 | assert.ok(pattern.test('/narnia'), '~> matches definition'); 255 | assert.ok(pattern.test('/narnia/'), '~> matches definition w/ trailing slash'); 256 | assert.ok(pattern.test('/narnia/reviews'), '~> does not match extra bits'); 257 | assert.not.ok(pattern.test('narnia'), '~> does not match path without lead slash'); 258 | let [_, value] = pattern.exec('/foo/bar/baz'); 259 | assert.is(value, 'foo/bar/baz', '~> executing pattern gives ALL values together'); 260 | }); 261 | 262 | test('execs', () => { 263 | // false = did not match 264 | 265 | // console.log('/books'); 266 | toExec('/books', '/', false); 267 | toExec('/books', '/books', {}); 268 | toExec('/books', '/books/', {}); 269 | toExec('/books', '/books/world/', false); 270 | toExec('/books', '/books/world', false); 271 | 272 | // console.log('/:title'); 273 | toExec('/:title', '/hello', { title:'hello' }); 274 | toExec('/:title', '/hello/', { title:'hello' }); 275 | toExec('/:title', '/hello/world/', false); 276 | toExec('/:title', '/hello/world', false); 277 | toExec('/:title', '/', false); 278 | 279 | // console.log('/:title?'); 280 | toExec('/:title?', '/', { title:null }); 281 | toExec('/:title?', '/hello', { title:'hello' }); 282 | toExec('/:title?', '/hello/', { title:'hello' }); 283 | toExec('/:title?', '/hello/world/', false); 284 | toExec('/:title?', '/hello/world', false); 285 | 286 | // console.log('/:title.mp4'); 287 | toExec('/:title.mp4', '/hello.mp4', { title:'hello' }); 288 | toExec('/:title.mp4', '/hello.mp4/', { title:'hello' }); 289 | toExec('/:title.mp4', '/hello.mp4/history/', false); 290 | toExec('/:title.mp4', '/hello.mp4/history', false); 291 | toExec('/:title.mp4', '/', false); 292 | 293 | // console.log('/:title/:genre'); 294 | toExec('/:title/:genre', '/hello/world', { title:'hello', genre:'world' }); 295 | toExec('/:title/:genre', '/hello/world/', { title:'hello', genre:'world' }); 296 | toExec('/:title/:genre', '/hello/world/mundo/', false); 297 | toExec('/:title/:genre', '/hello/world/mundo', false); 298 | toExec('/:title/:genre', '/hello/', false); 299 | toExec('/:title/:genre', '/hello', false); 300 | 301 | // console.log('/:title/:genre?'); 302 | toExec('/:title/:genre?', '/hello', { title:'hello', genre:null }); 303 | toExec('/:title/:genre?', '/hello/', { title:'hello', genre:null }); 304 | toExec('/:title/:genre?', '/hello/world', { title:'hello', genre:'world' }); 305 | toExec('/:title/:genre?', '/hello/world/', { title:'hello', genre:'world' }); 306 | toExec('/:title/:genre?', '/hello/world/mundo/', false); 307 | toExec('/:title/:genre?', '/hello/world/mundo', false); 308 | 309 | // console.log('/books/*'); 310 | toExec('/books/*', '/books', false); 311 | toExec('/books/*', '/books/', { '*':null }); 312 | toExec('/books/*', '/books/world', { '*':'world' }); 313 | toExec('/books/*', '/books/world/', { '*':'world/' }); 314 | toExec('/books/*', '/books/world/howdy', { '*':'world/howdy' }); 315 | toExec('/books/*', '/books/world/howdy/', { '*':'world/howdy/' }); 316 | 317 | // console.log('/books/*?'); 318 | toExec('/books/*?', '/books', { '*':null }); 319 | toExec('/books/*?', '/books/', { '*':null }); 320 | toExec('/books/*?', '/books/world', { '*':'world' }); 321 | toExec('/books/*?', '/books/world/', { '*':'world/' }); 322 | toExec('/books/*?', '/books/world/howdy', { '*':'world/howdy' }); 323 | toExec('/books/*?', '/books/world/howdy/', { '*':'world/howdy/' }); 324 | }); 325 | 326 | test('execs :: loose', () => { 327 | // false = did not match 328 | 329 | // console.log('/books'); 330 | toLooseExec('/books', '/', false); 331 | toLooseExec('/books', '/books', {}); 332 | toLooseExec('/books', '/books/', {}); 333 | toLooseExec('/books', '/books/world/', {}); 334 | toLooseExec('/books', '/books/world', {}); 335 | 336 | // console.log('/:title'); 337 | toLooseExec('/:title', '/hello', { title:'hello' }); 338 | toLooseExec('/:title', '/hello/', { title:'hello' }); 339 | toLooseExec('/:title', '/hello/world/', { title:'hello' }); 340 | toLooseExec('/:title', '/hello/world', { title:'hello' }); 341 | toLooseExec('/:title', '/', false); 342 | 343 | // console.log('/:title?'); 344 | toLooseExec('/:title?', '/', { title:null }); 345 | toLooseExec('/:title?', '/hello', { title:'hello' }); 346 | toLooseExec('/:title?', '/hello/', { title:'hello' }); 347 | toLooseExec('/:title?', '/hello/world/', { title:'hello' }); 348 | toLooseExec('/:title?', '/hello/world', { title:'hello' }); 349 | 350 | // console.log('/:title.mp4'); 351 | toLooseExec('/:title.mp4', '/hello.mp4', { title:'hello' }); 352 | toLooseExec('/:title.mp4', '/hello.mp4/', { title:'hello' }); 353 | toLooseExec('/:title.mp4', '/hello.mp4/history/', { title:'hello' }); 354 | toLooseExec('/:title.mp4', '/hello.mp4/history', { title:'hello' }); 355 | toLooseExec('/:title.mp4', '/', false); 356 | 357 | // console.log('/:title/:genre'); 358 | toLooseExec('/:title/:genre', '/hello/world', { title:'hello', genre:'world' }); 359 | toLooseExec('/:title/:genre', '/hello/world/', { title:'hello', genre:'world' }); 360 | toLooseExec('/:title/:genre', '/hello/world/mundo/', { title:'hello', genre:'world' }); 361 | toLooseExec('/:title/:genre', '/hello/world/mundo', { title:'hello', genre:'world' }); 362 | toLooseExec('/:title/:genre', '/hello/', false); 363 | toLooseExec('/:title/:genre', '/hello', false); 364 | 365 | // console.log('/:title/:genre?'); 366 | toLooseExec('/:title/:genre?', '/hello', { title:'hello', genre:null }); 367 | toLooseExec('/:title/:genre?', '/hello/', { title:'hello', genre:null }); 368 | toLooseExec('/:title/:genre?', '/hello/world', { title:'hello', genre:'world' }); 369 | toLooseExec('/:title/:genre?', '/hello/world/', { title:'hello', genre:'world' }); 370 | toLooseExec('/:title/:genre?', '/hello/world/mundo/', { title:'hello', genre:'world' }); 371 | toLooseExec('/:title/:genre?', '/hello/world/mundo', { title:'hello', genre:'world' }); 372 | 373 | // console.log('/books/*'); 374 | toLooseExec('/books/*', '/books', false); 375 | toLooseExec('/books/*', '/books/', { '*':null }); 376 | toLooseExec('/books/*', '/books/world', { '*':'world' }); 377 | toLooseExec('/books/*', '/books/world/', { '*':'world/' }); 378 | toLooseExec('/books/*', '/books/world/howdy', { '*':'world/howdy' }); 379 | toLooseExec('/books/*', '/books/world/howdy/', { '*':'world/howdy/' }); 380 | 381 | // console.log('/books/*?'); 382 | toLooseExec('/books/*?', '/books', { '*':null }); 383 | toLooseExec('/books/*?', '/books/', { '*':null }); 384 | toLooseExec('/books/*?', '/books/world', { '*':'world' }); 385 | toLooseExec('/books/*?', '/books/world/', { '*':'world/' }); 386 | toLooseExec('/books/*?', '/books/world/howdy', { '*':'world/howdy' }); 387 | toLooseExec('/books/*?', '/books/world/howdy/', { '*':'world/howdy/' }); 388 | }); 389 | 390 | test('(raw) exec', () => { 391 | // console.log('/foo ~> "/foo"'); 392 | let [url, ...vals] = raw('/foo', '/foo'); 393 | assert.is(url, '/foo', '~> parsed `url` correctly'); 394 | assert.equal(vals, [], '~> parsed value segments correctly'); 395 | 396 | // console.log('/foo ~> "/foo/"'); 397 | [url, ...vals] = raw('/foo/', '/foo/'); 398 | assert.is(url, '/foo/', '~> parsed `url` correctly'); 399 | assert.equal(vals, [], '~> parsed value segments correctly'); 400 | 401 | 402 | // console.log('/:path ~> "/foo"'); 403 | [url, ...vals] = raw('/:path', '/foo'); 404 | assert.is(url, '/foo', '~> parsed `url` correctly'); 405 | assert.equal(vals, ['foo'], '~> parsed value segments correctly'); 406 | 407 | // console.log('/:path ~> "/foo/"'); 408 | [url, ...vals] = raw('/:path', '/foo/'); 409 | assert.is(url, '/foo/', '~> parsed `url` correctly'); 410 | assert.equal(vals, ['foo'], '~> parsed value segments correctly'); 411 | 412 | 413 | // console.log('/:path/:sub ~> "/foo/bar"'); 414 | [url, ...vals] = raw('/:path/:sub', '/foo/bar'); 415 | assert.is(url, '/foo/bar', '~> parsed `url` correctly'); 416 | assert.equal(vals, ['foo', 'bar'], '~> parsed value segments correctly'); 417 | 418 | // console.log('/:path/:sub ~> "/foo/bar/"'); 419 | [url, ...vals] = raw('/:path/:sub', '/foo/bar/'); 420 | assert.is(url, '/foo/bar/', '~> parsed `url` correctly'); 421 | assert.equal(vals, ['foo', 'bar'], '~> parsed value segments correctly'); 422 | 423 | 424 | // console.log('/:path/:sub? ~> "/foo"'); 425 | [url, ...vals] = raw('/:path/:sub?', '/foo'); 426 | assert.is(url, '/foo', '~> parsed `url` correctly'); 427 | assert.equal(vals, ['foo', undefined], '~> parsed value segments correctly'); 428 | 429 | // console.log('/:path/:sub? ~> "/foo/"'); 430 | [url, ...vals] = raw('/:path/:sub?', '/foo/'); 431 | assert.is(url, '/foo/', '~> parsed `url` correctly'); 432 | assert.equal(vals, ['foo', undefined], '~> parsed value segments correctly'); 433 | 434 | 435 | // console.log('/:path/:sub? ~> "/foo/bar"'); 436 | [url, ...vals] = raw('/:path/:sub?', '/foo/bar'); 437 | assert.is(url, '/foo/bar', '~> parsed `url` correctly'); 438 | assert.equal(vals, ['foo', 'bar'], '~> parsed value segments correctly'); 439 | 440 | // console.log('/:path/:sub? ~> "/foo/bar/"'); 441 | [url, ...vals] = raw('/:path/:sub', '/foo/bar/'); 442 | assert.is(url, '/foo/bar/', '~> parsed `url` correctly'); 443 | assert.equal(vals, ['foo', 'bar'], '~> parsed value segments correctly'); 444 | 445 | 446 | // console.log('/:path/* ~> "/foo/bar/baz"'); 447 | [url, ...vals] = raw('/:path/*', '/foo/bar/baz'); 448 | assert.is(url, '/foo/bar/baz', '~> parsed `url` correctly'); 449 | assert.equal(vals, ['foo', 'bar/baz'], '~> parsed value segments correctly'); 450 | 451 | // console.log('/:path/* ~> "/foo/bar/baz/"'); 452 | [url, ...vals] = raw('/:path/*', '/foo/bar/baz/'); 453 | assert.is(url, '/foo/bar/baz/', '~> parsed `url` correctly'); 454 | assert.equal(vals, ['foo', 'bar/baz/'], '~> parsed value segments correctly'); 455 | 456 | 457 | // console.log('/foo/:path ~> "/foo/bar"'); 458 | [url, ...vals] = raw('/foo/:path', '/foo/bar'); 459 | assert.is(url, '/foo/bar', '~> parsed `url` correctly'); 460 | assert.equal(vals, ['bar'], '~> parsed value segments correctly'); 461 | 462 | // console.log('/foo/:path ~> "/foo/bar/"'); 463 | [url, ...vals] = raw('/foo/:path', '/foo/bar/'); 464 | assert.is(url, '/foo/bar/', '~> parsed `url` correctly'); 465 | assert.equal(vals, ['bar'], '~> parsed value segments correctly'); 466 | }); 467 | 468 | test('(raw) exec :: loose', () => { 469 | // console.log('/foo ~> "/foo"'); 470 | let [url, ...vals] = raw('/foo', '/foo', 1); 471 | assert.is(url, '/foo', '~> parsed `url` correctly'); 472 | assert.equal(vals, [], '~> parsed value segments correctly'); 473 | 474 | // console.log('/foo ~> "/foo/"'); 475 | [url, ...vals] = raw('/foo/', '/foo/', 1); 476 | assert.is(url, '/foo', '~> parsed `url` correctly'); 477 | assert.equal(vals, [], '~> parsed value segments correctly'); 478 | 479 | 480 | // console.log('/:path ~> "/foo"'); 481 | [url, ...vals] = raw('/:path', '/foo', 1); 482 | assert.is(url, '/foo', '~> parsed `url` correctly'); 483 | assert.equal(vals, ['foo'], '~> parsed value segments correctly'); 484 | 485 | // console.log('/:path ~> "/foo/"'); 486 | [url, ...vals] = raw('/:path', '/foo/', 1); 487 | assert.is(url, '/foo', '~> parsed `url` correctly'); 488 | assert.equal(vals, ['foo'], '~> parsed value segments correctly'); 489 | 490 | 491 | // console.log('/:path/:sub ~> "/foo/bar"'); 492 | [url, ...vals] = raw('/:path/:sub', '/foo/bar', 1); 493 | assert.is(url, '/foo/bar', '~> parsed `url` correctly'); 494 | assert.equal(vals, ['foo', 'bar'], '~> parsed value segments correctly'); 495 | 496 | // console.log('/:path/:sub ~> "/foo/bar/"'); 497 | [url, ...vals] = raw('/:path/:sub', '/foo/bar/', 1); 498 | assert.is(url, '/foo/bar', '~> parsed `url` correctly'); 499 | assert.equal(vals, ['foo', 'bar'], '~> parsed value segments correctly'); 500 | 501 | 502 | // console.log('/:path/:sub? ~> "/foo"'); 503 | [url, ...vals] = raw('/:path/:sub?', '/foo', 1); 504 | assert.is(url, '/foo', '~> parsed `url` correctly'); 505 | assert.equal(vals, ['foo', undefined], '~> parsed value segments correctly'); 506 | 507 | // console.log('/:path/:sub? ~> "/foo/"'); 508 | [url, ...vals] = raw('/:path/:sub?', '/foo/', 1); 509 | assert.is(url, '/foo', '~> parsed `url` correctly'); 510 | assert.equal(vals, ['foo', undefined], '~> parsed value segments correctly'); 511 | 512 | 513 | // console.log('/:path/:sub? ~> "/foo/bar"'); 514 | [url, ...vals] = raw('/:path/:sub?', '/foo/bar', 1); 515 | assert.is(url, '/foo/bar', '~> parsed `url` correctly'); 516 | assert.equal(vals, ['foo', 'bar'], '~> parsed value segments correctly'); 517 | 518 | // console.log('/:path/:sub? ~> "/foo/bar/"'); 519 | [url, ...vals] = raw('/:path/:sub', '/foo/bar/', 1); 520 | assert.is(url, '/foo/bar', '~> parsed `url` correctly'); 521 | assert.equal(vals, ['foo', 'bar'], '~> parsed value segments correctly'); 522 | 523 | 524 | // console.log('/:path/* ~> "/foo/bar/baz"'); 525 | [url, ...vals] = raw('/:path/*', '/foo/bar/baz', 1); 526 | assert.is(url, '/foo/bar/baz', '~> parsed `url` correctly'); 527 | assert.equal(vals, ['foo', 'bar/baz'], '~> parsed value segments correctly'); 528 | 529 | // console.log('/:path/* ~> "/foo/bar/baz/"'); 530 | [url, ...vals] = raw('/:path/*', '/foo/bar/baz/', 1); 531 | assert.is(url, '/foo/bar/baz/', '~> parsed `url` correctly'); // trail 532 | assert.equal(vals, ['foo', 'bar/baz/'], '~> parsed value segments correctly'); 533 | 534 | 535 | // console.log('/foo/:path ~> "/foo/bar"'); 536 | [url, ...vals] = raw('/foo/:path', '/foo/bar', 1); 537 | assert.is(url, '/foo/bar', '~> parsed `url` correctly'); 538 | assert.equal(vals, ['bar'], '~> parsed value segments correctly'); 539 | 540 | // console.log('/foo/:path ~> "/foo/bar/"'); 541 | [url, ...vals] = raw('/foo/:path', '/foo/bar/', 1); 542 | assert.is(url, '/foo/bar', '~> parsed `url` correctly'); 543 | assert.equal(vals, ['bar'], '~> parsed value segments correctly'); 544 | }); 545 | 546 | test('(extra) exec', () => { 547 | // Not matches! 548 | // console.log('/foo ~> "/foo/bar" (extra)'); 549 | assert.is(raw('/foo', '/foo/bar'), null, '~> does not match'); 550 | 551 | // console.log('/foo ~> "/foo/bar/" (extra)'); 552 | assert.is(raw('/foo/', '/foo/bar/'), null, '~> does not match'); 553 | 554 | 555 | // console.log('/:path ~> "/foo/bar" (extra)'); 556 | assert.is(raw('/:path', '/foo/bar'), null, '~> does not match'); 557 | 558 | // console.log('/:path ~> "/foo/bar/" (extra)'); 559 | assert.is(raw('/:path', '/foo/bar/'), null, '~> does not match'); 560 | }); 561 | 562 | test('(extra) exec :: loose', () => { 563 | // console.log('/foo ~> "/foo/bar" (extra)'); 564 | let [url, ...vals] = raw('/foo', '/foo/bar', 1); 565 | assert.is(url, '/foo', '~> parsed `url` correctly'); 566 | assert.equal(vals, [], '~> parsed value segments correctly'); 567 | 568 | // console.log('/foo ~> "/foo/bar/" (extra)'); 569 | [url, ...vals] = raw('/foo/', '/foo/bar/', 1); 570 | assert.is(url, '/foo', '~> parsed `url` correctly'); 571 | assert.equal(vals, [], '~> parsed value segments correctly'); 572 | 573 | 574 | // console.log('/:path ~> "/foo/bar" (extra)'); 575 | [url, ...vals] = raw('/:path', '/foo/bar', 1); 576 | assert.is(url, '/foo', '~> parsed `url` correctly'); 577 | assert.equal(vals, ['foo'], '~> parsed value segments correctly'); 578 | 579 | // console.log('/:path ~> "/foo/bar/" (extra)'); 580 | [url, ...vals] = raw('/:path', '/foo/bar/', 1); 581 | assert.is(url, '/foo', '~> parsed `url` correctly'); 582 | assert.equal(vals, ['foo'], '~> parsed value segments correctly'); 583 | }); 584 | 585 | // --- 586 | 587 | test('(RegExp) static', () => { 588 | let rgx = /^\/?books/; 589 | let { keys, pattern } = parse(rgx); 590 | assert.equal(keys, false, '~> keys = false'); 591 | assert.equal(rgx, pattern, '~> pattern = input'); 592 | assert.ok(pattern.test('/books'), '~> matches route'); 593 | assert.ok(pattern.test('/books/'), '~> matches trailing slash'); 594 | assert.ok(pattern.test('/books/'), '~> matches without leading slash'); 595 | }); 596 | 597 | if (hasNamedGroups) { 598 | test('(RegExp) param', () => { 599 | let rgx = /^\/(?<year>[0-9]{4})/i; 600 | let { keys, pattern } = parse(rgx); 601 | assert.equal(keys, false, '~> keys = false'); 602 | assert.equal(rgx, pattern, '~> pattern = input'); 603 | 604 | // RegExp testing (not regexparam related) 605 | assert.not.ok(pattern.test('/123'), '~> does not match 3-digit string'); 606 | assert.not.ok(pattern.test('/asdf'), '~> does not match 4 alpha characters'); 607 | assert.ok(pattern.test('/2019'), '~> matches definition'); 608 | assert.ok(pattern.test('/2019/'), '~> matches definition w/ trailing slash'); 609 | assert.not.ok(pattern.test('2019'), '~> does not match without lead slash'); 610 | assert.ok(pattern.test('/2019/narnia/hello'), '~> allows extra bits'); 611 | 612 | // exec results, array access 613 | let [url, value] = pattern.exec('/2019/books'); 614 | assert.is(url, '/2019', '~> executing pattern on correct trimming'); 615 | assert.is(value, '2019', '~> executing pattern gives correct value'); 616 | 617 | // exec results, named object 618 | toExec(rgx, '/2019/books', { year: '2019' }); 619 | toExec(rgx, '/2019/books/narnia', { year: '2019' }); 620 | }); 621 | 622 | test('(RegExp) param :: w/ static', () => { 623 | let rgx = /^\/books\/(?<title>[a-z]+)/i; 624 | let { keys, pattern } = parse(rgx); 625 | assert.equal(keys, false, '~> keys = false'); 626 | assert.equal(rgx, pattern, '~> pattern = input'); 627 | 628 | // RegExp testing (not regexparam related) 629 | assert.not.ok(pattern.test('/books'), '~> does not match naked base'); 630 | assert.not.ok(pattern.test('/books/'), '~> does not match naked base w/ trailing slash'); 631 | assert.ok(pattern.test('/books/narnia'), '~> matches definition'); 632 | assert.ok(pattern.test('/books/narnia/'), '~> matches definition w/ trailing slash'); 633 | assert.ok(pattern.test('/books/narnia/hello'), '~> allows extra bits'); 634 | assert.not.ok(pattern.test('books/narnia'), '~> does not match path without lead slash'); 635 | 636 | // exec results, array access 637 | let [url, value] = pattern.exec('/books/narnia'); 638 | assert.is(url, '/books/narnia', '~> executing pattern on correct trimming'); 639 | assert.is(value, 'narnia', '~> executing pattern gives correct value'); 640 | 641 | // exec results, named object 642 | toExec(rgx, '/books/narnia', { title: 'narnia' }); 643 | toExec(rgx, '/books/narnia/hello', { title: 'narnia' }); 644 | }); 645 | 646 | test('(RegExp) param :: multiple', () => { 647 | let rgx = /^\/(?<year>[0-9]{4})-(?<month>[0-9]{2})\/(?<day>[0-9]{2})/i; 648 | let { keys, pattern } = parse(rgx); 649 | assert.equal(keys, false, '~> keys = false'); 650 | assert.equal(rgx, pattern, '~> pattern = input'); 651 | 652 | // RegExp testing (not regexparam related) 653 | assert.not.ok(pattern.test('/123-1')); 654 | assert.not.ok(pattern.test('/123-10')); 655 | assert.not.ok(pattern.test('/1234-10')); 656 | assert.not.ok(pattern.test('/1234-10/1')); 657 | assert.not.ok(pattern.test('/1234-10/as')); 658 | assert.ok(pattern.test('/1234-10/01/')); 659 | assert.ok(pattern.test('/2019-10/30')); 660 | 661 | // exec results, array access 662 | let [url, year, month, day] = pattern.exec('/2019-05/30/'); 663 | assert.is(url, '/2019-05/30', '~> executing pattern on correct trimming'); 664 | assert.is(year, '2019', '~> executing pattern gives correct "year" value'); 665 | assert.is(month, '05', '~> executing pattern gives correct "month" value'); 666 | assert.is(day, '30', '~> executing pattern gives correct "day" value'); 667 | 668 | // exec results, named object 669 | toExec(rgx, '/2019-10/02', { year:'2019', month:'10', day:'02' }); 670 | toExec(rgx, '/2019-10/02/narnia', { year:'2019', month:'10', day:'02' }); 671 | }); 672 | 673 | test('(RegExp) param :: suffix', () => { 674 | let rgx = /^\/movies[/](?<title>\w+)\.mp4/i; 675 | let { keys, pattern } = parse(rgx); 676 | assert.equal(keys, false, '~> keys = false'); 677 | assert.equal(rgx, pattern, '~> pattern = input'); 678 | 679 | // RegExp testing (not regexparam related) 680 | assert.not.ok(pattern.test('/movies')); 681 | assert.not.ok(pattern.test('/movies/')); 682 | assert.not.ok(pattern.test('/movies/foo')); 683 | assert.not.ok(pattern.test('/movies/foo.mp3')); 684 | assert.ok(pattern.test('/movies/foo.mp4')); 685 | assert.ok(pattern.test('/movies/foo.mp4/')); 686 | 687 | // exec results, array access 688 | let [url, title] = pattern.exec('/movies/narnia.mp4'); 689 | assert.is(url, '/movies/narnia.mp4', '~> executing pattern on correct trimming'); 690 | assert.is(title, 'narnia', '~> executing pattern gives correct "title" value'); 691 | 692 | // exec results, named object 693 | toExec(rgx, '/movies/narnia.mp4', { title: 'narnia' }); 694 | toExec(rgx, '/movies/narnia.mp4/', { title: 'narnia' }); 695 | }); 696 | 697 | test('(RegExp) param :: suffices', () => { 698 | let rgx = /^\/movies[/](?<title>\w+)\.(mp4|mov)/i; 699 | let { keys, pattern } = parse(rgx); 700 | assert.equal(keys, false, '~> keys = false'); 701 | assert.equal(rgx, pattern, '~> pattern = input'); 702 | 703 | // RegExp testing (not regexparam related) 704 | assert.not.ok(pattern.test('/movies')); 705 | assert.not.ok(pattern.test('/movies/')); 706 | assert.not.ok(pattern.test('/movies/foo')); 707 | assert.not.ok(pattern.test('/movies/foo.mp3')); 708 | assert.ok(pattern.test('/movies/foo.mp4')); 709 | assert.ok(pattern.test('/movies/foo.mp4/')); 710 | assert.ok(pattern.test('/movies/foo.mov/')); 711 | 712 | // exec results, array access 713 | let [url, title] = pattern.exec('/movies/narnia.mov'); 714 | assert.is(url, '/movies/narnia.mov', '~> executing pattern on correct trimming'); 715 | assert.is(title, 'narnia', '~> executing pattern gives correct "title" value'); 716 | 717 | // exec results, named object 718 | toExec(rgx, '/movies/narnia.mov', { title: 'narnia' }); 719 | toExec(rgx, '/movies/narnia.mov/', { title: 'narnia' }); 720 | }); 721 | 722 | test('(RegExp) param :: optional', () => { 723 | let rgx = /^\/books[/](?<author>[^/]+)[/]?(?<title>[^/]+)?[/]?$/ 724 | let { keys, pattern } = parse(rgx); 725 | assert.equal(keys, false, '~> keys = false'); 726 | assert.equal(rgx, pattern, '~> pattern = input'); 727 | 728 | // RegExp testing (not regexparam related) 729 | assert.not.ok(pattern.test('/books')); 730 | assert.not.ok(pattern.test('/books/')); 731 | assert.ok(pattern.test('/books/smith')); 732 | assert.ok(pattern.test('/books/smith/')); 733 | assert.ok(pattern.test('/books/smith/narnia')); 734 | assert.ok(pattern.test('/books/smith/narnia/')); 735 | assert.not.ok(pattern.test('/books/smith/narnia/reviews')); 736 | assert.not.ok(pattern.test('books/smith/narnia')); 737 | 738 | // exec results, array access 739 | let [url, author, title] = pattern.exec('/books/smith/narnia/'); 740 | assert.is(url, '/books/smith/narnia/', '~> executing pattern on correct trimming'); 741 | assert.is(author, 'smith', '~> executing pattern gives correct value'); 742 | assert.is(title, 'narnia', '~> executing pattern gives correct value'); 743 | 744 | // exec results, named object 745 | toExec(rgx, '/books/smith/narnia', { author: 'smith', title: 'narnia' }); 746 | toExec(rgx, '/books/smith/narnia/', { author: 'smith', title: 'narnia' }); 747 | toExec(rgx, '/books/smith/', { author: 'smith', title: undefined }); 748 | }); 749 | } 750 | 751 | test('(RegExp) nameless', () => { 752 | // For whatever reason~ 753 | // ~> regexparam CANNOT give `keys` list cuz unknown 754 | let rgx = /^\/books[/]([^/]\w+)[/]?(\w+)?(?=\/|$)/i; 755 | let { keys, pattern } = parse(rgx); 756 | assert.equal(keys, false, '~> keys = false'); 757 | assert.equal(rgx, pattern, '~> pattern = input'); 758 | 759 | // RegExp testing (not regexparam related) 760 | assert.not.ok(pattern.test('/books')); 761 | assert.not.ok(pattern.test('/books/')); 762 | assert.ok(pattern.test('/books/smith')); 763 | assert.ok(pattern.test('/books/smith/')); 764 | assert.ok(pattern.test('/books/smith/narnia')); 765 | assert.ok(pattern.test('/books/smith/narnia/')); 766 | assert.not.ok(pattern.test('books/smith/narnia')); 767 | 768 | // exec results, array access 769 | let [url, author, title] = pattern.exec('/books/smith/narnia/'); 770 | assert.is(url, '/books/smith/narnia', '~> executing pattern on correct trimming'); 771 | assert.is(author, 'smith', '~> executing pattern gives correct value'); 772 | assert.is(title, 'narnia', '~> executing pattern gives correct value'); 773 | 774 | // exec results, named object 775 | // Note: UNKNOWN & UNNAMED KEYS 776 | toExec(rgx, '/books/smith/narnia', {}); 777 | toExec(rgx, '/books/smith/narnia/', {}); 778 | toExec(rgx, '/books/smith/', {}); 779 | }); 780 | 781 | test.run(); 782 | --------------------------------------------------------------------------------