zena:url

zena
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 ​

zena
import { URL, URLSearchParams } from 'zena:url';

let u = new URL('https://example.com/api?format=json');
let format = u.searchParams.get('format');

Classes

URL

zena
final class URL implements Hashable
Implements Hashable
Re-exported from zena:url/url.zena

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).

Constructors
zena
new(rec: UrlRecord)
#
Properties
zena
protocol: String { get; }
#
zena
username: String { get; }
#
zena
password: String { get; }
#
zena
hostname: String { get; }
#
zena
port: String { get; }
#
zena
pathname: String { get; }
#
zena
hash: String { get; }
#
zena
href: String { get; }
#
Methods
zena
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.

zena
static canParse(input: String, base: String | null = null): boolean
#
zena
host(): String
#

"hostname:port", with the port omitted when it is null.

zena
origin(): String
#
zena
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().

zena
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.

zena
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.

zena
withPassword(value: String): URL
#

This URL with a different password. Ignored like withUsername.

zena
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.

zena
withHostname(value: String): URL
#

This URL with a different host, ignoring any port in value.

zena
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.

zena
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.

zena
withSearch(value: String): URL
#

This URL with a different query. An empty string removes it; a leading '?' is optional.

zena
withSearchParams(params: URLSearchParams): URL
#

This URL with the query replaced by params.

zena
withHash(value: String): URL
#

This URL with a different fragment. An empty string removes it; a leading '#' is optional.

zena
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.

zena
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.

zena
toString(): String
#
Operators
zena
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

zena
sealed class Part
Re-exported from zena:url/pattern-parts.zena

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.

Properties
zena
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.

zena
abstract suffix: String
#
zena
abstract modifier: Modifier
#
zena
abstract urlComponentType: URLComponentType
#
Variants
zena
case FixedPart(value: String, prefix: String, suffix: String, modifier: Modifier, urlComponentType: URLComponentType)
#
zena
case SegmentWildcardPart(name: String, prefix: String, suffix: String, modifier: Modifier, urlComponentType: URLComponentType)
#
zena
case FullWildcardPart(name: String, prefix: String, suffix: String, modifier: Modifier, urlComponentType: URLComponentType)
#
zena
case RegexPart(name: String, source: String, prefix: String, suffix: String, modifier: Modifier, urlComponentType: URLComponentType)
#

PatternSyntaxError

zena
class PatternSyntaxError extends Error
Extends Error
Re-exported from zena:url/pattern-parts.zena

Raised for a pattern string that cannot be parsed.

Constructors
zena
new(message: String)
#
2 inherited members
From Error
Properties
zena
message: String
#
Methods
zena
getStackTrace(): String | null
#

URLPattern

zena
final class URLPattern
Re-exported from zena:url/pattern.zena

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.

Constructors
zena
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.

Properties
zena
protocol: String { get; }
#
zena
username: String { get; }
#
zena
password: String { get; }
#
zena
hostname: String { get; }
#
zena
port: String { get; }
#
zena
pathname: String { get; }
#
zena
hash: String { get; }
#

URLPatternInit

zena
final class URLPatternInit
Re-exported from zena:url/pattern.zena

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.

Constructors
zena
new()
#
Properties
zena
var protocol: String | null
#
zena
var username: String | null
#
zena
var password: String | null
#
zena
var hostname: String | null
#
zena
var port: String | null
#
zena
var pathname: String | null
#
zena
var hash: String | null
#
zena
var baseURL: String | null
#

URLSearchParams

zena
final class URLSearchParams with IterableUtils<(String, String)> implements Iterable<(String, String)>
Implements Iterable<(String, String)>
Mixes in IterableUtils<(String, String)>
Re-exported from zena:url/search-params.zena

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.

Constructors
zena
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.

Properties
zena
size: i32 { get; }
#

Number of pairs, counting duplicate names separately.

Methods
zena
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.

zena
get(name: String): String | null
#

The first value for name, or null when there is none.

zena
getAll(name: String): Array<String>
#

Every value for name, in order. Empty when there is none.

zena
append(name: String, value: String): void
#

Appends a pair, keeping any existing pairs with the same name.

zena
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.

zena
delete(name: String, value: String | null = null): void
#

Removes every pair with this name — or, when value is given, only the pairs matching both.

zena
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.

zena
toString(): String
#

Serializes to application/x-www-form-urlencoded (no leading ?).

6 inherited members
From Iterable
zena
contains(value: T): boolean
#

Returns true if the collection contains the specified value.

zena
all(predicate: (item: T) => boolean): boolean
#

Returns true if all elements match the given predicate. Returns true if the collection is empty.

zena
some(predicate: (item: T) => boolean): boolean
#

Returns true if at least one element matches the given predicate. Returns false if the collection is empty.

zena
fold<R>(initial: R, combine: (acc: R, item: T) => R): R
#

Reduces the collection to a single value by accumulating state.

zena
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.

zena
filter(predicate: (item: T) => boolean): Iterable<T>
#

Returns a new iterable containing only the elements that match the predicate.

ConcurrentModificationError

zena
class ConcurrentModificationError extends Error
Extends Error
Re-exported from zena:url/search-params.zena

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.

Constructors
zena
new()
#
2 inherited members
From Error
Properties
zena
message: String
#
Methods
zena
getStackTrace(): String | null
#

Enums

EncodeSet

zena
enum EncodeSet
Re-exported from zena:url/encoding.zena

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).

Members
zena
C0Control
#
zena
Fragment
#
zena
Query
#
zena
SpecialQuery
#
zena
Path
#
zena
Userinfo
#
zena
Component
#
zena
FormUrlencoded
#

Modifier

zena
enum Modifier
Re-exported from zena:url/pattern-parts.zena

How many times a part may repeat.

Members
zena
None
#
zena
Optional
#
zena
ZeroOrMore
#
zena
OneOrMore
#

URLComponentType

zena
enum URLComponentType
Re-exported from zena:url/pattern-parts.zena

Which URL component a pattern was written for.

Members
zena
Protocol
#
zena
Username
#
zena
Password
#
zena
Hostname
#
zena
Port
#
zena
Pathname
#
zena
Hash
#

Functions

byteNeedsPercentEncoding

zena
function byteNeedsPercentEncoding(b: i32, set: EncodeSet): boolean
Re-exported from zena:url/encoding.zena

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

zena
function percentEncode(input: String, set: EncodeSet, spaceAsPlus: boolean = false): String
Re-exported from zena:url/encoding.zena

Percent-encodes input for the given encode set. spaceAsPlus implements the form-urlencoded serializer's special case (0x20 → '+').

percentDecode

zena
function percentDecode(input: String, plusAsSpace: boolean = false): String
Re-exported from zena:url/encoding.zena

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

zena
function encodeByteInto(sb: StringBuilder, b: i32, set: EncodeSet): void
Re-exported from zena:url/encoding.zena

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

zena
function parseFormUrlencoded(input: String): Array<(String, String)>
Re-exported from zena:url/encoding.zena

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

zena
function serializeFormUrlencoded(pairs: Array<(String, String)>): String
Re-exported from zena:url/encoding.zena

Serializes ordered (name, value) pairs to application/x-www-form-urlencoded (https://url.spec.whatwg.org/#concept-urlencoded-serializer).

url

zena
function url(strings: TemplateStringsArray, values: FixedArray<String>): URL | null
Re-exported from zena:url/tag.zena

Builds a URL from a template, percent-encoding every interpolated value for the component it lands in.

zena
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

zena
function parsePattern(pattern: String, componentType: URLComponentType = URLComponentType.Pathname, encode: (s: String) => String = (s: String): String => s): Array<Part>
Re-exported from zena:url/pattern-parts.zena

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

zena
function punycodeEncode(label: String): String | null
Re-exported from zena:url/punycode.zena

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

zena
function punycodeDecode(label: String): String | null
Re-exported from zena:url/punycode.zena

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.