zena:url
import {…} from 'zena:url';
URL parsing, resolution, percent-encoding, and URL pattern matching.
Provides URL for manipulating URLs and their components, URLSearchParams
for query parameters, URLPattern for routing and matching, and
percent-encoding utilities.
Examples ​
import { URL, URLSearchParams } from 'zena:url';
let u = new URL('https://example.com/api?format=json');
let format = u.searchParams.get('format');
Classes
URL
final class URL implements Hashable
An immutable, parsed URL. Components are exposed with the same names and
string shapes as the web API (protocol includes ':', search includes
'?', hash includes '#', all empty when absent).
new(rec: UrlRecord)
static parse(input: String, base: String | null = null): URL | null
Parses input, optionally resolving it against base, and returns null
if either fails to parse.
A URL that does not parse is an ordinary, recoverable outcome rather than an exceptional one, so there is no throwing constructor. Nothing is lost by returning null: the spec's own failure mode is the bare word "failure" — there is no error code, position, or reason to hand back.
static canParse(input: String, base: String | null = null): boolean
host(): String
"hostname:port", with the port omitted when it is null.
origin(): String
https://url.spec.whatwg.org/#concept-url-origin ("null" when opaque).
searchParams(): URLSearchParams
The query parsed as URLSearchParams.
A SNAPSHOT, not the web API's live-bound object: URL is immutable
here, so mutating the result does not touch this URL. Write changes
back by building a new URL from params.toString().
withProtocol(value: String): URL
This URL with a different scheme, e.g. withProtocol('http:'). A
trailing ':' is optional.
Returns an unchanged URL when the change is one the spec forbids:
moving between special schemes (http, https, ws, wss, ftp, file) and
non-special ones, making a URL with credentials or a port into a file:
URL, or making a hostless file: URL special.
withUsername(value: String): URL
This URL with a different username. Ignored, per the spec, on a URL that
cannot carry credentials — one with no host, or a file: URL.
withPassword(value: String): URL
This URL with a different password. Ignored like withUsername.
withHost(value: String): URL
This URL with a different host, optionally including a port
(withHost('example.com:8080')). Ignored on a URL with an opaque path,
which has no authority to change.
withHostname(value: String): URL
This URL with a different host, ignoring any port in value.
withPort(value: String): URL
This URL with a different port. An empty string removes the port, as does a port that is the scheme's default. Ignored on a URL that cannot carry one.
withPathname(value: String): URL
This URL with a different path. Ignored on a URL with an opaque path, whose path is not a list of segments to replace.
withSearch(value: String): URL
This URL with a different query. An empty string removes it; a leading '?' is optional.
withSearchParams(params: URLSearchParams): URL
This URL with the query replaced by params.
withHash(value: String): URL
This URL with a different fragment. An empty string removes it; a leading '#' is optional.
withHref(value: String): URL | null
This URL reparsed from value, or null when value does not parse —
unlike the other with* methods, replacing the whole URL has no
component to fall back to.
hashCode(): i32
Hashes the serialization, so equal URLs always hash equal — the
Hashable contract. Delegating to String.hashCode also means the
cached href carries the string's own cached FNV-1a hash, making
repeated map probes cheap after the first.
toString(): String
operator ==(other: URL): boolean
Two URLs are equal when they serialize identically.
Comparing href rather than the records is not a shortcut: the parser
canonicalizes as it goes — lowercasing the scheme and host, dropping a
default port, resolving ./.. — so URLs that differ as text but mean
the same thing already share a serialization. https://EXAMPLE.com:443/a/../b
equals https://example.com/b.
This is the spec's own notion of URL equality (https://url.spec.whatwg.org/#concept-url-equals), minus its optional "exclude fragments" flag: a fragment is part of the URL and is compared like any other component.
Part
sealed class Part
One piece of a parsed pattern.
prefix and suffix are the literal text bound to the part rather than
standing on its own — the / in /users/:id belongs to the id part, so
that an optional part takes its separator with it when it is absent. Both
are empty when there is none, which is a real answer rather than an absent
one, so they live on the base alongside the modifier and the component.
The variants are suffixed Part because Regex alone would collide with
zena:regex's, and URLPattern will import both.
abstract prefix: String
Literal text matched as-is. Carries no name: nothing captures it.
:name — everything up to the next separator.
* — everything, separators included.
(regex) — whatever source matches.
abstract suffix: String
abstract modifier: Modifier
abstract urlComponentType: URLComponentType
case FixedPart(value: String, prefix: String, suffix: String, modifier: Modifier, urlComponentType: URLComponentType)
case SegmentWildcardPart(name: String, prefix: String, suffix: String, modifier: Modifier, urlComponentType: URLComponentType)
case FullWildcardPart(name: String, prefix: String, suffix: String, modifier: Modifier, urlComponentType: URLComponentType)
case RegexPart(name: String, source: String, prefix: String, suffix: String, modifier: Modifier, urlComponentType: URLComponentType)
PatternSyntaxError
class PatternSyntaxError extends Error
Raised for a pattern string that cannot be parsed.
new(message: String)
URLPattern
final class URLPattern
A pattern over the components of a URL.
Construction normalizes: every component is parsed and written back out, so
the pattern a URLPattern reports is a canonical spelling of the one it
was given rather than the text that came in.
Matching is not implemented yet — this builds and exposes the component patterns only.
new(init: URLPatternInit)
Builds a pattern from init, with every component it leaves out
defaulting to * or inherited from init.baseURL.
There is no case-insensitivity option yet. It would only change the flags
on the compiled matchers, which do not exist here, so it arrives with
exec rather than being accepted and ignored.
URLPatternInit
final class URLPatternInit
One component's pattern, as supplied to the constructor.
Every field is nullable because absent and empty mean different things
here: an absent component may be inherited from baseURL, while an empty
one is a pattern that matches only the empty string. JavaScript draws that
line with undefined, which Zena does not have, so null draws it instead.
The fields are mutable and the constructor takes no arguments, so callers set only what they mean to constrain.
new()
URLSearchParams
final class URLSearchParams with IterableUtils<(String, String)> implements Iterable<(String, String)>
An ordered, mutable list of (name, value) pairs — the
application/x-www-form-urlencoded half of the WHATWG URL Standard
(https://url.spec.whatwg.org/#interface-urlsearchparams).
Order is significant and duplicate names are allowed, so this is a
multimap over a list, not a map. Unlike URL this type is mutable: it
is a collection/builder, like GrowableArray or StringBuilder.
Names and values are held DECODED. Percent-encoding and +-for-space
happen only at toString, so a value containing & or = round-trips.
new(init: String = '')
Parses init as an urlencoded string. A single leading ? is
ignored, so new URLSearchParams(url.search) does the expected
thing. Defaults to empty.
The parser returns its list as the read-only Array interface; this
class appends to and sorts that same list, so it takes it back as the
GrowableArray it was built as.
size: i32 { get; }
Number of pairs, counting duplicate names separately.
has(name: String, value: String | null = null): boolean
True if any pair has this name — or, when value is given, if any
pair has both this name and that value.
get(name: String): String | null
The first value for name, or null when there is none.
getAll(name: String): Array<String>
Every value for name, in order. Empty when there is none.
append(name: String, value: String): void
Appends a pair, keeping any existing pairs with the same name.
set(name: String, value: String): void
Sets name to a single value: updates the first pair with that
name and drops the rest, or appends when there is none. The first
pair keeps its position.
delete(name: String, value: String | null = null): void
Removes every pair with this name — or, when value is given, only
the pairs matching both.
sort(): void
Sorts pairs by name, preserving the relative order of pairs that share a name (an insertion sort, which is stable).
The spec sorts by UTF-16 code unit; this compares UTF-8 bytes, which
is code POINT order. The two agree on everything except a
supplementary-plane name ordered against U+E000..U+FFFF — see
compareByteWise.
toString(): String
Serializes to application/x-www-form-urlencoded (no leading ?).
6 inherited members
Iterable
contains(value: T): boolean
Returns true if the collection contains the specified value.
all(predicate: (item: T) => boolean): boolean
Returns true if all elements match the given predicate. Returns true if the collection is empty.
some(predicate: (item: T) => boolean): boolean
Returns true if at least one element matches the given predicate. Returns false if the collection is empty.
fold<R>(initial: R, combine: (acc: R, item: T) => R): R
Reduces the collection to a single value by accumulating state.
find(predicate: (item: T) => boolean): inline (true, T) | inline (false, _)
Finds the first element that matches the predicate. Returns an inline tuple (true, item) if found, or (false, _) if not.
filter(predicate: (item: T) => boolean): Iterable<T>
Returns a new iterable containing only the elements that match the predicate.
ConcurrentModificationError
class ConcurrentModificationError extends Error
Thrown when a URLSearchParams is mutated while an iteration over it is
still in progress.
This is a deliberate divergence from the web API, which iterates live: in
JS, deleting a pair during a for...of silently SKIPS the following pair
(WebIDL's default pair iterator indexes into the current list and re-reads
it on every step), and appending during iteration never terminates. Both
produce a wrong answer with no signal, so this follows Java's fail-fast
collections instead and reports the bug where it happens.
Like Java's, the check is best-effort: it exists to catch mistakes, not to make concurrent mutation safe.
Lives here rather than in zena:error because URLSearchParams is the
only collection that currently detects this; it should move if others
adopt the same discipline.
new()
Enums
EncodeSet
enum EncodeSet
The percent-encode sets defined by the URL Standard. Each set includes all of the sets above it (C0 control ⊂ fragment ⊂ ... is not a strict chain — fragment and query diverge — but every set includes the C0 control set).
Modifier
enum Modifier
How many times a part may repeat.
URLComponentType
enum URLComponentType
Which URL component a pattern was written for.
Functions
byteNeedsPercentEncoding
function byteNeedsPercentEncoding(b: i32, set: EncodeSet): boolean
True when byte b must be percent-encoded for set. Bytes outside
0x20..0x7E (C0 controls, DEL, and all non-ASCII bytes) are encoded in every
set.
percentEncode
function percentEncode(input: String, set: EncodeSet, spaceAsPlus: boolean = false): String
Percent-encodes input for the given encode set. spaceAsPlus implements
the form-urlencoded serializer's special case (0x20 → '+').
percentDecode
function percentDecode(input: String, plusAsSpace: boolean = false): String
Percent-decodes input (https://url.spec.whatwg.org/#percent-decode).
A '%' not followed by two hex digits is passed through unchanged, per spec.
plusAsSpace implements the form-urlencoded parser's '+' → 0x20 rule.
encodeByteInto
function encodeByteInto(sb: StringBuilder, b: i32, set: EncodeSet): void
Appends b to sb, percent-encoded if set requires it. This is the
byte-at-a-time form the URL parser builds components with.
parseFormUrlencoded
function parseFormUrlencoded(input: String): Array<(String, String)>
Parses an application/x-www-form-urlencoded string into ordered (name, value) pairs (https://url.spec.whatwg.org/#urlencoded-parsing). A leading '?' is NOT stripped here — callers strip it (the URL API layer does, matching URLSearchParams).
serializeFormUrlencoded
function serializeFormUrlencoded(pairs: Array<(String, String)>): String
Serializes ordered (name, value) pairs to application/x-www-form-urlencoded (https://url.spec.whatwg.org/#concept-urlencoded-serializer).
url
function url(strings: TemplateStringsArray, values: FixedArray<String>): URL | null
Builds a URL from a template, percent-encoding every interpolated value
for the component it lands in.
let team = 'a/b team';
url`https://example.com/teams/${team}`; // .../teams/a%2Fb%20team
url`https://example.com/s?q=${team}`; // ...?q=a%2Fb%20team
Values are encoded with the component percent-encode set, which covers every
delimiter that could end the component early — /, ?, #, &, =, and
% itself — so an interpolation cannot add a path segment, start a query,
or smuggle in an extra parameter.
Interpolation is only allowed in the path, query, and fragment. A hole in
the scheme, credentials, host, or port returns null: those parts are not
percent-decoded when parsed — a host goes through IDNA and IP parsing on its
raw text — so there is no encoding that would make an untrusted value safe
there, and pretending otherwise is worse than refusing. Build those with
URL.parse and the with* methods if you need them dynamic.
The template must be an absolute URL; like URL.parse, anything that does
not parse comes back as null.
parsePattern
function parsePattern(pattern: String, componentType: URLComponentType = URLComponentType.Pathname, encode: (s: String) => String = (s: String): String => s): Array<Part>
Parses one component's pattern string into its parts.
componentType is carried on every part rather than used during parsing:
a prefix tree spanning a whole URL needs to know which component a part
came from, and the caller knows it here.
encode is applied to literal text only — fixed values, prefixes and
suffixes — and defaults to leaving it alone. URLPattern passes the
percent-encoder for the component being parsed, which is why it cannot be
done to the pattern string as a whole: that would encode the pattern
syntax along with the text.
punycodeEncode
function punycodeEncode(label: String): String | null
Encodes one label, per RFC 3492 section 6.3.
The result carries no "xn--" prefix. An all-ASCII label encodes to itself followed by a '-', which is what the RFC specifies even though IDNA never has a reason to encode such a label.
Returns null if the input is not well-formed UTF-8, or if the label is so pathological that the encoder's arithmetic would overflow.
punycodeDecode
function punycodeDecode(label: String): String | null
Decodes one label, per RFC 3492 section 6.2.
The input must not carry the "xn--" prefix. Returns null for a label that is not valid Punycode: a bad digit, arithmetic that would overflow, a non-ASCII byte in the literal portion, or a result outside the Unicode scalar range.