Skip to content

Commit

Permalink
Merge pull request #33 from easing/main
Browse files Browse the repository at this point in the history
Named capturing groups proposal
  • Loading branch information
ai authored Apr 17, 2024
2 parents ade1bad + 6c8e762 commit 6f3616c
Show file tree
Hide file tree
Showing 4 changed files with 44 additions and 28 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
A tiny URL router for [Nano Stores](https://github.com/nanostores/nanostores)
state manager.

* **Small.** 696 bytes (minified and brotlied). Zero dependencies.
* **Small.** 684 bytes (minified and brotlied). Zero dependencies.
* Good **TypeScript** support.
* Framework agnostic. Can be used with **React**, **Preact**, **Vue**,
**Svelte**, **Angular**, **Solid.js**, and vanilla JS.
Expand Down
2 changes: 1 addition & 1 deletion index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ type PathToParams<PathArray, Params = {}> = PathArray extends [

type ParseUrl<Path extends string> = PathToParams<Split<Path, '/'>>

export type RouterConfig = Record<string, Pattern<any> | string>
export type RouterConfig = Record<string, Pattern<any> | RegExp | string>

export type ConfigFromRouter<SomeRouter> = SomeRouter extends Router<
infer Config
Expand Down
55 changes: 30 additions & 25 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,28 +4,24 @@ export function createRouter(routes, opts = {}) {
let router = atom()
router.routes = Object.keys(routes).map(name => {
let value = routes[name]
if (typeof value === 'string') {
value = value.replace(/\/$/g, '') || '/'
let names = value.match(/(?<=\/:)\w+/g) || []
let pattern = value
.replace(/[\s!#$()+,.:<=?[\\\]^{|}]/g, '\\$&')
.replace(/\/\\:\w+\\\?/g, '(?:/((?<=/)[^/]+))?')
.replace(/\/\\:\w+/g, '/([^/]+)')
return [
name,
RegExp('^' + pattern + '$', 'i'),
(...matches) =>
matches.reduce((params, match, index) => {
// match === undefined when nothing captured in regexp group
// and we swap it with empty string for backward compatibility
params[names[index]] = match ? decodeURIComponent(match) : ''
return params
}, {}),
value
]
} else {
return [name, ...value]

if (typeof value !== 'string') {
return [name, ...[value].flat()]
}

value = value.replace(/\/$/g, '') || '/'

let pattern = value
.replace(/[\s!#$()+,.:<=?[\\\]^{|}]/g, '\\$&')
.replace(/\/\\:(\w+)\\\?/g, '(?:/(?<$1>(?<=/)[^/]+))?')
.replace(/\/\\:(\w+)/g, '/(?<$1>[^/]+)')

return [
name,
RegExp('^' + pattern + '$', 'i'),
null,
value
];
})

let prev
Expand All @@ -37,12 +33,21 @@ export function createRouter(routes, opts = {}) {
let url = new URL(path, 'http://a')
if (!opts.search) path = url.pathname

let search = Object.fromEntries(url.searchParams)

for (let [route, pattern, cb] of router.routes) {
for (let [route, pattern, callback] of router.routes) {
let match = path.match(pattern)
if (match) {
return { params: cb(...match.slice(1)), path, route, search }
return {
// If route has callback for params decoding use it. Otherwise decode params from named capture groups
params: callback ? callback(...match.slice(1)) : Object.keys({...match.groups}).reduce((pars, key) => {
// match === undefined when nothing captured in regexp group
// and we swap it with empty string for backward compatibility
pars[key] = match.groups[key] ? decodeURIComponent(match.groups[key]) : '';
return pars
}, {}),
path,
route,
search: Object.fromEntries(url.searchParams)
}
}
}
}
Expand Down
13 changes: 12 additions & 1 deletion test/router.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ function createTag(
let router = createRouter({
draft: [/\/posts\/(draft|new)\/(\d+)/, (type, id) => ({ id, type })],
home: '/',
named: /\/named\/(?<type>draft|new)\/(?<id>\d+)/,
optional: '/profile/:id?/:tab?',
post: '/posts/:categoryId/:id',
posts: '/posts/',
Expand Down Expand Up @@ -398,7 +399,7 @@ test('ignores the same URL in manual URL', () => {
deepStrictEqual(events, [])
})

test('allows RegExp routes', () => {
test('allows RegExp routes with callback', () => {
changePath('/posts/draft/10/')
deepStrictEqual(router.get(), {
params: { id: '10', type: 'draft' },
Expand All @@ -408,6 +409,16 @@ test('allows RegExp routes', () => {
})
})

test('allows RegExp routes without callback', () => {
changePath('/named/draft/10/')
deepStrictEqual(router.get(), {
params: { id: '10', type: 'draft' },
path: '/named/draft/10',
route: 'named',
search: {}
})
})

test('generates URLs', () => {
equal(getPagePath(router, 'home'), '/')
equal(getPagePath(router, 'home', {}), '/')
Expand Down

0 comments on commit 6f3616c

Please sign in to comment.