@gjsify/url 0.4.0 → 0.4.3

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/src/index.ts DELETED
@@ -1,601 +0,0 @@
1
- // Node.js url module for GJS
2
- // Uses GLib.Uri for WHATWG URL parsing since globalThis.URL is not available in GJS 1.86
3
- // See refs/deno/ext/node/polyfills/url.ts, refs/bun/src/js/node/url.ts, refs/node/lib/url.js
4
-
5
- import GLib from '@girs/glib-2.0';
6
-
7
- // ---- URLSearchParams ----
8
-
9
- const PARSE_FLAGS = GLib.UriFlags.HAS_PASSWORD | GLib.UriFlags.ENCODED | GLib.UriFlags.SCHEME_NORMALIZE;
10
-
11
- export class URLSearchParams {
12
- _entries: [string, string][] = [];
13
-
14
- constructor(init?: string | Record<string, string> | [string, string][] | URLSearchParams) {
15
- if (!init) return;
16
- if (typeof init === 'string') {
17
- const s = init.startsWith('?') ? init.slice(1) : init;
18
- if (s) {
19
- for (const pair of s.split('&')) {
20
- const eqIdx = pair.indexOf('=');
21
- if (eqIdx === -1) {
22
- this._entries.push([decodeComponent(pair), '']);
23
- } else {
24
- this._entries.push([decodeComponent(pair.slice(0, eqIdx)), decodeComponent(pair.slice(eqIdx + 1))]);
25
- }
26
- }
27
- }
28
- } else if (Array.isArray(init)) {
29
- for (const [k, v] of init) {
30
- this._entries.push([String(k), String(v)]);
31
- }
32
- } else if (init instanceof URLSearchParams) {
33
- this._entries = init._entries.map(([k, v]) => [k, v] as [string, string]);
34
- } else {
35
- for (const key of Object.keys(init)) {
36
- this._entries.push([key, String(init[key])]);
37
- }
38
- }
39
- }
40
-
41
- get(name: string): string | null {
42
- for (const [k, v] of this._entries) {
43
- if (k === name) return v;
44
- }
45
- return null;
46
- }
47
-
48
- getAll(name: string): string[] {
49
- return this._entries.filter(([k]) => k === name).map(([, v]) => v);
50
- }
51
-
52
- set(name: string, value: string): void {
53
- let found = false;
54
- this._entries = this._entries.filter(([k]) => {
55
- if (k === name) {
56
- if (!found) { found = true; return true; }
57
- return false;
58
- }
59
- return true;
60
- });
61
- if (found) {
62
- for (let i = 0; i < this._entries.length; i++) {
63
- if (this._entries[i][0] === name) { this._entries[i][1] = value; break; }
64
- }
65
- } else {
66
- this._entries.push([name, value]);
67
- }
68
- }
69
-
70
- has(name: string): boolean {
71
- return this._entries.some(([k]) => k === name);
72
- }
73
-
74
- delete(name: string): void {
75
- this._entries = this._entries.filter(([k]) => k !== name);
76
- }
77
-
78
- append(name: string, value: string): void {
79
- this._entries.push([name, value]);
80
- }
81
-
82
- sort(): void {
83
- this._entries.sort((a, b) => {
84
- if (a[0] < b[0]) return -1;
85
- if (a[0] > b[0]) return 1;
86
- return 0;
87
- });
88
- }
89
-
90
- toString(): string {
91
- return this._entries.map(([k, v]) => encodeComponent(k) + '=' + encodeComponent(v)).join('&');
92
- }
93
-
94
- forEach(callback: (value: string, key: string, parent: URLSearchParams) => void): void {
95
- for (const [k, v] of this._entries) {
96
- callback(v, k, this);
97
- }
98
- }
99
-
100
- *entries(): IterableIterator<[string, string]> {
101
- yield* this._entries;
102
- }
103
-
104
- *keys(): IterableIterator<string> {
105
- for (const [k] of this._entries) yield k;
106
- }
107
-
108
- *values(): IterableIterator<string> {
109
- for (const [, v] of this._entries) yield v;
110
- }
111
-
112
- [Symbol.iterator](): IterableIterator<[string, string]> {
113
- return this.entries();
114
- }
115
-
116
- get size(): number {
117
- return this._entries.length;
118
- }
119
- }
120
-
121
- function decodeComponent(s: string): string {
122
- try { return decodeURIComponent(s.replace(/\+/g, ' ')); } catch { return s; }
123
- }
124
-
125
- function encodeComponent(s: string): string {
126
- return encodeURIComponent(s).replace(/%20/g, '+');
127
- }
128
-
129
- // ---- URL class using GLib.Uri ----
130
-
131
- export class URL {
132
- #uri: any; // GLib.Uri
133
- #searchParams: URLSearchParams;
134
-
135
- constructor(url: string | URL, base?: string | URL) {
136
- const urlStr = url instanceof URL ? url.href : String(url);
137
-
138
- try {
139
- if (base !== undefined) {
140
- const baseStr = base instanceof URL ? base.href : String(base);
141
- const baseUri = GLib.Uri.parse(baseStr, PARSE_FLAGS);
142
- this.#uri = baseUri.parse_relative(urlStr, PARSE_FLAGS);
143
- } else {
144
- this.#uri = GLib.Uri.parse(urlStr, PARSE_FLAGS);
145
- }
146
- } catch (e: any) {
147
- throw new TypeError(`Invalid URL: ${urlStr}`);
148
- }
149
-
150
- if (!this.#uri) {
151
- throw new TypeError(`Invalid URL: ${urlStr}`);
152
- }
153
-
154
- this.#searchParams = new URLSearchParams(this.#uri.get_query() || '');
155
- }
156
-
157
- get protocol(): string {
158
- return this.#uri.get_scheme() + ':';
159
- }
160
-
161
- get hostname(): string {
162
- return (this.#uri.get_host() || '').toLowerCase();
163
- }
164
-
165
- get port(): string {
166
- const p = this.#uri.get_port();
167
- if (p === -1) return '';
168
- // WHATWG URL spec: port should be empty string for default ports
169
- const scheme = this.#uri.get_scheme();
170
- if ((scheme === 'http' || scheme === 'ws') && p === 80) return '';
171
- if ((scheme === 'https' || scheme === 'wss') && p === 443) return '';
172
- if (scheme === 'ftp' && p === 21) return '';
173
- return String(p);
174
- }
175
-
176
- get host(): string {
177
- const hostname = this.hostname;
178
- const port = this.port;
179
- return port ? `${hostname}:${port}` : hostname;
180
- }
181
-
182
- get pathname(): string {
183
- return this.#uri.get_path() || '/';
184
- }
185
-
186
- get search(): string {
187
- const q = this.#uri.get_query();
188
- return q ? '?' + q : '';
189
- }
190
-
191
- get hash(): string {
192
- const f = this.#uri.get_fragment();
193
- return f ? '#' + f : '';
194
- }
195
-
196
- get origin(): string {
197
- const p = this.protocol;
198
- if (p === 'http:' || p === 'https:' || p === 'ftp:') {
199
- return `${p}//${this.host}`;
200
- }
201
- return 'null';
202
- }
203
-
204
- get username(): string {
205
- return this.#uri.get_user() || '';
206
- }
207
-
208
- get password(): string {
209
- return this.#uri.get_password() || '';
210
- }
211
-
212
- get href(): string {
213
- let result = this.protocol;
214
- const scheme = this.#uri.get_scheme();
215
- const isSpecial = scheme === 'http' || scheme === 'https' || scheme === 'ftp' || scheme === 'file' || scheme === 'ws' || scheme === 'wss';
216
-
217
- if (isSpecial || this.hostname) {
218
- result += '//';
219
- }
220
-
221
- const user = this.username;
222
- const pass = this.password;
223
- if (user) {
224
- result += user;
225
- if (pass) result += ':' + pass;
226
- result += '@';
227
- }
228
-
229
- result += this.hostname;
230
- if (this.port) result += ':' + this.port;
231
-
232
- const pathname = this.pathname;
233
- result += pathname;
234
-
235
- result += this.search;
236
- result += this.hash;
237
-
238
- return result;
239
- }
240
-
241
- get searchParams(): URLSearchParams {
242
- return this.#searchParams;
243
- }
244
-
245
- toString(): string {
246
- return this.href;
247
- }
248
-
249
- toJSON(): string {
250
- return this.href;
251
- }
252
-
253
- // ---- URL.createObjectURL / URL.revokeObjectURL ----
254
- //
255
- // Consumers like Excalibur.js do `const src = URL.createObjectURL(blob);
256
- // image.src = src;`. For that to work on GJS we need `src` to be a path
257
- // `HTMLImageElement` / `HTMLAudioElement` / `FontFace` can actually read —
258
- // i.e. a `file://` URL. We implement this as a static method on our own
259
- // URL class (no globalThis monkey-patching):
260
- //
261
- // - Fast path: if the Blob already carries a `_tmpPath` (e.g. written
262
- // by `@gjsify/fetch` XHR when `responseType='blob'`), wrap it as
263
- // `file://<_tmpPath>`.
264
- // - Slow path: if the Blob has `arrayBuffer()`/bytes but no `_tmpPath`,
265
- // materialise the bytes into a GLib temp file and wrap that. This
266
- // path is async in the spec — but W3C `createObjectURL` is sync. We
267
- // read the bytes via `GLib.Bytes`-style synchronous access when
268
- // possible and fall back to a sentinel if not.
269
- //
270
- // Reference: https://developer.mozilla.org/en-US/docs/Web/API/URL/createObjectURL
271
-
272
- static _objectURLPaths = new Map<string, string>();
273
- static _objectURLCounter = 0;
274
-
275
- static createObjectURL(blob: { _tmpPath?: string; type?: string; size?: number }): string {
276
- const tmp = blob?._tmpPath;
277
- if (typeof tmp === 'string' && tmp.length > 0) {
278
- const url = `file://${tmp}`;
279
- URL._objectURLPaths.set(url, tmp);
280
- return url;
281
- }
282
- // No backing file — cannot hand this to GdkPixbuf / Gst / GLib. Surface
283
- // a clear sentinel so callers fail fast instead of silently loading a
284
- // phantom resource.
285
- return 'file:///dev/null';
286
- }
287
-
288
- static revokeObjectURL(url: string): void {
289
- const path = URL._objectURLPaths.get(url);
290
- if (!path) return;
291
- try {
292
- GLib.unlink(path);
293
- } catch {
294
- // best-effort cleanup
295
- }
296
- URL._objectURLPaths.delete(url);
297
- }
298
- }
299
-
300
- // ---- Legacy url.parse / url.format / url.resolve ----
301
-
302
- export interface UrlObject {
303
- protocol?: string | null;
304
- slashes?: boolean | null;
305
- auth?: string | null;
306
- host?: string | null;
307
- port?: string | null;
308
- hostname?: string | null;
309
- hash?: string | null;
310
- search?: string | null;
311
- query?: string | Record<string, string> | null;
312
- pathname?: string | null;
313
- path?: string | null;
314
- href?: string;
315
- }
316
-
317
- export interface Url extends UrlObject {
318
- href: string;
319
- }
320
-
321
- export function parse(urlString: string, parseQueryString?: boolean, slashesDenoteHost?: boolean): Url {
322
- if (typeof urlString !== 'string') {
323
- throw new TypeError('The "url" argument must be of type string. Received type ' + typeof urlString);
324
- }
325
-
326
- const result: Url = {
327
- protocol: null,
328
- slashes: null,
329
- auth: null,
330
- host: null,
331
- port: null,
332
- hostname: null,
333
- hash: null,
334
- search: null,
335
- query: null,
336
- pathname: null,
337
- path: null,
338
- href: urlString,
339
- };
340
-
341
- let rest = urlString.trim();
342
-
343
- // Extract hash
344
- const hashIdx = rest.indexOf('#');
345
- if (hashIdx !== -1) {
346
- result.hash = rest.slice(hashIdx);
347
- rest = rest.slice(0, hashIdx);
348
- }
349
-
350
- // Extract search/query
351
- const qIdx = rest.indexOf('?');
352
- if (qIdx !== -1) {
353
- result.search = rest.slice(qIdx);
354
- result.query = parseQueryString
355
- ? Object.fromEntries(new URLSearchParams(rest.slice(qIdx + 1)))
356
- : rest.slice(qIdx + 1);
357
- rest = rest.slice(0, qIdx);
358
- }
359
-
360
- // Extract protocol
361
- const protoMatch = /^([a-z][a-z0-9.+-]*:)/i.exec(rest);
362
- if (protoMatch) {
363
- result.protocol = protoMatch[1].toLowerCase();
364
- rest = rest.slice(result.protocol.length);
365
- }
366
-
367
- // Check for slashes
368
- if (slashesDenoteHost || result.protocol) {
369
- const hasSlashes = rest.startsWith('//');
370
- if (hasSlashes) {
371
- result.slashes = true;
372
- rest = rest.slice(2);
373
- }
374
- }
375
-
376
- // Extract host portion (only if we had slashes or protocol)
377
- if (result.slashes || (result.protocol && !['javascript:', 'data:', 'mailto:'].includes(result.protocol))) {
378
- let hostEnd = -1;
379
- for (let i = 0; i < rest.length; i++) {
380
- const ch = rest[i];
381
- if (ch === '/' || ch === '\\') {
382
- hostEnd = i;
383
- break;
384
- }
385
- }
386
-
387
- const hostPart = hostEnd === -1 ? rest : rest.slice(0, hostEnd);
388
- rest = hostEnd === -1 ? '' : rest.slice(hostEnd);
389
-
390
- const atIdx = hostPart.lastIndexOf('@');
391
- if (atIdx !== -1) {
392
- result.auth = decodeURIComponent(hostPart.slice(0, atIdx));
393
- const hostWithPort = hostPart.slice(atIdx + 1);
394
- parseHostPort(hostWithPort, result);
395
- } else {
396
- parseHostPort(hostPart, result);
397
- }
398
- }
399
-
400
- result.pathname = rest || (result.slashes ? '/' : null);
401
-
402
- if (result.pathname !== null || result.search !== null) {
403
- result.path = (result.pathname || '') + (result.search || '');
404
- }
405
-
406
- result.href = format(result);
407
-
408
- return result;
409
- }
410
-
411
- function parseHostPort(hostPart: string, result: Url): void {
412
- if (!hostPart) return;
413
-
414
- const bracketIdx = hostPart.indexOf('[');
415
- if (bracketIdx !== -1) {
416
- const bracketEnd = hostPart.indexOf(']', bracketIdx);
417
- if (bracketEnd !== -1) {
418
- const portStr = hostPart.slice(bracketEnd + 1);
419
- if (portStr.startsWith(':')) {
420
- result.port = portStr.slice(1);
421
- }
422
- result.hostname = hostPart.slice(bracketIdx, bracketEnd + 1);
423
- result.host = result.hostname + (result.port ? ':' + result.port : '');
424
- return;
425
- }
426
- }
427
-
428
- const colonIdx = hostPart.lastIndexOf(':');
429
- if (colonIdx !== -1) {
430
- const portCandidate = hostPart.slice(colonIdx + 1);
431
- if (/^\d*$/.test(portCandidate)) {
432
- result.port = portCandidate || null;
433
- result.hostname = hostPart.slice(0, colonIdx).toLowerCase();
434
- } else {
435
- result.hostname = hostPart.toLowerCase();
436
- }
437
- } else {
438
- result.hostname = hostPart.toLowerCase();
439
- }
440
-
441
- result.host = result.hostname + (result.port ? ':' + result.port : '');
442
- }
443
-
444
- export function format(urlObject: UrlObject | string | URL): string {
445
- if (typeof urlObject === 'string') {
446
- return urlObject;
447
- }
448
-
449
- if (urlObject instanceof URL) {
450
- return urlObject.href;
451
- }
452
-
453
- const obj = urlObject as UrlObject;
454
- let result = '';
455
-
456
- if (obj.protocol) {
457
- result += obj.protocol;
458
- }
459
-
460
- if (obj.slashes || (obj.protocol && !['javascript:', 'data:', 'mailto:'].includes(obj.protocol || ''))) {
461
- result += '//';
462
- }
463
-
464
- if (obj.auth) {
465
- result += encodeURIComponent(obj.auth) + '@';
466
- }
467
-
468
- if (obj.host) {
469
- result += obj.host;
470
- } else {
471
- if (obj.hostname) {
472
- result += obj.hostname;
473
- }
474
- if (obj.port) {
475
- result += ':' + obj.port;
476
- }
477
- }
478
-
479
- if (obj.pathname) {
480
- result += obj.pathname;
481
- }
482
-
483
- if (obj.search) {
484
- result += obj.search;
485
- } else if (obj.query && typeof obj.query === 'object') {
486
- const qs = new URLSearchParams(obj.query as Record<string, string>).toString();
487
- if (qs) result += '?' + qs;
488
- }
489
-
490
- if (obj.hash) {
491
- result += obj.hash;
492
- }
493
-
494
- return result;
495
- }
496
-
497
- export function resolve(from: string, to: string): string {
498
- return new URL(to, new URL(from, 'resolve://')).href.replace(/^resolve:\/\//, '');
499
- }
500
-
501
- // ---- File URL helpers ----
502
-
503
- export function fileURLToPath(url: string | URL): string {
504
- if (typeof url === 'string') {
505
- url = new URL(url);
506
- }
507
-
508
- if (!(url instanceof URL)) {
509
- throw new TypeError('The "url" argument must be of type string or URL. Received type ' + typeof url);
510
- }
511
-
512
- if (url.protocol !== 'file:') {
513
- throw new TypeError('The URL must be of scheme file');
514
- }
515
-
516
- if (url.hostname !== '' && url.hostname !== 'localhost') {
517
- throw new TypeError(
518
- `File URL host must be "localhost" or empty on linux`
519
- );
520
- }
521
-
522
- const pathname = url.pathname;
523
- for (let i = 0; i < pathname.length; i++) {
524
- if (pathname[i] === '%') {
525
- const third = pathname.codePointAt(i + 2)! | 0x20;
526
- if (pathname[i + 1] === '2' && third === 102) {
527
- throw new TypeError('File URL path must not include encoded / characters');
528
- }
529
- }
530
- }
531
-
532
- return decodeURIComponent(pathname);
533
- }
534
-
535
- export function pathToFileURL(filepath: string): URL {
536
- let resolved = filepath;
537
-
538
- if (filepath[0] !== '/') {
539
- if (typeof globalThis.process?.cwd === 'function') {
540
- resolved = globalThis.process.cwd() + '/' + filepath;
541
- } else {
542
- try {
543
- if (GLib?.get_current_dir) {
544
- resolved = GLib.get_current_dir() + '/' + filepath;
545
- }
546
- } catch {
547
- // Fall through
548
- }
549
- }
550
- }
551
-
552
- return new URL('file://' + encodePathForURL(resolved));
553
- }
554
-
555
- function encodePathForURL(filepath: string): string {
556
- let result = '';
557
- for (let i = 0; i < filepath.length; i++) {
558
- const ch = filepath[i];
559
- if (
560
- (ch >= 'a' && ch <= 'z') ||
561
- (ch >= 'A' && ch <= 'Z') ||
562
- (ch >= '0' && ch <= '9') ||
563
- ch === '/' || ch === '-' || ch === '_' || ch === '.' || ch === '~' ||
564
- ch === ':' || ch === '@' || ch === '!'
565
- ) {
566
- result += ch;
567
- } else {
568
- result += encodeURIComponent(ch);
569
- }
570
- }
571
- return result;
572
- }
573
-
574
- export function domainToASCII(domain: string): string {
575
- try {
576
- return new URL(`http://${domain}`).hostname;
577
- } catch {
578
- return '';
579
- }
580
- }
581
-
582
- export function domainToUnicode(domain: string): string {
583
- try {
584
- return new URL(`http://${domain}`).hostname;
585
- } catch {
586
- return '';
587
- }
588
- }
589
-
590
- // Default export
591
- export default {
592
- URL,
593
- URLSearchParams,
594
- parse,
595
- format,
596
- resolve,
597
- fileURLToPath,
598
- pathToFileURL,
599
- domainToASCII,
600
- domainToUnicode,
601
- };
package/src/test.ts DELETED
@@ -1,4 +0,0 @@
1
- import { run } from '@gjsify/unit';
2
- import indexTestSuite from './index.spec.js';
3
-
4
- run({indexTestSuite});
package/test/file.txt DELETED
@@ -1 +0,0 @@
1
- Hello World
package/tsconfig.json DELETED
@@ -1,29 +0,0 @@
1
- {
2
- "compilerOptions": {
3
- "module": "ESNext",
4
- "target": "ESNext",
5
- "moduleResolution": "bundler",
6
- "types": [
7
- "node"
8
- ],
9
- "experimentalDecorators": true,
10
- "emitDeclarationOnly": true,
11
- "declaration": true,
12
- "allowImportingTsExtensions": true,
13
- "outDir": "lib",
14
- "rootDir": "src",
15
- "declarationDir": "lib/types",
16
- "composite": true,
17
- "skipLibCheck": true,
18
- "allowJs": true,
19
- "checkJs": false,
20
- "strict": false
21
- },
22
- "include": [
23
- "src/**/*.ts"
24
- ],
25
- "exclude": [
26
- "src/test.ts",
27
- "src/test.mts"
28
- ]
29
- }