cizgile 0.2.0 → 0.3.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
@@ -2,23 +2,40 @@ import { _ as isUnreserved, a as readHexByte, c as isDigit, d as isPchar, f as i
2
2
 
3
3
  //#region src/uri/parse.ts
4
4
  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) {
5
+ /** 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. */
6
+ function parseUri(input, options = {}) {
7
7
  const [, scheme, authority, path = "", query, fragment] = URI_REFERENCE.exec(input) ?? [];
8
8
  const out = { path };
9
9
  if (scheme !== void 0) out.scheme = scheme;
10
- if (authority !== void 0) out.authority = authority;
10
+ if (authority !== void 0) {
11
+ out.authority = authority;
12
+ if (options.authority === true) {
13
+ const parts = parseAuthority(authority);
14
+ Object.assign(out, parts);
15
+ const port = parts.port;
16
+ if (port !== void 0 && port !== "" && port.length <= 5 && Number(port) <= 65535) out.portNumber = Number(port);
17
+ }
18
+ }
11
19
  if (query !== void 0) out.query = query;
12
20
  if (fragment !== void 0) out.fragment = fragment;
13
21
  return out;
14
22
  }
15
- /** Recomposes components per RFC 3986 section 5.3, inserting `/.` or `./` where the grammar requires it. */
23
+ function authorityOf(components) {
24
+ if (components.authority !== void 0) return components.authority;
25
+ if (components.host === void 0) return void 0;
26
+ const parts = { host: components.host };
27
+ if (components.userinfo !== void 0) parts.userinfo = components.userinfo;
28
+ if (components.port !== void 0) parts.port = components.port;
29
+ return serializeAuthority(parts);
30
+ }
31
+ /** Recomposes components per RFC 3986 section 5.3, inserting `/.` or `./` where the grammar requires it. `authority` wins over `host`, `port` and `userinfo`. */
16
32
  function serializeUri(components) {
17
33
  let out = "";
18
34
  let path = components.path;
35
+ const authority = authorityOf(components);
19
36
  if (components.scheme !== void 0) out += components.scheme + ":";
20
- if (components.authority !== void 0) {
21
- out += "//" + components.authority;
37
+ if (authority !== void 0) {
38
+ out += "//" + authority;
22
39
  if (path !== "" && !path.startsWith("/")) path = "/" + path;
23
40
  } else if (path.startsWith("//")) path = "/." + path;
24
41
  else if (components.scheme === void 0) {
@@ -29,57 +46,6 @@ function serializeUri(components) {
29
46
  if (components.fragment !== void 0) out += "#" + components.fragment;
30
47
  return out;
31
48
  }
32
-
33
- //#endregion
34
- //#region src/uri/path.ts
35
- function removeLastSegment(output) {
36
- const index = output.lastIndexOf("/");
37
- return index === -1 ? "" : output.slice(0, index);
38
- }
39
- /** RFC 3986 section 5.2.4, the two-buffer algorithm as written: resolves `.` and `..` segments. */
40
- function removeDotSegments(path) {
41
- let input = path;
42
- let output = "";
43
- while (input.length > 0) if (input.startsWith("../")) input = input.slice(3);
44
- else if (input.startsWith("./")) input = input.slice(2);
45
- else if (input.startsWith("/./")) input = input.slice(2);
46
- else if (input === "/.") input = "/";
47
- else if (input.startsWith("/../")) {
48
- input = input.slice(3);
49
- output = removeLastSegment(output);
50
- } else if (input === "/..") {
51
- input = "/";
52
- output = removeLastSegment(output);
53
- } else if (input === "." || input === "..") input = "";
54
- else {
55
- const start = input.startsWith("/") ? 1 : 0;
56
- const next = input.indexOf("/", start);
57
- const end = next === -1 ? input.length : next;
58
- output += input.slice(0, end);
59
- input = input.slice(end);
60
- }
61
- return output;
62
- }
63
- /** Removes dot segments and normalises percent-encoding in a path, relative paths included. */
64
- function normalizePath(path, options = {}) {
65
- let out = normalizePercentEncoding(path);
66
- if (out.startsWith("/")) out = removeDotSegments(out);
67
- const mode = options.trailingSlash ?? "keep";
68
- if (mode === "add" && out.length > 0 && !out.endsWith("/")) out += "/";
69
- if (mode === "remove" && out.length > 1 && out.endsWith("/")) out = out.slice(0, -1);
70
- return out;
71
- }
72
-
73
- //#endregion
74
- //#region src/uri/normalize.ts
75
- /** Default ports of the common schemes, used by `normalizeUri` and `equivalentUris`. */
76
- const DEFAULT_PORTS = {
77
- http: 80,
78
- https: 443,
79
- ws: 80,
80
- wss: 443,
81
- ftp: 21
82
- };
83
49
  /** Splits an authority into userinfo, host and port. */
84
50
  function parseAuthority(authority) {
85
51
  const at = authority.lastIndexOf("@");
@@ -109,184 +75,6 @@ function serializeAuthority(components) {
109
75
  if (components.port !== void 0) out += ":" + components.port;
110
76
  return out;
111
77
  }
112
- /** RFC 3986 section 6.2 syntax-based and scheme-based normalisation: case, percent-encoding, dot segments, default ports, empty path. */
113
- function normalizeUri(input, options = {}) {
114
- const ports = options.defaultPorts ?? DEFAULT_PORTS;
115
- const schemeBased = options.schemeBased ?? true;
116
- const c = parseUri(input);
117
- const scheme = c.scheme?.toLowerCase();
118
- if (scheme !== void 0) c.scheme = scheme;
119
- const defaultPort = scheme === void 0 || !schemeBased ? void 0 : ports[scheme];
120
- if (c.authority !== void 0) {
121
- const a = parseAuthority(c.authority);
122
- const userinfo = options.userinfo ?? "keep";
123
- if (userinfo === "strip") delete a.userinfo;
124
- else if (a.userinfo !== void 0) {
125
- if (userinfo === "strip-password") a.userinfo = a.userinfo.split(":", 1)[0] ?? "";
126
- a.userinfo = normalizePercentEncoding(a.userinfo);
127
- }
128
- a.host = normalizePercentEncoding(a.host.replace(/[A-Z]+/g, (m) => m.toLowerCase()));
129
- if (a.port !== void 0 && (a.port === "" || Number(a.port) === defaultPort)) delete a.port;
130
- c.authority = serializeAuthority(a);
131
- }
132
- c.path = normalizePercentEncoding(c.path);
133
- if (c.scheme !== void 0 || c.authority !== void 0) c.path = removeDotSegments(c.path);
134
- if (schemeBased && c.scheme !== void 0 && c.authority !== void 0 && c.path === "") c.path = "/";
135
- if (c.query !== void 0) c.query = normalizePercentEncoding(c.query);
136
- if (c.fragment !== void 0) c.fragment = normalizePercentEncoding(c.fragment);
137
- return serializeUri(c);
138
- }
139
-
140
- //#endregion
141
- //#region src/uri/resolve.ts
142
- /** RFC 3986 section 5.2.3: merges a relative path reference with the base path. */
143
- function mergePaths(base, referencePath) {
144
- if (base.authority !== void 0 && base.path === "") return "/" + referencePath;
145
- const index = base.path.lastIndexOf("/");
146
- return index === -1 ? referencePath : base.path.slice(0, index + 1) + referencePath;
147
- }
148
- /**
149
- * RFC 3986 section 5.2 reference resolution; every section 5.4 example passes.
150
- * @throws {TypeError} when `base` is not an absolute URI, unless `allowRelativeBase` is set.
151
- */
152
- function resolveUri(base, reference, options = {}) {
153
- const b = parseUri(base);
154
- if (b.scheme === void 0 && options.allowRelativeBase !== true) throw new RangeError("resolveUri: base must be an absolute-URI (RFC 3986 §5.1); pass { allowRelativeBase: true } to resolve against a relative reference");
155
- delete b.fragment;
156
- if (b.scheme !== void 0 || b.authority !== void 0) b.path = removeDotSegments(b.path);
157
- const r = parseUri(reference);
158
- if (!(options.strict ?? true) && r.scheme !== void 0 && b.scheme !== void 0 && r.scheme.toLowerCase() === b.scheme.toLowerCase()) delete r.scheme;
159
- const target = { path: "" };
160
- if (r.scheme !== void 0) {
161
- target.scheme = r.scheme;
162
- if (r.authority !== void 0) target.authority = r.authority;
163
- target.path = removeDotSegments(r.path);
164
- if (r.query !== void 0) target.query = r.query;
165
- } else {
166
- if (r.authority !== void 0) {
167
- target.authority = r.authority;
168
- target.path = removeDotSegments(r.path);
169
- if (r.query !== void 0) target.query = r.query;
170
- } else {
171
- if (r.path === "") {
172
- target.path = b.path;
173
- const query = r.query ?? b.query;
174
- if (query !== void 0) target.query = query;
175
- } else {
176
- target.path = removeDotSegments(r.path.startsWith("/") ? r.path : mergePaths(b, r.path));
177
- if (r.query !== void 0) target.query = r.query;
178
- }
179
- if (b.authority !== void 0) target.authority = b.authority;
180
- }
181
- if (b.scheme !== void 0) target.scheme = b.scheme;
182
- }
183
- if (r.fragment !== void 0) target.fragment = r.fragment;
184
- return serializeUri(target);
185
- }
186
-
187
- //#endregion
188
- //#region src/uri/compare.ts
189
- function withoutFragment(uri) {
190
- const c = parseUri(uri);
191
- delete c.fragment;
192
- return serializeUri(c);
193
- }
194
- function prepare(uri, options) {
195
- let out = options.base === void 0 ? uri : resolveUri(options.base, uri);
196
- if (options.ignoreFragment === true) out = withoutFragment(out);
197
- switch (options.level ?? "scheme") {
198
- case "simple": return out;
199
- case "syntax": return normalizeUri(out, { schemeBased: false });
200
- default: return options.defaultPorts === void 0 ? normalizeUri(out) : normalizeUri(out, { defaultPorts: options.defaultPorts });
201
- }
202
- }
203
- /** Whether two URIs identify the same resource under the chosen normalisation level. IRIs are never mapped to URIs (RFC 3987 section 5.3.1). */
204
- function equivalentUris(a, b, options = {}) {
205
- return prepare(a, options) === prepare(b, options);
206
- }
207
- /** RFC 3986 section 4.4: whether `reference` resolved against `base` differs from it only in the fragment. */
208
- function isSameDocumentReference(base, reference, options = {}) {
209
- const target = withoutFragment(resolveUri(base, reference));
210
- const origin = withoutFragment(base);
211
- return options.normalize === true ? normalizeUri(target) === normalizeUri(origin) : target === origin;
212
- }
213
-
214
- //#endregion
215
- //#region src/uri/encode.ts
216
- /** Encodes one path segment so that `/`, `?` and `#` inside it cannot be mistaken for delimiters. */
217
- function encodePathSegment(segment, options = {}) {
218
- return percentEncode(segment, options.noColon ? "segment-nz-nc" : "pchar");
219
- }
220
- /** Encodes each segment of `path`, keeping the `/` separators. */
221
- function encodePath(path, options = {}) {
222
- const noColonFirst = options.relative === true && !path.startsWith("/");
223
- return path.split("/").map((segment, index) => encodePathSegment(segment, { noColon: noColonFirst && index === 0 })).join("/");
224
- }
225
- /** Encodes `query` with the RFC 3986 `query` set, keeping `/`, `?`, `=` and `&`. */
226
- function encodeQuery(query) {
227
- return percentEncode(query, "query");
228
- }
229
- /** Encodes `fragment` with the RFC 3986 `fragment` set. */
230
- function encodeFragment(fragment) {
231
- return percentEncode(fragment, "fragment");
232
- }
233
- /** `application/x-www-form-urlencoded` encoding of one value: spaces become `+`, `*-._` and alphanumerics stay. */
234
- function encodeForm(value) {
235
- return percentEncode(value, "form");
236
- }
237
-
238
- //#endregion
239
- //#region src/uri/extract.ts
240
- const WRAPPERS = [
241
- ["<", ">"],
242
- ["\"", "\""],
243
- ["'", "'"],
244
- ["(", ")"],
245
- ["[", "]"],
246
- ["{", "}"]
247
- ];
248
- const TRAILING_PUNCTUATION = /[.,;:!?'"]+$/;
249
- const LEADING_PUNCTUATION = /^[(<["'{]+/;
250
- const MARKDOWN_LINK = /^\[[^\]]*\]\(\s*(?:<([^>]*)>|([^)\s]+))(?:\s+["'][^"']*["'])?\s*\)$/;
251
- const HTML_ATTRIBUTE = /\b(?:href|src|action|content)\s*=\s*(["'])(.*?)\1/i;
252
- /** 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
- function extractUri(text) {
254
- let out = text.trim().replace(/^url:\s*/i, "");
255
- const markdown = MARKDOWN_LINK.exec(out);
256
- if (markdown !== null) return extractUri(markdown[1] ?? markdown[2] ?? "");
257
- const attribute = HTML_ATTRIBUTE.exec(out);
258
- if (attribute !== null) return extractUri(attribute[2] ?? "");
259
- let wrapped = true;
260
- while (wrapped) {
261
- wrapped = false;
262
- for (const [open, close] of WRAPPERS) if (out.length >= 2 && out.startsWith(open) && out.endsWith(close)) {
263
- out = out.slice(open.length, -close.length).trim();
264
- wrapped = true;
265
- }
266
- for (const [open, close] of WRAPPERS) {
267
- const closeAt = out.lastIndexOf(close);
268
- if (out.startsWith(open) && closeAt !== -1 && open !== close) {
269
- out = out.slice(open.length, closeAt).trim();
270
- wrapped = true;
271
- break;
272
- }
273
- }
274
- }
275
- out = out.replace(LEADING_PUNCTUATION, "");
276
- out = out.replace(/\s+/g, "");
277
- let trimmed = true;
278
- while (trimmed) {
279
- trimmed = false;
280
- const before = out;
281
- out = out.replace(TRAILING_PUNCTUATION, "");
282
- for (const [open, close] of WRAPPERS) if (open !== close && out.endsWith(close) && count(out, open) < count(out, close)) out = out.slice(0, -close.length);
283
- if (out !== before) trimmed = true;
284
- }
285
- return out;
286
- }
287
- function count(text, char) {
288
- return text.split(char).length - 1;
289
- }
290
78
 
291
79
  //#endregion
292
80
  //#region src/uri/host.ts
@@ -318,6 +106,54 @@ function isIPv6Address(text) {
318
106
  const right = h16Groups(text.slice(gap + 2), true);
319
107
  return left >= 0 && right >= 0 && left + right <= 7;
320
108
  }
109
+ function splitGroups(part) {
110
+ return part === "" ? [] : part.split(":");
111
+ }
112
+ function zeroRun(groups) {
113
+ let bestStart = -1;
114
+ let bestLength = 0;
115
+ let i = 0;
116
+ while (i < groups.length) {
117
+ if (groups[i] !== "0") {
118
+ i += 1;
119
+ continue;
120
+ }
121
+ let j = i;
122
+ while (j < groups.length && groups[j] === "0") j += 1;
123
+ if (j - i > bestLength) {
124
+ bestStart = i;
125
+ bestLength = j - i;
126
+ }
127
+ i = j;
128
+ }
129
+ return [bestStart, bestLength];
130
+ }
131
+ /** 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. */
132
+ function normalizeIPv6Address(text) {
133
+ if (!isIPv6Address(text)) return text;
134
+ const lower = text.toLowerCase();
135
+ const lastColon = lower.lastIndexOf(":");
136
+ const tail = lower.includes(".", lastColon) ? lower.slice(lastColon + 1) : void 0;
137
+ const body = tail === void 0 ? lower : lower.slice(0, lastColon + 1);
138
+ const total = tail === void 0 ? 8 : 6;
139
+ const gap = body.indexOf("::");
140
+ let groups;
141
+ if (gap === -1) groups = splitGroups(body.replace(/:$/, ""));
142
+ else {
143
+ const left = splitGroups(body.slice(0, gap));
144
+ const right = splitGroups(body.slice(gap + 2).replace(/:$/, ""));
145
+ groups = [
146
+ ...left,
147
+ ...Array.from({ length: total - left.length - right.length }, () => "0"),
148
+ ...right
149
+ ];
150
+ }
151
+ groups = groups.map((group) => group.replace(/^0+(?=.)/, ""));
152
+ const [start, length] = zeroRun(groups);
153
+ const out = length >= 2 ? `${groups.slice(0, start).join(":")}::${groups.slice(start + length).join(":")}` : groups.join(":");
154
+ if (tail === void 0) return out;
155
+ return out.endsWith(":") ? out + tail : `${out}:${tail}`;
156
+ }
321
157
  /** RFC 3986 `IPvFuture`: `v` + hex + `.` + unreserved or sub-delims. */
322
158
  function isIPvFuture(text) {
323
159
  return IPV_FUTURE.test(text);
@@ -363,6 +199,46 @@ function isHost(text) {
363
199
  return parseHost(text) !== void 0;
364
200
  }
365
201
 
202
+ //#endregion
203
+ //#region src/uri/path.ts
204
+ function removeLastSegment(output) {
205
+ const index = output.lastIndexOf("/");
206
+ return index === -1 ? "" : output.slice(0, index);
207
+ }
208
+ /** RFC 3986 section 5.2.4, the two-buffer algorithm as written: resolves `.` and `..` segments. */
209
+ function removeDotSegments(path) {
210
+ let input = path;
211
+ let output = "";
212
+ while (input.length > 0) if (input.startsWith("../")) input = input.slice(3);
213
+ else if (input.startsWith("./")) input = input.slice(2);
214
+ else if (input.startsWith("/./")) input = input.slice(2);
215
+ else if (input === "/.") input = "/";
216
+ else if (input.startsWith("/../")) {
217
+ input = input.slice(3);
218
+ output = removeLastSegment(output);
219
+ } else if (input === "/..") {
220
+ input = "/";
221
+ output = removeLastSegment(output);
222
+ } else if (input === "." || input === "..") input = "";
223
+ else {
224
+ const start = input.startsWith("/") ? 1 : 0;
225
+ const next = input.indexOf("/", start);
226
+ const end = next === -1 ? input.length : next;
227
+ output += input.slice(0, end);
228
+ input = input.slice(end);
229
+ }
230
+ return output;
231
+ }
232
+ /** Removes dot segments and normalises percent-encoding in a path, relative paths included. */
233
+ function normalizePath(path, options = {}) {
234
+ let out = normalizePercentEncoding(path);
235
+ if (out.startsWith("/")) out = removeDotSegments(out);
236
+ const mode = options.trailingSlash ?? "keep";
237
+ if (mode === "add" && out.length > 0 && !out.endsWith("/")) out += "/";
238
+ if (mode === "remove" && out.length > 1 && out.endsWith("/")) out = out.slice(0, -1);
239
+ return out;
240
+ }
241
+
366
242
  //#endregion
367
243
  //#region src/uri/punycode.ts
368
244
  const BASE = 36;
@@ -542,6 +418,293 @@ function domainToUnicode(host) {
542
418
  return host.endsWith(".") && out !== "" ? `${out}.` : out;
543
419
  }
544
420
 
421
+ //#endregion
422
+ //#region src/uri/normalize.ts
423
+ /** Default ports of the common schemes, used by `normalizeUri` and `equivalentUris`. */
424
+ const DEFAULT_PORTS = {
425
+ http: 80,
426
+ https: 443,
427
+ ws: 80,
428
+ wss: 443,
429
+ ftp: 21
430
+ };
431
+ /** RFC 3986 section 6.2 syntax-based and scheme-based normalisation: case, percent-encoding, dot segments, default ports, empty path. */
432
+ function normalizeUri(input, options = {}) {
433
+ const ports = options.defaultPorts ?? DEFAULT_PORTS;
434
+ const schemeBased = options.schemeBased ?? true;
435
+ const c = parseUri(input);
436
+ const scheme = c.scheme?.toLowerCase();
437
+ if (scheme !== void 0) c.scheme = scheme;
438
+ const defaultPort = scheme === void 0 || !schemeBased ? void 0 : ports[scheme];
439
+ if (c.authority !== void 0) {
440
+ const a = parseAuthority(c.authority);
441
+ const userinfo = options.userinfo ?? "keep";
442
+ if (userinfo === "strip") delete a.userinfo;
443
+ else if (a.userinfo !== void 0) {
444
+ if (userinfo === "strip-password") a.userinfo = a.userinfo.split(":", 1)[0] ?? "";
445
+ a.userinfo = normalizePercentEncoding(a.userinfo);
446
+ }
447
+ a.host = normalizePercentEncoding(a.host.replace(/[A-Z]+/g, (m) => m.toLowerCase()));
448
+ if (options.strict === true) {
449
+ if (!isHost(a.host)) throw new TypeError(`normalizeUri: invalid host ${JSON.stringify(a.host)}`);
450
+ if (a.port !== void 0 && !/^\d*$/.test(a.port)) throw new TypeError(`normalizeUri: invalid port ${JSON.stringify(a.port)}`);
451
+ }
452
+ if (isIPLiteral(a.host)) a.host = `[${normalizeIPv6Address(a.host.slice(1, -1))}]`;
453
+ else if (options.host === "idna") a.host = domainToAscii(a.host);
454
+ else if (options.host === "unicode") a.host = domainToUnicode(a.host);
455
+ if (a.port !== void 0 && (a.port === "" || Number(a.port) === defaultPort)) delete a.port;
456
+ c.authority = serializeAuthority(a);
457
+ }
458
+ c.path = normalizePercentEncoding(c.path);
459
+ if (c.scheme !== void 0 || c.authority !== void 0) c.path = removeDotSegments(c.path);
460
+ if (schemeBased && c.scheme !== void 0 && c.authority !== void 0 && c.path === "") c.path = "/";
461
+ const trailing = options.trailingSlash ?? "keep";
462
+ if (trailing === "remove" && c.path.length > 1 && c.path.endsWith("/")) c.path = c.path.replace(/\/+$/, "");
463
+ else if (trailing === "add" && c.path !== "" && !c.path.endsWith("/")) c.path += "/";
464
+ if (c.query !== void 0) c.query = normalizePercentEncoding(c.query);
465
+ if (c.fragment !== void 0) c.fragment = normalizePercentEncoding(c.fragment);
466
+ if (options.emptyQuery === "remove" && c.query === "") delete c.query;
467
+ if (options.emptyFragment === "remove" && c.fragment === "") delete c.fragment;
468
+ return serializeUri(c);
469
+ }
470
+
471
+ //#endregion
472
+ //#region src/uri/helpers.ts
473
+ /** 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. */
474
+ function getOrigin(uri) {
475
+ const c = parseUri(uri, { authority: true });
476
+ if (c.scheme === void 0 || c.host === void 0) return void 0;
477
+ const scheme = c.scheme.toLowerCase();
478
+ 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}`}`;
479
+ }
480
+ /** Whether two URIs share an RFC 6454 origin. Two URIs without an origin never match. */
481
+ function isSameOrigin(a, b) {
482
+ const origin = getOrigin(a);
483
+ return origin !== void 0 && origin === getOrigin(b);
484
+ }
485
+ /** The URI without its fragment. */
486
+ function stripFragment(uri) {
487
+ const c = parseUri(uri);
488
+ if (c.fragment === void 0) return uri;
489
+ delete c.fragment;
490
+ return serializeUri(c);
491
+ }
492
+ /** Decodes an `application/x-www-form-urlencoded` query (`a=1&b=2`, `+` as space) into pairs in order; a leading `?` is ignored. */
493
+ function parseQuery(query) {
494
+ const text = query.startsWith("?") ? query.slice(1) : query;
495
+ const out = [];
496
+ if (text === "") return out;
497
+ for (const part of text.split("&")) {
498
+ if (part === "") continue;
499
+ const eq = part.indexOf("=");
500
+ const name = eq === -1 ? part : part.slice(0, eq);
501
+ const value = eq === -1 ? "" : part.slice(eq + 1);
502
+ out.push([percentDecode(name, { plusAsSpace: true }), percentDecode(value, { plusAsSpace: true })]);
503
+ }
504
+ return out;
505
+ }
506
+ /** Encodes pairs as an `application/x-www-form-urlencoded` query without the leading `?`. */
507
+ function stringifyQuery(pairs) {
508
+ const parts = [];
509
+ for (const [name, value] of pairs) parts.push(`${percentEncode(name, "form")}=${percentEncode(value, "form")}`);
510
+ return parts.join("&");
511
+ }
512
+ /** The URI with its query parameters sorted by name, then value, byte-wise on the encoded text; parameters are otherwise left as written. */
513
+ function sortQuery(uri) {
514
+ const c = parseUri(uri);
515
+ if (c.query === void 0 || c.query === "") return uri;
516
+ const parts = c.query.split("&").filter((part) => part !== "");
517
+ parts.sort((x, y) => {
518
+ const [xn, xv] = splitPair(x);
519
+ const [yn, yv] = splitPair(y);
520
+ return xn < yn ? -1 : xn > yn ? 1 : xv < yv ? -1 : xv > yv ? 1 : 0;
521
+ });
522
+ c.query = parts.join("&");
523
+ return serializeUri(c);
524
+ }
525
+ function splitPair(part) {
526
+ const eq = part.indexOf("=");
527
+ return eq === -1 ? [part, ""] : [part.slice(0, eq), part.slice(eq + 1)];
528
+ }
529
+ /** 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. */
530
+ function joinPaths(...pieces) {
531
+ const parts = pieces.filter((piece) => piece !== "");
532
+ if (parts.length === 0) return "";
533
+ const first = parts[0] ?? "";
534
+ const last = parts[parts.length - 1] ?? "";
535
+ const leading = first.startsWith("/") ? "/" : "";
536
+ const trailing = last.endsWith("/") && last !== "/" ? "/" : "";
537
+ const body = parts.map((piece) => piece.replace(/^\/+|\/+$/g, "")).filter((piece) => piece !== "").join("/");
538
+ if (body === "") return leading === "" ? "" : "/";
539
+ const joined = normalizePath(leading + body.replace(/\/{2,}/g, "/"));
540
+ return joined === "/" || joined === "" ? joined : joined + trailing;
541
+ }
542
+
543
+ //#endregion
544
+ //#region src/uri/resolve.ts
545
+ /** RFC 3986 section 5.2.3: merges a relative path reference with the base path. */
546
+ function mergePaths(base, referencePath) {
547
+ if (base.authority !== void 0 && base.path === "") return "/" + referencePath;
548
+ const index = base.path.lastIndexOf("/");
549
+ return index === -1 ? referencePath : base.path.slice(0, index + 1) + referencePath;
550
+ }
551
+ /**
552
+ * RFC 3986 section 5.2 reference resolution; every section 5.4 example passes.
553
+ * @throws {TypeError} when `base` is not an absolute URI, unless `allowRelativeBase` is set.
554
+ */
555
+ function resolveUri(base, reference, options = {}) {
556
+ const b = parseUri(base);
557
+ if (b.scheme === void 0 && options.allowRelativeBase !== true) throw new RangeError("resolveUri: base must be an absolute-URI (RFC 3986 §5.1); pass { allowRelativeBase: true } to resolve against a relative reference");
558
+ delete b.fragment;
559
+ if (b.scheme !== void 0 || b.authority !== void 0) b.path = removeDotSegments(b.path);
560
+ const r = parseUri(reference);
561
+ if (!(options.strict ?? true) && r.scheme !== void 0 && b.scheme !== void 0 && r.scheme.toLowerCase() === b.scheme.toLowerCase()) delete r.scheme;
562
+ const target = { path: "" };
563
+ if (r.scheme !== void 0) {
564
+ target.scheme = r.scheme;
565
+ if (r.authority !== void 0) target.authority = r.authority;
566
+ target.path = removeDotSegments(r.path);
567
+ if (r.query !== void 0) target.query = r.query;
568
+ } else {
569
+ if (r.authority !== void 0) {
570
+ target.authority = r.authority;
571
+ target.path = removeDotSegments(r.path);
572
+ if (r.query !== void 0) target.query = r.query;
573
+ } else {
574
+ if (r.path === "") {
575
+ target.path = b.path;
576
+ const query = r.query ?? b.query;
577
+ if (query !== void 0) target.query = query;
578
+ } else {
579
+ target.path = removeDotSegments(r.path.startsWith("/") ? r.path : mergePaths(b, r.path));
580
+ if (r.query !== void 0) target.query = r.query;
581
+ }
582
+ if (b.authority !== void 0) target.authority = b.authority;
583
+ }
584
+ if (b.scheme !== void 0) target.scheme = b.scheme;
585
+ }
586
+ if (r.fragment !== void 0) target.fragment = r.fragment;
587
+ return serializeUri(target);
588
+ }
589
+
590
+ //#endregion
591
+ //#region src/uri/compare.ts
592
+ function prepare(uri, options) {
593
+ let out = options.base === void 0 ? uri : resolveUri(options.base, uri);
594
+ if (options.ignoreFragment === true) out = stripFragment(out);
595
+ switch (options.level ?? "scheme") {
596
+ case "simple": return out;
597
+ case "syntax": return normalizeUri(out, { schemeBased: false });
598
+ default: return options.defaultPorts === void 0 ? normalizeUri(out) : normalizeUri(out, { defaultPorts: options.defaultPorts });
599
+ }
600
+ }
601
+ /** Whether two URIs identify the same resource under the chosen normalisation level. IRIs are never mapped to URIs (RFC 3987 section 5.3.1). */
602
+ function equivalentUris(a, b, options = {}) {
603
+ return prepare(a, options) === prepare(b, options);
604
+ }
605
+ /** RFC 3986 section 4.4: whether `reference` resolved against `base` differs from it only in the fragment. */
606
+ function isSameDocumentReference(base, reference, options = {}) {
607
+ const target = stripFragment(resolveUri(base, reference));
608
+ const origin = stripFragment(base);
609
+ return options.normalize === true ? normalizeUri(target) === normalizeUri(origin) : target === origin;
610
+ }
611
+
612
+ //#endregion
613
+ //#region src/uri/encode.ts
614
+ /** Encodes one path segment so that `/`, `?` and `#` inside it cannot be mistaken for delimiters. */
615
+ function encodePathSegment(segment, options = {}) {
616
+ return percentEncode(segment, options.noColon ? "segment-nz-nc" : "pchar");
617
+ }
618
+ /** Encodes each segment of `path`, keeping the `/` separators. */
619
+ function encodePath(path, options = {}) {
620
+ const noColonFirst = options.relative === true && !path.startsWith("/");
621
+ return path.split("/").map((segment, index) => encodePathSegment(segment, { noColon: noColonFirst && index === 0 })).join("/");
622
+ }
623
+ /** Encodes `query` with the RFC 3986 `query` set, keeping `/`, `?`, `=` and `&`. */
624
+ function encodeQuery(query) {
625
+ return percentEncode(query, "query");
626
+ }
627
+ /** Encodes `fragment` with the RFC 3986 `fragment` set. */
628
+ function encodeFragment(fragment) {
629
+ return percentEncode(fragment, "fragment");
630
+ }
631
+ /** `application/x-www-form-urlencoded` encoding of one value: spaces become `+`, `*-._` and alphanumerics stay. */
632
+ function encodeForm(value) {
633
+ return percentEncode(value, "form");
634
+ }
635
+
636
+ //#endregion
637
+ //#region src/uri/extract.ts
638
+ const WRAPPERS = [
639
+ ["<", ">"],
640
+ ["\"", "\""],
641
+ ["'", "'"],
642
+ ["(", ")"],
643
+ ["[", "]"],
644
+ ["{", "}"]
645
+ ];
646
+ const TRAILING_PUNCTUATION = /[.,;:!?'"]+$/;
647
+ const LEADING_PUNCTUATION = /^[(<["'{]+/;
648
+ const MARKDOWN_LINK = /^\[[^\]]*\]\(\s*(?:<([^>]*)>|([^)\s]+))(?:\s+["'][^"']*["'])?\s*\)$/;
649
+ const HTML_ATTRIBUTE = /\b(?:href|src|action|content)\s*=\s*(["'])(.*?)\1/i;
650
+ const IN_PROSE = /\b(?:[A-Za-z][A-Za-z0-9+.-]*:\/\/|(?:mailto|tel|urn|data|sms|geo|magnet|xmpp|sip|sips):|www\.)[^\s<>"']+/g;
651
+ /** 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. */
652
+ function findUris(text) {
653
+ const out = [];
654
+ for (const match of text.matchAll(IN_PROSE)) {
655
+ const uri = trimTrailing(match[0]);
656
+ if (uri === "" || uri.startsWith("www.") && !uri.includes(".", 4)) continue;
657
+ out.push({
658
+ uri,
659
+ start: match.index,
660
+ end: match.index + uri.length
661
+ });
662
+ }
663
+ return out;
664
+ }
665
+ /** 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. */
666
+ function extractUri(text) {
667
+ let out = text.trim().replace(/^url:\s*/i, "");
668
+ const markdown = MARKDOWN_LINK.exec(out);
669
+ if (markdown !== null) return extractUri(markdown[1] ?? markdown[2] ?? "");
670
+ const attribute = HTML_ATTRIBUTE.exec(out);
671
+ if (attribute !== null) return extractUri(attribute[2] ?? "");
672
+ let wrapped = true;
673
+ while (wrapped) {
674
+ wrapped = false;
675
+ for (const [open, close] of WRAPPERS) if (out.length >= 2 && out.startsWith(open) && out.endsWith(close)) {
676
+ out = out.slice(open.length, -close.length).trim();
677
+ wrapped = true;
678
+ }
679
+ for (const [open, close] of WRAPPERS) {
680
+ const closeAt = out.lastIndexOf(close);
681
+ if (out.startsWith(open) && closeAt !== -1 && open !== close) {
682
+ out = out.slice(open.length, closeAt).trim();
683
+ wrapped = true;
684
+ break;
685
+ }
686
+ }
687
+ }
688
+ out = out.replace(LEADING_PUNCTUATION, "");
689
+ out = out.replace(/\s+/g, "");
690
+ return trimTrailing(out);
691
+ }
692
+ function trimTrailing(text) {
693
+ let out = text;
694
+ let trimmed = true;
695
+ while (trimmed) {
696
+ trimmed = false;
697
+ const before = out;
698
+ out = out.replace(TRAILING_PUNCTUATION, "");
699
+ for (const [open, close] of WRAPPERS) if (open !== close && out.endsWith(close) && count(out, open) < count(out, close)) out = out.slice(0, -close.length);
700
+ if (out !== before) trimmed = true;
701
+ }
702
+ return out;
703
+ }
704
+ function count(text, char) {
705
+ return text.split(char).length - 1;
706
+ }
707
+
545
708
  //#endregion
546
709
  //#region src/uri/iri.ts
547
710
  /** RFC 3987 `ucschar`: the non-ASCII code points an IRI may contain unencoded. */
@@ -793,4 +956,4 @@ function isIri(input) {
793
956
  }
794
957
 
795
958
  //#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 };
959
+ 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 };