@fedify/vocab-tools 2.5.0-dev.2339 → 2.5.0-dev.2355
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/README.md +14 -4
- package/deno.json +1 -1
- package/dist/mod.cjs +73 -4
- package/dist/mod.js +73 -4
- package/package.json +2 -2
- package/src/__snapshots__/class.test.ts.deno.snap +2121 -158
- package/src/__snapshots__/class.test.ts.node.snap +2121 -158
- package/src/__snapshots__/class.test.ts.snap +2121 -158
- package/src/class.ts +55 -3
- package/src/codec.test.ts +71 -1
- package/src/codec.ts +21 -5
- package/src/type.ts +19 -0
package/src/class.ts
CHANGED
|
@@ -12,6 +12,7 @@ import { emitOverride } from "./type.ts";
|
|
|
12
12
|
|
|
13
13
|
const XSD_ANY_URI = "http://www.w3.org/2001/XMLSchema#anyURI";
|
|
14
14
|
const FEDIFY_URL = "fedify:url";
|
|
15
|
+
const FEDIFY_ABSOLUTE_IRI = "fedify:absoluteIri";
|
|
15
16
|
const INTERNAL_RUNTIME_IMPORTS = [
|
|
16
17
|
"compactJsonLdCache",
|
|
17
18
|
"createScopedContextLoader",
|
|
@@ -255,7 +256,8 @@ function canContainIriValue(
|
|
|
255
256
|
types: Record<string, TypeSchema>,
|
|
256
257
|
): boolean {
|
|
257
258
|
return property.range.some((typeUri) =>
|
|
258
|
-
typeUri === XSD_ANY_URI || typeUri === FEDIFY_URL ||
|
|
259
|
+
typeUri === XSD_ANY_URI || typeUri === FEDIFY_URL ||
|
|
260
|
+
typeUri === FEDIFY_ABSOLUTE_IRI || types[typeUri]?.entity
|
|
259
261
|
);
|
|
260
262
|
}
|
|
261
263
|
|
|
@@ -334,7 +336,45 @@ function isValidLanguageTag(language: string): boolean {
|
|
|
334
336
|
}
|
|
335
337
|
}
|
|
336
338
|
`;
|
|
337
|
-
|
|
339
|
+
if (
|
|
340
|
+
Object.values(types).some((type) =>
|
|
341
|
+
type.properties.some((property) =>
|
|
342
|
+
property.range.includes(FEDIFY_ABSOLUTE_IRI)
|
|
343
|
+
)
|
|
344
|
+
)
|
|
345
|
+
) {
|
|
346
|
+
// An absolute IRI may arrive as an IRI node or, when the publisher's
|
|
347
|
+
// context does not coerce the term, as a string or IRI-typed literal.
|
|
348
|
+
// It is never resolved against a base IRI.
|
|
349
|
+
yield `
|
|
350
|
+
function getAbsoluteIriValue(value: unknown): string | undefined {
|
|
351
|
+
if (value == null || typeof value !== "object") return undefined;
|
|
352
|
+
const node = value as Record<string, unknown>;
|
|
353
|
+
if (typeof node["@id"] === "string") {
|
|
354
|
+
return node["@id"].startsWith("_:") ? undefined : node["@id"];
|
|
355
|
+
}
|
|
356
|
+
// A language tag can come from a context's default language, so it does
|
|
357
|
+
// not make a literal any less of an IRI:
|
|
358
|
+
if (typeof node["@value"] !== "string") return undefined;
|
|
359
|
+
const type = node["@type"];
|
|
360
|
+
return type == null ||
|
|
361
|
+
type === "http://www.w3.org/2001/XMLSchema#string" ||
|
|
362
|
+
type === "http://www.w3.org/2001/XMLSchema#anyURI"
|
|
363
|
+
? node["@value"]
|
|
364
|
+
: undefined;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
function isAbsoluteIriValue(value: unknown): boolean {
|
|
368
|
+
const iri = getAbsoluteIriValue(value);
|
|
369
|
+
return iri != null && canDecodeIri(iri);
|
|
370
|
+
}
|
|
371
|
+
`;
|
|
372
|
+
}
|
|
373
|
+
// Contexts are activated by the expanded property IRIs of the values to be
|
|
374
|
+
// compacted, and by the contexts that already serialized children carry.
|
|
375
|
+
// They are detected before compaction, because a default context may
|
|
376
|
+
// define a prefix that would otherwise hide a property IRI behind a compact
|
|
377
|
+
// IRI such as `dc:license`.
|
|
338
378
|
const extraContexts = Object.fromEntries(
|
|
339
379
|
Object.values(types).flatMap((type) =>
|
|
340
380
|
type.properties.flatMap((property) =>
|
|
@@ -349,6 +389,9 @@ function isValidLanguageTag(language: string): boolean {
|
|
|
349
389
|
const extraPropertyContexts: Readonly<Record<string, string>> =
|
|
350
390
|
${JSON.stringify(extraContexts)};
|
|
351
391
|
|
|
392
|
+
const extraContextUrls: readonly string[] =
|
|
393
|
+
${JSON.stringify([...new Set(Object.values(extraContexts))])};
|
|
394
|
+
|
|
352
395
|
function getExtraContexts(document: unknown): string[] {
|
|
353
396
|
const contexts = new Set<string>();
|
|
354
397
|
const pending: unknown[] = [document];
|
|
@@ -361,7 +404,16 @@ function getExtraContexts(document: unknown): string[] {
|
|
|
361
404
|
}
|
|
362
405
|
if ("@value" in value) continue;
|
|
363
406
|
for (const [key, child] of globalThis.Object.entries(value)) {
|
|
364
|
-
if (key === "@context")
|
|
407
|
+
if (key === "@context") {
|
|
408
|
+
for (const context of Array.isArray(child) ? child : [child]) {
|
|
409
|
+
if (
|
|
410
|
+
typeof context === "string" && extraContextUrls.includes(context)
|
|
411
|
+
) {
|
|
412
|
+
contexts.add(context);
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
continue;
|
|
416
|
+
}
|
|
365
417
|
if (globalThis.Object.hasOwn(extraPropertyContexts, key)) {
|
|
366
418
|
contexts.add(extraPropertyContexts[key]);
|
|
367
419
|
}
|
package/src/codec.test.ts
CHANGED
|
@@ -59,7 +59,12 @@ async function withFixture(
|
|
|
59
59
|
const terms: Record<string, unknown> = { ...context };
|
|
60
60
|
for (const name of ["value", "first", "second"]) {
|
|
61
61
|
const term: Record<string, unknown> = { "@id": `${NS}${name}` };
|
|
62
|
-
if (
|
|
62
|
+
if (
|
|
63
|
+
property.range[0] === `${XSD}anyURI` ||
|
|
64
|
+
property.range[0] === "fedify:absoluteIri"
|
|
65
|
+
) {
|
|
66
|
+
term["@type"] = "@id";
|
|
67
|
+
}
|
|
63
68
|
if ("container" in property && property.container != null) {
|
|
64
69
|
term["@container"] = `@${property.container}`;
|
|
65
70
|
}
|
|
@@ -407,3 +412,68 @@ for (const functional of [false, true]) {
|
|
|
407
412
|
}
|
|
408
413
|
});
|
|
409
414
|
}
|
|
415
|
+
|
|
416
|
+
test("absolute IRIs are read from nodes and plain literals without a base", async () => {
|
|
417
|
+
await withFixture({
|
|
418
|
+
range: ["fedify:absoluteIri"],
|
|
419
|
+
redundantPropertiesWrite: "canonical",
|
|
420
|
+
redundantProperties: [{ uri: `${NS}first` }],
|
|
421
|
+
}, async (Fixture, contextLoader, source) => {
|
|
422
|
+
match(source, /PORTABLE_IRI_KEYS[^\n]+https:\/\/example.com\/value/);
|
|
423
|
+
match(source, /PORTABLE_IRI_KEYS[^\n]+https:\/\/example.com\/first/);
|
|
424
|
+
const values = [
|
|
425
|
+
{ "@id": "https://example.com/node" },
|
|
426
|
+
literal("urn:example:literal"),
|
|
427
|
+
{ "@value": "https://example.com/any-uri", "@type": `${XSD}anyURI` },
|
|
428
|
+
{ "@value": "https://example.com/string", "@type": `${XSD}string` },
|
|
429
|
+
// A context's default language tags uncoerced strings:
|
|
430
|
+
{ "@value": "https://example.com/tagged", "@language": "und" },
|
|
431
|
+
// Relative IRIs are never resolved against the object's ID:
|
|
432
|
+
{ "@id": "CC-BY-4.0" },
|
|
433
|
+
{ "@id": "_:b0" },
|
|
434
|
+
literal("Creative Commons Attribution 4.0 International"),
|
|
435
|
+
{ "@value": "https://example.com/date", "@type": `${XSD}date` },
|
|
436
|
+
];
|
|
437
|
+
const expected = [
|
|
438
|
+
new URL("https://example.com/node"),
|
|
439
|
+
new URL("urn:example:literal"),
|
|
440
|
+
new URL("https://example.com/any-uri"),
|
|
441
|
+
new URL("https://example.com/string"),
|
|
442
|
+
new URL("https://example.com/tagged"),
|
|
443
|
+
];
|
|
444
|
+
for (const key of ["value", "first"]) {
|
|
445
|
+
const parsed = await Fixture.fromJsonLd(
|
|
446
|
+
{
|
|
447
|
+
"@id": "https://example.com/objects/1",
|
|
448
|
+
...document({ [`${NS}${key}`]: values }),
|
|
449
|
+
},
|
|
450
|
+
{ contextLoader, baseUrl: new URL("https://example.com/base/") },
|
|
451
|
+
);
|
|
452
|
+
deepStrictEqual(parsed.values, expected);
|
|
453
|
+
}
|
|
454
|
+
// An invalid canonical set is not replaced by a synonym:
|
|
455
|
+
const invalid = await Fixture.fromJsonLd(
|
|
456
|
+
document({
|
|
457
|
+
[`${NS}value`]: [{ "@id": "CC-BY-4.0" }],
|
|
458
|
+
[`${NS}first`]: [{ "@id": "https://example.com/fallback" }],
|
|
459
|
+
}),
|
|
460
|
+
{ contextLoader },
|
|
461
|
+
);
|
|
462
|
+
deepStrictEqual(invalid.values, []);
|
|
463
|
+
const instance = new Fixture({ values: expected });
|
|
464
|
+
const expanded = await instance.toJsonLd({
|
|
465
|
+
format: "expand",
|
|
466
|
+
contextLoader,
|
|
467
|
+
}) as unknown as Record<string, unknown>[];
|
|
468
|
+
deepStrictEqual(
|
|
469
|
+
expanded[0][`${NS}value`],
|
|
470
|
+
expected.map((url) => ({ "@id": url.href })),
|
|
471
|
+
);
|
|
472
|
+
ok(!(`${NS}first` in expanded[0]));
|
|
473
|
+
const compact = await instance.toJsonLd({
|
|
474
|
+
format: "compact",
|
|
475
|
+
contextLoader,
|
|
476
|
+
});
|
|
477
|
+
deepStrictEqual(compact.value, expected.map((url) => url.href));
|
|
478
|
+
});
|
|
479
|
+
});
|
package/src/codec.ts
CHANGED
|
@@ -288,17 +288,33 @@ export async function* generateEncoder(
|
|
|
288
288
|
}
|
|
289
289
|
let docContext: Parameters<typeof jsonld.compact>[1] = options.context ??
|
|
290
290
|
${JSON.stringify(type.defaultContext)};
|
|
291
|
+
`;
|
|
292
|
+
const hasExtraContexts = Object.values(types).some((t) =>
|
|
293
|
+
t.properties.some((p) => p.extraContext != null)
|
|
294
|
+
);
|
|
295
|
+
if (hasExtraContexts) {
|
|
296
|
+
yield `
|
|
297
|
+
if (options.context == null) {
|
|
298
|
+
const currentContexts = Array.isArray(docContext) ? docContext : [docContext];
|
|
299
|
+
const additionalContexts = getExtraContexts(values).filter(
|
|
300
|
+
context => !currentContexts.includes(context)
|
|
301
|
+
);
|
|
302
|
+
if (additionalContexts.length > 0) {
|
|
303
|
+
docContext = [...currentContexts, ...additionalContexts];
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
`;
|
|
307
|
+
}
|
|
308
|
+
yield `
|
|
291
309
|
let compacted = await jsonld.compact(
|
|
292
310
|
values,
|
|
293
311
|
docContext,
|
|
294
312
|
{ documentLoader: options.contextLoader },
|
|
295
313
|
);
|
|
296
314
|
`;
|
|
297
|
-
if (
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
)
|
|
301
|
-
) {
|
|
315
|
+
if (hasExtraContexts) {
|
|
316
|
+
// A cached child may map a property with its own inline context, which
|
|
317
|
+
// the scan above cannot recognize; compaction then leaves its full IRI:
|
|
302
318
|
yield `
|
|
303
319
|
if (options.context == null) {
|
|
304
320
|
const currentContexts = Array.isArray(docContext) ? docContext : [docContext];
|
package/src/type.ts
CHANGED
|
@@ -334,6 +334,25 @@ const scalarTypes: Record<string, ScalarType> = {
|
|
|
334
334
|
},
|
|
335
335
|
skipUnparsable: true,
|
|
336
336
|
},
|
|
337
|
+
"fedify:absoluteIri": {
|
|
338
|
+
name: "URL",
|
|
339
|
+
typeGuard(v) {
|
|
340
|
+
return `${v} instanceof URL`;
|
|
341
|
+
},
|
|
342
|
+
encoder(v) {
|
|
343
|
+
return `{ "@id": formatIri(${v}) }`;
|
|
344
|
+
},
|
|
345
|
+
compactEncoder(v) {
|
|
346
|
+
return `formatIri(${v})`;
|
|
347
|
+
},
|
|
348
|
+
dataCheck(v) {
|
|
349
|
+
return `isAbsoluteIriValue(${v})`;
|
|
350
|
+
},
|
|
351
|
+
decoder(v) {
|
|
352
|
+
return `decodeIri(getAbsoluteIriValue(${v})!)`;
|
|
353
|
+
},
|
|
354
|
+
skipUnparsable: true,
|
|
355
|
+
},
|
|
337
356
|
"fedify:gatewayUrl": {
|
|
338
357
|
name: "URL",
|
|
339
358
|
typeGuard(v) {
|