npm package: @qezor/urlex
Description: Zero-dependency URL pattern matcher with named params, wildcards, ranged globs and literal sets
Author: Abdul Ahad
Latest version: 1.0.10

# @qezor/urlex

A **zero-dependency, production-grade URL pattern matcher** for Node.js and any JavaScript runtime.

Patterns are compiled once and reused for efficient repeated matching. Supports host-based routing, named params, wildcards, ranged globs with backtracking, literal sets, signed integer bounds, and literal escaping.

---

## Installation

```bash
npm install @qezor/urlex
```

---

## Import / Require

```js
// CommonJS
const { compile, match, parse, matchHost, matchPath } = require("@qezor/urlex")

// ES Modules
import { compile, match, parse, matchHost, matchPath } from "@qezor/urlex"
```

---

## Quick Start

```js
const { compile, match } = require("@qezor/urlex")

const pattern = compile("example.com/users/:id")

// With param capture
const params = match(pattern, "https://example.com/users/42", true)
console.log(params)  // { id: "42", "**": [] }

// Match only — zero allocation
const ok = match(pattern, "https://example.com/users/42", false)
console.log(ok)  // true

// No match
console.log(match(pattern, "https://example.com/posts/1", true))  // null
```

---

## Pattern Syntax

### Literal segment

Exact match. Case-sensitive.

```
example.com/users/profile
```

Matches `https://example.com/users/profile` only.

---

### `:name` — named required param

Captures exactly one segment.

```js
const c = compile("example.com/users/:id")
match(c, "https://example.com/users/42", true)
// → { id: "42", "**": [] }
```

---

### `*:name` — named optional param

Consumes one segment if present, empty string if absent. Best used at the end of a pattern.

```js
const c = compile("example.com/api/*:version/users")
match(c, "https://example.com/api/v2/users", true)  // → { version: "v2", "**": [] }
match(c, "https://example.com/api/users",    true)  // → { version: "",   "**": [] }
```

---

### `*` — unnamed single-segment wildcard

Matches exactly one segment, no capture.

```js
const c = compile("example.com/files/*/download")
match(c, "https://example.com/files/report/download", false)  // → true
match(c, "https://example.com/files/download",        false)  // → null
```

---

### `**` — unlimited glob

Matches all remaining segments. **Must be the last segment** — throws at compile time otherwise.
Captured in `params["**"][n]` as an array of segments.

```js
const c = compile("example.com/static/**")
match(c, "https://example.com/static/assets/img/logo.png", true)
// → { "**": [ ["assets", "img", "logo.png"] ] }

match(c, "https://example.com/static/", true)
// → { "**": [ [] ] }   ← zero remaining segments
```

---

### `[n]` — exact segment count

Matches exactly `n` segments. Captured in `params["**"]`.

```js
const c = compile("example.com/[3]/end")
match(c, "https://example.com/a/b/c/end", true)
// → { "**": [ ["a", "b", "c"] ] }
```

---

### `[min,max]` — ranged segment count

Matches between `min` and `max` segments. Range nodes can appear **anywhere** in a pattern — backtracking handles downstream matching automatically.

```js
const c = compile("example.com/[1,3]/end")
match(c, "https://example.com/a/b/end",       true)  // → matched, "**"[0] = ["a","b"]
match(c, "https://example.com/a/b/c/d/end",   true)  // → null (over max)
```

Variants:

| Pattern | Meaning |
|---|---|
| `[n]` | exactly n segments |
| `[min,max]` | between min and max |
| `[min,]` | at least min |
| `[,max]` | at most max |
| `[-3,5]` | signed bounds — negative min supported |
| `[-5,-1]` | both bounds negative |

---

### `{a,b,c}` — literal set (OR match)

Matches exactly one segment that equals any value in the set. Useful for file extensions, method names, fixed enums.

```js
const c = compile("example.com/files/{jpg,png,gif}")
match(c, "https://example.com/files/jpg", false)  // → true
match(c, "https://example.com/files/pdf", false)  // → null
```

---

### Escaping

Backslash escapes a special character, treating it as a plain literal.

| Escape | Produces |
|---|---|
| `\{` | literal `{` |
| `\*` | literal `*` |
| `\:` | literal `:` |
| `\\` | literal `\` |

```js
compile("example.com/\\{3}")   // matches the literal string "{3}"
compile("example.com/\\*")     // matches the literal string "*"
compile("example.com/\\:id")   // matches the literal string ":id"
```

---

## Host Matching

Hosts are matched **right-to-left**, making wildcard subdomain patterns natural.
Host labels are matched **case-insensitively** and `*` matches exactly one host label.

```js
compile("*.example.com/page")        // any subdomain
compile("api.example.com/v1/**")     // specific subdomain
compile("*.*.example.com/path")      // two-level wildcard
compile("/users/:id")                // path-only — matches any host
```

```js
const c = compile("*.example.com/page")
match(c, "https://blog.example.com/page",   false)  // → true
match(c, "https://cdn.example.com/page",    false)  // → true
match(c, "https://example.com/page",        false)  // → null
match(c, "https://other.com/page",          false)  // → null
```

---

## Glob Captures — `params["**"]`

`params["**"]` is always an array — one entry per glob or range in the pattern, in pattern order. Each entry is an array of the matched segments (never joined — no information loss).

```js
// Single glob
const c = compile("example.com/static/**")
match(c, "https://example.com/static/img/logo.png", true)
// → { "**": [ ["img", "logo.png"] ] }
//              ↑ index 0 = first glob

// Multiple globs in order
const c2 = compile("example.com/[1,2]/mid/[1,2]/end")
match(c2, "https://example.com/a/b/mid/x/y/end", true)
// → { "**": [ ["a","b"], ["x","y"] ] }
//              ↑ index 0   ↑ index 1

// Mix of named params and ranges
const c3 = compile("example.com/:version/[1,2]/users/:id")
match(c3, "https://example.com/v2/a/b/users/99", true)
// → { version: "v2", id: "99", "**": [ ["a","b"] ] }
```

When no glob or range is in the pattern, `params["**"]` is always `[]` — never null, never undefined, safe to access without checks.

---

## `save=false` — zero allocation fast path

When you only need to know if a URL matches without capturing params, pass `save=false`. No params object allocated, no array created — pure boolean check.

```js
// Route filtering — check match only
if (match(compiled, url, false)) {
  // matched
}
```

---

## URL Handling

The parser handles any protocol, strips port, query string, and hash automatically.

```
https://example.com:8080/users/42?q=hello#section
                        ↓
                 example.com/users/42
```

Supported protocols: `http`, `https`, `ws`, `wss`, `ftp`, and any other `scheme://` format.

---

## API

### `compile(pattern)` → CompiledPattern

Compiles a pattern string into an optimized structure. Throws clearly at compile time on invalid or illegal patterns — never silently at match time.

```js
const compiled = compile("example.com/users/:id")
// → { host: ["example","com"], path: [":id"] }
```

Throws on:
- `**` not in last position
- Empty literal set `{}`
- Invalid range `[5,2]` (max < min)
- Empty range `[]`

### `match(compiled, url, save)` → MatchParams | true | null

```
null  → no match
true  → matched, save=false
object → matched, save=true — params["**"] always present
```

### `parse(url)` → { host: string[], path: string[] }

Splits a raw URL into segments. Useful for building custom matchers on top.

```js
parse("https://api.example.com:3000/users/42?page=1")
// → { host: ["api","example","com"], path: ["users","42"] }
```

### `matchHost(pattern, actual)` → boolean

Low-level host matcher. Right-to-left, `["*"]` matches any host.

### `matchPath(pattern, actual, params, save)` → { matched, glob }

Low-level path matcher. Thin wrapper around the recursive backtracker.

---

## Error Handling

All errors thrown by `compile()` are plain `Error` instances with descriptive messages — caught at build time, never at request time.

```js
try {
  compile("example.com/**/users")   // ** not last
} catch (err) {
  console.log(err.message)
  // → '"**" must be the last segment in pattern: "example.com/**/users"'
}

compile("example.com/[5,2]/end")    // max < min
// → 'Range max < min: "[5,2]"'

compile("example.com/{}/end")       // empty set
// → 'Invalid literal set: "{}"'
```

---

## Testing

```bash
npm test
```

```
52 passed, 0 failed
```

---

## License

MIT © Abdul Ahad