cizgile 0.2.0 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/uri.mjs CHANGED
@@ -1,24 +1,42 @@
1
1
  import { _ as isUnreserved, a as readHexByte, c as isDigit, d as isPchar, f as isQueryChar, g as isSubDelim, h as isSegmentNzNc, i as encodeUtf8, l as isGenDelim, m as isScheme, n as percentDecode, o as readUtf8, p as isReserved, r as percentEncode, s as isAlpha, t as normalizePercentEncoding, u as isHexDigit, v as resolveEncodeSet } from "./shared/percent-DG-pZhJI.mjs";
2
+ import { i as punycodeEncode, n as domainToUnicode, r as punycodeDecode, t as domainToAscii } from "./shared/punycode-MAFJqHdd.mjs";
2
3
 
3
4
  //#region src/uri/parse.ts
4
5
  const URI_REFERENCE = /^(?:([A-Za-z][A-Za-z0-9+.-]*):)?(?:\/\/([^/?#]*))?([^?#]*)(?:\?([^#]*))?(?:#(.*))?$/s;
5
- /** Splits a URI reference into its components with the RFC 3986 Appendix B regular expression; never throws. */
6
- function parseUri(input) {
6
+ /** Splits a URI reference into its components with the RFC 3986 Appendix B regular expression; never throws. With `authority: true` the authority is split further. */
7
+ function parseUri(input, options = {}) {
7
8
  const [, scheme, authority, path = "", query, fragment] = URI_REFERENCE.exec(input) ?? [];
8
9
  const out = { path };
9
10
  if (scheme !== void 0) out.scheme = scheme;
10
- if (authority !== void 0) out.authority = authority;
11
+ if (authority !== void 0) {
12
+ out.authority = authority;
13
+ if (options.authority === true) {
14
+ const parts = parseAuthority(authority);
15
+ Object.assign(out, parts);
16
+ const port = parts.port;
17
+ if (port !== void 0 && port !== "" && port.length <= 5 && Number(port) <= 65535) out.portNumber = Number(port);
18
+ }
19
+ }
11
20
  if (query !== void 0) out.query = query;
12
21
  if (fragment !== void 0) out.fragment = fragment;
13
22
  return out;
14
23
  }
15
- /** Recomposes components per RFC 3986 section 5.3, inserting `/.` or `./` where the grammar requires it. */
24
+ function authorityOf(components) {
25
+ if (components.authority !== void 0) return components.authority;
26
+ if (components.host === void 0) return void 0;
27
+ const parts = { host: components.host };
28
+ if (components.userinfo !== void 0) parts.userinfo = components.userinfo;
29
+ if (components.port !== void 0) parts.port = components.port;
30
+ return serializeAuthority(parts);
31
+ }
32
+ /** Recomposes components per RFC 3986 section 5.3, inserting `/.` or `./` where the grammar requires it. `authority` wins over `host`, `port` and `userinfo`. */
16
33
  function serializeUri(components) {
17
34
  let out = "";
18
35
  let path = components.path;
36
+ const authority = authorityOf(components);
19
37
  if (components.scheme !== void 0) out += components.scheme + ":";
20
- if (components.authority !== void 0) {
21
- out += "//" + components.authority;
38
+ if (authority !== void 0) {
39
+ out += "//" + authority;
22
40
  if (path !== "" && !path.startsWith("/")) path = "/" + path;
23
41
  } else if (path.startsWith("//")) path = "/." + path;
24
42
  else if (components.scheme === void 0) {
@@ -29,6 +47,158 @@ function serializeUri(components) {
29
47
  if (components.fragment !== void 0) out += "#" + components.fragment;
30
48
  return out;
31
49
  }
50
+ /** Splits an authority into userinfo, host and port. */
51
+ function parseAuthority(authority) {
52
+ const at = authority.lastIndexOf("@");
53
+ const out = { host: "" };
54
+ let hostPort = authority;
55
+ if (at !== -1) {
56
+ out.userinfo = authority.slice(0, at);
57
+ hostPort = authority.slice(at + 1);
58
+ }
59
+ const bracket = hostPort.startsWith("[") ? hostPort.indexOf("]") : -1;
60
+ const colon = hostPort.lastIndexOf(":");
61
+ if (hostPort.startsWith("[") && bracket === -1) {
62
+ out.host = hostPort;
63
+ return out;
64
+ }
65
+ if (colon !== -1 && colon > bracket && /^:\d*$/.test(hostPort.slice(colon))) {
66
+ out.host = hostPort.slice(0, colon);
67
+ out.port = hostPort.slice(colon + 1);
68
+ } else out.host = hostPort;
69
+ return out;
70
+ }
71
+ /** Recomposes an authority from its parts. */
72
+ function serializeAuthority(components) {
73
+ let out = "";
74
+ if (components.userinfo !== void 0) out += components.userinfo + "@";
75
+ out += components.host;
76
+ if (components.port !== void 0) out += ":" + components.port;
77
+ return out;
78
+ }
79
+
80
+ //#endregion
81
+ //#region src/uri/host.ts
82
+ const DEC_OCTET = /^(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])$/;
83
+ const H16 = /^[0-9A-Fa-f]{1,4}$/;
84
+ const IPV_FUTURE = /^v[0-9A-Fa-f]+\.[A-Za-z0-9\-._~!$&'()*+,;=:]+$/;
85
+ /** RFC 3986 `IPv4address`: four decimal octets, no leading zeros; `0x7f.0.0.1` is a registered name, not an address. */
86
+ function isIPv4Address(text) {
87
+ const parts = text.split(".");
88
+ return parts.length === 4 && parts.every((part) => DEC_OCTET.test(part));
89
+ }
90
+ function h16Groups(part, allowIPv4Tail) {
91
+ if (part === "") return 0;
92
+ const items = part.split(":");
93
+ let count = 0;
94
+ for (const [index, item] of items.entries()) if (allowIPv4Tail && index === items.length - 1 && item.includes(".")) {
95
+ if (!isIPv4Address(item)) return -1;
96
+ count += 2;
97
+ } else if (H16.test(item)) count += 1;
98
+ else return -1;
99
+ return count;
100
+ }
101
+ /** RFC 3986 `IPv6address`, all nine ABNF alternatives including the embedded IPv4 form. */
102
+ function isIPv6Address(text) {
103
+ const gap = text.indexOf("::");
104
+ if (gap === -1) return h16Groups(text, true) === 8;
105
+ if (text.indexOf("::", gap + 1) !== -1) return false;
106
+ const left = h16Groups(text.slice(0, gap), false);
107
+ const right = h16Groups(text.slice(gap + 2), true);
108
+ return left >= 0 && right >= 0 && left + right <= 7;
109
+ }
110
+ function splitGroups(part) {
111
+ return part === "" ? [] : part.split(":");
112
+ }
113
+ function zeroRun(groups) {
114
+ let bestStart = -1;
115
+ let bestLength = 0;
116
+ let i = 0;
117
+ while (i < groups.length) {
118
+ if (groups[i] !== "0") {
119
+ i += 1;
120
+ continue;
121
+ }
122
+ let j = i;
123
+ while (j < groups.length && groups[j] === "0") j += 1;
124
+ if (j - i > bestLength) {
125
+ bestStart = i;
126
+ bestLength = j - i;
127
+ }
128
+ i = j;
129
+ }
130
+ return [bestStart, bestLength];
131
+ }
132
+ /** RFC 5952 canonical text of an IPv6 address: lowercase, no leading zeros, the longest zero run as `::`, an embedded IPv4 tail kept. Anything that is not an IPv6 address is returned as is. */
133
+ function normalizeIPv6Address(text) {
134
+ if (!isIPv6Address(text)) return text;
135
+ const lower = text.toLowerCase();
136
+ const lastColon = lower.lastIndexOf(":");
137
+ const tail = lower.includes(".", lastColon) ? lower.slice(lastColon + 1) : void 0;
138
+ const body = tail === void 0 ? lower : lower.slice(0, lastColon + 1);
139
+ const total = tail === void 0 ? 8 : 6;
140
+ const gap = body.indexOf("::");
141
+ let groups;
142
+ if (gap === -1) groups = splitGroups(body.replace(/:$/, ""));
143
+ else {
144
+ const left = splitGroups(body.slice(0, gap));
145
+ const right = splitGroups(body.slice(gap + 2).replace(/:$/, ""));
146
+ groups = [
147
+ ...left,
148
+ ...Array.from({ length: total - left.length - right.length }, () => "0"),
149
+ ...right
150
+ ];
151
+ }
152
+ groups = groups.map((group) => group.replace(/^0+(?=.)/, ""));
153
+ const [start, length] = zeroRun(groups);
154
+ const out = length >= 2 ? `${groups.slice(0, start).join(":")}::${groups.slice(start + length).join(":")}` : groups.join(":");
155
+ if (tail === void 0) return out;
156
+ return out.endsWith(":") ? out + tail : `${out}:${tail}`;
157
+ }
158
+ /** RFC 3986 `IPvFuture`: `v` + hex + `.` + unreserved or sub-delims. */
159
+ function isIPvFuture(text) {
160
+ return IPV_FUTURE.test(text);
161
+ }
162
+ /** RFC 3986 `IP-literal`: an IPv6 address or IPvFuture in square brackets. */
163
+ function isIPLiteral(text) {
164
+ if (!text.startsWith("[") || !text.endsWith("]")) return false;
165
+ const inner = text.slice(1, -1);
166
+ return isIPv6Address(inner) || isIPvFuture(inner);
167
+ }
168
+ /** RFC 3986 `reg-name`: unreserved, percent-encoded and sub-delims characters, possibly empty. */
169
+ function isRegName(text) {
170
+ let i = 0;
171
+ while (i < text.length) {
172
+ const cp = text.codePointAt(i) ?? 0;
173
+ if (cp === 37) {
174
+ if (readHexByte(text, i) === -1) return false;
175
+ i += 3;
176
+ continue;
177
+ }
178
+ if (!isUnreserved(cp) && !isSubDelim(cp)) return false;
179
+ i += 1;
180
+ }
181
+ return true;
182
+ }
183
+ /** Classifies `text` as an IP literal, IPv4 address or registered name, or returns `undefined`. */
184
+ function parseHost(text) {
185
+ if (isIPLiteral(text)) return {
186
+ kind: "ip-literal",
187
+ value: text.slice(1, -1)
188
+ };
189
+ if (isIPv4Address(text)) return {
190
+ kind: "ipv4",
191
+ value: text
192
+ };
193
+ if (isRegName(text)) return {
194
+ kind: "reg-name",
195
+ value: text
196
+ };
197
+ }
198
+ /** Whether `text` is a valid RFC 3986 `host`. */
199
+ function isHost(text) {
200
+ return parseHost(text) !== void 0;
201
+ }
32
202
 
33
203
  //#endregion
34
204
  //#region src/uri/path.ts
@@ -80,35 +250,6 @@ const DEFAULT_PORTS = {
80
250
  wss: 443,
81
251
  ftp: 21
82
252
  };
83
- /** Splits an authority into userinfo, host and port. */
84
- function parseAuthority(authority) {
85
- const at = authority.lastIndexOf("@");
86
- const out = { host: "" };
87
- let hostPort = authority;
88
- if (at !== -1) {
89
- out.userinfo = authority.slice(0, at);
90
- hostPort = authority.slice(at + 1);
91
- }
92
- const bracket = hostPort.startsWith("[") ? hostPort.indexOf("]") : -1;
93
- const colon = hostPort.lastIndexOf(":");
94
- if (hostPort.startsWith("[") && bracket === -1) {
95
- out.host = hostPort;
96
- return out;
97
- }
98
- if (colon !== -1 && colon > bracket && /^:\d*$/.test(hostPort.slice(colon))) {
99
- out.host = hostPort.slice(0, colon);
100
- out.port = hostPort.slice(colon + 1);
101
- } else out.host = hostPort;
102
- return out;
103
- }
104
- /** Recomposes an authority from its parts. */
105
- function serializeAuthority(components) {
106
- let out = "";
107
- if (components.userinfo !== void 0) out += components.userinfo + "@";
108
- out += components.host;
109
- if (components.port !== void 0) out += ":" + components.port;
110
- return out;
111
- }
112
253
  /** RFC 3986 section 6.2 syntax-based and scheme-based normalisation: case, percent-encoding, dot segments, default ports, empty path. */
113
254
  function normalizeUri(input, options = {}) {
114
255
  const ports = options.defaultPorts ?? DEFAULT_PORTS;
@@ -126,16 +267,100 @@ function normalizeUri(input, options = {}) {
126
267
  a.userinfo = normalizePercentEncoding(a.userinfo);
127
268
  }
128
269
  a.host = normalizePercentEncoding(a.host.replace(/[A-Z]+/g, (m) => m.toLowerCase()));
270
+ if (options.strict === true) {
271
+ if (!isHost(a.host)) throw new TypeError(`normalizeUri: invalid host ${JSON.stringify(a.host)}`);
272
+ if (a.port !== void 0 && !/^\d*$/.test(a.port)) throw new TypeError(`normalizeUri: invalid port ${JSON.stringify(a.port)}`);
273
+ }
274
+ if (isIPLiteral(a.host)) a.host = `[${normalizeIPv6Address(a.host.slice(1, -1))}]`;
275
+ else if (options.host === "idna") a.host = domainToAscii(a.host);
276
+ else if (options.host === "unicode") a.host = domainToUnicode(a.host);
129
277
  if (a.port !== void 0 && (a.port === "" || Number(a.port) === defaultPort)) delete a.port;
130
278
  c.authority = serializeAuthority(a);
131
279
  }
132
280
  c.path = normalizePercentEncoding(c.path);
133
281
  if (c.scheme !== void 0 || c.authority !== void 0) c.path = removeDotSegments(c.path);
134
282
  if (schemeBased && c.scheme !== void 0 && c.authority !== void 0 && c.path === "") c.path = "/";
283
+ const trailing = options.trailingSlash ?? "keep";
284
+ if (trailing === "remove" && c.path.length > 1 && c.path.endsWith("/")) c.path = c.path.replace(/\/+$/, "");
285
+ else if (trailing === "add" && c.path !== "" && !c.path.endsWith("/")) c.path += "/";
135
286
  if (c.query !== void 0) c.query = normalizePercentEncoding(c.query);
136
287
  if (c.fragment !== void 0) c.fragment = normalizePercentEncoding(c.fragment);
288
+ if (options.emptyQuery === "remove" && c.query === "") delete c.query;
289
+ if (options.emptyFragment === "remove" && c.fragment === "") delete c.fragment;
290
+ return serializeUri(c);
291
+ }
292
+
293
+ //#endregion
294
+ //#region src/uri/helpers.ts
295
+ /** RFC 6454 origin of a URI: lowercase `scheme://host` plus the port unless it is the scheme's default. `undefined` when there is no scheme or no authority. */
296
+ function getOrigin(uri) {
297
+ const c = parseUri(uri, { authority: true });
298
+ if (c.scheme === void 0 || c.host === void 0) return void 0;
299
+ const scheme = c.scheme.toLowerCase();
300
+ return `${scheme}://${c.host.replace(/[A-Z]+/g, (m) => m.toLowerCase())}${c.port === void 0 || c.port === "" || Number(c.port) === DEFAULT_PORTS[scheme] ? "" : `:${c.port}`}`;
301
+ }
302
+ /** Whether two URIs share an RFC 6454 origin. Two URIs without an origin never match. */
303
+ function isSameOrigin(a, b) {
304
+ const origin = getOrigin(a);
305
+ return origin !== void 0 && origin === getOrigin(b);
306
+ }
307
+ /** The URI without its fragment. */
308
+ function stripFragment(uri) {
309
+ const c = parseUri(uri);
310
+ if (c.fragment === void 0) return uri;
311
+ delete c.fragment;
312
+ return serializeUri(c);
313
+ }
314
+ /** Decodes an `application/x-www-form-urlencoded` query (`a=1&b=2`, `+` as space) into pairs in order; a leading `?` is ignored. */
315
+ function parseQuery(query) {
316
+ const text = query.startsWith("?") ? query.slice(1) : query;
317
+ const out = [];
318
+ if (text === "") return out;
319
+ for (const part of text.split("&")) {
320
+ if (part === "") continue;
321
+ const eq = part.indexOf("=");
322
+ const name = eq === -1 ? part : part.slice(0, eq);
323
+ const value = eq === -1 ? "" : part.slice(eq + 1);
324
+ out.push([percentDecode(name, { plusAsSpace: true }), percentDecode(value, { plusAsSpace: true })]);
325
+ }
326
+ return out;
327
+ }
328
+ /** Encodes pairs as an `application/x-www-form-urlencoded` query without the leading `?`. */
329
+ function stringifyQuery(pairs) {
330
+ const parts = [];
331
+ for (const [name, value] of pairs) parts.push(`${percentEncode(name, "form")}=${percentEncode(value, "form")}`);
332
+ return parts.join("&");
333
+ }
334
+ /** The URI with its query parameters sorted by name, then value, byte-wise on the encoded text; parameters are otherwise left as written. */
335
+ function sortQuery(uri) {
336
+ const c = parseUri(uri);
337
+ if (c.query === void 0 || c.query === "") return uri;
338
+ const parts = c.query.split("&").filter((part) => part !== "");
339
+ parts.sort((x, y) => {
340
+ const [xn, xv] = splitPair(x);
341
+ const [yn, yv] = splitPair(y);
342
+ return xn < yn ? -1 : xn > yn ? 1 : xv < yv ? -1 : xv > yv ? 1 : 0;
343
+ });
344
+ c.query = parts.join("&");
137
345
  return serializeUri(c);
138
346
  }
347
+ function splitPair(part) {
348
+ const eq = part.indexOf("=");
349
+ return eq === -1 ? [part, ""] : [part.slice(0, eq), part.slice(eq + 1)];
350
+ }
351
+ /** Joins path pieces with single slashes, keeps the first piece's leading slash and the last piece's trailing slash, and resolves `.` and `..` so the result never contains `//` or a dot segment. */
352
+ function joinPaths(...pieces) {
353
+ const parts = pieces.filter((piece) => piece !== "");
354
+ if (parts.length === 0) return "";
355
+ const first = parts[0] ?? "";
356
+ const last = parts[parts.length - 1] ?? "";
357
+ const leading = first.startsWith("/") ? "/" : "";
358
+ const trailing = last.endsWith("/") && last !== "/" ? "/" : "";
359
+ const body = parts.map((piece) => piece.replace(/^\/+|\/+$/g, "")).filter((piece) => piece !== "").join("/");
360
+ if (body === "") return leading === "" ? "" : "/";
361
+ const joined = normalizePath(leading + body.replace(/\/{2,}/g, "/"));
362
+ return joined === "/" || joined === "" ? joined : joined + trailing;
363
+ }
139
364
 
140
365
  //#endregion
141
366
  //#region src/uri/resolve.ts
@@ -186,14 +411,9 @@ function resolveUri(base, reference, options = {}) {
186
411
 
187
412
  //#endregion
188
413
  //#region src/uri/compare.ts
189
- function withoutFragment(uri) {
190
- const c = parseUri(uri);
191
- delete c.fragment;
192
- return serializeUri(c);
193
- }
194
414
  function prepare(uri, options) {
195
415
  let out = options.base === void 0 ? uri : resolveUri(options.base, uri);
196
- if (options.ignoreFragment === true) out = withoutFragment(out);
416
+ if (options.ignoreFragment === true) out = stripFragment(out);
197
417
  switch (options.level ?? "scheme") {
198
418
  case "simple": return out;
199
419
  case "syntax": return normalizeUri(out, { schemeBased: false });
@@ -206,8 +426,8 @@ function equivalentUris(a, b, options = {}) {
206
426
  }
207
427
  /** RFC 3986 section 4.4: whether `reference` resolved against `base` differs from it only in the fragment. */
208
428
  function isSameDocumentReference(base, reference, options = {}) {
209
- const target = withoutFragment(resolveUri(base, reference));
210
- const origin = withoutFragment(base);
429
+ const target = stripFragment(resolveUri(base, reference));
430
+ const origin = stripFragment(base);
211
431
  return options.normalize === true ? normalizeUri(target) === normalizeUri(origin) : target === origin;
212
432
  }
213
433
 
@@ -249,6 +469,21 @@ const TRAILING_PUNCTUATION = /[.,;:!?'"]+$/;
249
469
  const LEADING_PUNCTUATION = /^[(<["'{]+/;
250
470
  const MARKDOWN_LINK = /^\[[^\]]*\]\(\s*(?:<([^>]*)>|([^)\s]+))(?:\s+["'][^"']*["'])?\s*\)$/;
251
471
  const HTML_ATTRIBUTE = /\b(?:href|src|action|content)\s*=\s*(["'])(.*?)\1/i;
472
+ const IN_PROSE = /\b(?:[A-Za-z][A-Za-z0-9+.-]*:\/\/|(?:mailto|tel|urn|data|sms|geo|magnet|xmpp|sip|sips):|www\.)[^\s<>"']+/g;
473
+ /** Finds the URIs in ordinary text: anything with `scheme://`, a `www.` host, or a `mailto:`, `tel:`, `urn:`, `data:` style scheme, ending at whitespace or a quote and trimmed of trailing punctuation the way `extractUri` does. */
474
+ function findUris(text) {
475
+ const out = [];
476
+ for (const match of text.matchAll(IN_PROSE)) {
477
+ const uri = trimTrailing(match[0]);
478
+ if (uri === "" || uri.startsWith("www.") && !uri.includes(".", 4)) continue;
479
+ out.push({
480
+ uri,
481
+ start: match.index,
482
+ end: match.index + uri.length
483
+ });
484
+ }
485
+ return out;
486
+ }
252
487
  /** RFC 3986 Appendix C: recovers a URI from text that wraps it in `<>`, quotes or brackets, prefixes it with `URL:`, breaks it across lines or ends it with punctuation. Markdown links and `href`/`src` attributes yield their URL. */
253
488
  function extractUri(text) {
254
489
  let out = text.trim().replace(/^url:\s*/i, "");
@@ -274,6 +509,10 @@ function extractUri(text) {
274
509
  }
275
510
  out = out.replace(LEADING_PUNCTUATION, "");
276
511
  out = out.replace(/\s+/g, "");
512
+ return trimTrailing(out);
513
+ }
514
+ function trimTrailing(text) {
515
+ let out = text;
277
516
  let trimmed = true;
278
517
  while (trimmed) {
279
518
  trimmed = false;
@@ -288,260 +527,6 @@ function count(text, char) {
288
527
  return text.split(char).length - 1;
289
528
  }
290
529
 
291
- //#endregion
292
- //#region src/uri/host.ts
293
- const DEC_OCTET = /^(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])$/;
294
- const H16 = /^[0-9A-Fa-f]{1,4}$/;
295
- const IPV_FUTURE = /^v[0-9A-Fa-f]+\.[A-Za-z0-9\-._~!$&'()*+,;=:]+$/;
296
- /** RFC 3986 `IPv4address`: four decimal octets, no leading zeros; `0x7f.0.0.1` is a registered name, not an address. */
297
- function isIPv4Address(text) {
298
- const parts = text.split(".");
299
- return parts.length === 4 && parts.every((part) => DEC_OCTET.test(part));
300
- }
301
- function h16Groups(part, allowIPv4Tail) {
302
- if (part === "") return 0;
303
- const items = part.split(":");
304
- let count = 0;
305
- for (const [index, item] of items.entries()) if (allowIPv4Tail && index === items.length - 1 && item.includes(".")) {
306
- if (!isIPv4Address(item)) return -1;
307
- count += 2;
308
- } else if (H16.test(item)) count += 1;
309
- else return -1;
310
- return count;
311
- }
312
- /** RFC 3986 `IPv6address`, all nine ABNF alternatives including the embedded IPv4 form. */
313
- function isIPv6Address(text) {
314
- const gap = text.indexOf("::");
315
- if (gap === -1) return h16Groups(text, true) === 8;
316
- if (text.indexOf("::", gap + 1) !== -1) return false;
317
- const left = h16Groups(text.slice(0, gap), false);
318
- const right = h16Groups(text.slice(gap + 2), true);
319
- return left >= 0 && right >= 0 && left + right <= 7;
320
- }
321
- /** RFC 3986 `IPvFuture`: `v` + hex + `.` + unreserved or sub-delims. */
322
- function isIPvFuture(text) {
323
- return IPV_FUTURE.test(text);
324
- }
325
- /** RFC 3986 `IP-literal`: an IPv6 address or IPvFuture in square brackets. */
326
- function isIPLiteral(text) {
327
- if (!text.startsWith("[") || !text.endsWith("]")) return false;
328
- const inner = text.slice(1, -1);
329
- return isIPv6Address(inner) || isIPvFuture(inner);
330
- }
331
- /** RFC 3986 `reg-name`: unreserved, percent-encoded and sub-delims characters, possibly empty. */
332
- function isRegName(text) {
333
- let i = 0;
334
- while (i < text.length) {
335
- const cp = text.codePointAt(i) ?? 0;
336
- if (cp === 37) {
337
- if (readHexByte(text, i) === -1) return false;
338
- i += 3;
339
- continue;
340
- }
341
- if (!isUnreserved(cp) && !isSubDelim(cp)) return false;
342
- i += 1;
343
- }
344
- return true;
345
- }
346
- /** Classifies `text` as an IP literal, IPv4 address or registered name, or returns `undefined`. */
347
- function parseHost(text) {
348
- if (isIPLiteral(text)) return {
349
- kind: "ip-literal",
350
- value: text.slice(1, -1)
351
- };
352
- if (isIPv4Address(text)) return {
353
- kind: "ipv4",
354
- value: text
355
- };
356
- if (isRegName(text)) return {
357
- kind: "reg-name",
358
- value: text
359
- };
360
- }
361
- /** Whether `text` is a valid RFC 3986 `host`. */
362
- function isHost(text) {
363
- return parseHost(text) !== void 0;
364
- }
365
-
366
- //#endregion
367
- //#region src/uri/punycode.ts
368
- const BASE = 36;
369
- const T_MIN = 1;
370
- const T_MAX = 26;
371
- const SKEW = 38;
372
- const DAMP = 700;
373
- const INITIAL_BIAS = 72;
374
- const INITIAL_N = 128;
375
- const DELIMITER = "-";
376
- const PREFIX = "xn--";
377
- const MAX_INT = 2147483647;
378
- const LABEL_SEPARATORS = /[.\u3002\uFF0E\uFF61]/;
379
- function adapt(delta, numPoints, firstTime) {
380
- let d = firstTime ? Math.floor(delta / DAMP) : delta >> 1;
381
- d += Math.floor(d / numPoints);
382
- let k = 0;
383
- while (d > 455) {
384
- d = Math.floor(d / 35);
385
- k += BASE;
386
- }
387
- return k + Math.floor(36 * d / (d + SKEW));
388
- }
389
- function digit(value) {
390
- return String.fromCodePoint(value + (value < 26 ? 97 : 22));
391
- }
392
- function digitValue(cp) {
393
- if (cp >= 48 && cp <= 57) return cp - 22;
394
- if (cp >= 65 && cp <= 90) return cp - 65;
395
- if (cp >= 97 && cp <= 122) return cp - 97;
396
- return -1;
397
- }
398
- /** RFC 3492 Punycode encoding of one label, without the `xn--` prefix. */
399
- function punycodeEncode(input) {
400
- const points = Array.from(input, (ch) => ch.codePointAt(0) ?? 0);
401
- let out = "";
402
- for (const cp of points) if (cp < 128) out += String.fromCodePoint(cp);
403
- const basicCount = out.length;
404
- let handled = basicCount;
405
- if (basicCount > 0) out += DELIMITER;
406
- let n = INITIAL_N;
407
- let delta = 0;
408
- let bias = INITIAL_BIAS;
409
- while (handled < points.length) {
410
- let m = 1114112;
411
- for (const cp of points) if (cp >= n && cp < m) m = cp;
412
- delta += (m - n) * (handled + 1);
413
- n = m;
414
- for (const cp of points) {
415
- if (cp < n) delta += 1;
416
- if (cp === n) {
417
- let q = delta;
418
- for (let k = BASE;; k += BASE) {
419
- const t = k <= bias ? T_MIN : k >= bias + T_MAX ? T_MAX : k - bias;
420
- if (q < t) break;
421
- out += digit(t + (q - t) % (BASE - t));
422
- q = Math.floor((q - t) / (BASE - t));
423
- }
424
- out += digit(q);
425
- bias = adapt(delta, handled + 1, handled === basicCount);
426
- delta = 0;
427
- handled += 1;
428
- }
429
- }
430
- delta += 1;
431
- n += 1;
432
- }
433
- return out;
434
- }
435
- /**
436
- * RFC 3492 Punycode decoding of one label, without the `xn--` prefix.
437
- * @throws {RangeError} on malformed input.
438
- */
439
- function punycodeDecode(input) {
440
- const output = [];
441
- const last = input.lastIndexOf(DELIMITER);
442
- const basicEnd = last > 0 ? last : 0;
443
- for (let i = 0; i < basicEnd; i++) {
444
- const cp = input.codePointAt(i) ?? 0;
445
- if (cp >= 128) throw new RangeError("punycodeDecode: invalid basic code point");
446
- output.push(cp);
447
- }
448
- let n = INITIAL_N;
449
- let bias = INITIAL_BIAS;
450
- let i = 0;
451
- let index = basicEnd > 0 ? basicEnd + 1 : 0;
452
- while (index < input.length) {
453
- const oldI = i;
454
- let w = 1;
455
- for (let k = BASE;; k += BASE) {
456
- if (index >= input.length) throw new RangeError("punycodeDecode: truncated input");
457
- const d = digitValue(input.codePointAt(index) ?? 0);
458
- index += 1;
459
- if (d < 0) throw new RangeError("punycodeDecode: invalid digit");
460
- i += d * w;
461
- if (i > MAX_INT) throw new RangeError("punycodeDecode: overflow");
462
- const t = k <= bias ? T_MIN : k >= bias + T_MAX ? T_MAX : k - bias;
463
- if (d < t) break;
464
- w *= BASE - t;
465
- }
466
- const count = output.length + 1;
467
- bias = adapt(i - oldI, count, oldI === 0);
468
- n += Math.floor(i / count);
469
- i %= count;
470
- if (n > 1114111) throw new RangeError("punycodeDecode: code point out of range");
471
- output.splice(i, 0, n);
472
- i += 1;
473
- }
474
- return String.fromCodePoint(...output);
475
- }
476
- function isAsciiLabel(label) {
477
- for (const ch of label) if ((ch.codePointAt(0) ?? 0) >= 128) return false;
478
- return true;
479
- }
480
- function lowercaseCodePoints(text) {
481
- let out = "";
482
- for (const ch of text) out += ch.toLowerCase();
483
- return out;
484
- }
485
- function mapLabel(label) {
486
- return lowercaseCodePoints(label.replace(/\p{Cf}/gu, "").normalize("NFKC")).normalize("NFC");
487
- }
488
- function invalid(fn, label, reason) {
489
- return /* @__PURE__ */ new RangeError(`${fn}: label ${JSON.stringify(label)} ${reason}`);
490
- }
491
- function decodeLabel(fn, label) {
492
- let decoded;
493
- try {
494
- decoded = punycodeDecode(label.slice(4));
495
- } catch {
496
- throw invalid(fn, label, "is not valid Punycode");
497
- }
498
- if (decoded === "" || isAsciiLabel(decoded) || /[\p{C}\p{Z}]/u.test(decoded) || mapLabel(decoded) !== decoded || PREFIX + punycodeEncode(decoded) !== label) throw invalid(fn, label, "is not a valid A-label");
499
- return decoded;
500
- }
501
- function checkAsciiLabel(fn, label, original) {
502
- if (label.length > 63) throw invalid(fn, original, "exceeds 63 octets");
503
- if (!/^[a-z0-9-]*$/.test(label)) throw invalid(fn, original, "contains characters outside letters, digits and hyphen");
504
- if (label.startsWith("-") || label.endsWith("-")) throw invalid(fn, original, "may not start or end with a hyphen");
505
- if (label.startsWith(PREFIX)) decodeLabel(fn, label);
506
- else if (label.slice(2, 4) === "--") throw invalid(fn, original, "has hyphens in positions 3 and 4");
507
- return label;
508
- }
509
- function checkDomain(fn, labels, host) {
510
- if (labels.length > 1 && labels[labels.length - 1] === "") labels = labels.slice(0, -1);
511
- for (const label of labels) if (label === "") throw invalid(fn, host, "contains an empty label");
512
- const out = labels.join(".");
513
- if (out.length > 253) throw new RangeError(`${fn}: ${JSON.stringify(host)} exceeds 253 octets`);
514
- return out;
515
- }
516
- /**
517
- * Converts a domain to its A-label form: maps and lowercases each label the way UTS #46 does, punycodes the non-ASCII ones, and validates lengths, hyphens and existing `xn--` labels. IP literals pass through.
518
- * @throws {RangeError} when a label or the domain is not valid.
519
- */
520
- function domainToAscii(host) {
521
- if (host === "" || host.startsWith("[")) return host;
522
- const out = checkDomain("domainToAscii", host.split(LABEL_SEPARATORS).flatMap((label) => {
523
- if (isAsciiLabel(label)) return [checkAsciiLabel("domainToAscii", label.toLowerCase(), label)];
524
- const mapped = mapLabel(label);
525
- if (mapped.includes(".")) return domainToAscii(mapped).split(".");
526
- if (isAsciiLabel(mapped)) return [checkAsciiLabel("domainToAscii", mapped, label)];
527
- return [checkAsciiLabel("domainToAscii", PREFIX + punycodeEncode(mapped), label)];
528
- }), host);
529
- return host.endsWith(".") && out !== "" ? `${out}.` : out;
530
- }
531
- /**
532
- * Converts the `xn--` labels of a domain back to Unicode and lowercases the rest.
533
- * @throws {RangeError} when an `xn--` label does not decode to a valid U-label.
534
- */
535
- function domainToUnicode(host) {
536
- if (host === "" || host.startsWith("[")) return host;
537
- const out = checkDomain("domainToUnicode", host.split(".").map((label) => {
538
- const lower = label.toLowerCase();
539
- if (!isAsciiLabel(lower)) return lower;
540
- return lower.startsWith(PREFIX) ? decodeLabel("domainToUnicode", lower) : lower;
541
- }), host);
542
- return host.endsWith(".") && out !== "" ? `${out}.` : out;
543
- }
544
-
545
530
  //#endregion
546
531
  //#region src/uri/iri.ts
547
532
  /** RFC 3987 `ucschar`: the non-ASCII code points an IRI may contain unencoded. */
@@ -793,4 +778,4 @@ function isIri(input) {
793
778
  }
794
779
 
795
780
  //#endregion
796
- export { DEFAULT_PORTS, classifyReference, domainToAscii, domainToUnicode, encodeForm, encodeFragment, encodePath, encodePathSegment, encodeQuery, equivalentUris, extractUri, hasBidiControls, iriToUri, isAbsoluteUri, isAlpha, isBidiControl, isDigit, isGenDelim, isHexDigit, isHost, isIPLiteral, isIPv4Address, isIPv6Address, isIPvFuture, isIpchar, isIprivate, isIri, isIriReference, isIunreserved, isPchar, isQueryChar, isRegName, isRelativeReference, isReserved, isSameDocumentReference, isScheme, isSegmentNzNc, isSubDelim, isUcschar, isUnreserved, isUri, isUriReference, mergePaths, normalizePath, normalizePercentEncoding, normalizeUri, parseAuthority, parseHost, parseUri, pathForm, percentDecode, percentEncode, punycodeDecode, punycodeEncode, relativize, removeDotSegments, resolveEncodeSet, resolveUri, serializeAuthority, serializeUri, uriToIri };
781
+ export { DEFAULT_PORTS, classifyReference, domainToAscii, domainToUnicode, encodeForm, encodeFragment, encodePath, encodePathSegment, encodeQuery, equivalentUris, extractUri, findUris, getOrigin, hasBidiControls, iriToUri, isAbsoluteUri, isAlpha, isBidiControl, isDigit, isGenDelim, isHexDigit, isHost, isIPLiteral, isIPv4Address, isIPv6Address, isIPvFuture, isIpchar, isIprivate, isIri, isIriReference, isIunreserved, isPchar, isQueryChar, isRegName, isRelativeReference, isReserved, isSameDocumentReference, isSameOrigin, isScheme, isSegmentNzNc, isSubDelim, isUcschar, isUnreserved, isUri, isUriReference, joinPaths, mergePaths, normalizeIPv6Address, normalizePath, normalizePercentEncoding, normalizeUri, parseAuthority, parseHost, parseQuery, parseUri, pathForm, percentDecode, percentEncode, punycodeDecode, punycodeEncode, relativize, removeDotSegments, resolveEncodeSet, resolveUri, serializeAuthority, serializeUri, sortQuery, stringifyQuery, stripFragment, uriToIri };