@fedify/vocab-tools 2.4.0-pr.934.40 → 2.4.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/src/class.ts CHANGED
@@ -14,10 +14,35 @@ const XSD_ANY_URI = "http://www.w3.org/2001/XMLSchema#anyURI";
14
14
  const FEDIFY_URL = "fedify:url";
15
15
  const INTERNAL_RUNTIME_IMPORTS = [
16
16
  "compactJsonLdCache",
17
+ "createScopedContextLoader",
17
18
  "getJsonLdContext",
19
+ "getPortableActorGateways",
18
20
  "isTrustedIriOrigin",
19
21
  "normalizeJsonLdIris",
20
22
  ].join(",\n ");
23
+ const PORTABLE_DEREFERENCE_IMPORTS = [
24
+ "copyPortableProvenance",
25
+ "createSnapshotContextLoader",
26
+ "dereferencePortableIri",
27
+ "getPortableResponseClaim",
28
+ "getReferrerGateways",
29
+ "isPortableIri",
30
+ "isPortableMode",
31
+ "isUnsecuredPortableObject",
32
+ "isUnverifiedPortableClaim",
33
+ "markUnverifiedPortableClaim",
34
+ "mustDereferencePortableObject",
35
+ "parseCompatibleEf61Reference",
36
+ "type PortableResponseClaim",
37
+ "recordPortableReferrer",
38
+ "rejectMalformedCompatibleReference",
39
+ "warnUnverifiableEmbeddedObject",
40
+ ].join(",\n ");
41
+ const SIGNED_REPRESENTATION_IMPORTS = [
42
+ "enterSignedValueScope",
43
+ "resolveSignedValues",
44
+ "retainedSignedValueRef",
45
+ ].join(",\n ");
21
46
  const RUNTIME_IMPORTS = [
22
47
  "canParseDecimal",
23
48
  "decodeMultibase",
@@ -31,10 +56,13 @@ const RUNTIME_IMPORTS = [
31
56
  "importMultibaseKey",
32
57
  "importPem",
33
58
  "isDecimal",
59
+ "isGatewayUrl",
34
60
  "LanguageString",
35
61
  "parseDecimal",
62
+ "parseGatewayUrl",
36
63
  "parseIri",
37
64
  "parseJsonLdId",
65
+ "type PortableObjectVerifier",
38
66
  "type RemoteDocument",
39
67
  ].join(",\n ");
40
68
 
@@ -84,6 +112,7 @@ async function* generateClass(
84
112
  readonly #documentLoader?: DocumentLoader;
85
113
  readonly #contextLoader?: DocumentLoader;
86
114
  readonly #tracerProvider?: TracerProvider;
115
+ readonly #verifyPortableObject?: PortableObjectVerifier;
87
116
  readonly #warning?: {
88
117
  category: string[];
89
118
  message: string;
@@ -91,6 +120,7 @@ async function* generateClass(
91
120
  };
92
121
  #cachedJsonLd?: unknown;
93
122
  readonly #_baseUrl?: URL;
123
+ #shouldCacheJsonLd = true;
94
124
  readonly id: URL | null;
95
125
 
96
126
  protected get _documentLoader(): DocumentLoader | undefined {
@@ -105,6 +135,10 @@ async function* generateClass(
105
135
  return this.#tracerProvider;
106
136
  }
107
137
 
138
+ protected get _verifyPortableObject(): PortableObjectVerifier | undefined {
139
+ return this.#verifyPortableObject;
140
+ }
141
+
108
142
  protected get _warning(): {
109
143
  category: string[];
110
144
  message: string;
@@ -124,6 +158,20 @@ async function* generateClass(
124
158
  protected get _baseUrl(): URL | undefined {
125
159
  return this.#_baseUrl;
126
160
  }
161
+
162
+ protected get _shouldCacheJsonLd(): boolean {
163
+ return this.#shouldCacheJsonLd;
164
+ }
165
+
166
+ protected set _shouldCacheJsonLd(value: boolean) {
167
+ this.#shouldCacheJsonLd = value;
168
+ }
169
+
170
+ protected static _shouldCacheDecodedJsonLd(value: unknown): boolean {
171
+ if (value == null || typeof value !== "object") return true;
172
+ if (!("_shouldCacheJsonLd" in value)) return true;
173
+ return (value as { _shouldCacheJsonLd: boolean })._shouldCacheJsonLd;
174
+ }
127
175
  `;
128
176
  }
129
177
  yield `
@@ -251,10 +299,79 @@ export async function* generateClasses(
251
299
  from "@opentelemetry/api";\n`;
252
300
  yield `import {\n ${RUNTIME_IMPORTS}\n} from "@fedify/vocab-runtime";\n`;
253
301
  yield `import {\n ${INTERNAL_RUNTIME_IMPORTS}\n} from "@fedify/vocab-runtime/internal/jsonld-cache";\n`;
302
+ yield `import {\n ${PORTABLE_DEREFERENCE_IMPORTS}\n} from "@fedify/vocab-runtime/internal/portable-dereference";\n`;
303
+ yield `import {\n ${SIGNED_REPRESENTATION_IMPORTS}\n} from "@fedify/vocab-runtime/internal/signed-representation";\n`;
254
304
  yield `import {
255
305
  isTemporalDuration,
256
306
  isTemporalInstant,
257
307
  } from "@fedify/vocab-runtime/temporal";\n`;
308
+ yield `
309
+ function canDecodeIri(iri: string): boolean {
310
+ try { parseIri(iri); return true; } catch { return iri.startsWith("at://"); }
311
+ }
312
+
313
+ function decodeIri(iri: string): URL {
314
+ return !URL.canParse(iri) && iri.startsWith("at://")
315
+ ? new URL("at://" + encodeURIComponent(iri.substring(5)))
316
+ : parseIri(iri);
317
+ }
318
+
319
+ function inspectIri(iri: URL): string {
320
+ // Inspecting an object must not throw, even for a URL that cannot be
321
+ // formatted as an IRI, e.g., a malformed portable ID:
322
+ try { return formatIri(iri); } catch { return iri.href; }
323
+ }
324
+ `;
325
+ yield `
326
+ function isValidLanguageTag(language: string): boolean {
327
+ try {
328
+ new Intl.Locale(language);
329
+ return true;
330
+ } catch (error) {
331
+ if (error instanceof RangeError) return false;
332
+ throw error;
333
+ }
334
+ }
335
+ `;
336
+ // Contexts are activated by expanded property IRIs left after compaction.
337
+ const extraContexts = Object.fromEntries(
338
+ Object.values(types).flatMap((type) =>
339
+ type.properties.flatMap((property) =>
340
+ property.extraContext == null
341
+ ? []
342
+ : [[property.uri, property.extraContext]]
343
+ )
344
+ ),
345
+ );
346
+ if (Object.keys(extraContexts).length > 0) {
347
+ yield `
348
+ const extraPropertyContexts: Readonly<Record<string, string>> =
349
+ ${JSON.stringify(extraContexts)};
350
+
351
+ function getExtraContexts(document: unknown): string[] {
352
+ const contexts = new Set<string>();
353
+ const pending: unknown[] = [document];
354
+ while (pending.length > 0) {
355
+ const value = pending.pop();
356
+ if (value == null || typeof value !== "object") continue;
357
+ if (Array.isArray(value)) {
358
+ for (const item of value) pending.push(item);
359
+ continue;
360
+ }
361
+ if ("@value" in value) continue;
362
+ for (const [key, child] of globalThis.Object.entries(value)) {
363
+ if (key === "@context") continue;
364
+ if (globalThis.Object.hasOwn(extraPropertyContexts, key)) {
365
+ contexts.add(extraPropertyContexts[key]);
366
+ }
367
+ pending.push(child);
368
+ }
369
+ }
370
+ return [...contexts];
371
+ }
372
+ `;
373
+ }
374
+ yield "\n\n";
258
375
  const portableIriKeys = new Set(["@id", "id"]);
259
376
  for (const type of Object.values(types)) {
260
377
  for (const property of type.properties) {
package/src/codec.ts CHANGED
@@ -6,12 +6,14 @@ import {
6
6
  areAllScalarTypes,
7
7
  emitOverride,
8
8
  getAllProperties,
9
+ getDataCheck,
9
10
  getDecoder,
10
11
  getDecoders,
11
12
  getEncoders,
12
13
  getSubtypes,
13
14
  getTypeNames,
14
15
  isCompactableType,
16
+ skipsUnparsable,
15
17
  } from "./type.ts";
16
18
 
17
19
  function* generatePreprocessorBlock(
@@ -43,6 +45,7 @@ function* generatePreprocessorBlock(
43
45
  documentLoader: options.documentLoader,
44
46
  contextLoader: options.contextLoader,
45
47
  tracerProvider: options.tracerProvider,
48
+ verifyPortableObject: options.verifyPortableObject,
46
49
  baseUrl: ${baseUrlExpr},
47
50
  });
48
51
  if (_result instanceof Error) throw _result;
@@ -95,6 +98,10 @@ export async function* generateEncoder(
95
98
  ...options,
96
99
  contextLoader: options.contextLoader ?? getDocumentLoader(),
97
100
  };
101
+ // Joins the enclosing frame's signed-value scope, or starts one when this
102
+ // frame is the outermost. Only the owning frame puts retained signed
103
+ // child documents back in place of their placeholders.
104
+ const signedValues = enterSignedValueScope(options);
98
105
  `;
99
106
  if (isCompactableType(typeUri, types)) {
100
107
  yield `
@@ -127,6 +134,7 @@ export async function* generateEncoder(
127
134
  const item = (
128
135
  `;
129
136
  if (!areAllScalarTypes(property.range, types)) {
137
+ yield "retainedSignedValueRef(v, signedValues.scope) ?? (";
130
138
  yield "v instanceof URL ? formatIri(v) : ";
131
139
  }
132
140
  const encoders = getEncoders(
@@ -137,6 +145,7 @@ export async function* generateEncoder(
137
145
  true,
138
146
  );
139
147
  for (const code of encoders) yield code;
148
+ if (!areAllScalarTypes(property.range, types)) yield ")";
140
149
  yield `
141
150
  );
142
151
  compactItems.push(item);
@@ -180,7 +189,9 @@ export async function* generateEncoder(
180
189
  }
181
190
  if (this.id != null) result["id"] = formatIri(this.id);
182
191
  result["@context"] = ${JSON.stringify(type.defaultContext)};
183
- return result;
192
+ return signedValues.owner
193
+ ? resolveSignedValues(result, signedValues.scope)
194
+ : result;
184
195
  }
185
196
  `;
186
197
  }
@@ -207,16 +218,23 @@ export async function* generateEncoder(
207
218
  yield `
208
219
  array = [];
209
220
  for (const v of this.${await getFieldName(property.uri)}) {
210
- const element = (
221
+ let element = (
211
222
  `;
212
223
  if (!areAllScalarTypes(property.range, types)) {
224
+ yield "retainedSignedValueRef(v, signedValues.scope) ?? (";
213
225
  yield 'v instanceof URL ? { "@id": formatIri(v) } : ';
214
226
  }
215
227
  for (const code of getEncoders(property.range, types, "v", "options")) {
216
228
  yield code;
217
229
  }
230
+ if (!areAllScalarTypes(property.range, types)) yield ")";
218
231
  yield `
219
232
  );
233
+ if (Array.isArray(element)) {
234
+ if (element.length < 1) continue;
235
+ if (element.length == 1) element = element[0];
236
+ }
237
+ if (typeof element === "undefined") continue;
220
238
  `;
221
239
  if (isNonFunctionalProperty(property) && property.container === "graph") {
222
240
  yield `array.push({ "@graph": element });`;
@@ -254,16 +272,41 @@ export async function* generateEncoder(
254
272
  if (options.format === "expand") {
255
273
  return await jsonld.expand(
256
274
  values,
257
- { documentLoader: options.contextLoader },
275
+ {
276
+ documentLoader: options.contextLoader,
277
+ keepFreeFloatingNodes: true,
278
+ },
258
279
  );
259
280
  }
260
- const docContext = options.context ??
281
+ let docContext: Parameters<typeof jsonld.compact>[1] = options.context ??
261
282
  ${JSON.stringify(type.defaultContext)};
262
- const compacted = await jsonld.compact(
283
+ let compacted = await jsonld.compact(
263
284
  values,
264
285
  docContext,
265
286
  { documentLoader: options.contextLoader },
266
287
  );
288
+ `;
289
+ if (
290
+ Object.values(types).some((t) =>
291
+ t.properties.some((p) => p.extraContext != null)
292
+ )
293
+ ) {
294
+ yield `
295
+ if (options.context == null) {
296
+ const currentContexts = Array.isArray(docContext) ? docContext : [docContext];
297
+ const additionalContexts = getExtraContexts(compacted).filter(
298
+ context => !currentContexts.includes(context)
299
+ );
300
+ if (additionalContexts.length > 0) {
301
+ docContext = [...currentContexts, ...additionalContexts];
302
+ compacted = await jsonld.compact(values, docContext, {
303
+ documentLoader: options.contextLoader,
304
+ });
305
+ }
306
+ }
307
+ `;
308
+ }
309
+ yield `
267
310
  if (docContext != null) {
268
311
  // Embed context
269
312
  `;
@@ -295,13 +338,18 @@ export async function* generateEncoder(
295
338
  }
296
339
  yield `
297
340
  }
298
- return compacted;
341
+ return signedValues.owner
342
+ ? resolveSignedValues(compacted, signedValues.scope)
343
+ : compacted;
299
344
  }
300
345
 
301
346
  protected ${emitOverride(typeUri, types)} isCompactable(): boolean {
302
347
  `;
303
348
  for (const property of type.properties) {
304
- if (!property.range.every((r) => isCompactableType(r, types))) {
349
+ if (
350
+ property.extraContext != null ||
351
+ !property.range.every((r) => isCompactableType(r, types))
352
+ ) {
305
353
  yield `
306
354
  if (
307
355
  this.${await getFieldName(property.uri)} != null &&
@@ -331,6 +379,10 @@ export async function* generateDecoder(
331
379
  * - \`contextLoader\`: The loader for remote JSON-LD contexts.
332
380
  * - \`tracerProvider\`: The OpenTelemetry tracer provider to use.
333
381
  * If omitted, the global tracer provider is used.
382
+ * - \`verifyPortableObject\`: The default FEP-ef61 portable
383
+ * object verifier for the property accessors of the
384
+ * returned object and the objects obtained from it. It
385
+ * does not verify the given \`json\` itself.
334
386
  * @returns The object of this type.
335
387
  * @throws {TypeError} If the given \`json\` is invalid.
336
388
  */
@@ -340,6 +392,7 @@ export async function* generateDecoder(
340
392
  documentLoader?: DocumentLoader,
341
393
  contextLoader?: DocumentLoader,
342
394
  tracerProvider?: TracerProvider,
395
+ verifyPortableObject?: PortableObjectVerifier,
343
396
  baseUrl?: URL,
344
397
  } = {},
345
398
  ): Promise<${type.name}> {
@@ -378,6 +431,7 @@ export async function* generateDecoder(
378
431
  documentLoader?: DocumentLoader,
379
432
  contextLoader?: DocumentLoader,
380
433
  tracerProvider?: TracerProvider,
434
+ verifyPortableObject?: PortableObjectVerifier,
381
435
  baseUrl?: URL,
382
436
  } = {},
383
437
  ): Promise<${type.name}> {
@@ -394,7 +448,17 @@ export async function* generateDecoder(
394
448
  // deno-lint-ignore no-explicit-any
395
449
  let values: Record<string, any[]> & { "@id"?: string };
396
450
  let expanded: unknown[];
397
- if (globalThis.Object.keys(json).length == 0) {
451
+ if (
452
+ "_fromSubclass" in options &&
453
+ options._fromSubclass &&
454
+ typeof json === "object" &&
455
+ !Array.isArray(json)
456
+ ) {
457
+ values =
458
+ // deno-lint-ignore no-explicit-any
459
+ json as (Record<string, any[]> & { "@id"?: string });
460
+ expanded = [values];
461
+ } else if (globalThis.Object.keys(json).length == 0) {
398
462
  values = {};
399
463
  expanded = [values];
400
464
  } else {
@@ -455,8 +519,9 @@ export async function* generateDecoder(
455
519
  `;
456
520
  } else {
457
521
  yield `
458
- delete values["@type"];
459
- const instance = await super.fromJsonLd(values, {
522
+ const superValues = structuredClone(values);
523
+ delete superValues["@type"];
524
+ const instance = await super.fromJsonLd(superValues, {
460
525
  ...options,
461
526
  // @ts-ignore: an internal option
462
527
  _fromSubclass: true,
@@ -466,12 +531,18 @@ export async function* generateDecoder(
466
531
  }
467
532
  `;
468
533
  }
534
+ yield `
535
+ let shouldCacheJsonLd = instance._shouldCacheJsonLd;
536
+ `;
469
537
  for (const property of type.properties) {
470
538
  const variable = await getFieldName(property.uri, "");
471
539
  yield await generateField(property, types, "const ");
472
540
  const arrayVariable = `${variable}__array`;
541
+ const propertyValues = property.uri === "https://w3id.org/fep/ef61/gateways"
542
+ ? "getPortableActorGateways(values) as typeof values[string]"
543
+ : `values[${JSON.stringify(property.uri)}]`;
473
544
  yield `
474
- let ${arrayVariable} = values[${JSON.stringify(property.uri)}];
545
+ let ${arrayVariable} = ${propertyValues};
475
546
  `;
476
547
  if (property.functional && property.redundantProperties != null) {
477
548
  for (const prop of property.redundantProperties) {
@@ -482,13 +553,25 @@ export async function* generateDecoder(
482
553
  `;
483
554
  }
484
555
  }
556
+ let itemsExpression =
557
+ `${arrayVariable}.length === 1 && "@list" in ${arrayVariable}[0]
558
+ ? ${arrayVariable}[0]["@list"]
559
+ : ${arrayVariable}`;
560
+ if (
561
+ isNonFunctionalProperty(property) && property.container === "graph"
562
+ ) {
563
+ itemsExpression = `(${itemsExpression}).flatMap((item: unknown) =>
564
+ item != null && typeof item === "object" && "@graph" in item &&
565
+ Array.isArray((item as { "@graph": unknown })["@graph"])
566
+ ? (item as { "@graph": unknown[] })["@graph"]
567
+ : [item]
568
+ )`;
569
+ }
485
570
  yield `
486
571
  for (
487
572
  const v of ${arrayVariable} == null
488
573
  ? []
489
- : ${arrayVariable}.length === 1 && "@list" in ${arrayVariable}[0]
490
- ? ${arrayVariable}[0]["@list"]
491
- : ${arrayVariable}
574
+ : ${itemsExpression}
492
575
  ) {
493
576
  if (v == null) continue;
494
577
  `;
@@ -510,23 +593,25 @@ export async function* generateDecoder(
510
593
  }
511
594
  `;
512
595
  }
596
+ yield `
597
+ const decoded =
598
+ `;
599
+ const lenient = property.range.length == 1 &&
600
+ skipsUnparsable(property.range[0]);
513
601
  if (property.range.length == 1) {
514
- yield `
515
- const decoded = ${
516
- getDecoder(
517
- property.range[0],
518
- types,
519
- "v",
520
- "options",
521
- propertyBaseUrl,
522
- )
523
- };
524
- if (typeof decoded === "undefined") continue;
525
- ${variable}.push(decoded);`;
602
+ if (lenient) {
603
+ yield getDataCheck(property.range[0], types, "v");
604
+ yield " ? ";
605
+ }
606
+ yield getDecoder(
607
+ property.range[0],
608
+ types,
609
+ "v",
610
+ "options",
611
+ propertyBaseUrl,
612
+ );
613
+ if (lenient) yield " : undefined";
526
614
  } else {
527
- yield `
528
- const decoded =
529
- `;
530
615
  const decoders = getDecoders(
531
616
  property.range,
532
617
  types,
@@ -535,19 +620,33 @@ export async function* generateDecoder(
535
620
  propertyBaseUrl,
536
621
  );
537
622
  for (const code of decoders) yield code;
538
- yield `
623
+ }
624
+ yield `
539
625
  ;
540
- if (typeof decoded === "undefined") continue;
626
+ `;
627
+ yield `
628
+ if (typeof decoded === "undefined") {
629
+ shouldCacheJsonLd = false;
630
+ continue;
631
+ }
632
+ `;
633
+ yield `
634
+ if (!this._shouldCacheDecodedJsonLd(decoded)) {
635
+ shouldCacheJsonLd = false;
636
+ }
541
637
  ${variable}.push(decoded);
542
- `;
543
- }
638
+ `;
544
639
  yield `
545
640
  }
546
641
  instance.${await getFieldName(property.uri)} = ${variable};
547
642
  `;
548
643
  }
549
644
  yield `
550
- if (!("_fromSubclass" in options) || !options._fromSubclass) {
645
+ instance._shouldCacheJsonLd = shouldCacheJsonLd;
646
+ if (
647
+ shouldCacheJsonLd &&
648
+ (!("_fromSubclass" in options) || !options._fromSubclass)
649
+ ) {
551
650
  try {
552
651
  if (cacheJsonLd != null && cacheJsonLd !== expanded) {
553
652
  const compactArray = Array.isArray(json) && json.length === 1;
@@ -89,6 +89,7 @@ export async function* generateConstructor(
89
89
  documentLoader?: DocumentLoader,
90
90
  contextLoader?: DocumentLoader,
91
91
  tracerProvider?: TracerProvider,
92
+ verifyPortableObject?: PortableObjectVerifier,
92
93
  } = {},
93
94
  ) {
94
95
  `;
@@ -97,6 +98,7 @@ export async function* generateConstructor(
97
98
  this.#documentLoader = options.documentLoader;
98
99
  this.#contextLoader = options.contextLoader;
99
100
  this.#tracerProvider = options.tracerProvider;
101
+ this.#verifyPortableObject = options.verifyPortableObject;
100
102
  const baseUrl = (options as { baseUrl?: URL }).baseUrl;
101
103
  this.#_baseUrl = baseUrl == null ? undefined : new URL(baseUrl.href);
102
104
  if ("$warning" in options) {
@@ -208,6 +210,13 @@ export async function* generateCloner(
208
210
  yield `
209
211
  /**
210
212
  * Clones this instance, optionally updating it with the given values.
213
+ *
214
+ * A clone never inherits a signed JSON-LD representation retained by
215
+ * \`signObject()\`: the clone may differ from the document that the proof
216
+ * covers, so embedding the original secured JSON in a parent document
217
+ * would be wrong. Sign the clone again if it has to be embedded as a
218
+ * secured child.
219
+ *
211
220
  * @param values The values to update the clone with.
212
221
  * @param options The options to use for cloning.
213
222
  * @returns The cloned instance.
@@ -221,6 +230,7 @@ export async function* generateCloner(
221
230
  options: {
222
231
  documentLoader?: DocumentLoader,
223
232
  contextLoader?: DocumentLoader,
233
+ verifyPortableObject?: PortableObjectVerifier,
224
234
  } = {}
225
235
  ): ${type.name} {
226
236
  if (this._warning != null) {
@@ -239,6 +249,7 @@ export async function* generateCloner(
239
249
  { id: values.id ?? this.id },
240
250
  options
241
251
  );
252
+ copyPortableProvenance(this, clone);
242
253
  `;
243
254
  } else {
244
255
  yield `const clone = super.clone(values, options) as unknown as ${type.name};`;
package/src/inspector.ts CHANGED
@@ -21,12 +21,12 @@ export async function* generateInspector(
21
21
  [Symbol.for("Deno.customInspect")]: (
22
22
  inspect: typeof Deno.inspect,
23
23
  options: Deno.InspectOptions,
24
- ): string => "URL " + inspect(this.id!.href, options),
24
+ ): string => "URL " + inspect(inspectIri(this.id!), options),
25
25
  [Symbol.for("nodejs.util.inspect.custom")]: (
26
26
  _depth: number,
27
27
  options: unknown,
28
28
  inspect: (value: unknown, options: unknown) => string,
29
- ): string => "URL " + inspect(this.id!.href, options),
29
+ ): string => "URL " + inspect(inspectIri(this.id!), options),
30
30
  };
31
31
  }
32
32
  `;
@@ -44,12 +44,12 @@ export async function* generateInspector(
44
44
  [Symbol.for("Deno.customInspect")]: (
45
45
  inspect: typeof Deno.inspect,
46
46
  options: Deno.InspectOptions,
47
- ): string => "URL " + inspect(v.href, options),
47
+ ): string => "URL " + inspect(inspectIri(v), options),
48
48
  [Symbol.for("nodejs.util.inspect.custom")]: (
49
49
  _depth: number,
50
50
  options: unknown,
51
51
  inspect: (value: unknown, options: unknown) => string,
52
- ): string => "URL " + inspect(v.href, options),
52
+ ): string => "URL " + inspect(inspectIri(v), options),
53
53
  }
54
54
  : v);
55
55
  `;