diff --git a/.travis.yml b/.travis.yml index 88a8dbe..ccec9c7 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,3 +1,4 @@ language: node_js node_js: + - 10 - 6 diff --git a/package.json b/package.json index 7100c2c..6a8576e 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,8 @@ "name": "regexparam", "version": "1.2.2", "repository": "lukeed/regexparam", - "description": "A tiny (285B) utility that converts route patterns into RegExp. Limited alternative to `path-to-regexp` 🙇‍", + "description": "A tiny (308B) utility that converts route patterns into RegExp. Limited alternative to `path-to-regexp` 🙇‍", + "unpkg": "dist/regexparam.min.js", "module": "dist/regexparam.mjs", "main": "dist/regexparam.js", "types": "types.d.ts", diff --git a/readme.md b/readme.md index 4a0e45e..91a696e 100644 --- a/readme.md +++ b/readme.md @@ -1,6 +1,6 @@ -# regexparam [![Build Status](https://travis-ci.org/lukeed/regexparam.svg?branch=master)](https://travis-ci.org/lukeed/regexparam) +# regexparam [![Build Status](https://badgen.now.sh/travis/lukeed/regexparam)](https://travis-ci.org/lukeed/regexparam) -> A tiny (285B) utility that converts route patterns into RegExp. Limited alternative to [`path-to-regexp`](https://github.com/pillarjs/path-to-regexp) 🙇 +> A tiny (308B) utility that converts route patterns into RegExp. Limited alternative to [`path-to-regexp`](https://github.com/pillarjs/path-to-regexp) 🙇 With `regexparam`, you may turn a pathing string (eg, `/users/:id`) into a regular expression. @@ -87,12 +87,46 @@ exec('/users/lukeed/repos/new', baz); > **Important:** When matching/testing against a generated RegExp, your path **must** begin with a leading slash (`"/"`)! +## Regular Expressions + +For fine-tuned control, you may pass a `RegExp` value directly to `regexparam` as its only parameter. + +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. + +This also means that you must manage and parse your own `keys`~!
+You may use [named capture groups](https://javascript.info/regexp-groups#named-groups) or traverse the matched segments manually the "old-fashioned" way: + +> **Important:** Please check your target browsers' and target [Node.js runtimes' support](https://node.green/#ES2018-features--RegExp-named-capture-groups)! + +```js +// Named capture group +const named = regexparam(/^\/posts[/](?[0-9]{4})[/](?[0-9]{2})[/](?[^\/]+)/i); +const { groups } = named.pattern.exec('/posts/2019/05/hello-world'); +console.log(groups); +//=> { year: '2019', month: '05', title: 'hello-world' } + +// Widely supported / "Old-fashioned" +const named = regexparam(/^\/posts[/]([0-9]{4})[/]([0-9]{2})[/]([^\/]+)/i); +const [url, year, month, title] = named.pattern.exec('/posts/2019/05/hello-world'); +console.log(year, month, title); +//=> 2019 05 hello-world +``` + ## API +There are two API variants: + +1) When passing a `String` input, the `loose` parameter is able to affect the output. [View API](#regexparamstr-loose) + +2) When passing a `RegExp` value, that must be `regexparam`'s _only_ argument.<br> +Your pattern is saved as written, so `loose` is ignored entirely. [View API](#regexparamrgx) + ### regexparam(str, loose) Returns: `Object` +Returns a `{ keys, pattern }` object, where `pattern` is a generated `RegExp` instance and `keys` is a list of extracted parameter names. + #### str Type: `String` @@ -117,6 +151,18 @@ rgx('/users/:name').pattern.test('/users/lukeed/repos'); //=> false rgx('/users/:name', true).pattern.test('/users/lukeed/repos'); //=> true ``` +### regexparam(rgx) +Returns: `Object` + +Returns a `{ keys, pattern }` object, where pattern is _identical_ to your `rgx` and `keys` is `false`, always. + +#### rgx +Type: `RegExp` + +Your RegExp pattern. + +> **Important:** This pattern is used _as is_! No parsing or interpreting is done on your behalf. + ## Related diff --git a/src/index.js b/src/index.js index db4762b..9f6181a 100644 --- a/src/index.js +++ b/src/index.js @@ -1,5 +1,6 @@ export default function (str, loose) { - var c, o, tmp, ext, keys=[], pattern='', arr=str.split('/'); + if (str instanceof RegExp) return { keys:false, pattern:str }; + var c, o, tmp, ext, keys=[], pattern='', arr = str.split('/'); arr[0] || arr.shift(); while (tmp = arr.shift()) { diff --git a/test/index.js b/test/index.js index 60d2ce6..7e563d0 100644 --- a/test/index.js +++ b/test/index.js @@ -1,10 +1,13 @@ const test = require('tape'); const fn = require('../dist/regexparam'); +const hasNamedGroups = 'groups' in /x/.exec('x'); + function run(route, url, loose) { let i=0, out={}, result=fn(route, !!loose); let matches = result.pattern.exec(url); if (matches === null) return false; + if (matches.groups) return matches.groups; while (i < result.keys.length) { out[ result.keys[i] ] = matches[++i] || null; } @@ -568,3 +571,214 @@ test('(extra) exec :: loose', t => { t.end(); }); + +// --- + +test('(RegExp) static', t => { + let rgx = /^\/?books/; + let { keys, pattern } = fn(rgx); + t.same(keys, false, '~> keys = false'); + t.same(rgx, pattern, '~> pattern = input'); + t.true(pattern.test('/books'), '~> matches route'); + t.true(pattern.test('/books/'), '~> matches trailing slash'); + t.true(pattern.test('/books/'), '~> matches without leading slash'); + t.end(); +}); + +if (hasNamedGroups) { + test('(RegExp) param', t => { + let rgx = /^\/(?<year>[0-9]{4})/i; + let { keys, pattern } = fn(rgx); + t.same(keys, false, '~> keys = false'); + t.same(rgx, pattern, '~> pattern = input'); + + // RegExp testing (not regexparam related) + t.false(pattern.test('/123'), '~> does not match 3-digit string'); + t.false(pattern.test('/asdf'), '~> does not match 4 alpha characters'); + t.true(pattern.test('/2019'), '~> matches definition'); + t.true(pattern.test('/2019/'), '~> matches definition w/ trailing slash'); + t.false(pattern.test('2019'), '~> does not match without lead slash'); + t.true(pattern.test('/2019/narnia/hello'), '~> allows extra bits'); + + // exec results, array access + let [url, value] = pattern.exec('/2019/books'); + t.is(url, '/2019', '~> executing pattern on correct trimming'); + t.is(value, '2019', '~> executing pattern gives correct value'); + + // exec results, named object + t.toExec(rgx, '/2019/books', { year: '2019' }); + t.toExec(rgx, '/2019/books/narnia', { year: '2019' }); + + t.end(); + }); + + test('(RegExp) param :: w/ static', t => { + let rgx = /^\/books\/(?<title>[a-z]+)/i; + let { keys, pattern } = fn(rgx); + t.same(keys, false, '~> keys = false'); + t.same(rgx, pattern, '~> pattern = input'); + + // RegExp testing (not regexparam related) + t.false(pattern.test('/books'), '~> does not match naked base'); + t.false(pattern.test('/books/'), '~> does not match naked base w/ trailing slash'); + t.true(pattern.test('/books/narnia'), '~> matches definition'); + t.true(pattern.test('/books/narnia/'), '~> matches definition w/ trailing slash'); + t.true(pattern.test('/books/narnia/hello'), '~> allows extra bits'); + t.false(pattern.test('books/narnia'), '~> does not match path without lead slash'); + + // exec results, array access + let [url, value] = pattern.exec('/books/narnia'); + t.is(url, '/books/narnia', '~> executing pattern on correct trimming'); + t.is(value, 'narnia', '~> executing pattern gives correct value'); + + // exec results, named object + t.toExec(rgx, '/books/narnia', { title: 'narnia' }); + t.toExec(rgx, '/books/narnia/hello', { title: 'narnia' }); + + t.end(); + }); + + test('(RegExp) param :: multiple', t => { + let rgx = /^\/(?<year>[0-9]{4})-(?<month>[0-9]{2})\/(?<day>[0-9]{2})/i; + let { keys, pattern } = fn(rgx); + t.same(keys, false, '~> keys = false'); + t.same(rgx, pattern, '~> pattern = input'); + + // RegExp testing (not regexparam related) + t.false(pattern.test('/123-1')); + t.false(pattern.test('/123-10')); + t.false(pattern.test('/1234-10')); + t.false(pattern.test('/1234-10/1')); + t.false(pattern.test('/1234-10/as')); + t.true(pattern.test('/1234-10/01/')); + t.true(pattern.test('/2019-10/30')); + + // exec results, array access + let [url, year, month, day] = pattern.exec('/2019-05/30/'); + t.is(url, '/2019-05/30', '~> executing pattern on correct trimming'); + t.is(year, '2019', '~> executing pattern gives correct "year" value'); + t.is(month, '05', '~> executing pattern gives correct "month" value'); + t.is(day, '30', '~> executing pattern gives correct "day" value'); + + // exec results, named object + t.toExec(rgx, '/2019-10/02', { year:'2019', month:'10', day:'02' }); + t.toExec(rgx, '/2019-10/02/narnia', { year:'2019', month:'10', day:'02' }); + + t.end(); + }); + + test('(RegExp) param :: suffix', t => { + let rgx = /^\/movies[/](?<title>\w+)\.mp4/i; + let { keys, pattern } = fn(rgx); + t.same(keys, false, '~> keys = false'); + t.same(rgx, pattern, '~> pattern = input'); + + // RegExp testing (not regexparam related) + t.false(pattern.test('/movies')); + t.false(pattern.test('/movies/')); + t.false(pattern.test('/movies/foo')); + t.false(pattern.test('/movies/foo.mp3')); + t.true(pattern.test('/movies/foo.mp4')); + t.true(pattern.test('/movies/foo.mp4/')); + + // exec results, array access + let [url, title] = pattern.exec('/movies/narnia.mp4'); + t.is(url, '/movies/narnia.mp4', '~> executing pattern on correct trimming'); + t.is(title, 'narnia', '~> executing pattern gives correct "title" value'); + + // exec results, named object + t.toExec(rgx, '/movies/narnia.mp4', { title: 'narnia' }); + t.toExec(rgx, '/movies/narnia.mp4/', { title: 'narnia' }); + + t.end(); + }); + + test('(RegExp) param :: suffices', t => { + let rgx = /^\/movies[/](?<title>\w+)\.(mp4|mov)/i; + let { keys, pattern } = fn(rgx); + t.same(keys, false, '~> keys = false'); + t.same(rgx, pattern, '~> pattern = input'); + + // RegExp testing (not regexparam related) + t.false(pattern.test('/movies')); + t.false(pattern.test('/movies/')); + t.false(pattern.test('/movies/foo')); + t.false(pattern.test('/movies/foo.mp3')); + t.true(pattern.test('/movies/foo.mp4')); + t.true(pattern.test('/movies/foo.mp4/')); + t.true(pattern.test('/movies/foo.mov/')); + + // exec results, array access + let [url, title] = pattern.exec('/movies/narnia.mov'); + t.is(url, '/movies/narnia.mov', '~> executing pattern on correct trimming'); + t.is(title, 'narnia', '~> executing pattern gives correct "title" value'); + + // exec results, named object + t.toExec(rgx, '/movies/narnia.mov', { title: 'narnia' }); + t.toExec(rgx, '/movies/narnia.mov/', { title: 'narnia' }); + + t.end(); + }); + + test('(RegExp) param :: optional', t => { + let rgx = /^\/books[/](?<author>[^/]+)[/]?(?<title>[^/]+)?[/]?$/ + let { keys, pattern } = fn(rgx); + t.same(keys, false, '~> keys = false'); + t.same(rgx, pattern, '~> pattern = input'); + + // RegExp testing (not regexparam related) + t.false(pattern.test('/books')); + t.false(pattern.test('/books/')); + t.true(pattern.test('/books/smith')); + t.true(pattern.test('/books/smith/')); + t.true(pattern.test('/books/smith/narnia')); + t.true(pattern.test('/books/smith/narnia/')); + t.false(pattern.test('/books/smith/narnia/reviews')); + t.false(pattern.test('books/smith/narnia')); + + // exec results, array access + let [url, author, title] = pattern.exec('/books/smith/narnia/'); + t.is(url, '/books/smith/narnia/', '~> executing pattern on correct trimming'); + t.is(author, 'smith', '~> executing pattern gives correct value'); + t.is(title, 'narnia', '~> executing pattern gives correct value'); + + // exec results, named object + t.toExec(rgx, '/books/smith/narnia', { author: 'smith', title: 'narnia' }); + t.toExec(rgx, '/books/smith/narnia/', { author: 'smith', title: 'narnia' }); + t.toExec(rgx, '/books/smith/', { author: 'smith', title: undefined }); + + t.end(); + }); +} + +test('(RegExp) nameless', t => { + // For whatever reason~ + // ~> regexparam CANNOT give `keys` list cuz unknown + let rgx = /^\/books[/]([^/]\w+)[/]?(\w+)?(?=\/|$)/i; + let { keys, pattern } = fn(rgx); + t.same(keys, false, '~> keys = false'); + t.same(rgx, pattern, '~> pattern = input'); + + // RegExp testing (not regexparam related) + t.false(pattern.test('/books')); + t.false(pattern.test('/books/')); + t.true(pattern.test('/books/smith')); + t.true(pattern.test('/books/smith/')); + t.true(pattern.test('/books/smith/narnia')); + t.true(pattern.test('/books/smith/narnia/')); + t.false(pattern.test('books/smith/narnia')); + + // exec results, array access + let [url, author, title] = pattern.exec('/books/smith/narnia/'); + t.is(url, '/books/smith/narnia', '~> executing pattern on correct trimming'); + t.is(author, 'smith', '~> executing pattern gives correct value'); + t.is(title, 'narnia', '~> executing pattern gives correct value'); + + // exec results, named object + // Note: UNKNOWN & UNNAMED KEYS + t.toExec(rgx, '/books/smith/narnia', {}); + t.toExec(rgx, '/books/smith/narnia/', {}); + t.toExec(rgx, '/books/smith/', {}); + + t.end(); +}); diff --git a/types.d.ts b/types.d.ts index 167bd15..5aa4f10 100644 --- a/types.d.ts +++ b/types.d.ts @@ -1,6 +1,11 @@ -export interface RouteParsed { +declare function regexparam(route: string, loose?: boolean): { keys: Array<string>, pattern: RegExp } -declare const regexparam: (route: string, loose?: boolean) => RouteParsed; + +declare function regexparam(route: RegExp): { + keys: false, + pattern: RegExp +} + export default regexparam;