@kgdiff/formats 0.0.0-stage → 0.2.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.
Files changed (58) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +68 -2
  3. package/dist/detect.d.ts +47 -0
  4. package/dist/detect.d.ts.map +1 -0
  5. package/dist/detect.js +425 -0
  6. package/dist/detect.js.map +1 -0
  7. package/dist/index.d.ts +203 -0
  8. package/dist/index.d.ts.map +1 -0
  9. package/dist/index.js +114 -0
  10. package/dist/index.js.map +1 -0
  11. package/dist/iri.d.ts +13 -0
  12. package/dist/iri.d.ts.map +1 -0
  13. package/dist/iri.js +91 -0
  14. package/dist/iri.js.map +1 -0
  15. package/dist/json.d.ts +31 -0
  16. package/dist/json.d.ts.map +1 -0
  17. package/dist/json.js +787 -0
  18. package/dist/json.js.map +1 -0
  19. package/dist/jsonld.d.ts +16 -0
  20. package/dist/jsonld.d.ts.map +1 -0
  21. package/dist/jsonld.js +1249 -0
  22. package/dist/jsonld.js.map +1 -0
  23. package/dist/obo.d.ts +25 -0
  24. package/dist/obo.d.ts.map +1 -0
  25. package/dist/obo.js +478 -0
  26. package/dist/obo.js.map +1 -0
  27. package/dist/prefixes.d.ts +49 -0
  28. package/dist/prefixes.d.ts.map +1 -0
  29. package/dist/prefixes.js +177 -0
  30. package/dist/prefixes.js.map +1 -0
  31. package/dist/rdf.d.ts +46 -0
  32. package/dist/rdf.d.ts.map +1 -0
  33. package/dist/rdf.js +1047 -0
  34. package/dist/rdf.js.map +1 -0
  35. package/dist/rdfxml.d.ts +8 -0
  36. package/dist/rdfxml.d.ts.map +1 -0
  37. package/dist/rdfxml.js +529 -0
  38. package/dist/rdfxml.js.map +1 -0
  39. package/dist/shared.d.ts +117 -0
  40. package/dist/shared.d.ts.map +1 -0
  41. package/dist/shared.js +355 -0
  42. package/dist/shared.js.map +1 -0
  43. package/dist/xml.d.ts +50 -0
  44. package/dist/xml.d.ts.map +1 -0
  45. package/dist/xml.js +508 -0
  46. package/dist/xml.js.map +1 -0
  47. package/package.json +55 -3
  48. package/src/detect.ts +435 -0
  49. package/src/index.ts +296 -0
  50. package/src/iri.ts +86 -0
  51. package/src/json.ts +889 -0
  52. package/src/jsonld.ts +1376 -0
  53. package/src/obo.ts +557 -0
  54. package/src/prefixes.ts +195 -0
  55. package/src/rdf.ts +1181 -0
  56. package/src/rdfxml.ts +633 -0
  57. package/src/shared.ts +408 -0
  58. package/src/xml.ts +550 -0
package/dist/jsonld.js ADDED
@@ -0,0 +1,1249 @@
1
+ /**
2
+ * JSON-LD 1.1 → RDF quads, synchronously: context processing, expansion and deserialization to
3
+ * RDF (JSON-LD 1.1 Processing Algorithms and API §4, §5, §8). The quads go through the same
4
+ * mapping as Turtle (`rdf.ts`), so a graph diffs identically in either syntax.
5
+ *
6
+ * - Contexts are inline, or come from the caller's synchronous `documentLoader` (remote
7
+ * `@context` URLs, `@import`). Nothing is ever fetched: results never depend on the network.
8
+ * - Without a base IRI, relative IRIs stay relative (as in Turtle without `@base`) instead of
9
+ * being dropped; IRIs with characters RDF forbids are dropped, as the spec says.
10
+ * - Keys that expand to no IRI (no term, no `@vocab`) are dropped and reported together.
11
+ * - `@direction` is not represented (`rdfDirection: null`); named graphs are merged like
12
+ * N-Quads graphs; `@json` literals use JCS canonical JSON.
13
+ * - Errors carry the JSON-LD error code (`invalid IRI mapping`, `protected term redefinition`…).
14
+ */
15
+ import { BlankNode, DataFactory, NamedNode } from 'n3';
16
+ import { isAbsoluteIri, resolveIri } from './iri.js';
17
+ import { RDF_NS, XSD_NS } from './prefixes.js';
18
+ import { compareStrings, invalidInput, plural, setOwn } from './shared.js';
19
+ /** A JSON-LD error (code from the spec, plus what it concerns); never escapes this module. */
20
+ class JsonLdError {
21
+ code;
22
+ detail;
23
+ constructor(code, detail) {
24
+ this.code = code;
25
+ this.detail = detail;
26
+ }
27
+ }
28
+ /** Offending JSON values are quoted in messages up to this length. */
29
+ const DETAIL_LENGTH = 120;
30
+ /** `detail` is an explanation, or a JSON value quoted (and cut) as found. */
31
+ const fail = (code, detail) => {
32
+ if (typeof detail === 'string')
33
+ return new JsonLdError(code, detail);
34
+ const text = JSON.stringify(detail) ?? String(detail);
35
+ return new JsonLdError(code, text.length > DETAIL_LENGTH ? `${text.slice(0, DETAIL_LENGTH)}…` : text);
36
+ };
37
+ const KEYWORDS = new Set([
38
+ '@base',
39
+ '@container',
40
+ '@context',
41
+ '@direction',
42
+ '@graph',
43
+ '@id',
44
+ '@import',
45
+ '@included',
46
+ '@index',
47
+ '@json',
48
+ '@language',
49
+ '@list',
50
+ '@nest',
51
+ '@none',
52
+ '@prefix',
53
+ '@propagate',
54
+ '@protected',
55
+ '@reverse',
56
+ '@set',
57
+ '@type',
58
+ '@value',
59
+ '@version',
60
+ '@vocab',
61
+ ]);
62
+ /** Context entries that are not term definitions. */
63
+ const CONTEXT_KEYWORDS = new Set([
64
+ '@base',
65
+ '@direction',
66
+ '@import',
67
+ '@language',
68
+ '@propagate',
69
+ '@protected',
70
+ '@version',
71
+ '@vocab',
72
+ ]);
73
+ const TERM_DEFINITION_KEYS = new Set([
74
+ '@id',
75
+ '@reverse',
76
+ '@container',
77
+ '@context',
78
+ '@direction',
79
+ '@index',
80
+ '@language',
81
+ '@nest',
82
+ '@prefix',
83
+ '@protected',
84
+ '@type',
85
+ ]);
86
+ const CONTAINERS = new Set([
87
+ '@graph',
88
+ '@id',
89
+ '@index',
90
+ '@language',
91
+ '@list',
92
+ '@set',
93
+ '@type',
94
+ ]);
95
+ const VALUE_KEYS = new Set([
96
+ '@direction',
97
+ '@index',
98
+ '@language',
99
+ '@type',
100
+ '@value',
101
+ ]);
102
+ /** Reserved for future keywords: ignored, never expanded (JSON-LD 1.1 §3.3). */
103
+ const KEYWORD_FORM = /^@[a-zA-Z]+$/;
104
+ /** An IRI ending like this can serve as a prefix (gen-delims, RFC 3986). */
105
+ const GEN_DELIM_END = /[:/?#[\]@]$/;
106
+ /** Characters an RDF IRI cannot contain besides controls and the space (RFC 3987). */
107
+ const ILLEGAL_IRI_CHARACTERS = '<>"{}|\\^`';
108
+ const LANGUAGE_TAG = /^[a-zA-Z]{1,8}(?:-[a-zA-Z0-9]{1,8})*$/;
109
+ /** More remote contexts than this in one chain is a `context overflow`. */
110
+ const MAX_REMOTE_CONTEXTS = 32;
111
+ /**
112
+ * Deepest nesting accepted. Expansion recurses (about three stack frames per level), so this
113
+ * stays far below what small engine stacks allow; real documents nest a few levels.
114
+ */
115
+ export const MAX_JSON_LD_NESTING = 200;
116
+ const RDF_TYPE = `${RDF_NS}type`;
117
+ const RDF_FIRST = `${RDF_NS}first`;
118
+ const RDF_REST = `${RDF_NS}rest`;
119
+ const RDF_NIL = `${RDF_NS}nil`;
120
+ const RDF_JSON = `${RDF_NS}JSON`;
121
+ const RDF_LANG_STRING = `${RDF_NS}langString`;
122
+ const XSD_BOOLEAN = `${XSD_NS}boolean`;
123
+ const XSD_DOUBLE = `${XSD_NS}double`;
124
+ const XSD_INTEGER = `${XSD_NS}integer`;
125
+ const isMap = (value) => typeof value === 'object' && value !== null && !Array.isArray(value);
126
+ const has = (map, key) => Object.hasOwn(map, key);
127
+ const arrayify = (value) => (Array.isArray(value) ? value : [value]);
128
+ const isScalar = (value) => typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean';
129
+ const isValueObject = (value) => isMap(value) && has(value, '@value');
130
+ const isListObject = (value) => isMap(value) && has(value, '@list');
131
+ const isGraphObject = (value) => isMap(value) &&
132
+ has(value, '@graph') &&
133
+ Object.keys(value).every((key) => key === '@graph' || key === '@id' || key === '@index');
134
+ const isNodeObject = (value) => isMap(value) && !has(value, '@value') && !has(value, '@list') && !has(value, '@set');
135
+ /** Appends values to `map[key]`, which always holds an array. */
136
+ function addValues(map, key, values) {
137
+ const existing = map[key];
138
+ if (Array.isArray(existing))
139
+ for (const value of values)
140
+ existing.push(value);
141
+ else
142
+ setOwn(map, key, existing === undefined ? [...values] : [existing, ...values]);
143
+ }
144
+ /** Container mappings JSON-LD 1.1 allows (§4.2.2, step 21.1). */
145
+ function validContainer(container) {
146
+ const set = new Set(container);
147
+ if (set.size !== container.length || !container.every((value) => CONTAINERS.has(value))) {
148
+ return false;
149
+ }
150
+ if (container.length === 1)
151
+ return true;
152
+ if (set.has('@list'))
153
+ return false;
154
+ if (set.has('@graph') && (set.has('@id') || set.has('@index'))) {
155
+ return (!(set.has('@id') && set.has('@index')) &&
156
+ container.every((value) => value !== '@type' && value !== '@language'));
157
+ }
158
+ return set.has('@set') && container.length === 2;
159
+ }
160
+ const isThenable = (value) => typeof value === 'object' &&
161
+ value !== null &&
162
+ typeof value.then === 'function';
163
+ /**
164
+ * Reads a parsed JSON-LD document into quads. Throws `KgdiffError('invalid-input')` with the
165
+ * JSON-LD error code for invalid documents and for remote contexts no loader provides.
166
+ */
167
+ export function readJsonLd(document, options, warnings) {
168
+ const reader = new JsonLdReader(options.baseIRI, options.documentLoader);
169
+ try {
170
+ reader.read(document);
171
+ }
172
+ catch (error) {
173
+ if (!(error instanceof JsonLdError))
174
+ throw error;
175
+ throw invalidInput(`Invalid JSON-LD: ${error.code}: ${error.detail}.`, {
176
+ format: 'jsonld',
177
+ code: error.code,
178
+ });
179
+ }
180
+ if (reader.unmapped.size > 0) {
181
+ const keys = [...reader.unmapped].sort(compareStrings);
182
+ const shown = keys.slice(0, 5).map((key) => JSON.stringify(key));
183
+ warnings.add('jsonld-unmapped-terms', `Dropped ${plural(keys.length, 'JSON-LD key')} that map to no IRI (no @context term or @vocab): ${shown.join(', ')}${keys.length > shown.length ? ', …' : ''}.`);
184
+ }
185
+ return { quads: reader.quads, prefixes: reader.prefixes };
186
+ }
187
+ class JsonLdReader {
188
+ baseIRI;
189
+ documentLoader;
190
+ quads = [];
191
+ prefixes = {};
192
+ /** Keys dropped because they expand to no IRI. */
193
+ unmapped = new Set();
194
+ /** `@context` of every remote document, by URL (loaded once per parse). */
195
+ loaded = new Map();
196
+ scoped = new WeakMap();
197
+ iris = new Map();
198
+ defaultGraph = DataFactory.defaultGraph();
199
+ blanks = 0;
200
+ constructor(baseIRI, documentLoader) {
201
+ this.baseIRI = baseIRI;
202
+ this.documentLoader = documentLoader;
203
+ }
204
+ read(document) {
205
+ const initial = {
206
+ terms: new Map(),
207
+ base: this.baseIRI,
208
+ originalBase: this.baseIRI,
209
+ vocab: undefined,
210
+ language: undefined,
211
+ direction: undefined,
212
+ previous: undefined,
213
+ };
214
+ let expanded = this.expand(initial, null, document, this.baseIRI, false, 0);
215
+ if (isMap(expanded) && Object.keys(expanded).length === 1 && has(expanded, '@graph')) {
216
+ expanded = expanded['@graph'] ?? null;
217
+ }
218
+ for (const node of arrayify(expanded)) {
219
+ if (isMap(node))
220
+ this.emitNode(node, this.defaultGraph);
221
+ }
222
+ }
223
+ // ─── Context processing (§4.1) ──────────────────────────────────────────
224
+ processContext(active, localContext, baseUrl, remote, overrideProtected, propagateDefault, validateScoped) {
225
+ let result = { ...active, terms: new Map(active.terms) };
226
+ let propagate = propagateDefault;
227
+ if (isMap(localContext) && has(localContext, '@propagate')) {
228
+ const value = localContext['@propagate'];
229
+ if (typeof value !== 'boolean')
230
+ throw fail('invalid @propagate value', value ?? null);
231
+ propagate = value;
232
+ }
233
+ if (!propagate && result.previous === undefined)
234
+ result.previous = active;
235
+ for (const context of arrayify(localContext)) {
236
+ if (context === null) {
237
+ if (!overrideProtected && [...result.terms.values()].some((term) => term.protected)) {
238
+ throw fail('invalid context nullification', 'a context with protected terms');
239
+ }
240
+ const previous = result;
241
+ result = {
242
+ terms: new Map(),
243
+ base: active.originalBase,
244
+ originalBase: active.originalBase,
245
+ vocab: undefined,
246
+ language: undefined,
247
+ direction: undefined,
248
+ previous: propagate ? undefined : previous,
249
+ };
250
+ continue;
251
+ }
252
+ if (typeof context === 'string') {
253
+ const url = resolveIri(context, baseUrl);
254
+ if (!validateScoped && remote.includes(url))
255
+ continue;
256
+ if (remote.length >= MAX_REMOTE_CONTEXTS)
257
+ throw fail('context overflow', url);
258
+ const loaded = this.remoteContext(url);
259
+ result = this.processContext(result, loaded, url, [...remote, url], false, true, validateScoped);
260
+ continue;
261
+ }
262
+ if (!isMap(context))
263
+ throw fail('invalid local context', context);
264
+ let map = context;
265
+ if (has(map, '@version') && map['@version'] !== 1.1) {
266
+ throw fail('invalid @version value', map['@version'] ?? null);
267
+ }
268
+ if (has(map, '@import')) {
269
+ const value = map['@import'];
270
+ if (typeof value !== 'string')
271
+ throw fail('invalid @import value', value ?? null);
272
+ const imported = this.remoteContext(resolveIri(value, baseUrl));
273
+ if (!isMap(imported))
274
+ throw fail('invalid remote context', value);
275
+ if (has(imported, '@import'))
276
+ throw fail('invalid context entry', `${value} has @import`);
277
+ map = { ...imported, ...map };
278
+ }
279
+ if (has(map, '@base') && remote.length === 0) {
280
+ const value = map['@base'];
281
+ if (value === null)
282
+ result.base = undefined;
283
+ else if (typeof value === 'string') {
284
+ // Lenient where the spec errs: a relative @base without a base stays relative.
285
+ result.base =
286
+ isAbsoluteIri(value) || result.base === undefined
287
+ ? value
288
+ : resolveIri(value, result.base);
289
+ }
290
+ else
291
+ throw fail('invalid base IRI', value ?? null);
292
+ }
293
+ if (has(map, '@vocab')) {
294
+ const value = map['@vocab'];
295
+ if (value === null)
296
+ result.vocab = undefined;
297
+ else if (typeof value === 'string') {
298
+ const vocab = this.expandIri(result, value, { vocab: true, documentRelative: true });
299
+ if (vocab === null || KEYWORDS.has(vocab))
300
+ throw fail('invalid vocab mapping', value);
301
+ result.vocab = vocab;
302
+ }
303
+ else
304
+ throw fail('invalid vocab mapping', value ?? null);
305
+ }
306
+ if (has(map, '@language')) {
307
+ const value = map['@language'];
308
+ if (value === null)
309
+ result.language = undefined;
310
+ else if (typeof value === 'string')
311
+ result.language = value.toLowerCase();
312
+ else
313
+ throw fail('invalid default language', value ?? null);
314
+ }
315
+ if (has(map, '@direction')) {
316
+ const value = map['@direction'];
317
+ if (value === null)
318
+ result.direction = undefined;
319
+ else if (value === 'ltr' || value === 'rtl')
320
+ result.direction = value;
321
+ else
322
+ throw fail('invalid base direction', value ?? null);
323
+ }
324
+ if (has(map, '@protected') && typeof map['@protected'] !== 'boolean') {
325
+ throw fail('invalid @protected value', map['@protected'] ?? null);
326
+ }
327
+ const definitions = {
328
+ local: map,
329
+ defined: new Map(),
330
+ baseUrl,
331
+ overrideProtected,
332
+ remote,
333
+ validateScoped,
334
+ };
335
+ for (const term of Object.keys(map)) {
336
+ if (!CONTEXT_KEYWORDS.has(term))
337
+ this.createTermDefinition(result, term, definitions);
338
+ }
339
+ }
340
+ return result;
341
+ }
342
+ /** Context processing for property-, type- and map-scoped contexts, memoized per context. */
343
+ scopedContext(active, local, baseUrl, overrideProtected, propagate) {
344
+ let byLocal = this.scoped.get(active);
345
+ if (!byLocal) {
346
+ byLocal = new Map();
347
+ this.scoped.set(active, byLocal);
348
+ }
349
+ let byFlags = byLocal.get(local);
350
+ if (!byFlags) {
351
+ byFlags = new Map();
352
+ byLocal.set(local, byFlags);
353
+ }
354
+ const key = `${overrideProtected}|${propagate}|${baseUrl ?? ''}`;
355
+ let context = byFlags.get(key);
356
+ if (!context) {
357
+ context = this.processContext(active, local, baseUrl, [], overrideProtected, propagate, true);
358
+ byFlags.set(key, context);
359
+ }
360
+ return context;
361
+ }
362
+ /** The `@context` of a remote document, from the caller's loader (never the network). */
363
+ remoteContext(url) {
364
+ const cached = this.loaded.get(url);
365
+ if (cached !== undefined)
366
+ return cached;
367
+ if (!this.documentLoader) {
368
+ throw fail('loading remote context failed', `"${url}" was not loaded, as kgdiff never fetches documents; inline the context, or pass ParseOptions.jsonld.documentLoader`);
369
+ }
370
+ let document;
371
+ try {
372
+ document = this.documentLoader(url);
373
+ }
374
+ catch (error) {
375
+ throw fail('loading remote context failed', `"${url}": ${error instanceof Error ? error.message : String(error)}`);
376
+ }
377
+ if (isThenable(document)) {
378
+ throw fail('loading remote context failed', `documentLoader returned a Promise for "${url}"; it must return the document itself (load contexts beforehand)`);
379
+ }
380
+ if (typeof document === 'string') {
381
+ try {
382
+ document = JSON.parse(document);
383
+ }
384
+ catch {
385
+ throw fail('loading remote context failed', `"${url}" is not JSON`);
386
+ }
387
+ }
388
+ if (document === undefined || document === null) {
389
+ throw fail('loading remote context failed', `documentLoader has no document for "${url}"`);
390
+ }
391
+ if (!isMap(document) || !has(document, '@context')) {
392
+ throw fail('invalid remote context', url);
393
+ }
394
+ const context = document['@context'] ?? null;
395
+ this.loaded.set(url, context);
396
+ return context;
397
+ }
398
+ // ─── Term definitions (§4.2) ────────────────────────────────────────────
399
+ createTermDefinition(active, term, scope) {
400
+ const { local, defined } = scope;
401
+ const state = defined.get(term);
402
+ if (state === true)
403
+ return;
404
+ if (state === false)
405
+ throw fail('cyclic IRI mapping', term);
406
+ if (term === '')
407
+ throw fail('invalid term definition', 'an empty term');
408
+ defined.set(term, false);
409
+ const raw = local[term] ?? null;
410
+ if (term === '@type' && isMap(raw)) {
411
+ const keys = Object.keys(raw);
412
+ const valid = keys.length > 0 && keys.every((key) => key === '@container' || key === '@protected');
413
+ if (!valid || (has(raw, '@container') && raw['@container'] !== '@set')) {
414
+ throw fail('keyword redefinition', term);
415
+ }
416
+ }
417
+ else if (KEYWORDS.has(term))
418
+ throw fail('keyword redefinition', term);
419
+ else if (KEYWORD_FORM.test(term)) {
420
+ defined.set(term, true);
421
+ return;
422
+ }
423
+ const previous = active.terms.get(term);
424
+ active.terms.delete(term);
425
+ let value;
426
+ let simpleTerm = false;
427
+ if (raw === null)
428
+ value = { '@id': null };
429
+ else if (typeof raw === 'string') {
430
+ value = { '@id': raw };
431
+ simpleTerm = true;
432
+ }
433
+ else if (isMap(raw))
434
+ value = raw;
435
+ else
436
+ throw fail('invalid term definition', term);
437
+ const protectedDefault = local['@protected'] === true;
438
+ const definition = {
439
+ iri: null,
440
+ prefix: false,
441
+ protected: protectedDefault,
442
+ reverse: false,
443
+ container: [],
444
+ };
445
+ if (has(value, '@protected')) {
446
+ const flag = value['@protected'];
447
+ if (typeof flag !== 'boolean')
448
+ throw fail('invalid @protected value', term);
449
+ definition.protected = flag;
450
+ }
451
+ if (has(value, '@type')) {
452
+ const type = value['@type'];
453
+ if (typeof type !== 'string')
454
+ throw fail('invalid type mapping', term);
455
+ const expanded = this.expandIri(active, type, { vocab: true, definitions: scope });
456
+ const keywordType = expanded === '@id' || expanded === '@json' || expanded === '@none' || expanded === '@vocab';
457
+ if (expanded === null ||
458
+ (!keywordType && (KEYWORDS.has(expanded) || !isAbsoluteIri(expanded)))) {
459
+ throw fail('invalid type mapping', term);
460
+ }
461
+ definition.type = expanded;
462
+ }
463
+ if (has(value, '@reverse')) {
464
+ if (has(value, '@id') || has(value, '@nest'))
465
+ throw fail('invalid reverse property', term);
466
+ const reverse = value['@reverse'];
467
+ if (typeof reverse !== 'string')
468
+ throw fail('invalid IRI mapping', term);
469
+ if (KEYWORD_FORM.test(reverse) && !KEYWORDS.has(reverse)) {
470
+ defined.set(term, true);
471
+ return;
472
+ }
473
+ const iri = this.expandIri(active, reverse, { vocab: true, definitions: scope });
474
+ if (iri === null || !iri.includes(':'))
475
+ throw fail('invalid IRI mapping', term);
476
+ definition.iri = iri;
477
+ if (has(value, '@container')) {
478
+ const container = value['@container'];
479
+ if (container !== null && container !== '@set' && container !== '@index') {
480
+ throw fail('invalid reverse property', term);
481
+ }
482
+ definition.container = container === null ? [] : [container];
483
+ }
484
+ definition.reverse = true;
485
+ active.terms.set(term, definition);
486
+ defined.set(term, true);
487
+ return;
488
+ }
489
+ if (has(value, '@id') && value['@id'] !== term) {
490
+ const id = value['@id'];
491
+ if (id !== null) {
492
+ if (typeof id !== 'string')
493
+ throw fail('invalid IRI mapping', term);
494
+ if (KEYWORD_FORM.test(id) && !KEYWORDS.has(id)) {
495
+ defined.set(term, true);
496
+ return;
497
+ }
498
+ const iri = this.expandIri(active, id, { vocab: true, definitions: scope });
499
+ if (iri === null || (!KEYWORDS.has(iri) && !iri.includes(':'))) {
500
+ throw fail('invalid IRI mapping', term);
501
+ }
502
+ if (iri === '@context')
503
+ throw fail('invalid keyword alias', term);
504
+ definition.iri = iri;
505
+ const colon = term.indexOf(':', 1);
506
+ if ((colon > 0 && colon < term.length - 1) || term.includes('/')) {
507
+ defined.set(term, true);
508
+ const expandedTerm = this.expandIri(active, term, { vocab: true, definitions: scope });
509
+ if (expandedTerm !== iri)
510
+ throw fail('invalid IRI mapping', `${term} does not expand to its @id`);
511
+ }
512
+ if (!term.includes(':') &&
513
+ !term.includes('/') &&
514
+ simpleTerm &&
515
+ (GEN_DELIM_END.test(iri) || iri.startsWith('_:'))) {
516
+ definition.prefix = true;
517
+ }
518
+ }
519
+ }
520
+ else if (term.indexOf(':', 1) > 0) {
521
+ const colon = term.indexOf(':', 1);
522
+ const prefix = term.slice(0, colon);
523
+ if (has(local, prefix))
524
+ this.createTermDefinition(active, prefix, scope);
525
+ const prefixIri = active.terms.get(prefix)?.iri;
526
+ definition.iri =
527
+ prefixIri !== undefined && prefixIri !== null ? prefixIri + term.slice(colon + 1) : term;
528
+ }
529
+ else if (term.includes('/')) {
530
+ const iri = this.expandIri(active, term, { vocab: true });
531
+ if (iri === null || !isAbsoluteIri(iri))
532
+ throw fail('invalid IRI mapping', term);
533
+ definition.iri = iri;
534
+ }
535
+ else if (term === '@type') {
536
+ definition.iri = '@type';
537
+ }
538
+ else if (active.vocab !== undefined) {
539
+ definition.iri = active.vocab + term;
540
+ }
541
+ else {
542
+ throw fail('invalid IRI mapping', `${term} (no @id and no @vocab)`);
543
+ }
544
+ if (has(value, '@container')) {
545
+ const raw = value['@container'];
546
+ const container = Array.isArray(raw) ? raw : [raw];
547
+ if (!container.every((item) => typeof item === 'string') ||
548
+ !validContainer(container)) {
549
+ throw fail('invalid container mapping', term);
550
+ }
551
+ definition.container = [...container].sort(compareStrings);
552
+ if (container.includes('@type')) {
553
+ if (definition.type === undefined)
554
+ definition.type = '@id';
555
+ else if (definition.type !== '@id' && definition.type !== '@vocab') {
556
+ throw fail('invalid type mapping', term);
557
+ }
558
+ }
559
+ }
560
+ if (has(value, '@index')) {
561
+ const index = value['@index'];
562
+ if (!definition.container.includes('@index') || typeof index !== 'string') {
563
+ throw fail('invalid term definition', term);
564
+ }
565
+ const expanded = this.expandIri(active, index, { vocab: true });
566
+ if (expanded === null || KEYWORDS.has(expanded) || !isAbsoluteIri(expanded)) {
567
+ throw fail('invalid term definition', term);
568
+ }
569
+ definition.index = index;
570
+ }
571
+ if (has(value, '@context')) {
572
+ const context = value['@context'] ?? null;
573
+ try {
574
+ this.processContext(active, context, scope.baseUrl, scope.remote, true, true, false);
575
+ }
576
+ catch (error) {
577
+ if (error instanceof JsonLdError)
578
+ throw fail('invalid scoped context', term);
579
+ throw error;
580
+ }
581
+ definition.context = context;
582
+ definition.baseUrl = scope.baseUrl;
583
+ }
584
+ if (has(value, '@language') && !has(value, '@type')) {
585
+ const language = value['@language'];
586
+ if (language !== null && typeof language !== 'string') {
587
+ throw fail('invalid language mapping', term);
588
+ }
589
+ definition.language = language === null ? null : language.toLowerCase();
590
+ }
591
+ if (has(value, '@direction') && !has(value, '@type')) {
592
+ const direction = value['@direction'];
593
+ if (direction !== null && direction !== 'ltr' && direction !== 'rtl') {
594
+ throw fail('invalid base direction', term);
595
+ }
596
+ definition.direction = direction;
597
+ }
598
+ if (has(value, '@nest')) {
599
+ const nest = value['@nest'];
600
+ if (typeof nest !== 'string' || (KEYWORDS.has(nest) && nest !== '@nest')) {
601
+ throw fail('invalid @nest value', term);
602
+ }
603
+ definition.nest = nest;
604
+ }
605
+ if (has(value, '@prefix')) {
606
+ const prefix = value['@prefix'];
607
+ if (term.includes(':') || term.includes('/'))
608
+ throw fail('invalid term definition', term);
609
+ if (typeof prefix !== 'boolean')
610
+ throw fail('invalid @prefix value', term);
611
+ if (prefix && definition.iri !== null && KEYWORDS.has(definition.iri)) {
612
+ throw fail('invalid term definition', term);
613
+ }
614
+ definition.prefix = prefix;
615
+ }
616
+ for (const key of Object.keys(value)) {
617
+ if (!TERM_DEFINITION_KEYS.has(key))
618
+ throw fail('invalid term definition', `${term}: ${key}`);
619
+ }
620
+ if (!scope.overrideProtected && previous?.protected) {
621
+ if (!sameDefinition(previous, definition))
622
+ throw fail('protected term redefinition', term);
623
+ active.terms.set(term, previous);
624
+ }
625
+ else
626
+ active.terms.set(term, definition);
627
+ defined.set(term, true);
628
+ }
629
+ // ─── IRI expansion (§5.2) ───────────────────────────────────────────────
630
+ expandIri(active, value, options) {
631
+ if (KEYWORDS.has(value))
632
+ return value;
633
+ if (KEYWORD_FORM.test(value))
634
+ return null;
635
+ const scope = options.definitions;
636
+ if (scope && has(scope.local, value) && scope.defined.get(value) !== true) {
637
+ this.createTermDefinition(active, value, scope);
638
+ }
639
+ const definition = active.terms.get(value);
640
+ if (definition?.iri != null && KEYWORDS.has(definition.iri))
641
+ return definition.iri;
642
+ if (options.vocab && definition !== undefined)
643
+ return definition.iri;
644
+ const colon = value.indexOf(':', 1);
645
+ if (colon > 0) {
646
+ const prefix = value.slice(0, colon);
647
+ const suffix = value.slice(colon + 1);
648
+ if (prefix === '_' || suffix.startsWith('//'))
649
+ return value;
650
+ if (scope && has(scope.local, prefix) && scope.defined.get(prefix) !== true) {
651
+ this.createTermDefinition(active, prefix, scope);
652
+ }
653
+ const prefixDefinition = active.terms.get(prefix);
654
+ if (prefixDefinition?.iri != null && prefixDefinition.prefix) {
655
+ return prefixDefinition.iri + suffix;
656
+ }
657
+ if (isAbsoluteIri(value))
658
+ return value;
659
+ }
660
+ if (options.vocab && active.vocab !== undefined)
661
+ return active.vocab + value;
662
+ if (options.documentRelative)
663
+ return resolveIri(value, active.base);
664
+ return value;
665
+ }
666
+ // ─── Expansion (§5.1) ───────────────────────────────────────────────────
667
+ expand(active, activeProperty, element, baseUrl, fromMap, depth) {
668
+ if (depth > MAX_JSON_LD_NESTING) {
669
+ throw invalidInput(`JSON-LD nested deeper than ${MAX_JSON_LD_NESTING} levels is not supported.`, {
670
+ format: 'jsonld',
671
+ });
672
+ }
673
+ if (element === null)
674
+ return null;
675
+ const definition = activeProperty === null ? undefined : active.terms.get(activeProperty);
676
+ if (isScalar(element)) {
677
+ if (activeProperty === null || activeProperty === '@graph')
678
+ return null;
679
+ const context = definition?.context !== undefined
680
+ ? this.scopedContext(active, definition.context, definition.baseUrl, false, true)
681
+ : active;
682
+ return this.expandValue(context, activeProperty, element);
683
+ }
684
+ if (Array.isArray(element)) {
685
+ const result = [];
686
+ const list = definition?.container.includes('@list') ?? false;
687
+ for (const item of element) {
688
+ let expanded = this.expand(active, activeProperty, item, baseUrl, fromMap, depth + 1);
689
+ if (list && Array.isArray(expanded))
690
+ expanded = { '@list': expanded };
691
+ if (Array.isArray(expanded))
692
+ for (const value of expanded)
693
+ result.push(value);
694
+ else if (expanded !== null)
695
+ result.push(expanded);
696
+ }
697
+ return result;
698
+ }
699
+ return this.expandObject(active, activeProperty, definition, element, baseUrl, fromMap, depth);
700
+ }
701
+ expandObject(active, activeProperty, definition, element, baseUrl, fromMap, depth) {
702
+ let context = active;
703
+ const keys = Object.keys(element);
704
+ if (context.previous !== undefined && !fromMap) {
705
+ const keeps = keys.some((key) => {
706
+ const expanded = this.expandIri(context, key, { vocab: true });
707
+ return expanded === '@value' || (expanded === '@id' && keys.length === 1);
708
+ });
709
+ if (!keeps)
710
+ context = context.previous;
711
+ }
712
+ if (definition?.context !== undefined) {
713
+ context = this.scopedContext(context, definition.context, definition.baseUrl, true, true);
714
+ }
715
+ if (has(element, '@context')) {
716
+ context = this.processContext(context, element['@context'] ?? null, baseUrl, [], false, true, true);
717
+ if (activeProperty === null)
718
+ this.collectPrefixes(context);
719
+ }
720
+ const typeScoped = context;
721
+ let inputType;
722
+ for (const key of [...keys].sort(compareStrings)) {
723
+ if (this.expandIri(context, key, { vocab: true }) !== '@type')
724
+ continue;
725
+ const value = element[key] ?? null;
726
+ const types = arrayify(value)
727
+ .filter((type) => typeof type === 'string')
728
+ .sort(compareStrings);
729
+ for (const type of types) {
730
+ const typeDefinition = typeScoped.terms.get(type);
731
+ if (typeDefinition?.context !== undefined) {
732
+ context = this.scopedContext(context, typeDefinition.context, typeDefinition.baseUrl, false, false);
733
+ }
734
+ }
735
+ if (inputType === undefined) {
736
+ const last = arrayify(value).at(-1);
737
+ if (typeof last === 'string')
738
+ inputType = this.expandIri(context, last, { vocab: true }) ?? undefined;
739
+ }
740
+ }
741
+ const result = {};
742
+ this.expandEntries(context, typeScoped, activeProperty, element, baseUrl, result, inputType, depth);
743
+ let output = result;
744
+ if (has(result, '@value')) {
745
+ const resultKeys = Object.keys(result);
746
+ if (resultKeys.some((key) => !VALUE_KEYS.has(key)) ||
747
+ (has(result, '@type') && (has(result, '@language') || has(result, '@direction')))) {
748
+ throw fail('invalid value object', resultKeys);
749
+ }
750
+ const type = result['@type'];
751
+ const value = result['@value'];
752
+ if (type === '@json') {
753
+ // Any JSON value.
754
+ }
755
+ else if (value === null)
756
+ return null;
757
+ else if (typeof value !== 'string' && has(result, '@language')) {
758
+ throw fail('invalid language-tagged value', value ?? null);
759
+ }
760
+ else if (has(result, '@type') &&
761
+ (typeof type !== 'string' || !isAbsoluteIri(type) || !wellFormedIri(type))) {
762
+ throw fail('invalid typed value', type ?? null);
763
+ }
764
+ }
765
+ else if (has(result, '@type') && !Array.isArray(result['@type'])) {
766
+ result['@type'] = [result['@type'] ?? null];
767
+ }
768
+ else if (has(result, '@set') || has(result, '@list')) {
769
+ const resultKeys = Object.keys(result);
770
+ if (resultKeys.length > 2 || (resultKeys.length === 2 && !has(result, '@index'))) {
771
+ throw fail('invalid set or list object', resultKeys);
772
+ }
773
+ if (has(result, '@set'))
774
+ output = result['@set'] ?? null;
775
+ }
776
+ if (isMap(output) && Object.keys(output).length === 1 && has(output, '@language'))
777
+ return null;
778
+ if ((activeProperty === null || activeProperty === '@graph') && isMap(output)) {
779
+ const outputKeys = Object.keys(output);
780
+ if (outputKeys.length === 0 || has(output, '@value') || has(output, '@list'))
781
+ return null;
782
+ if (outputKeys.length === 1 && has(output, '@id'))
783
+ return null;
784
+ }
785
+ return output;
786
+ }
787
+ /** Step 13 (and 14, nested properties) of the expansion algorithm. */
788
+ expandEntries(context, typeScoped, activeProperty, element, baseUrl, result, inputType, depth) {
789
+ const nests = [];
790
+ // Keys in code unit order, as jsonld.js does: the result never depends on key order.
791
+ for (const key of Object.keys(element).sort(compareStrings)) {
792
+ if (key === '@context')
793
+ continue;
794
+ const value = element[key] ?? null;
795
+ const property = this.expandIri(context, key, { vocab: true });
796
+ if (property === null || (!property.includes(':') && !KEYWORDS.has(property))) {
797
+ this.unmapped.add(key);
798
+ continue;
799
+ }
800
+ if (KEYWORDS.has(property)) {
801
+ if (activeProperty === '@reverse')
802
+ throw fail('invalid reverse property map', key);
803
+ if (has(result, property) && property !== '@included' && property !== '@type') {
804
+ throw fail('colliding keywords', property);
805
+ }
806
+ let expanded;
807
+ switch (property) {
808
+ case '@id':
809
+ if (typeof value !== 'string')
810
+ throw fail('invalid @id value', value);
811
+ expanded = this.expandIri(context, value, { documentRelative: true });
812
+ break;
813
+ case '@type': {
814
+ const types = arrayify(value);
815
+ if (!types.every((type) => typeof type === 'string')) {
816
+ throw fail('invalid type value', value);
817
+ }
818
+ const iris = [];
819
+ for (const type of types) {
820
+ const iri = this.expandIri(typeScoped, type, { vocab: true, documentRelative: true });
821
+ if (iri !== null)
822
+ iris.push(iri);
823
+ }
824
+ expanded = has(result, '@type')
825
+ ? [...arrayify(result['@type'] ?? null), ...iris]
826
+ : iris;
827
+ if (!Array.isArray(value) && !has(result, '@type') && iris.length === 1) {
828
+ expanded = iris[0] ?? null;
829
+ }
830
+ break;
831
+ }
832
+ case '@graph':
833
+ expanded = arrayify(this.expand(context, '@graph', value, baseUrl, false, depth + 1));
834
+ break;
835
+ case '@included': {
836
+ const included = arrayify(this.expand(context, null, value, baseUrl, false, depth + 1));
837
+ if (!included.every(isNodeObject))
838
+ throw fail('invalid @included value', value);
839
+ expanded = has(result, '@included')
840
+ ? [...arrayify(result['@included'] ?? null), ...included]
841
+ : included;
842
+ break;
843
+ }
844
+ case '@value':
845
+ if (inputType !== '@json' && value !== null && !isScalar(value)) {
846
+ throw fail('invalid value object value', value);
847
+ }
848
+ expanded = value;
849
+ break;
850
+ case '@language':
851
+ if (typeof value !== 'string')
852
+ throw fail('invalid language-tagged string', value);
853
+ expanded = value.toLowerCase();
854
+ break;
855
+ case '@direction':
856
+ if (value !== 'ltr' && value !== 'rtl')
857
+ throw fail('invalid base direction', value);
858
+ expanded = value;
859
+ break;
860
+ case '@index':
861
+ if (typeof value !== 'string')
862
+ throw fail('invalid @index value', value);
863
+ expanded = value;
864
+ break;
865
+ case '@list':
866
+ if (activeProperty === null || activeProperty === '@graph')
867
+ continue;
868
+ expanded = arrayify(this.expand(context, activeProperty, value, baseUrl, false, depth + 1));
869
+ break;
870
+ case '@set':
871
+ expanded = this.expand(context, activeProperty, value, baseUrl, false, depth + 1);
872
+ break;
873
+ case '@reverse':
874
+ this.expandReverse(context, value, baseUrl, result, depth);
875
+ continue;
876
+ case '@nest':
877
+ nests.push(key);
878
+ continue;
879
+ default:
880
+ continue;
881
+ }
882
+ // A null @value is kept: whether @type means a datatype depends on it.
883
+ setOwn(result, property, expanded);
884
+ continue;
885
+ }
886
+ const definition = context.terms.get(key);
887
+ const container = definition?.container ?? [];
888
+ let expanded;
889
+ if (definition?.type === '@json')
890
+ expanded = { '@value': value, '@type': '@json' };
891
+ else if (container.includes('@language') && isMap(value)) {
892
+ expanded = this.expandLanguageMap(context, definition, value);
893
+ }
894
+ else if (definition !== undefined &&
895
+ (container.includes('@index') ||
896
+ container.includes('@type') ||
897
+ container.includes('@id')) &&
898
+ isMap(value)) {
899
+ expanded = this.expandIndexMap(context, key, definition, value, baseUrl, depth);
900
+ }
901
+ else {
902
+ expanded = this.expand(context, key, value, baseUrl, false, depth + 1);
903
+ }
904
+ if (expanded === null)
905
+ continue;
906
+ if (container.includes('@list') && !isListObject(expanded)) {
907
+ expanded = { '@list': arrayify(expanded) };
908
+ }
909
+ if (container.includes('@graph') &&
910
+ !container.includes('@id') &&
911
+ !container.includes('@index')) {
912
+ expanded = arrayify(expanded).map((item) => ({ '@graph': arrayify(item) }));
913
+ }
914
+ if (definition?.reverse) {
915
+ const reverseMap = this.reverseMapOf(result);
916
+ for (const item of arrayify(expanded)) {
917
+ if (isValueObject(item) || isListObject(item)) {
918
+ throw fail('invalid reverse property value', key);
919
+ }
920
+ addValues(reverseMap, property, [item]);
921
+ }
922
+ }
923
+ else
924
+ addValues(result, property, arrayify(expanded));
925
+ }
926
+ // Nested properties belong to the node, read with the nesting term's scoped context.
927
+ for (const nest of nests.sort(compareStrings)) {
928
+ const nestDefinition = context.terms.get(nest);
929
+ const nestContext = nestDefinition?.context !== undefined
930
+ ? this.scopedContext(context, nestDefinition.context, nestDefinition.baseUrl, true, true)
931
+ : context;
932
+ for (const nested of arrayify(element[nest] ?? null)) {
933
+ if (!isMap(nested) ||
934
+ Object.keys(nested).some((key) => this.expandIri(nestContext, key, { vocab: true }) === '@value')) {
935
+ throw fail('invalid @nest value', nested);
936
+ }
937
+ this.expandEntries(nestContext, typeScoped, nest, nested, baseUrl, result, inputType, depth + 1);
938
+ }
939
+ }
940
+ }
941
+ expandReverse(context, value, baseUrl, result, depth) {
942
+ if (!isMap(value))
943
+ throw fail('invalid @reverse value', value);
944
+ const expanded = this.expand(context, '@reverse', value, baseUrl, false, depth + 1);
945
+ if (!isMap(expanded))
946
+ return;
947
+ const doubled = expanded['@reverse'];
948
+ if (isMap(doubled)) {
949
+ for (const property of Object.keys(doubled)) {
950
+ addValues(result, property, arrayify(doubled[property] ?? null));
951
+ }
952
+ }
953
+ for (const property of Object.keys(expanded)) {
954
+ if (property === '@reverse')
955
+ continue;
956
+ const reverseMap = this.reverseMapOf(result);
957
+ for (const item of arrayify(expanded[property] ?? null)) {
958
+ if (isValueObject(item) || isListObject(item)) {
959
+ throw fail('invalid reverse property value', property);
960
+ }
961
+ addValues(reverseMap, property, [item]);
962
+ }
963
+ }
964
+ }
965
+ reverseMapOf(result) {
966
+ const existing = result['@reverse'];
967
+ if (isMap(existing))
968
+ return existing;
969
+ const map = {};
970
+ result['@reverse'] = map;
971
+ return map;
972
+ }
973
+ expandLanguageMap(context, definition, value) {
974
+ const out = [];
975
+ const direction = definition?.direction !== undefined ? definition.direction : context.direction;
976
+ for (const language of Object.keys(value).sort(compareStrings)) {
977
+ const none = this.expandIri(context, language, { vocab: true }) === '@none';
978
+ for (const item of arrayify(value[language] ?? null)) {
979
+ if (item === null)
980
+ continue;
981
+ if (typeof item !== 'string')
982
+ throw fail('invalid language map value', item);
983
+ const expanded = { '@value': item };
984
+ if (!none)
985
+ expanded['@language'] = language.toLowerCase();
986
+ if (direction)
987
+ expanded['@direction'] = direction;
988
+ out.push(expanded);
989
+ }
990
+ }
991
+ return out;
992
+ }
993
+ /** Index, id and type maps (§5.1.2 step 13.8). */
994
+ expandIndexMap(context, key, definition, value, baseUrl, depth) {
995
+ const { container } = definition;
996
+ const out = [];
997
+ const indexKey = definition.index ?? '@index';
998
+ const byIdOrType = container.includes('@id') || container.includes('@type');
999
+ for (const index of Object.keys(value).sort(compareStrings)) {
1000
+ let mapContext = byIdOrType ? (context.previous ?? context) : context;
1001
+ const indexDefinition = mapContext.terms.get(index);
1002
+ if (container.includes('@type') && indexDefinition?.context !== undefined) {
1003
+ mapContext = this.scopedContext(mapContext, indexDefinition.context, indexDefinition.baseUrl, false, true);
1004
+ }
1005
+ const expandedIndex = this.expandIri(context, index, { vocab: true });
1006
+ const items = arrayify(this.expand(mapContext, key, arrayify(value[index] ?? null), baseUrl, true, depth + 1));
1007
+ for (let item of items) {
1008
+ if (container.includes('@graph') && !isGraphObject(item))
1009
+ item = { '@graph': arrayify(item) };
1010
+ if (!isMap(item))
1011
+ continue;
1012
+ if (container.includes('@index') && indexKey !== '@index' && expandedIndex !== '@none') {
1013
+ if (isValueObject(item))
1014
+ throw fail('invalid value object', item);
1015
+ const reExpanded = this.expandValue(context, indexKey, index);
1016
+ const indexProperty = this.expandIri(context, indexKey, { vocab: true });
1017
+ if (indexProperty !== null) {
1018
+ setOwn(item, indexProperty, [reExpanded, ...arrayify(item[indexProperty] ?? [])]);
1019
+ }
1020
+ }
1021
+ else if (container.includes('@index') &&
1022
+ !has(item, '@index') &&
1023
+ expandedIndex !== '@none') {
1024
+ item['@index'] = index;
1025
+ }
1026
+ else if (container.includes('@id') && !has(item, '@id') && expandedIndex !== '@none') {
1027
+ item['@id'] = this.expandIri(context, index, { documentRelative: true });
1028
+ }
1029
+ else if (container.includes('@type') && expandedIndex !== '@none') {
1030
+ item['@type'] = [expandedIndex, ...arrayify(item['@type'] ?? [])];
1031
+ }
1032
+ out.push(item);
1033
+ }
1034
+ }
1035
+ return out;
1036
+ }
1037
+ /** Value expansion (§5.3). */
1038
+ expandValue(context, activeProperty, value) {
1039
+ const definition = context.terms.get(activeProperty);
1040
+ const type = definition?.type;
1041
+ if (type === '@id' && typeof value === 'string') {
1042
+ return { '@id': this.expandIri(context, value, { documentRelative: true }) };
1043
+ }
1044
+ if (type === '@vocab' && typeof value === 'string') {
1045
+ return { '@id': this.expandIri(context, value, { vocab: true, documentRelative: true }) };
1046
+ }
1047
+ const result = { '@value': value };
1048
+ if (type !== undefined && type !== '@id' && type !== '@vocab' && type !== '@none') {
1049
+ result['@type'] = type;
1050
+ }
1051
+ else if (typeof value === 'string') {
1052
+ const language = definition?.language !== undefined ? definition.language : context.language;
1053
+ const direction = definition?.direction !== undefined ? definition.direction : context.direction;
1054
+ if (language !== null && language !== undefined)
1055
+ result['@language'] = language;
1056
+ if (direction !== null && direction !== undefined)
1057
+ result['@direction'] = direction;
1058
+ }
1059
+ return result;
1060
+ }
1061
+ /** Prefix-capable terms of a top-level context, reported for display like `@prefix`. */
1062
+ collectPrefixes(context) {
1063
+ for (const [term, definition] of context.terms) {
1064
+ const iri = definition.iri;
1065
+ if (!definition.prefix || iri === null || KEYWORDS.has(iri) || iri.startsWith('_:'))
1066
+ continue;
1067
+ if (!Object.hasOwn(this.prefixes, term))
1068
+ setOwn(this.prefixes, term, iri);
1069
+ }
1070
+ if (context.vocab !== undefined && !Object.hasOwn(this.prefixes, '')) {
1071
+ setOwn(this.prefixes, '', context.vocab);
1072
+ }
1073
+ }
1074
+ // ─── Deserialization to RDF (§8) ────────────────────────────────────────
1075
+ /**
1076
+ * Emits the triples of a node object (and, recursively, of every node it embeds) and returns
1077
+ * its subject, or `undefined` when its id is not a usable IRI.
1078
+ */
1079
+ emitNode(node, graph) {
1080
+ const subject = this.resourceTerm(has(node, '@id') ? (node['@id'] ?? null) : undefined);
1081
+ for (const type of arrayify(node['@type'] ?? [])) {
1082
+ const object = typeof type === 'string' ? this.resourceTerm(type) : undefined;
1083
+ if (subject && object)
1084
+ this.emit(subject, this.iri(RDF_TYPE), object, graph);
1085
+ }
1086
+ for (const property of Object.keys(node)) {
1087
+ if (KEYWORDS.has(property))
1088
+ continue;
1089
+ const predicate = this.predicateTerm(property);
1090
+ for (const item of arrayify(node[property] ?? null)) {
1091
+ const object = this.objectTerm(item, graph);
1092
+ if (subject && predicate && object)
1093
+ this.emit(subject, predicate, object, graph);
1094
+ }
1095
+ }
1096
+ const reverse = node['@reverse'];
1097
+ if (isMap(reverse)) {
1098
+ for (const property of Object.keys(reverse)) {
1099
+ const predicate = this.predicateTerm(property);
1100
+ for (const item of arrayify(reverse[property] ?? null)) {
1101
+ const object = isMap(item) ? this.emitNode(item, graph) : undefined;
1102
+ if (subject && predicate && object)
1103
+ this.emit(object, predicate, subject, graph);
1104
+ }
1105
+ }
1106
+ }
1107
+ // A graph whose name RDF cannot represent is dropped with its triples, as the spec says.
1108
+ if (has(node, '@graph') && subject) {
1109
+ for (const child of arrayify(node['@graph'] ?? null)) {
1110
+ if (isMap(child))
1111
+ this.emitNode(child, subject);
1112
+ }
1113
+ }
1114
+ for (const child of arrayify(node['@included'] ?? [])) {
1115
+ if (isMap(child))
1116
+ this.emitNode(child, graph);
1117
+ }
1118
+ return subject;
1119
+ }
1120
+ /** Object to RDF (§8.2): a resource, a literal, or a list head. */
1121
+ objectTerm(item, graph) {
1122
+ if (!isMap(item))
1123
+ return undefined;
1124
+ if (has(item, '@value'))
1125
+ return this.literalTerm(item);
1126
+ if (has(item, '@list'))
1127
+ return this.listTerm(arrayify(item['@list'] ?? null), graph);
1128
+ return this.emitNode(item, graph);
1129
+ }
1130
+ /** List to RDF (§8.3): cells are created even for items that produce no object. */
1131
+ listTerm(items, graph) {
1132
+ if (items.length === 0)
1133
+ return this.iri(RDF_NIL);
1134
+ const cells = items.map(() => this.blank());
1135
+ items.forEach((item, i) => {
1136
+ const cell = cells[i];
1137
+ const object = this.objectTerm(item, graph);
1138
+ if (object)
1139
+ this.emit(cell, this.iri(RDF_FIRST), object, graph);
1140
+ this.emit(cell, this.iri(RDF_REST), cells[i + 1] ?? this.iri(RDF_NIL), graph);
1141
+ });
1142
+ return cells[0];
1143
+ }
1144
+ literalTerm(item) {
1145
+ const value = item['@value'] ?? null;
1146
+ let datatype = typeof item['@type'] === 'string' ? item['@type'] : undefined;
1147
+ const language = typeof item['@language'] === 'string' ? item['@language'] : undefined;
1148
+ if (datatype !== undefined && datatype !== '@json' && !wellFormedIri(datatype))
1149
+ return undefined;
1150
+ if (language !== undefined && !LANGUAGE_TAG.test(language))
1151
+ return undefined;
1152
+ let lexical;
1153
+ if (datatype === '@json') {
1154
+ lexical = canonicalJson(value);
1155
+ datatype = RDF_JSON;
1156
+ }
1157
+ else if (typeof value === 'boolean') {
1158
+ lexical = String(value);
1159
+ datatype ??= XSD_BOOLEAN;
1160
+ }
1161
+ else if (typeof value === 'number' &&
1162
+ (!Number.isInteger(value) || Math.abs(value) >= 1e21 || datatype === XSD_DOUBLE)) {
1163
+ lexical = canonicalDouble(value);
1164
+ datatype ??= XSD_DOUBLE;
1165
+ }
1166
+ else if (typeof value === 'number') {
1167
+ lexical = value.toFixed(0);
1168
+ datatype ??= XSD_INTEGER;
1169
+ }
1170
+ else if (typeof value === 'string') {
1171
+ lexical = value;
1172
+ }
1173
+ else
1174
+ return undefined;
1175
+ if (language !== undefined && (datatype === undefined || datatype === RDF_LANG_STRING)) {
1176
+ return DataFactory.literal(lexical, language);
1177
+ }
1178
+ return datatype === undefined
1179
+ ? DataFactory.literal(lexical)
1180
+ : DataFactory.literal(lexical, this.iri(datatype));
1181
+ }
1182
+ /** A node id or type: blank node id, IRI, or nothing when RDF cannot represent it. */
1183
+ resourceTerm(id) {
1184
+ if (id === undefined)
1185
+ return this.blank();
1186
+ if (typeof id !== 'string')
1187
+ return undefined;
1188
+ if (id.startsWith('_:'))
1189
+ return new BlankNode(`j${id.slice(2)}`);
1190
+ return wellFormedIri(id) ? this.iri(id) : undefined;
1191
+ }
1192
+ /** Blank node predicates would be generalized RDF, which is not produced. */
1193
+ predicateTerm(property) {
1194
+ return !property.startsWith('_:') && wellFormedIri(property) ? this.iri(property) : undefined;
1195
+ }
1196
+ blank() {
1197
+ return new BlankNode(`g${this.blanks++}`);
1198
+ }
1199
+ iri(value) {
1200
+ let node = this.iris.get(value);
1201
+ if (!node) {
1202
+ node = new NamedNode(value);
1203
+ this.iris.set(value, node);
1204
+ }
1205
+ return node;
1206
+ }
1207
+ emit(subject, predicate, object, graph) {
1208
+ this.quads.push(DataFactory.quad(subject, predicate, object, graph));
1209
+ }
1210
+ }
1211
+ /** No characters RDF forbids, and at most one `#` (a fragment cannot contain another). */
1212
+ function wellFormedIri(iri) {
1213
+ if (iri === '' || iri.indexOf('#') !== iri.lastIndexOf('#'))
1214
+ return false;
1215
+ for (let i = 0; i < iri.length; i++) {
1216
+ if (iri.charCodeAt(i) <= 0x20 || ILLEGAL_IRI_CHARACTERS.includes(iri.charAt(i)))
1217
+ return false;
1218
+ }
1219
+ return true;
1220
+ }
1221
+ /** Term definitions are "the same" for `@protected` purposes when all but `protected` match. */
1222
+ function sameDefinition(a, b) {
1223
+ const comparable = ({ protected: _protected, baseUrl: _baseUrl, ...rest }) => rest;
1224
+ return canonicalJson(comparable(a)) === canonicalJson(comparable(b));
1225
+ }
1226
+ /**
1227
+ * JSON Canonicalization Scheme (RFC 8785): sorted keys, ECMAScript number and string forms.
1228
+ * Absent (`undefined`) entries are left out, as `JSON.stringify` does.
1229
+ */
1230
+ function canonicalJson(value) {
1231
+ if (value === undefined)
1232
+ return 'null';
1233
+ if (Array.isArray(value))
1234
+ return `[${value.map(canonicalJson).join(',')}]`;
1235
+ if (typeof value === 'object' && value !== null) {
1236
+ const record = value;
1237
+ const keys = Object.keys(record)
1238
+ .filter((key) => record[key] !== undefined)
1239
+ .sort(compareStrings);
1240
+ return `{${keys.map((key) => `${JSON.stringify(key)}:${canonicalJson(record[key])}`).join(',')}}`;
1241
+ }
1242
+ return JSON.stringify(value);
1243
+ }
1244
+ /** Canonical xsd:double: `1.1E0`, `1.0E-4`, `-5.0E21` (shortest digits that round-trip). */
1245
+ function canonicalDouble(value) {
1246
+ const [mantissa = '0', exponent = '0'] = value.toExponential().split('e');
1247
+ return `${mantissa.includes('.') ? mantissa : `${mantissa}.0`}E${Number(exponent)}`;
1248
+ }
1249
+ //# sourceMappingURL=jsonld.js.map