@fedify/vocab-runtime 2.3.11-dev.2296 → 2.3.12-dev.2341
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/deno.json +2 -2
- package/dist/mod.cjs +91 -4
- package/dist/mod.d.cts +30 -2
- package/dist/mod.d.ts +30 -2
- package/dist/mod.js +91 -5
- package/dist/tests/{body-B4vYiUPw.mjs → body-BqvaGUs-.mjs} +1 -1
- package/dist/tests/{body-E_Ip3EI_.cjs → body-aK0uA0NH.cjs} +1 -1
- package/dist/tests/body.test.cjs +1 -1
- package/dist/tests/body.test.mjs +1 -1
- package/dist/tests/decimal.test.cjs +3 -2
- package/dist/tests/decimal.test.mjs +3 -2
- package/dist/tests/{docloader-zA5iGORs.cjs → docloader-DJgiJ5vW.cjs} +43 -3
- package/dist/tests/{docloader-DBzA04Tp.mjs → docloader-E_pvjPB1.mjs} +43 -3
- package/dist/tests/docloader.test.cjs +168 -43
- package/dist/tests/docloader.test.mjs +158 -34
- package/dist/tests/{langstr-DDLc4833.mjs → langstr-ABdAOKlF.mjs} +5 -2
- package/dist/tests/{langstr-BOQHfJDr.cjs → langstr-BsqFJupv.cjs} +5 -2
- package/dist/tests/langstr.test.cjs +8 -1
- package/dist/tests/langstr.test.mjs +9 -2
- package/dist/tests/langtag-DgySESmk.cjs +50 -0
- package/dist/tests/langtag-LDBqlxHv.mjs +45 -0
- package/dist/tests/langtag.test.cjs +46 -0
- package/dist/tests/langtag.test.d.cts +1 -0
- package/dist/tests/langtag.test.d.mts +1 -0
- package/dist/tests/langtag.test.mjs +46 -0
- package/dist/tests/multibase/multibase.test.cjs +7 -7
- package/dist/tests/multibase/multibase.test.mjs +5 -5
- package/dist/tests/{request-CzD6ie9f.mjs → request-DHf5kF_j.mjs} +1 -1
- package/dist/tests/{request-DfDR4M7o.cjs → request-Fp69kS6q.cjs} +1 -1
- package/dist/tests/request.test.cjs +1 -1
- package/dist/tests/request.test.mjs +1 -1
- package/package.json +6 -3
- package/scripts/test-bun.mjs +18 -0
- package/src/contexts/fep-22cd.json +14 -0
- package/src/contexts/fep-6757.json +12 -0
- package/src/contexts/joinmastodon.json +1 -1
- package/src/contexts.ts +16 -0
- package/src/docloader.test.ts +222 -67
- package/src/docloader.ts +2 -0
- package/src/langstr.test.ts +18 -1
- package/src/langstr.ts +6 -2
- package/src/langtag.test.ts +53 -0
- package/src/langtag.ts +76 -0
- package/src/mod.ts +1 -0
- package/src/multibase/multibase.test.ts +5 -5
package/src/docloader.test.ts
CHANGED
|
@@ -2,12 +2,13 @@ import { configure, type LogRecord, reset } from "@logtape/logtape";
|
|
|
2
2
|
import fetchMock from "fetch-mock";
|
|
3
3
|
import { deepStrictEqual, ok, rejects } from "node:assert";
|
|
4
4
|
import dns from "node:dns/promises";
|
|
5
|
-
import { test } from "
|
|
5
|
+
import { test } from "@fedify/fixture";
|
|
6
6
|
import { createServer } from "node:http";
|
|
7
7
|
import { gzipSync } from "node:zlib";
|
|
8
8
|
import preloadedContexts from "./contexts.ts";
|
|
9
9
|
import cidV1Context from "./contexts/cid-v1.json" with { type: "json" };
|
|
10
10
|
import { getDocumentLoader, getRemoteDocument } from "./docloader.ts";
|
|
11
|
+
import jsonld from "./jsonld.ts";
|
|
11
12
|
import { FetchError } from "./request.ts";
|
|
12
13
|
import { UrlError } from "./url.ts";
|
|
13
14
|
|
|
@@ -44,7 +45,7 @@ test("getDocumentLoader()", async (t) => {
|
|
|
44
45
|
},
|
|
45
46
|
});
|
|
46
47
|
|
|
47
|
-
await t.
|
|
48
|
+
await t.step("ok", async () => {
|
|
48
49
|
deepStrictEqual(await fetchDocumentLoader("https://example.com/object"), {
|
|
49
50
|
contextUrl: null,
|
|
50
51
|
documentUrl: "https://example.com/object",
|
|
@@ -101,7 +102,7 @@ test("getDocumentLoader()", async (t) => {
|
|
|
101
102
|
},
|
|
102
103
|
});
|
|
103
104
|
|
|
104
|
-
await t.
|
|
105
|
+
await t.step("Link header", async () => {
|
|
105
106
|
deepStrictEqual(await fetchDocumentLoader("https://example.com/link-ctx"), {
|
|
106
107
|
contextUrl: "https://www.w3.org/ns/activitystreams",
|
|
107
108
|
documentUrl: "https://example.com/link-ctx",
|
|
@@ -124,7 +125,7 @@ test("getDocumentLoader()", async (t) => {
|
|
|
124
125
|
});
|
|
125
126
|
});
|
|
126
127
|
|
|
127
|
-
await t.
|
|
128
|
+
await t.step("Link header relative url", async () => {
|
|
128
129
|
deepStrictEqual(await fetchDocumentLoader("https://example.com/link-ctx"), {
|
|
129
130
|
contextUrl: "https://www.w3.org/ns/activitystreams",
|
|
130
131
|
documentUrl: "https://example.com/link-ctx",
|
|
@@ -150,7 +151,7 @@ test("getDocumentLoader()", async (t) => {
|
|
|
150
151
|
);
|
|
151
152
|
});
|
|
152
153
|
|
|
153
|
-
await t.
|
|
154
|
+
await t.step("wrong Link header syntax", async () => {
|
|
154
155
|
deepStrictEqual(
|
|
155
156
|
await fetchDocumentLoader("https://example.com/obj-w-wrong-link"),
|
|
156
157
|
{
|
|
@@ -179,7 +180,7 @@ test("getDocumentLoader()", async (t) => {
|
|
|
179
180
|
headers: { "Content-Type": "text/html; charset=utf-8" },
|
|
180
181
|
});
|
|
181
182
|
|
|
182
|
-
await t.
|
|
183
|
+
await t.step("HTML <link>", async () => {
|
|
183
184
|
deepStrictEqual(
|
|
184
185
|
await fetchDocumentLoader("https://example.com/html-link"),
|
|
185
186
|
{
|
|
@@ -208,7 +209,7 @@ test("getDocumentLoader()", async (t) => {
|
|
|
208
209
|
headers: { "Content-Type": "application/xhtml+xml; charset=utf-8" },
|
|
209
210
|
});
|
|
210
211
|
|
|
211
|
-
await t.
|
|
212
|
+
await t.step("XHTML <link>", async () => {
|
|
212
213
|
deepStrictEqual(
|
|
213
214
|
await fetchDocumentLoader("https://example.com/xhtml-link"),
|
|
214
215
|
{
|
|
@@ -239,7 +240,7 @@ test("getDocumentLoader()", async (t) => {
|
|
|
239
240
|
headers: { "Content-Type": "text/html; charset=utf-8" },
|
|
240
241
|
});
|
|
241
242
|
|
|
242
|
-
await t.
|
|
243
|
+
await t.step("HTML <a>", async () => {
|
|
243
244
|
deepStrictEqual(await fetchDocumentLoader("https://example.com/html-a"), {
|
|
244
245
|
contextUrl: null,
|
|
245
246
|
documentUrl: "https://example.com/object",
|
|
@@ -262,7 +263,7 @@ test("getDocumentLoader()", async (t) => {
|
|
|
262
263
|
headers: { "Content-Type": "text/html; charset=utf-8" },
|
|
263
264
|
});
|
|
264
265
|
|
|
265
|
-
await t.
|
|
266
|
+
await t.step("Wrong Content-Type", async () => {
|
|
266
267
|
deepStrictEqual(
|
|
267
268
|
await fetchDocumentLoader("https://example.com/wrong-content-type"),
|
|
268
269
|
{
|
|
@@ -280,7 +281,7 @@ test("getDocumentLoader()", async (t) => {
|
|
|
280
281
|
|
|
281
282
|
fetchMock.get("https://example.com/404", { status: 404 });
|
|
282
283
|
|
|
283
|
-
await t.
|
|
284
|
+
await t.step("not ok", async () => {
|
|
284
285
|
await rejects(
|
|
285
286
|
() => fetchDocumentLoader("https://example.com/404"),
|
|
286
287
|
FetchError,
|
|
@@ -288,7 +289,7 @@ test("getDocumentLoader()", async (t) => {
|
|
|
288
289
|
);
|
|
289
290
|
});
|
|
290
291
|
|
|
291
|
-
await t.
|
|
292
|
+
await t.step("preloaded contexts", async () => {
|
|
292
293
|
for (const [url, document] of Object.entries(preloadedContexts)) {
|
|
293
294
|
deepStrictEqual(await fetchDocumentLoader(url), {
|
|
294
295
|
contextUrl: null,
|
|
@@ -298,13 +299,37 @@ test("getDocumentLoader()", async (t) => {
|
|
|
298
299
|
}
|
|
299
300
|
});
|
|
300
301
|
|
|
302
|
+
await t.step("preloaded Mastodon attribution domains", async () => {
|
|
303
|
+
const context = [
|
|
304
|
+
"https://www.w3.org/ns/activitystreams",
|
|
305
|
+
"http://joinmastodon.org/ns",
|
|
306
|
+
];
|
|
307
|
+
const expanded = await jsonld.expand({
|
|
308
|
+
"@context": context,
|
|
309
|
+
id: "https://social.example/users/alice",
|
|
310
|
+
type: "Person",
|
|
311
|
+
attributionDomains: ["blog.example"],
|
|
312
|
+
}, { documentLoader: fetchDocumentLoader });
|
|
313
|
+
deepStrictEqual(expanded, [{
|
|
314
|
+
"@id": "https://social.example/users/alice",
|
|
315
|
+
"@type": ["https://www.w3.org/ns/activitystreams#Person"],
|
|
316
|
+
"http://joinmastodon.org/ns#attributionDomains": [
|
|
317
|
+
{ "@value": "blog.example" },
|
|
318
|
+
],
|
|
319
|
+
}]);
|
|
320
|
+
const compacted = await jsonld.compact(expanded, context, {
|
|
321
|
+
documentLoader: fetchDocumentLoader,
|
|
322
|
+
});
|
|
323
|
+
deepStrictEqual(compacted.attributionDomains, ["blog.example"]);
|
|
324
|
+
});
|
|
325
|
+
|
|
301
326
|
// Controlled Identifiers v1.0 requires JSON-LD processors to treat this
|
|
302
327
|
// context URL as already resolved. A temporary W3C outage must not prevent
|
|
303
328
|
// an otherwise valid document from being processed.
|
|
304
329
|
// See: https://www.w3.org/TR/cid-1.0/#json-ld-context
|
|
305
330
|
// https://github.com/fedify-dev/fedify/issues/932
|
|
306
331
|
fetchMock.get("https://www.w3.org/ns/cid/v1", { status: 503 });
|
|
307
|
-
await t.
|
|
332
|
+
await t.step("preloaded CID v1 context", async () => {
|
|
308
333
|
const url = "https://www.w3.org/ns/cid/v1";
|
|
309
334
|
deepStrictEqual(await fetchDocumentLoader(url), {
|
|
310
335
|
contextUrl: null,
|
|
@@ -318,7 +343,7 @@ test("getDocumentLoader()", async (t) => {
|
|
|
318
343
|
// document referencing it fails before application handlers run. It has to
|
|
319
344
|
// be resolved from the built-in copy rather than over the network.
|
|
320
345
|
// See: https://github.com/fedify-dev/fedify/issues/982
|
|
321
|
-
await t.
|
|
346
|
+
await t.step("preloaded FEP-ef61 context", async () => {
|
|
322
347
|
const url = "https://w3id.org/fep/ef61";
|
|
323
348
|
ok(url in preloadedContexts);
|
|
324
349
|
deepStrictEqual(await fetchDocumentLoader(url), {
|
|
@@ -341,7 +366,7 @@ test("getDocumentLoader()", async (t) => {
|
|
|
341
366
|
// A Codeberg Pages outage must not prevent loading the FEP-7aa9 context.
|
|
342
367
|
// See: https://github.com/fedify-dev/fedify/issues/1078
|
|
343
368
|
fetchMock.get("https://w3id.org/fep/7aa9", { status: 502 });
|
|
344
|
-
await t.
|
|
369
|
+
await t.step("preloaded FEP-7aa9 context", async () => {
|
|
345
370
|
const url = "https://w3id.org/fep/7aa9";
|
|
346
371
|
deepStrictEqual(await fetchDocumentLoader(url), {
|
|
347
372
|
contextUrl: null,
|
|
@@ -375,7 +400,130 @@ test("getDocumentLoader()", async (t) => {
|
|
|
375
400
|
deepStrictEqual(fetchMock.callHistory.calls(url).length, 0);
|
|
376
401
|
});
|
|
377
402
|
|
|
378
|
-
|
|
403
|
+
// A Codeberg Pages outage must not prevent loading or expanding FEP-6757.
|
|
404
|
+
// See: https://github.com/fedify-dev/fedify/issues/1211
|
|
405
|
+
fetchMock.get("https://w3id.org/fep/6757", { status: 502 });
|
|
406
|
+
await t.step("preloaded FEP-6757 context", async () => {
|
|
407
|
+
const url = "https://w3id.org/fep/6757";
|
|
408
|
+
deepStrictEqual(await fetchDocumentLoader(url), {
|
|
409
|
+
contextUrl: null,
|
|
410
|
+
documentUrl: url,
|
|
411
|
+
document: {
|
|
412
|
+
"@context": {
|
|
413
|
+
license: {
|
|
414
|
+
"@id": "http://purl.org/dc/terms/license",
|
|
415
|
+
"@type": "@id",
|
|
416
|
+
},
|
|
417
|
+
preferredLicense: {
|
|
418
|
+
"@id": "https://w3id.org/fep/6757#preferredLicense",
|
|
419
|
+
"@type": "@id",
|
|
420
|
+
},
|
|
421
|
+
},
|
|
422
|
+
},
|
|
423
|
+
});
|
|
424
|
+
deepStrictEqual(
|
|
425
|
+
await jsonld.expand({
|
|
426
|
+
"@context": ["https://www.w3.org/ns/activitystreams", url],
|
|
427
|
+
type: "Person",
|
|
428
|
+
license: "https://creativecommons.org/licenses/by/4.0/",
|
|
429
|
+
preferredLicense: "https://creativecommons.org/licenses/by-nc/4.0/",
|
|
430
|
+
}, { documentLoader: fetchDocumentLoader }),
|
|
431
|
+
[{
|
|
432
|
+
"@type": ["https://www.w3.org/ns/activitystreams#Person"],
|
|
433
|
+
"http://purl.org/dc/terms/license": [{
|
|
434
|
+
"@id": "https://creativecommons.org/licenses/by/4.0/",
|
|
435
|
+
}],
|
|
436
|
+
"https://w3id.org/fep/6757#preferredLicense": [{
|
|
437
|
+
"@id": "https://creativecommons.org/licenses/by-nc/4.0/",
|
|
438
|
+
}],
|
|
439
|
+
}],
|
|
440
|
+
);
|
|
441
|
+
deepStrictEqual(fetchMock.callHistory.calls(url).length, 0);
|
|
442
|
+
});
|
|
443
|
+
|
|
444
|
+
// A Codeberg Pages outage must not prevent expanding translation metadata.
|
|
445
|
+
// See: https://github.com/fedify-dev/fedify/issues/1214
|
|
446
|
+
fetchMock.get("https://w3id.org/fep/22cd", { status: 502 });
|
|
447
|
+
await t.step("preloaded FEP-22cd context", async () => {
|
|
448
|
+
const url = "https://w3id.org/fep/22cd";
|
|
449
|
+
const articleId = "https://example.com/articles/1";
|
|
450
|
+
const translatorId = "https://example.com/users/alice";
|
|
451
|
+
const revisionId = "https://example.com/articles/1/revisions/1";
|
|
452
|
+
deepStrictEqual(await fetchDocumentLoader(url), {
|
|
453
|
+
contextUrl: null,
|
|
454
|
+
documentUrl: url,
|
|
455
|
+
document: {
|
|
456
|
+
"@context": {
|
|
457
|
+
"fep-22cd": "https://w3id.org/fep/22cd#",
|
|
458
|
+
schema: "https://schema.org/",
|
|
459
|
+
xsd: "http://www.w3.org/2001/XMLSchema#",
|
|
460
|
+
translations: {
|
|
461
|
+
"@id": "fep-22cd:translations",
|
|
462
|
+
"@container": "@set",
|
|
463
|
+
},
|
|
464
|
+
Translation: "fep-22cd:Translation",
|
|
465
|
+
sourceUpdated: {
|
|
466
|
+
"@id": "fep-22cd:sourceUpdated",
|
|
467
|
+
"@type": "xsd:dateTime",
|
|
468
|
+
},
|
|
469
|
+
translator: {
|
|
470
|
+
"@id": "schema:translator",
|
|
471
|
+
"@type": "@id",
|
|
472
|
+
"@container": "@set",
|
|
473
|
+
},
|
|
474
|
+
inLanguage: "schema:inLanguage",
|
|
475
|
+
translationOfWork: {
|
|
476
|
+
"@id": "schema:translationOfWork",
|
|
477
|
+
"@type": "@id",
|
|
478
|
+
},
|
|
479
|
+
isBasedOn: { "@id": "schema:isBasedOn", "@type": "@id" },
|
|
480
|
+
},
|
|
481
|
+
},
|
|
482
|
+
});
|
|
483
|
+
deepStrictEqual(
|
|
484
|
+
await jsonld.expand({
|
|
485
|
+
"@context": ["https://www.w3.org/ns/activitystreams", url],
|
|
486
|
+
id: articleId,
|
|
487
|
+
type: "Article",
|
|
488
|
+
contentMap: { en: "Original", ko: "Translation" },
|
|
489
|
+
translations: [{
|
|
490
|
+
type: "Translation",
|
|
491
|
+
inLanguage: "ko",
|
|
492
|
+
translator: [translatorId],
|
|
493
|
+
translationOfWork: articleId,
|
|
494
|
+
sourceUpdated: "2026-09-01T00:00:00Z",
|
|
495
|
+
isBasedOn: revisionId,
|
|
496
|
+
}],
|
|
497
|
+
}, { documentLoader: fetchDocumentLoader }),
|
|
498
|
+
[{
|
|
499
|
+
"@id": articleId,
|
|
500
|
+
"@type": ["https://www.w3.org/ns/activitystreams#Article"],
|
|
501
|
+
"https://www.w3.org/ns/activitystreams#content": [
|
|
502
|
+
{ "@language": "en", "@value": "Original" },
|
|
503
|
+
{ "@language": "ko", "@value": "Translation" },
|
|
504
|
+
],
|
|
505
|
+
"https://w3id.org/fep/22cd#translations": [{
|
|
506
|
+
"@type": ["https://w3id.org/fep/22cd#Translation"],
|
|
507
|
+
"https://schema.org/inLanguage": [{ "@value": "ko" }],
|
|
508
|
+
"https://schema.org/translator": [{ "@id": translatorId }],
|
|
509
|
+
"https://schema.org/translationOfWork": [{ "@id": articleId }],
|
|
510
|
+
"https://w3id.org/fep/22cd#sourceUpdated": [{
|
|
511
|
+
"@type": "http://www.w3.org/2001/XMLSchema#dateTime",
|
|
512
|
+
"@value": "2026-09-01T00:00:00Z",
|
|
513
|
+
}],
|
|
514
|
+
"https://schema.org/isBasedOn": [{ "@id": revisionId }],
|
|
515
|
+
}],
|
|
516
|
+
}],
|
|
517
|
+
);
|
|
518
|
+
deepStrictEqual(fetchMock.callHistory.calls(url).length, 0);
|
|
519
|
+
deepStrictEqual(
|
|
520
|
+
fetchMock.callHistory.calls("https://www.w3.org/ns/activitystreams")
|
|
521
|
+
.length,
|
|
522
|
+
0,
|
|
523
|
+
);
|
|
524
|
+
});
|
|
525
|
+
|
|
526
|
+
await t.step("deny non-HTTP/HTTPS", async () => {
|
|
379
527
|
await rejects(
|
|
380
528
|
() => fetchDocumentLoader("ftp://localhost"),
|
|
381
529
|
UrlError,
|
|
@@ -409,7 +557,7 @@ test("getDocumentLoader()", async (t) => {
|
|
|
409
557
|
},
|
|
410
558
|
});
|
|
411
559
|
|
|
412
|
-
await t.
|
|
560
|
+
await t.step("allowPrivateAddress: false", async () => {
|
|
413
561
|
await rejects(
|
|
414
562
|
() => fetchDocumentLoader("https://localhost/object"),
|
|
415
563
|
UrlError,
|
|
@@ -426,7 +574,7 @@ test("getDocumentLoader()", async (t) => {
|
|
|
426
574
|
|
|
427
575
|
const fetchDocumentLoader2 = getDocumentLoader({ allowPrivateAddress: true });
|
|
428
576
|
|
|
429
|
-
await t.
|
|
577
|
+
await t.step("allowPrivateAddress: true", async () => {
|
|
430
578
|
const expected = {
|
|
431
579
|
contextUrl: null,
|
|
432
580
|
documentUrl: "https://localhost/object",
|
|
@@ -463,7 +611,7 @@ test("getDocumentLoader()", async (t) => {
|
|
|
463
611
|
};
|
|
464
612
|
});
|
|
465
613
|
|
|
466
|
-
await t.
|
|
614
|
+
await t.step("too many redirects", async () => {
|
|
467
615
|
redirectAttempts = 0;
|
|
468
616
|
await rejects(
|
|
469
617
|
() => fetchDocumentLoader("https://example.com/too-many-redirects/0"),
|
|
@@ -473,7 +621,7 @@ test("getDocumentLoader()", async (t) => {
|
|
|
473
621
|
deepStrictEqual(redirectAttempts, 21);
|
|
474
622
|
});
|
|
475
623
|
|
|
476
|
-
await t.
|
|
624
|
+
await t.step("custom max redirection", async () => {
|
|
477
625
|
redirectAttempts = 0;
|
|
478
626
|
const loader = getDocumentLoader({ maxRedirection: 1 });
|
|
479
627
|
await rejects(
|
|
@@ -500,7 +648,7 @@ test("getDocumentLoader()", async (t) => {
|
|
|
500
648
|
};
|
|
501
649
|
});
|
|
502
650
|
|
|
503
|
-
await t.
|
|
651
|
+
await t.step("redirect loop", async () => {
|
|
504
652
|
loopAttempts = 0;
|
|
505
653
|
await rejects(
|
|
506
654
|
() => fetchDocumentLoader("https://example.com/redirect-loop-a"),
|
|
@@ -519,7 +667,7 @@ test("getDocumentLoader()", async (t) => {
|
|
|
519
667
|
};
|
|
520
668
|
});
|
|
521
669
|
|
|
522
|
-
await t.
|
|
670
|
+
await t.step("redirect loop with relative location", async () => {
|
|
523
671
|
relativeLoopAttempts = 0;
|
|
524
672
|
await rejects(
|
|
525
673
|
() => fetchDocumentLoader("https://example.com/redirect-loop-relative"),
|
|
@@ -539,7 +687,7 @@ test("getDocumentLoader()", async (t) => {
|
|
|
539
687
|
headers: { "Content-Type": "text/html; charset=utf-8" },
|
|
540
688
|
});
|
|
541
689
|
|
|
542
|
-
await t.
|
|
690
|
+
await t.step("ReDoS resistance (CVE-2025-68475)", async () => {
|
|
543
691
|
const start = performance.now();
|
|
544
692
|
// The malicious HTML will fail JSON parsing, but the important thing is
|
|
545
693
|
// that it should complete quickly (not hang due to ReDoS)
|
|
@@ -688,12 +836,13 @@ test("getDocumentLoader() logs DNS failures as such", async (t) => {
|
|
|
688
836
|
"Deno" in globalThis &&
|
|
689
837
|
(await Deno.permissions.query({ name: "net" })).state !== "granted"
|
|
690
838
|
) {
|
|
691
|
-
|
|
692
|
-
|
|
839
|
+
throw new Error(
|
|
840
|
+
"This DNS test requires Deno network permission (--allow-net).",
|
|
841
|
+
);
|
|
693
842
|
}
|
|
694
843
|
const loader = getDocumentLoader();
|
|
695
844
|
for (const result of ["throws", "empty", "private"] as const) {
|
|
696
|
-
await t.
|
|
845
|
+
await t.step(result, async () => {
|
|
697
846
|
// Stubbing works only because url.ts uses the default node:dns/promises
|
|
698
847
|
// import; see the FIXME there.
|
|
699
848
|
const originalLookup = dns.lookup;
|
|
@@ -749,7 +898,7 @@ test("getDocumentLoader() bounds alternate document chains", async (t) => {
|
|
|
749
898
|
const loader = getDocumentLoader({ allowPrivateAddress: true });
|
|
750
899
|
for (const mode of ["header", "html", "mixed"] as const) {
|
|
751
900
|
for (const hops of [20, 21]) {
|
|
752
|
-
await t.
|
|
901
|
+
await t.step(`${mode}: ${hops} hops`, async () => {
|
|
753
902
|
fetchMock.mockGlobal();
|
|
754
903
|
let requests = 0;
|
|
755
904
|
fetchMock.get(`begin:${base}`, ({ url }) => {
|
|
@@ -794,7 +943,7 @@ test("getDocumentLoader() bounds alternate document chains", async (t) => {
|
|
|
794
943
|
"redirect-intermediate",
|
|
795
944
|
]
|
|
796
945
|
) {
|
|
797
|
-
await t.
|
|
946
|
+
await t.step(`${mode}: cycle`, async () => {
|
|
798
947
|
fetchMock.mockGlobal();
|
|
799
948
|
let requests = 0;
|
|
800
949
|
fetchMock.get(`begin:${base}`, ({ url }) => {
|
|
@@ -828,49 +977,55 @@ test("getDocumentLoader() bounds alternate document chains", async (t) => {
|
|
|
828
977
|
});
|
|
829
978
|
}
|
|
830
979
|
|
|
831
|
-
await t.
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
requests
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
await rejects(loader(`${base}0`), (error: unknown) => {
|
|
844
|
-
ok(error instanceof FetchError);
|
|
845
|
-
ok(error.message.includes("Too many redirections (21)"));
|
|
846
|
-
return true;
|
|
980
|
+
await t.step(
|
|
981
|
+
"alternate followed by 20 redirects shares the limit",
|
|
982
|
+
async () => {
|
|
983
|
+
fetchMock.mockGlobal();
|
|
984
|
+
let requests = 0;
|
|
985
|
+
fetchMock.get(`begin:${base}`, ({ url }) => {
|
|
986
|
+
requests++;
|
|
987
|
+
const index = Number(new URL(url).pathname.split("/").at(-1));
|
|
988
|
+
if (index === 21) return Response.json({ done: true });
|
|
989
|
+
return index === 0
|
|
990
|
+
? alternate(`${base}1`, false)
|
|
991
|
+
: Response.redirect(`${base}${index + 1}`, 302);
|
|
847
992
|
});
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
993
|
+
try {
|
|
994
|
+
await rejects(loader(`${base}0`), (error: unknown) => {
|
|
995
|
+
ok(error instanceof FetchError);
|
|
996
|
+
ok(error.message.includes("Too many redirections (21)"));
|
|
997
|
+
return true;
|
|
998
|
+
});
|
|
999
|
+
deepStrictEqual(requests, 21);
|
|
1000
|
+
} finally {
|
|
1001
|
+
fetchMock.hardReset();
|
|
1002
|
+
}
|
|
1003
|
+
},
|
|
1004
|
+
);
|
|
853
1005
|
|
|
854
|
-
await t.
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
1006
|
+
await t.step(
|
|
1007
|
+
"relative alternate after redirect and isolated calls",
|
|
1008
|
+
async () => {
|
|
1009
|
+
fetchMock.mockGlobal();
|
|
1010
|
+
fetchMock.get(`${base}start`, Response.redirect(`${base}html`, 302));
|
|
1011
|
+
fetchMock.get(`${base}html`, alternate("./document", true));
|
|
1012
|
+
fetchMock.get(`${base}document`, Response.json({ done: true }));
|
|
1013
|
+
try {
|
|
1014
|
+
for (let i = 0; i < 2; i++) {
|
|
1015
|
+
const results = await Promise.all([
|
|
1016
|
+
loader(`${base}start`),
|
|
1017
|
+
loader(`${base}start`),
|
|
1018
|
+
]);
|
|
1019
|
+
for (const result of results) {
|
|
1020
|
+
deepStrictEqual(result.document, { done: true });
|
|
1021
|
+
deepStrictEqual(result.documentUrl, `${base}document`);
|
|
1022
|
+
}
|
|
868
1023
|
}
|
|
1024
|
+
} finally {
|
|
1025
|
+
fetchMock.hardReset();
|
|
869
1026
|
}
|
|
870
|
-
}
|
|
871
|
-
|
|
872
|
-
}
|
|
873
|
-
});
|
|
1027
|
+
},
|
|
1028
|
+
);
|
|
874
1029
|
|
|
875
1030
|
function alternate(next: string, html: boolean): Response {
|
|
876
1031
|
return html
|
|
@@ -893,7 +1048,7 @@ test("getDocumentLoader() preserves cancellation across alternates", async (t) =
|
|
|
893
1048
|
const base = "https://example.com/alternate-abort/";
|
|
894
1049
|
const loader = getDocumentLoader({ allowPrivateAddress: true });
|
|
895
1050
|
for (const html of [false, true]) {
|
|
896
|
-
await t.
|
|
1051
|
+
await t.step(html ? "HTML" : "Link header", async () => {
|
|
897
1052
|
fetchMock.mockGlobal();
|
|
898
1053
|
const controller = new AbortController();
|
|
899
1054
|
let requests = 0;
|
package/src/docloader.ts
CHANGED
|
@@ -288,6 +288,8 @@ export interface GetDocumentLoaderOptions extends DocumentLoaderFactoryOptions {
|
|
|
288
288
|
* - <https://w3id.org/security/multikey/v1>
|
|
289
289
|
* - <https://w3id.org/fep/ef61>
|
|
290
290
|
* - <https://w3id.org/fep/7aa9>
|
|
291
|
+
* - <https://w3id.org/fep/6757>
|
|
292
|
+
* - <https://w3id.org/fep/22cd>
|
|
291
293
|
* - <https://purl.archive.org/socialweb/webfinger>
|
|
292
294
|
* - <http://schema.org/>
|
|
293
295
|
* @param options Options for the document loader.
|
package/src/langstr.test.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { deepStrictEqual } from "node:assert";
|
|
1
|
+
import { deepStrictEqual, throws } from "node:assert";
|
|
2
2
|
import { test } from "node:test";
|
|
3
3
|
import util from "node:util";
|
|
4
4
|
import { LanguageString } from "./langstr.ts";
|
|
@@ -11,6 +11,23 @@ test("new LanguageString()", () => {
|
|
|
11
11
|
deepStrictEqual(new LanguageString("Hello", new Intl.Locale("en")), langStr);
|
|
12
12
|
});
|
|
13
13
|
|
|
14
|
+
test("new LanguageString() with extlang tags", () => {
|
|
15
|
+
// Intl.Locale objects have no own enumerable properties, so their base names
|
|
16
|
+
// are compared instead:
|
|
17
|
+
deepStrictEqual(new LanguageString("你好", "zh-YUE").locale.baseName, "yue");
|
|
18
|
+
deepStrictEqual(
|
|
19
|
+
new LanguageString("你好", "zh-yue-HK").locale.baseName,
|
|
20
|
+
"yue-HK",
|
|
21
|
+
);
|
|
22
|
+
// Whether cmn is canonicalized to zh depends on the runtime:
|
|
23
|
+
deepStrictEqual(
|
|
24
|
+
new LanguageString("你好", "zh-cmn-Hant").locale.baseName,
|
|
25
|
+
new Intl.Locale("cmn-Hant").baseName,
|
|
26
|
+
);
|
|
27
|
+
throws(() => new LanguageString("Hello", "en-USA"), RangeError);
|
|
28
|
+
throws(() => new LanguageString("你好", "zh-yue-"), RangeError);
|
|
29
|
+
});
|
|
30
|
+
|
|
14
31
|
test("Deno.inspect(LanguageString)", () => {
|
|
15
32
|
const langStr = new LanguageString("Hello, 'world'", "en");
|
|
16
33
|
deepStrictEqual(
|
package/src/langstr.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { normalizeLanguageTag } from "./langtag.ts";
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
4
|
* A language-tagged string which corresponds to the `rdf:langString` type.
|
|
3
5
|
*/
|
|
@@ -12,12 +14,14 @@ export class LanguageString extends String {
|
|
|
12
14
|
* Constructs a new `LanguageString`.
|
|
13
15
|
* @param value A string value written in the given language.
|
|
14
16
|
* @param language The language of the string. If a string is given, it will
|
|
15
|
-
* be parsed as a `Intl.Locale` object.
|
|
17
|
+
* be parsed as a `Intl.Locale` object. Extended language
|
|
18
|
+
* subtags are normalized first, e.g., `zh-yue` is parsed as
|
|
19
|
+
* `yue`.
|
|
16
20
|
*/
|
|
17
21
|
constructor(value: string, language: Intl.Locale | string) {
|
|
18
22
|
super(value);
|
|
19
23
|
this.locale = typeof language === "string"
|
|
20
|
-
? new Intl.Locale(language)
|
|
24
|
+
? new Intl.Locale(normalizeLanguageTag(language))
|
|
21
25
|
: language;
|
|
22
26
|
}
|
|
23
27
|
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { deepStrictEqual } from "node:assert";
|
|
2
|
+
import { test } from "node:test";
|
|
3
|
+
import { normalizeLanguageTag } from "./langtag.ts";
|
|
4
|
+
|
|
5
|
+
test("normalizeLanguageTag() removes extlang subtags", () => {
|
|
6
|
+
const cases: [string, string][] = [
|
|
7
|
+
["zh-yue", "yue"],
|
|
8
|
+
["zh-YUE", "yue"],
|
|
9
|
+
["ZH-Yue-hk", "yue-hk"],
|
|
10
|
+
["zh-yue-HK", "yue-HK"],
|
|
11
|
+
["zh-cmn-Hant", "cmn-Hant"],
|
|
12
|
+
["zh-yue-u-nu-hanidec", "yue-u-nu-hanidec"],
|
|
13
|
+
["sgn-ase", "ase"],
|
|
14
|
+
["ar-arz", "arz"],
|
|
15
|
+
["ms-zlm", "zlm"],
|
|
16
|
+
["kok-gom", "gom"],
|
|
17
|
+
["koK-gom", "gom"],
|
|
18
|
+
["lv-ltg", "ltg"],
|
|
19
|
+
["sw-swc", "swc"],
|
|
20
|
+
["uz-uzs", "uzs"],
|
|
21
|
+
// Valid tags never have more than one extlang, so only the first one is
|
|
22
|
+
// removed and the result is still rejected by Intl.Locale:
|
|
23
|
+
["zh-yue-cmn", "yue-cmn"],
|
|
24
|
+
["zh-yue-", "yue-"],
|
|
25
|
+
];
|
|
26
|
+
for (const [input, expected] of cases) {
|
|
27
|
+
deepStrictEqual(normalizeLanguageTag(input), expected, input);
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test("normalizeLanguageTag() leaves other tags unchanged", () => {
|
|
32
|
+
const tags = [
|
|
33
|
+
"",
|
|
34
|
+
"en",
|
|
35
|
+
"en-US",
|
|
36
|
+
"en-USA",
|
|
37
|
+
"en-yue",
|
|
38
|
+
"zh",
|
|
39
|
+
"zh-Hant-TW",
|
|
40
|
+
"yue",
|
|
41
|
+
"yue-HK",
|
|
42
|
+
"x-private",
|
|
43
|
+
"i-klingon",
|
|
44
|
+
"zh-min-nan",
|
|
45
|
+
"koK-gom",
|
|
46
|
+
"zh-yuK",
|
|
47
|
+
"constructor-yue",
|
|
48
|
+
"toString-yue",
|
|
49
|
+
];
|
|
50
|
+
for (const tag of tags) {
|
|
51
|
+
deepStrictEqual(normalizeLanguageTag(tag), tag, tag);
|
|
52
|
+
}
|
|
53
|
+
});
|