Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
language: node_js
node_js:
- 10
- 6
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
50 changes: 48 additions & 2 deletions readme.md
Original file line number Diff line number Diff line change
@@ -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.

Expand Down Expand Up @@ -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`~!<br>
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[/](?<year>[0-9]{4})[/](?<month>[0-9]{2})[/](?<title>[^\/]+)/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`

Expand All @@ -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

Expand Down
3 changes: 2 additions & 1 deletion src/index.js
Original file line number Diff line number Diff line change
@@ -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()) {
Expand Down
214 changes: 214 additions & 0 deletions test/index.js
Original file line number Diff line number Diff line change
@@ -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;
}
Expand Down Expand Up @@ -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();
});
9 changes: 7 additions & 2 deletions types.d.ts
Original file line number Diff line number Diff line change
@@ -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;