npm package: @jreusch/router
Description: Core of @jreusch/router, providing framework-agnostic parsing/matching functionality.
Author: Joshua Reusch
Latest version: 1.0.3

This is a simple yet powerful routing library with bindings for various frameworks that I use.

This package contains the core logic (parsing and matching patterns and urls), and does not contain any real "router" or framework-specific stuff. It has 0 dependencies aside from built-in support for modern Javascript (modules, regular expressions, etc.) and should be able to run anywhere where there is Javascript.

# Features

* 🚀 **Tiny** - only 300LOC of Typescript, ~1k minified+gziped in production
* 🤘 **Well-tested** - Lots of tests for the core functionality, used in hundreds of websites
* 🤩 **Powerful** supports custom patterns, backtracking, capturing arrays, different router backends, and more!

# Matching API

The matching API is the core of the libary. It parses `patterns`, which are special URLs, and provides functions to match patterns against other URLs, or stringify a pattern back into a URL using the extracted values.

You can learn more about the way patterns and segments work by studying the examples, or looking at the [in-depth guide](#in-depth-syntax-and-semantics) below.

## parse(pattern: string): Pattern

parses a pattern string into a pattern, which is the internal representation this library uses to then be able to match or stringify things. The exact pattern syntax and semantics are described below in the [in-depth guide](#in-depth-syntax-and-semantics).

Patterns support static matches, named variabless, optional and repeatable variables, custom regular expressions, and wildcards.

The syntax is roughly similar to [vue-router](https://router.vuejs.org/guide/essentials/route-matching-syntax.html), if you are familiar with that. Patterns might look like this:

### Examples:

```js
parse('/hello/world') // static route
parse('/user/:userId') // simple variable
parse('/posts/:postId?') // optional variable
parse('/user/:userId([0-9]+)') // variable with a custom regex
parse('/*') // wildcard
parse('/:slug*') // named wildcard
parse('/:slug+/edit') // ... with support for backtracking
```

## match(pattern: Pattern, url: string, allowPartial = false): Params|null

Check if a url matches a pattern. If a match is successful, returns an object with all variable values. Repeatable variables will always create an array, regardless of whether or not the value is actually repeated. Optional variables will be `undefined`.

### Examples:

```js
match(parse('/user/:userId'), '/user/1234') // { userId: '1234' }
match(parse('/posts/:postId?'), '/posts') // {}
match(parse('/posts/:postId?'), '/posts/hello-world') // { postId: 'hello-world' }
match(parse('/:slug*'), '/hello/world') // { slug: ['hello', 'world'] }
match(parse('/:slug+/edit'), '/hello/world/edit') // { slug: ['hello', 'world'] }
match(parse('/:slug+/edit'), '/hello/world/edit/edit') // { slug: ['hello', 'world', 'edit'] }
```

If you pass `allowPartial`, match returns as soons as the pattern is exhausted instead of trying to match the whole URL somehow. While this is useful for checking if a URL starts with a pattern, it might lead to somewhat unexpected params being returned:

```js
// since slug is matched after a single part, this returns { slug: ['hello'] }
// note that it does NOT match the entire URL, and it does NOT return { slug: [] },
// even though '/' would also already satisfy the pattern!
match(parse('/:slug*'), '/hello/world', true) // { slug: ['hello'] }
```

## stringify(pattern: Pattern, params: Params): string

Do the reverse of match, and create an absolute url based on a pattern and some parameters.

### Examples:

```js
stringify(parse('/user/:userId'), { userId: 1234 }) // '/user/1234'
stringify(parse('/posts/:postId?'), { postId: null }) // '/posts'
stringify(parse('/posts/:postId?'), { postId: 'hello' }) // '/posts/hello'
```

## encode(url: string): string

Escapes the URL to make sure it is parsed correctly as a static URL.
You do not need to call this function if the URL is encoded by `encodeURIComponent` or `encodeURI` already, for example, URLs returned by `stringify`.

Currently, the only thing that needs to be escaped is colons following slashes:

```js
encode('/:hello world') // ==> '/%2Ahello world'
```

## getUrl(pattern: string, params?: Params): string

Utility function that parses and stringifies the pattern, if params are provided. Otherwise, the provided pattern is `encode()`-ed and returned without parsing it further.

```js
getUrl('/hello/:world') // ==> '/%hello/%2Aworld'
getUrl('/hello/:world', { world: 'sailor' }) // ==> /hello/sailor
getUrl('/hello/:world', {}) // ==> /hello
```

# Client-side routers

Routers provide an abstraction similar to HTML5's `History` API. They provide the backbone of the various client-side framework integrations. If you want to use this library for server-side routing, don't worry! All of these components are entirely optional and perfectly tree-shakable, and it's perfectly fine and encouraged even to not use the provided router implementations, or none at all!

Using a router is usually wrapped by the framework-specific library for the most part, but using them directly might look like this:

```js
// create a router object
const router = createPathRouter()

// subscribe to URL changes
let url = router.getUrl()
const unsubscribe = router.subscribe(newUrl => {
    url = newUrl
})

// programmatically navigate
const params = match(parse('/posts/:postId'), url);
if (params) {
    router.navigate(getUrl('/blog/:postId', params), true)
}
```

## createPathRouter(): Router

Create a Router using the `location.pathname` property, using the browsers history API under the hood.

## createPathRouterWithBase(base: string): Router

Similar to `createPathRouter`, but also allows specifying a base url. All routes will be prefixed by that base, and URLs outside of the base will not routed.

```js
createPathRouterWithBase('/') // Same as createPathRouter()
createPathRouterWithBase('/base') // pathname = '/base/hello', getUrl() = '/hello'
```

## createHashRouter(): Router

Creates a Router using the `location.hash` property, resulting in URLs like this:

```
/#/
/widget/#/hello
/index.php#/blog/1234/hello-sailor
```

Hash-based routing still uses the history API under the hood and is provided for situations where using the pathname is not feasable, not to support pre-historic (pun intended) browsers.

## createMemoryRouter(base? string): Router

A router working fully in-memory, suitable for server-side rendering purposes.
You can optionally provide a `base` url to change the href urls returned by `toHref`.

```js
createMemoryRouter() // basic usage
createMemoryRouter('/base') // <Link href="/path"> ==> <a href="/base/path">
```

# Router API

## router.getUrl(): string|null

Get the current URL that should be  used to match against.
If `null` is returned, the router cannot handle the current URL, for example because the base does not match.

## router.toHref(url: string): string

Take a URL (in the form `getUrl` would return), and turn it into a "real" URL that can be set as an href attribute on `<a>` tags, for example.

## router.subscribe(onUrlChange: (newUrl: string|null) => any): () => void

Register a new event handler for whenever the URL changes, either programmatically by calling other router methods, or by some other user interaction. Returns a `unsubscribe` function, making it easy to use as an effect hook.

## router.navigate(url: string, replace = false)

Navigate to a different url. URL is supposted to be in the format `getUrl` returns (and **not** the one `toHref` would produce), so for example if there is a base url, this function would expect to be given a URL **without** that base.

If `replace` is set, replaces the current history entry instead of navigating away.

## router.go(delta: number)

Go to a different place in the history, analogous to the browsers function of the same name.

```js
router.go(-1) // go back, if possible
router.go(1) // go forward, if possible
```

# In-depth syntax and semantics

URLs and patterns in this library are generally thought of being slash-separated lists of so-called `segments`. Empty segments are by convention always ignored. Let's look at a pattern as an example:

```
/blog/:postId([0-9]+)/:slug*
```

This pattern is made up of 3 segments:

```js
['blog', 'postId([0-9]+)', ':slug*']
```

Every segment can then be categorized into 4 different types: Static, Variable, Custom, and Wildcard, each matching a corresponding URL differently.

So the 2 core functions `parse` and `match` do exactly that: `parse` turns a pattern string into an array of parsed segments, and `match` then compares 2 lists of segments, collecting parameter values along the way.

## Static Segments

The most basic type of segment is called _Static_. Static Segments are the pieces of your URL pattern that cannot change, so in our example, `blog` would be a static segment. Actual URLs can often be thought of as pattern made up entirely of Static Segments. The `<Link>` and `useMatch` components provided by the framework libraries work exactly like that, by turning a URL into a valid static pattern, using the `encode` function to make sure we do not accidentily run into weird bits in the URL.

Basically, every segment that does not fall into any of the other categories will be parsed as a Static Segment instead.

Static Segments match URL segments only if they are entirely equal to each other, but ignoring case. Our `blog` segment might therefore match URLs like this:

```
/blog
/BLOG
/BlOg/
```

However, it does **NOT** match these:

```
/blo
/blög
/blog-posts
/b/log/
```

## Variable Segments

Variable segments allow you to capture a piece of the URL in a parameter variable, which is the main method dynamic routing is enabled.

Variable segments always start with a colon (`':'`) followed by the name of the variable. While it is not enforced, I highly recommend to keep variable names simple, and to use `camelCase` names following Javascript conventions.

After the variable name, you can also specify a modifier, changing the way this segment is matched:

| Modifier | Cardinality | Effect                                                          |
|----------|-------------|-----------------------------------------------------------------|
| `*`      | 0-many      | variable can match none or multiple segments                    |
| `+`      | 1-many      | variable can match multiple segments, but must match at least 1 |
| `?`      | 0 or 1      | optional variable                                               |

In our example, `:slug*` would be a variable segment, setting the parameter `slug`, by matching any number of remaining segments.


Here are some URLs this variable would match, including the resulting parameter values:

```js
'/something-wicked-this-way-comes' // ['something-wicked-this-way-comes']
'/here/comes/the/money' // ['here', 'comes', 'the', 'money']
```

Repeated segments (`*` and `+`) always always work in a _lazy_ manner, so they only consume as much as they absolutetly need. Matching will always try to first continue with the next segment, backtracking only if that turns out to be impossible.

Optional segments are always tried first before backtracking, so `?` and `*` also behave eagerly. For `*`, this means that the first match is eagerly done, while subsequent parts will be matched lazily.

The most unexpected property of this is that optional segments that matched something are never backtracked - for example, this pattern does not match:

```js
match(parse('/:a?/:b+'), '/a') // === null
```

`/a` is matched by the first optional segment (`:a?`). The `:b+` segment is _not_ optional, but fails to match anything since the input got already fully consumed.

**Using multiple wildcards in the same pattern might therefore be susceptible to attacks similar to regex-based DOS attacks and should be avoided at all costs!**

For the most common case, where you might want to partially match a URL with a wildcard in the middle, you can use the optional `allowPartial` argument to `match` instead, which will stop eagerly.

## Wildcard Segments

A simple variation of variables are wildcard patterns, which are the result of just leaving out the variable name, resulting into patterns like this:

| Wildcard | Description                                    |
|----------|------------------------------------------------|
| `/:`     | Matches a single segment                       |
| `/*`     | Matches anything, or nothing                   |
| `/+`     | Matches a single segment, or multiple segments |
| `/?`     | Matches a single segment, if not at the end    |

Note that `/?` in patterns has a different special meaning than in URLs, so I recommend always using `/:?` or `/*` instead.

## Custom Segments

Custom Segments extend Variable Segments by allowing you to provide a custom regular expression for that segment. The variable will then only match the URL if the regex also matches.

The URL segment has to match the regex entirely in a case-insensitive manner. Note that the `match` algorithm does **not** use regular expressions under the hood itself, and this is **not** a way to "paste in" some custom regex code into the middle.

You provide a custom regex by putting it into parentheses after the variable name. Since you most likely provide a string literal here, please remember to double-escape all regex escape patterns, like you would when using the `RegExp` constructor (so `/\d+/` becomes `':id(\\d+)'`).

Custom Segments can also be combined with modifiers following the regular expression.

```js
// "useless" regex, behaving exacly like a normal variable would
':title(.*)'

// match numeric ids only
':postId([0-9]+)' // matches '/0123', but not '/asdf'

// match UUIDs with nested parens
':uuid([0-9a-f]{8}(-[0-9a-f]{4}){3}-[0-9a-f]{12})'

// match more than 1 single-word segments
':words(\\w+)+'
```

# Support / Climate action

This library was made with ☕, 💦 and 💜 by [joshua](https://joshi.monster)
If you really like what you see, you can [Buy me more ☕](https://www.paypal.com/donate/?hosted_button_id=CNT7EUVAM6MZS), or [get in touch](jreusch4+oss@gmail.com)!

If you work on limiting climate change, preserving the environment, et al. and any of my software is useful to you, please let me know if I can help and prioritize issues!