WHATWG URLPattern

Elide implements the WHATWG [URL Pattern Standard][spec] URLPattern global — a routing-oriented URL matcher with named captures, per-component regex groups, and the full {...} grouping / modifier grammar. The underlying regex engine is graal-js's TRegex, so user-provided (regex) groups get full ECMAScript RegExp semantics (including Unicode property escapes and variable-length lookbehind).

[spec]: https://urlpattern.spec.whatwg.org/

Construction

js
new URLPattern(input, baseURL?, options?)   // string form
new URLPattern(init, options?)               // init-dict form
ArgumentTypeNotes
inputUSVString \URLPatternInitFull-URL pattern string (e.g. "https://*.example.com/api/:id") or an init dict with per-component patterns.
baseURLUSVStringOnly valid for the string form; resolves a relative pattern against the base URL's protocol / host / etc.
optionsURLPatternOptions{ ignoreCase?: boolean = false } — when true, all component regexes compile with the i flag.
Calling URLPattern(...) without new throws TypeError — per WebIDL, interface objects with constructors must be invoked with new.

URLPatternInit

ts
interface URLPatternInit {
  protocol?: string;
  username?: string;
  password?: string;
  hostname?: string;
  port?: string;
  pathname?: string;
  search?: string;
  hash?: string;
  baseURL?: string;
}

Any component not specified defaults to the pattern "" (matches an empty component). The optional baseURL field lays down defaults for unspecified components.

Readonly attributes

NameTypeNotes
protocolstringThe compiled pattern source for this component (already canonicalised).
usernamestring
passwordstring
hostnamestring
portstring
pathnamestring
searchstringWithout a leading ?.
hashstringWithout a leading #.
hasRegExpGroupsbooleantrue if any component pattern uses an explicit (regex) group.

Operations

js
pattern.test(input?, baseURL?)     // → boolean
pattern.exec(input?, baseURL?)     // → URLPatternResult | null
  • input may be a URL string or a URLPatternInit dict. When it's a string and baseURL is provided, the string is resolved against the base.
  • exec returns null on mismatch; otherwise a URLPatternResult:
ts
interface URLPatternResult {
  inputs: Array<string | URLPatternInit>;
  protocol: { input: string; groups: Record<string, string | undefined> };
  username: { input: string; groups: Record<string, string | undefined> };
  // ... one entry per URL component
}

Pattern grammar

The pattern source goes through a tokenizer, a component-aware parser, and a regex generator before being handed to the ECMAScript regex compiler:
SyntaxMeaning
literalMatches verbatim (canonicalised per component).
:nameNamed capture; matches the component's segment-wildcard regex.
:name(regex)Named capture using a custom ECMA regex.
(regex)Unnamed (auto-indexed) capture using a custom regex.
*Full-wildcard shorthand for (.*).
{group}Explicit grouping; prefix / suffix literals between braces are treated atomically.
?, +, *Modifiers after a part — optional, one-or-more, zero-or-more.
\xEscape — matches the literal character x even if it's a metacharacter.
Per-component defaults (segment-wildcard and separator):
ComponentPrefixSegment-wildcard regex
protocol[^:]+?
username[^:@/]+?
password[^:@/]+?
hostname[^.:/?#]+?
port[^/?#]+?
pathname/[^/?#]+?
search[^#]+?
hash[^]+?

Canonicalization

Pattern and input strings are both canonicalised per the URL Standard before matching, so a pattern written against a canonical form matches a loosely-typed user URL:

  • protocol — ASCII-lowercased.
  • hostname — IDNA toASCII + lowercased. IPv6 literals pass through verbatim.
  • port — leading zeros stripped, range-validated (0..65535).
  • pathname / search / hash / userinfo — percent-encoded per the URL spec's per-component encode sets (fragment set for hash, query set for search, path set for pathname, userinfo set for username / password). Already-well-formed percent triplets pass through without double-encoding.

Pattern inputs with embedded metacharacters (: { } ( ) * ? + \) preserve those characters verbatim during canonicalization so they're seen by the pattern tokenizer.

Examples

js
// Named path segment.
const p = new URLPattern({ pathname: "/users/:id" });
p.test({ pathname: "/users/42" });                // true
p.exec({ pathname: "/users/42" }).pathname.groups; // { id: "42" }

// Full URL with wildcard host.
const api = new URLPattern("https:<<>>
api.test("https://v2.example.com/api/users");     // true
api.exec("https:<<>>

<<>>
const numeric = new URLPattern({ pathname: "/items/:id(\\d+)" });
numeric.test({ pathname: "/items/abc" });         <<>>
numeric.test({ pathname: "/items/123" });         <<>>

<<>>
const opt = new URLPattern({ pathname: "/users/:id?" });
opt.test({ pathname: "/users" });                 <<>>
opt.test({ pathname: "/users/42" });              <<>>

<<>>
const multi = new URLPattern({ pathname: "/files/:path+" });
multi.exec({ pathname: "/files/a/b/c" }).pathname.groups.path;  <<>>

<<>>
const rel = new URLPattern("/items/:id", "https://example.com/");
rel.test("https://example.com/items/7");          // true

Notes on the implementation

  • Plain-call URLPattern(...) is routed to TypeError via a manual construct?-ternary patch in the generated JSURLPatternClassBase.java (same pattern as the other Phase 2 classes). elide-dev/bindgen emits this branch automatically on the next regenerate.
  • Compiled regexes are produced via RegexCompilerInterface.compile(...) — graal-js's TRegex compiler — so custom (regex) groups get full ECMAScript semantics, not Java's near-compatible subset.
  • Capture names are tracked in a parallel list rather than baked into the regex as (?...). This is necessary because the spec's + / * modifier forms require an outer repeating group that ECMA doesn't allow to re-bind a named group on each repetition; the two-list design keeps group extraction a single array lookup.
  • The class is installed via installAsGlobal (not putBuiltinClass) for the same reason as AbortSignal / AbortController: graal-js's per-realm shape-slot array is fixed at JSContext creation and our non-ordinary URLPatternObject layout collides with graal-js's own JSOrdinaryObject shapes at high indices. The custom installer uses JSObjectFactory.createBound to keep the shape on the factory itself.