diff --git a/lib/ContextParser.ts b/lib/ContextParser.ts index 64fe917..107ffcc 100644 --- a/lib/ContextParser.ts +++ b/lib/ContextParser.ts @@ -93,13 +93,14 @@ export class ContextParser { */ public idifyReverseTerms(context: IJsonLdContextNormalizedRaw): IJsonLdContextNormalizedRaw { for (const key of Object.keys(context)) { - const value: IPrefixValue = context[key]; + let value = context[key]; if (value && typeof value === 'object') { if (value['@reverse'] && !value['@id']) { if (typeof value['@reverse'] !== 'string' || Util.isValidKeyword(value['@reverse'])) { throw new ErrorCoded(`Invalid @reverse value, must be absolute IRI or blank node: '${value['@reverse']}'`, ERROR_CODES.INVALID_IRI_MAPPING); } + value = context[key] = {...value, '@id': value['@reverse']}; value['@id'] = value['@reverse']; if (Util.isPotentialKeyword(value['@reverse'])) { delete value['@reverse']; @@ -162,7 +163,7 @@ Tried mapping ${key} to ${JSON.stringify(keyValue)}`, ERROR_CODES.INVALID_KEYWOR if ('@id' in value) { // Use @id value for expansion if (id !== undefined && id !== null && typeof id === 'string') { - contextRaw[key]['@id'] = context.expandTerm(id, true); + contextRaw[key] = { ...contextRaw[key], '@id': context.expandTerm(id, true) }; changed = changed || id !== contextRaw[key]['@id']; } } else if (!Util.isPotentialKeyword(key) && canAddIdEntry) { @@ -170,7 +171,7 @@ Tried mapping ${key} to ${JSON.stringify(keyValue)}`, ERROR_CODES.INVALID_KEYWOR const newId = context.expandTerm(key, true); if (newId !== key) { // Don't set @id if expansion failed - contextRaw[key]['@id'] = newId; + contextRaw[key] = { ...contextRaw[key], '@id': newId }; changed = true; } } @@ -178,11 +179,14 @@ Tried mapping ${key} to ${JSON.stringify(keyValue)}`, ERROR_CODES.INVALID_KEYWOR && (!value['@container'] || !( value['@container'])['@type']) && canAddIdEntry) { // First check @vocab, then fallback to @base - contextRaw[key]['@type'] = context.expandTerm(type, true); - if (expandContentTypeToBase && type === contextRaw[key]['@type']) { - contextRaw[key]['@type'] = context.expandTerm(type, false); + let expandedType = context.expandTerm(type, true); + if (expandContentTypeToBase && type === expandedType) { + expandedType = context.expandTerm(type, false); + } + if (expandedType !== type) { + changed = true; + contextRaw[key] = { ...contextRaw[key], '@type': expandedType }; } - changed = changed || type !== contextRaw[key]['@type']; } } if (!changed) { @@ -209,7 +213,10 @@ Tried mapping ${key} to ${JSON.stringify(keyValue)}`, ERROR_CODES.INVALID_KEYWOR const value = context[key]; if (value && typeof value === 'object') { if (typeof value['@language'] === 'string') { - value['@language'] = value['@language'].toLowerCase(); + const lowercase = value['@language'].toLowerCase(); + if (lowercase !== value['@language']) { + context[key] = {...value, '@language': lowercase}; + } } } } @@ -226,16 +233,17 @@ Tried mapping ${key} to ${JSON.stringify(keyValue)}`, ERROR_CODES.INVALID_KEYWOR const value = context[key]; if (value && typeof value === 'object') { if (typeof value['@container'] === 'string') { - value['@container'] = { [value['@container']]: true }; + context[key] = { ...value, '@container': { [value['@container']]: true } }; } else if (Array.isArray(value['@container'])) { const newValue: {[key: string]: boolean} = {}; for (const containerValue of value['@container']) { newValue[containerValue] = true; } - value['@container'] = newValue; + context[key] = { ...value, '@container': newValue }; } } } + return context; } /** @@ -256,7 +264,7 @@ Tried mapping ${key} to ${JSON.stringify(keyValue)}`, ERROR_CODES.INVALID_KEYWOR if (value && typeof value === 'object') { if (!('@protected' in context[key])) { // Mark terms with object values as protected if they don't have an @protected: false annotation - context[key]['@protected'] = true; + context[key] = {...context[key], '@protected': true}; } } else { // Convert string-based term values to object-based values with @protected: true @@ -265,7 +273,7 @@ Tried mapping ${key} to ${JSON.stringify(keyValue)}`, ERROR_CODES.INVALID_KEYWOR '@protected': true, }; if (Util.isSimpleTermDefinitionPrefix(value, expandOptions)) { - context[key]['@prefix'] = true + context[key] = {...context[key], '@prefix': true}; } } } @@ -298,7 +306,7 @@ Tried mapping ${key} to ${JSON.stringify(keyValue)}`, ERROR_CODES.INVALID_KEYWOR // We modify this deliberately, // as we need it for the value comparison (they must be identical modulo '@protected')), // and for the fact that this new value will override the first one. - contextAfter[key]['@protected'] = true; + contextAfter[key] = {...contextAfter[key], '@protected': true}; const valueAfter = canonicalizeJson(contextAfter[key]); // Error if they are not identical @@ -607,8 +615,7 @@ must be one of ${Util.CONTAINERS.join(', ')}`, ERROR_CODES.INVALID_CONTAINER_MAP // https://w3c.github.io/json-ld-api/#h-note-10 if (this.validateContext) { try { - const parentContext = {...context}; - parentContext[key] = {...parentContext[key]}; + const parentContext = {...context, [key]: {...context[key]}}; delete parentContext[key]['@context']; await this.parse(value['@context'], { ...options, external: false, parentContext, ignoreProtection: true, ignoreRemoteScopedContexts: true, ignoreScopedContexts: true }); @@ -616,10 +623,9 @@ must be one of ${Util.CONTAINERS.join(', ')}`, ERROR_CODES.INVALID_CONTAINER_MAP throw new ErrorCoded(e.message, ERROR_CODES.INVALID_SCOPED_CONTEXT); } } - - value['@context'] = (await this.parse(value['@context'], - { ...options, external: false, minimalProcessing: true, ignoreRemoteScopedContexts: true, parentContext: context })) - .getContextRaw(); + context[key] = {...value, '@context': (await this.parse(value['@context'], + { ...options, external: false, minimalProcessing: true, ignoreRemoteScopedContexts: true, parentContext: context })) + .getContextRaw()} } } } @@ -718,9 +724,9 @@ must be one of ${Util.CONTAINERS.join(', ')}`, ERROR_CODES.INVALID_CONTAINER_MAP } // Make a deep clone of the given context, to avoid modifying it. - context = JSON.parse(JSON.stringify(context)); // No better way in JS at the moment. + context = {...context}; if (parentContext && !minimalProcessing) { - parentContext = JSON.parse(JSON.stringify(parentContext)); + parentContext = {...parentContext}; } // According to the JSON-LD spec, @base must be ignored from external contexts. @@ -733,7 +739,7 @@ must be one of ${Util.CONTAINERS.join(', ')}`, ERROR_CODES.INVALID_CONTAINER_MAP // Hashify container entries // Do this before protected term validation as that influences term format - this.containersToHash(context); + context = this.containersToHash(context); // Don't perform any other modifications if only minimal processing is needed. if (minimalProcessing) { @@ -767,21 +773,20 @@ must be one of ${Util.CONTAINERS.join(', ')}`, ERROR_CODES.INVALID_CONTAINER_MAP newContext = { ...parentContext, ...newContext }; } + // Parse inner contexts with minimal processing + newContext = await this.parseInnerContexts(newContext, options); + const newContextWrapped = new JsonLdContextNormalized(newContext); - // Parse inner contexts with minimal processing - await this.parseInnerContexts(newContext, options); // In JSON-LD 1.1, @vocab can be relative to @vocab in the parent context, or a compact IRI. if ((newContext && newContext['@version'] || ContextParser.DEFAULT_PROCESSING_MODE) >= 1.1 && ((context['@vocab'] && typeof context['@vocab'] === 'string') || context['@vocab'] === '')) { if (parentContext && '@vocab' in parentContext && context['@vocab'].indexOf(':') < 0) { newContext['@vocab'] = parentContext['@vocab'] + context['@vocab']; - } else { - if (Util.isCompactIri(context['@vocab']) || context['@vocab'] in newContextWrapped.getContextRaw()) { + } else if (Util.isCompactIri(context['@vocab']) || context['@vocab'] in newContext) { // @vocab is a compact IRI or refers exactly to a prefix - newContext['@vocab'] = newContextWrapped.expandTerm(context['@vocab'], true); - } + newContext['@vocab'] = newContextWrapped.expandTerm(context['@vocab'], true); } } @@ -816,7 +821,7 @@ must be one of ${Util.CONTAINERS.join(', ')}`, ERROR_CODES.INVALID_CONTAINER_MAP // First try to retrieve the context from cache const cached = this.documentCache[url]; if (cached) { - return typeof cached === 'string' ? cached : Array.isArray(cached) ? cached.slice() : {... cached}; + return cached; } // If not in cache, load it @@ -863,8 +868,8 @@ must be one of ${Util.CONTAINERS.join(', ')}`, ERROR_CODES.INVALID_CONTAINER_MAP * @param importContextIri The full URI of an @import value. */ public async loadImportContext(importContextIri: string): Promise { - // Load the context - const importContext = await this.load(importContextIri); + // Load the context - and do a deep clone since we are about to mutate it + let importContext = await this.load(importContextIri); // Require the context to be a non-array object if (typeof importContext !== 'object' || Array.isArray(importContext)) { @@ -877,11 +882,11 @@ must be one of ${Util.CONTAINERS.join(', ')}`, ERROR_CODES.INVALID_CONTAINER_MAP throw new ErrorCoded('An imported context can not import another context: ' + importContextIri, ERROR_CODES.INVALID_CONTEXT_ENTRY); } + importContext = {...importContext}; // Containers have to be converted into hash values the same way as for the importing context // Otherwise context validation will fail for container values - this.containersToHash(importContext); - return importContext; + return this.containersToHash(importContext); } }