]> Lady’s Gitweb - Pisces/blob - iri.js
More comprehensive support for RFC 3986 & RFC 3987
[Pisces] / iri.js
1 // ♓🌟 Piscēs ∷ iri.js
2 // ====================================================================
3 //
4 // Copyright © 2020, 2022 Lady [@ Lady’s Computer].
5 //
6 // This Source Code Form is subject to the terms of the Mozilla Public
7 // License, v. 2.0. If a copy of the MPL was not distributed with this
8 // file, You can obtain one at <https://mozilla.org/MPL/2.0/>.
9
10 const sub·delims = String.raw`[!\$&'()*+,;=]`;
11 const gen·delims = String.raw`[:/?#\[\]@]`;
12 //deno-lint-ignore no-unused-vars
13 const reserved = String.raw`${gen·delims}|${sub·delims}`;
14 const unreserved = String.raw`[A-Za-z0-9\-\._~]`;
15 const pct·encoded = String.raw`%[0-9A-Fa-f][0-9A-Fa-f]`;
16 const dec·octet = String.raw
17 `[0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5]`;
18 const IPv4address = String.raw
19 `(?:${dec·octet})\.(?:${dec·octet})\.(?:${dec·octet})\.(?:${dec·octet})`;
20 const h16 = String.raw`[0-9A-Fa-f]{1,4}`;
21 const ls32 = String.raw`(?:${h16}):(?:${h16})|${IPv4address}`;
22 const IPv6address = String.raw
23 `(?:(?:${h16}):){6}(?:${ls32})|::(?:(?:${h16}):){5}(?:${ls32})|(?:${h16})?::(?:(?:${h16}):){4}(?:${ls32})|(?:(?:(?:${h16}):){0,1}(?:${h16}))?::(?:(?:${h16}):){3}(?:${ls32})|(?:(?:(?:${h16}):){0,2}(?:${h16}))?::(?:(?:${h16}):){2}(?:${ls32})|(?:(?:(?:${h16}):){0,3}(?:${h16}))?::(?:${h16}):(?:${ls32})|(?:(?:(?:${h16}):){0,4}(?:${h16}))?::(?:${ls32})|(?:(?:(?:${h16}):){0,5}(?:${h16}))?::(?:${h16})|(?:(?:(?:${h16}):){0,6}(?:${h16}))?::`;
24 const IPvFuture = String.raw
25 `v[0-9A-Fa-f]{1,}\.(?:${unreserved}|${sub·delims}|:)`;
26 const IP·literal = String.raw`\[(?:${IPv6address}|${IPvFuture})\]`;
27 const port = String.raw`[0-9]*`;
28 const scheme = String.raw`[A-Za-z][A-Za-z0-9+\-\.]*`;
29 const pchar = String.raw
30 `${unreserved}|${pct·encoded}|${sub·delims}|[:@]`;
31 const fragment = String.raw`(?:${pchar}|[/?])*`;
32 const query = String.raw`(?:${pchar}|[/?])*`;
33 const segment·nz·nc = String.raw
34 `(?:${unreserved}|${pct·encoded}|${sub·delims}|@)+`;
35 const segment·nz = String.raw`(?:${pchar})+`;
36 const segment = String.raw`(?:${pchar})*`;
37 const path·empty = String.raw``;
38 const path·rootless = String.raw
39 `(?:${segment·nz})(?:/(?:${segment}))*`;
40 const path·noscheme = String.raw
41 `(?:${segment·nz·nc})(?:/(?:${segment}))*`;
42 const path·absolute = String.raw
43 `/(?:(?:${segment·nz})(?:/(?:${segment}))*)?`;
44 const path·abempty = String.raw`(?:/(?:${segment}))*`;
45 //deno-lint-ignore no-unused-vars
46 const path = String.raw
47 `${path·abempty}|${path·absolute}|${path·noscheme}|${path·rootless}|${path·empty}`;
48 const reg·name = String.raw
49 `(?:${unreserved}|${pct·encoded}|${sub·delims})*`;
50 const host = String.raw`${IP·literal}|${IPv4address}|${reg·name}`;
51 const userinfo = String.raw
52 `(?:${unreserved}|${pct·encoded}|${sub·delims}|:)*`;
53 const authority = String.raw
54 `(?:(?:${userinfo})@)?(?:${host})(?::(?:${port}))?`;
55 const relative·part = String.raw
56 `//(?:${authority})(?:${path·abempty})|(?:${path·absolute})|(?:${path·noscheme})|(?:${path·empty})`;
57 const relative·ref = String.raw
58 `(?:${relative·part})(?:\?(?:${query}))?(?:#(?:${fragment}))?`;
59 const hier·part = String.raw
60 `//(?:${authority})(?:${path·abempty})|(?:${path·absolute})|(?:${path·rootless})|(?:${path·empty})`;
61 const absolute·URI = String.raw
62 `(?:${scheme}):(?:${hier·part})(?:\?(?:${query}))?`;
63 const URI = String.raw
64 `(?:${scheme}):(?:${hier·part})(?:\?(?:${query}))?(?:#(?:${fragment}))?`;
65 const URI·reference = String.raw`(?:${URI})|(?:${relative·ref})`;
66
67 const iprivate = String.raw
68 `[\u{E000}-\u{F8FF}\u{F0000}-\u{FFFFD}\u{100000}-\u{10FFFD}]`;
69 const ucschar = String.raw
70 `[\u{A0}-\u{D7FF}\u{F900}-\u{FDCF}\u{FDF0}-\u{FFEF}\u{10000}-\u{1FFFD}\u{20000}-\u{2FFFD}\u{30000}-\u{3FFFD}\u{40000}-\u{4FFFD}\u{50000}-\u{5FFFD}\u{60000}-\u{6FFFD}\u{70000}-\u{7FFFD}\u{80000}-\u{8FFFD}\u{90000}-\u{9FFFD}\u{A0000}-\u{AFFFD}\u{B0000}-\u{BFFFD}\u{C0000}-\u{CFFFD}\u{D0000}-\u{DFFFD}\u{E0000}-\u{EFFFD}]`;
71 const iunreserved = String.raw`[A-Za-z0-9\-\._~]|${ucschar}`;
72 const ipchar = String.raw
73 `${iunreserved}|${pct·encoded}|${sub·delims}|[:@]`;
74 const ifragment = String.raw`(?:${ipchar}|[/?])*`;
75 const iquery = String.raw`(?:${ipchar}|${iprivate}|[/?])*`;
76 const isegment·nz·nc = String.raw
77 `(?:${iunreserved}|${pct·encoded}|${sub·delims}|@)+`;
78 const isegment·nz = String.raw`(?:${ipchar})+`;
79 const isegment = String.raw`(?:${ipchar})*`;
80 const ipath·empty = String.raw``;
81 const ipath·rootless = String.raw
82 `(?:${isegment·nz})(?:/(?:${isegment}))*`;
83 const ipath·noscheme = String.raw
84 `(?:${isegment·nz·nc})(?:/(?:${isegment}))*`;
85 const ipath·absolute = String.raw
86 `/(?:(?:${isegment·nz})(?:/(?:${isegment}))*)?`;
87 const ipath·abempty = String.raw`(?:/(?:${isegment}))*`;
88 //deno-lint-ignore no-unused-vars
89 const ipath = String.raw
90 `${ipath·abempty}|${ipath·absolute}|${ipath·noscheme}|${ipath·rootless}|${ipath·empty}`;
91 const ireg·name = String.raw
92 `(?:${iunreserved}|${pct·encoded}|${sub·delims})*`;
93 const ihost = String.raw`${IP·literal}|${IPv4address}|${ireg·name}`;
94 const iuserinfo = String.raw
95 `(?:${iunreserved}|${pct·encoded}|${sub·delims}|:)*`;
96 const iauthority = String.raw
97 `(?:(?:${iuserinfo})@)?(?:${ihost})(?::(?:${port}))?`;
98 const irelative·part = String.raw
99 `//(?:${iauthority})(?:${ipath·abempty})|(?:${ipath·absolute})|(?:${ipath·noscheme})|(?:${ipath·empty})`;
100 const irelative·ref = String.raw
101 `(?:${irelative·part})(?:\?(?:${iquery}))?(?:#(?:${ifragment}))?`;
102 const ihier·part = String.raw
103 `//(?:${iauthority})(?:${ipath·abempty})|(?:${ipath·absolute})|(?:${ipath·rootless})|(?:${ipath·empty})`;
104 const absolute·IRI = String.raw
105 `(?:${scheme}):(?:${ihier·part})(?:\?(?:${iquery}))?`;
106 const IRI = String.raw
107 `(?:${scheme}):(?:${ihier·part})(?:\?(?:${iquery}))?(?:#(?:${ifragment}))?`;
108 const IRI·reference = String.raw`(?:${IRI})|(?:${irelative·ref})`;
109
110 export const {
111 isAbsoluteURI, // U·R·I with no fragment
112 isURI,
113 isURIReference,
114 isAbsoluteIRI, // I·R·I with no fragment
115 isIRI,
116 isIRIReference,
117 } = Object.fromEntries(
118 Object.entries({
119 isAbsoluteIRI: absolute·IRI,
120 isAbsoluteURI: absolute·URI,
121 isIRI: IRI,
122 isIRIReference: IRI·reference,
123 isURI: URI,
124 isURIReference: URI·reference,
125 }).map(([key, value]) => {
126 const regExp = new RegExp(`^(?:${value})$`, "u");
127 return [
128 key,
129 Object.defineProperties(
130 ($) => typeof $ == "string" && regExp.test($),
131 {
132 name: { value: key },
133 [Symbol.match]: {
134 configurable: true,
135 enumerable: false,
136 get: () => regExp[Symbol.match].bind(regExp),
137 set: undefined,
138 },
139 },
140 ),
141 ];
142 }),
143 );
144
145 /**
146 * Recomposes an I·R·I reference from its component parts.
147 *
148 * See §5.3 of R·F·C 3986.
149 */
150 export const composeReference = ($) => {
151 const result = [];
152 const { scheme, authority, path, query, fragment } = $;
153 if (scheme != null) {
154 result.push(scheme, ":");
155 } else {
156 /* do nothing */
157 }
158 if (authority != null) {
159 result.push("//", authority);
160 } else {
161 /* do nothing */
162 }
163 result.push(path ?? "");
164 if (query != null) {
165 result.push("?", query);
166 } else {
167 /* do nothing */
168 }
169 if (fragment != null) {
170 result.push("#", fragment);
171 } else {
172 /* do nothing */
173 }
174 return result.join("");
175 };
176
177 /**
178 * Converts an I·R·I to the corresponding U·R·I by percent‐encoding
179 * unsupported characters.
180 *
181 * This does not punycode the authority.
182 */
183 export const iri2uri = ($) =>
184 [...function* () {
185 const encoder = new TextEncoder();
186 for (const character of $) {
187 if (new RegExp(`${ucschar}|${iprivate}`, "u").test(character)) {
188 for (const byte of encoder.encode(character)) {
189 yield `%${byte.toString(0x10).toUpperCase()}`;
190 }
191 } else {
192 yield character;
193 }
194 }
195 }()].join("");
196
197 /**
198 * Merges a reference path with a base path.
199 *
200 * See §5.2.3 of R·F·C 3986.
201 */
202 export const mergePaths = (base, reference) => {
203 const baseStr = `${base}`;
204 return `${
205 baseStr.substring(0, baseStr.lastIndexOf("/") + 1)
206 }${reference}`;
207 };
208
209 /**
210 * Returns the `scheme`, `authority`, `path`, `query`, and `fragment`
211 * of the provided I·R·I reference.
212 */
213 export const parseReference = ($) => {
214 const regExp = new RegExp(
215 String.raw
216 `^(?:(?<absolute·scheme>${scheme}):(?://(?<absolute·authority>${iauthority})(?<absolute·patha>${ipath·abempty})|(?<absolute·pathb>(?:${ipath·absolute})|(?:${ipath·rootless})|(?:${ipath·empty})))(?:\?(?<absolute·query>${iquery}))?(?:#(?<absolute·fragment>${ifragment}))?|(?://(?<relative·authority>${iauthority})(?<relative·patha>${ipath·abempty})|(?<relative·pathb>(?:${ipath·absolute})|(?:${ipath·noscheme})|(?:${ipath·empty})))(?:\?(?<relative·query>${iquery}))?(?:#(?<relative·fragment>${ifragment}))?)$`,
217 "u",
218 );
219 const {
220 absolute·scheme,
221 absolute·authority,
222 absolute·patha,
223 absolute·pathb,
224 absolute·query,
225 absolute·fragment,
226 relative·authority,
227 relative·patha,
228 relative·pathb,
229 relative·query,
230 relative·fragment,
231 } = regExp.exec($)?.groups ?? {};
232 return {
233 scheme: absolute·scheme,
234 authority: absolute·authority ?? relative·authority,
235 path: absolute·patha ?? absolute·pathb ?? relative·patha ??
236 relative·pathb,
237 query: absolute·query ?? relative·query,
238 fragment: absolute·fragment ?? relative·fragment,
239 };
240 };
241
242 /**
243 * Removes all dot segments ("." or "..") from the provided I·R·I.
244 *
245 * See §5.2.4 of R·F·C 3986.
246 */
247 export const removeDotSegments = ($) => {
248 const input = `${$}`;
249 const output = [];
250 const { length } = input;
251 let index = 0;
252 while (index < length) {
253 if (input.startsWith("../", index)) {
254 // The input starts with a double leader; drop it. This can only
255 // occur at the beginning of the input.
256 index += 3;
257 } else if (input.startsWith("./", index)) {
258 // The input starts with a single leader; drop it. This can only
259 // occur at the beginning of the input.
260 index += 2;
261 } else if (input.startsWith("/./", index)) {
262 // The input starts with a slash, single leader, and another
263 // slash. Ignore it, and move the input to just before the second
264 // slash.
265 index += 2;
266 } else if (input.startsWith("/.", index) && index + 2 == length) {
267 // The input starts with a slash and single leader, and this
268 // exhausts the string. Push an empty segment and move the index
269 // to the end of the string.
270 output.push("/");
271 index = length;
272 } else if (input.startsWith("/../", index)) {
273 // The input starts with a slash, double leader, and another
274 // slash. Drop a segment from the output, and move the input to
275 // just before the second slash.
276 index += 3;
277 output.splice(-1, 1);
278 } else if (input.startsWith("/..", index) && index + 3 == length) {
279 // The input starts with a slash and single leader, and this
280 // exhausts the string. Drop a segment from the output, push an
281 // empty segment, and move the index to the end of the string.
282 output.splice(-1, 1, "/");
283 index = length;
284 } else if (
285 input.startsWith(".", index) && index + 1 == length ||
286 input.startsWith("..", index) && index + 2 == length
287 ) {
288 // The input starts with a single or double leader, and this
289 // exhausts the string. Do nothing (this can only occur at the
290 // beginning of input) and move the index to the end of the
291 // string.
292 index = length;
293 } else {
294 // The input does not start with a leader. Advance the index to
295 // the position before the next slash and push the segment
296 // between the old and new positions.
297 const nextIndex = input.indexOf("/", index + 1);
298 if (nextIndex == -1) {
299 // No slash remains; set index to the end of the string.
300 output.push(input.substring(index));
301 index = length;
302 } else {
303 // There are further path segments.
304 output.push(input.substring(index, nextIndex));
305 index = nextIndex;
306 }
307 }
308 }
309 return output.join("");
310 };
311
312 /**
313 * Resolves the provided reference relative to the provided base I·R·I.
314 *
315 * See §5.2 of R·F·C 3986.
316 */
317 export const resolveReference = (R, Base = location ?? "") => {
318 const {
319 scheme: Base·scheme,
320 authority: Base·authority,
321 path: Base·path,
322 query: Base·query,
323 } = parseReference(Base);
324 if (Base·scheme == null) {
325 throw new TypeError(
326 `Piscēs: Base I·R·I did not have a scheme: ${Base}.`,
327 );
328 } else {
329 const {
330 scheme: R·scheme,
331 authority: R·authority,
332 path: R·path,
333 query: R·query,
334 fragment: R·fragment,
335 } = parseReference(R);
336 return composeReference(
337 R·scheme != null
338 ? {
339 scheme: R·scheme,
340 authority: R·authority,
341 path: removeDotSegments(R·path),
342 query: R·query,
343 fragment: R·fragment,
344 }
345 : R·authority != null
346 ? {
347 scheme: Base·scheme,
348 authority: R·authority,
349 path: removeDotSegments(R·path),
350 query: R·query,
351 fragment: R·fragment,
352 }
353 : !R·path
354 ? {
355 scheme: Base·scheme,
356 authority: Base·authority,
357 path: Base·path,
358 query: R·query ?? Base·query,
359 fragment: R·fragment,
360 }
361 : {
362 scheme: Base·scheme,
363 authority: Base·authority,
364 path: R·path[0] == "/"
365 ? removeDotSegments(R·path)
366 : removeDotSegments(mergePaths(Base·path || "/", R·path)),
367 query: R·query,
368 fragment: R·fragment,
369 },
370 );
371 }
372 };
This page took 0.111 seconds and 5 git commands to generate.