From 3f8c98f2a89da42a9080b703abdbc09ecf0b7314 Mon Sep 17 00:00:00 2001 From: Jesse Wright <63333554+jeswr@users.noreply.github.com> Date: Sat, 28 Oct 2023 22:18:37 +0100 Subject: [PATCH 01/18] fix: deep copy imported contexts --- lib/ContextParser.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/ContextParser.ts b/lib/ContextParser.ts index 64fe917..549a6d4 100644 --- a/lib/ContextParser.ts +++ b/lib/ContextParser.ts @@ -863,8 +863,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 + const importContext = JSON.parse(JSON.stringify(await this.load(importContextIri))); // Require the context to be a non-array object if (typeof importContext !== 'object' || Array.isArray(importContext)) { From 776e12a95811febed84cd9088669cbaa5e94a800 Mon Sep 17 00:00:00 2001 From: Jesse Wright <63333554+jeswr@users.noreply.github.com> Date: Sat, 28 Oct 2023 22:55:26 +0100 Subject: [PATCH 02/18] chore: add assignment markers --- lib/ContextParser.ts | 44 ++++++++++++++++++++++++++++++++------------ 1 file changed, 32 insertions(+), 12 deletions(-) diff --git a/lib/ContextParser.ts b/lib/ContextParser.ts index 549a6d4..4b4714e 100644 --- a/lib/ContextParser.ts +++ b/lib/ContextParser.ts @@ -92,8 +92,7 @@ export class ContextParser { * @return {IJsonLdContextNormalizedRaw} The mutated input context. */ public idifyReverseTerms(context: IJsonLdContextNormalizedRaw): IJsonLdContextNormalizedRaw { - for (const key of Object.keys(context)) { - const value: IPrefixValue = context[key]; + for (const value of Object.values(context)) { if (value && typeof value === 'object') { if (value['@reverse'] && !value['@id']) { if (typeof value['@reverse'] !== 'string' || Util.isValidKeyword(value['@reverse'])) { @@ -102,6 +101,7 @@ export class ContextParser { } value['@id'] = value['@reverse']; if (Util.isPotentialKeyword(value['@reverse'])) { + // @assignment delete value['@reverse']; } else { value['@reverse'] = true; @@ -152,6 +152,7 @@ Tried mapping ${key} to ${JSON.stringify(keyValue)}`, ERROR_CODES.INVALID_KEYWOR const value: IPrefixValue = contextRaw[key]; let changed: boolean = false; if (typeof value === 'string') { + // @assignment contextRaw[key] = context.expandTerm(value, true); changed = changed || value !== contextRaw[key]; } else { @@ -162,6 +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') { + // @assignment contextRaw[key]['@id'] = context.expandTerm(id, true); changed = changed || id !== contextRaw[key]['@id']; } @@ -170,6 +172,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 + // @assignment contextRaw[key]['@id'] = newId; changed = true; } @@ -178,8 +181,10 @@ Tried mapping ${key} to ${JSON.stringify(keyValue)}`, ERROR_CODES.INVALID_KEYWOR && (!value['@container'] || !( value['@container'])['@type']) && canAddIdEntry) { // First check @vocab, then fallback to @base + // @assignment contextRaw[key]['@type'] = context.expandTerm(type, true); if (expandContentTypeToBase && type === contextRaw[key]['@type']) { + // @assignment contextRaw[key]['@type'] = context.expandTerm(type, false); } changed = changed || type !== contextRaw[key]['@type']; @@ -222,20 +227,22 @@ Tried mapping ${key} to ${JSON.stringify(keyValue)}`, ERROR_CODES.INVALID_KEYWOR * @param {IJsonLdContextNormalizedRaw} context A context. */ public containersToHash(context: IJsonLdContextNormalizedRaw) { - for (const key of Object.keys(context)) { - const value = context[key]; + for (const value of Object.values(context)) { if (value && typeof value === 'object') { if (typeof value['@container'] === 'string') { + // @assignment 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; } + // @assignment value['@container'] = newValue; } } } + return context; } /** @@ -256,20 +263,24 @@ 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 + // @assignment context[key]['@protected'] = true; } } else { // Convert string-based term values to object-based values with @protected: true + // @assignment context[key] = { '@id': value, '@protected': true, }; if (Util.isSimpleTermDefinitionPrefix(value, expandOptions)) { + // @assignment context[key]['@prefix'] = true } } } } + // @assignment delete context['@protected']; } } @@ -290,6 +301,7 @@ Tried mapping ${key} to ${JSON.stringify(keyValue)}`, ERROR_CODES.INVALID_KEYWOR // If the new entry is in string-mode, convert it to object-mode // before checking if it is identical. if (typeof contextAfter[key] === 'string') { + // @assignment contextAfter[key] = { '@id': contextAfter[key] }; } @@ -298,6 +310,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. + // @assignment contextAfter[key]['@protected'] = true; const valueAfter = canonicalizeJson(contextAfter[key]); @@ -543,8 +556,10 @@ must be one of ${Util.CONTAINERS.join(', ')}`, ERROR_CODES.INVALID_CONTAINER_MAP // Give priority to @base in the parent context if (inheritFromParent && !('@base' in context) && options.parentContext && typeof options.parentContext === 'object' && '@base' in options.parentContext) { + // @assignment context['@base'] = options.parentContext['@base']; if (options.parentContext['@__baseDocument']) { + // @assignment context['@__baseDocument'] = true; } } @@ -553,11 +568,14 @@ must be one of ${Util.CONTAINERS.join(', ')}`, ERROR_CODES.INVALID_CONTAINER_MAP if (options.baseIRI && !options.external) { if (!('@base' in context)) { // The context base is the document base + // @assignment context['@base'] = options.baseIRI; + // @assignment context['@__baseDocument'] = true; } else if (context['@base'] !== null && typeof context['@base'] === 'string' && !Util.isValidIri( context['@base'])) { // The context base is relative to the document base + // @assignment context['@base'] = resolve( context['@base'], options.parentContext && options.parentContext['@base'] || options.baseIRI); } @@ -596,6 +614,7 @@ must be one of ${Util.CONTAINERS.join(', ')}`, ERROR_CODES.INVALID_CONTAINER_MAP */ public async parseInnerContexts(context: IJsonLdContextNormalizedRaw, options: IParseOptions) : Promise { + let newContext: IJsonLdContextNormalizedRaw = {}; for (const key of Object.keys(context)) { const value = context[key]; if (value && typeof value === 'object') { @@ -607,8 +626,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 +634,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(); + .getContextRaw() ; } } } @@ -725,6 +742,7 @@ must be one of ${Util.CONTAINERS.join(', ')}`, ERROR_CODES.INVALID_CONTAINER_MAP // According to the JSON-LD spec, @base must be ignored from external contexts. if (external) { + // @assignment delete context['@base']; } @@ -733,7 +751,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) { @@ -752,6 +770,7 @@ must be one of ${Util.CONTAINERS.join(', ')}`, ERROR_CODES.INVALID_CONTAINER_MAP // Load context importContext = await this.loadImportContext(this.normalizeContextIri(context['@import'], baseIRI)); + // @assignment delete context['@import']; } else { throw new ErrorCoded('Context importing is not supported in JSON-LD 1.0', @@ -776,10 +795,12 @@ must be one of ${Util.CONTAINERS.join(', ')}`, ERROR_CODES.INVALID_CONTAINER_MAP 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) { + // @assignment newContext['@vocab'] = parentContext['@vocab'] + context['@vocab']; } else { if (Util.isCompactIri(context['@vocab']) || context['@vocab'] in newContextWrapped.getContextRaw()) { // @vocab is a compact IRI or refers exactly to a prefix + // @assignment newContext['@vocab'] = newContextWrapped.expandTerm(context['@vocab'], true); } } @@ -816,7 +837,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 @@ -880,8 +901,7 @@ must be one of ${Util.CONTAINERS.join(', ')}`, ERROR_CODES.INVALID_CONTAINER_MAP // 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); } } From c15dda3bdea1059f25067537238d387c2974e621 Mon Sep 17 00:00:00 2001 From: Jesse Wright <63333554+jeswr@users.noreply.github.com> Date: Sat, 28 Oct 2023 23:01:33 +0100 Subject: [PATCH 03/18] chore: remove some assignments --- lib/ContextParser.ts | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/lib/ContextParser.ts b/lib/ContextParser.ts index 4b4714e..1b9805c 100644 --- a/lib/ContextParser.ts +++ b/lib/ContextParser.ts @@ -614,9 +614,9 @@ must be one of ${Util.CONTAINERS.join(', ')}`, ERROR_CODES.INVALID_CONTAINER_MAP */ public async parseInnerContexts(context: IJsonLdContextNormalizedRaw, options: IParseOptions) : Promise { - let newContext: IJsonLdContextNormalizedRaw = {}; - for (const key of Object.keys(context)) { - const value = context[key]; + let newContext: IJsonLdContextNormalizedRaw = {...context}; + for (const key of Object.keys(newContext)) { + const value = newContext[key]; if (value && typeof value === 'object') { if ('@context' in value && value['@context'] !== null && !options.ignoreScopedContexts) { // Simulate a processing based on the parent context to check if there are any (potential errors). @@ -626,7 +626,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, [key]: {...context[key]}}; + const parentContext = {...newContext, [key]: {...newContext[key]}}; delete parentContext[key]['@context']; await this.parse(value['@context'], { ...options, external: false, parentContext, ignoreProtection: true, ignoreRemoteScopedContexts: true, ignoreScopedContexts: true }); @@ -636,11 +636,11 @@ must be one of ${Util.CONTAINERS.join(', ')}`, ERROR_CODES.INVALID_CONTAINER_MAP } value['@context'] = (await this.parse(value['@context'], { ...options, external: false, minimalProcessing: true, ignoreRemoteScopedContexts: true, parentContext: context })) - .getContextRaw() ; + .getContextRaw(); } } } - return context; + return newContext; } /** @@ -786,10 +786,11 @@ 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 From 4d8bb125ec34a91c69dd1a110ba41c3d2ef7dcd6 Mon Sep 17 00:00:00 2001 From: Jesse Wright <63333554+jeswr@users.noreply.github.com> Date: Sun, 29 Oct 2023 00:00:23 +0100 Subject: [PATCH 04/18] perf: remove need for deep cloning --- lib/ContextParser.ts | 111 +++++++++++++++++++++++-------------------- 1 file changed, 59 insertions(+), 52 deletions(-) diff --git a/lib/ContextParser.ts b/lib/ContextParser.ts index 1b9805c..154293a 100644 --- a/lib/ContextParser.ts +++ b/lib/ContextParser.ts @@ -9,6 +9,13 @@ import {Util} from "./Util"; // tslint:disable-next-line:no-var-requires const canonicalizeJson = require('canonicalize'); +const deepFreeze = (obj: any) => { + Object.keys(obj).forEach(prop => { + if (typeof obj[prop] === 'object' && !Object.isFrozen(obj[prop])) deepFreeze(obj[prop]); + }); + return Object.freeze(obj); + // return obj; +}; /** * Parses JSON-LD contexts. @@ -92,16 +99,18 @@ export class ContextParser { * @return {IJsonLdContextNormalizedRaw} The mutated input context. */ public idifyReverseTerms(context: IJsonLdContextNormalizedRaw): IJsonLdContextNormalizedRaw { - for (const value of Object.values(context)) { + for (const key of Object.keys(context)) { + 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); } + // @-assignment + value = context[key] = {...value, '@id': value['@reverse']}; value['@id'] = value['@reverse']; if (Util.isPotentialKeyword(value['@reverse'])) { - // @assignment delete value['@reverse']; } else { value['@reverse'] = true; @@ -152,7 +161,7 @@ Tried mapping ${key} to ${JSON.stringify(keyValue)}`, ERROR_CODES.INVALID_KEYWOR const value: IPrefixValue = contextRaw[key]; let changed: boolean = false; if (typeof value === 'string') { - // @assignment + // @-assignment contextRaw[key] = context.expandTerm(value, true); changed = changed || value !== contextRaw[key]; } else { @@ -163,8 +172,8 @@ 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') { - // @assignment - contextRaw[key]['@id'] = context.expandTerm(id, true); + // @-assignment + contextRaw[key] = { ...contextRaw[key], '@id': context.expandTerm(id, true) }; changed = changed || id !== contextRaw[key]['@id']; } } else if (!Util.isPotentialKeyword(key) && canAddIdEntry) { @@ -172,8 +181,8 @@ 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 - // @assignment - contextRaw[key]['@id'] = newId; + // @-assignment + contextRaw[key] = { ...contextRaw[key], '@id': newId }; changed = true; } } @@ -181,13 +190,12 @@ Tried mapping ${key} to ${JSON.stringify(keyValue)}`, ERROR_CODES.INVALID_KEYWOR && (!value['@container'] || !( value['@container'])['@type']) && canAddIdEntry) { // First check @vocab, then fallback to @base - // @assignment - contextRaw[key]['@type'] = context.expandTerm(type, true); - if (expandContentTypeToBase && type === contextRaw[key]['@type']) { - // @assignment - contextRaw[key]['@type'] = context.expandTerm(type, false); + // @-assignment + const expandedType = context.expandTerm(type, !(expandContentTypeToBase && type === contextRaw[key]['@type'])); + if (expandedType !== type) { + changed = true; + contextRaw[key] = { ...contextRaw[key], '@type': expandedType }; } - changed = changed || type !== contextRaw[key]['@type']; } } if (!changed) { @@ -214,7 +222,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}; + } } } } @@ -227,18 +238,20 @@ Tried mapping ${key} to ${JSON.stringify(keyValue)}`, ERROR_CODES.INVALID_KEYWOR * @param {IJsonLdContextNormalizedRaw} context A context. */ public containersToHash(context: IJsonLdContextNormalizedRaw) { - for (const value of Object.values(context)) { + // let context = { ...c} + for (const key of Object.keys(context)) { + const value = context[key]; if (value && typeof value === 'object') { if (typeof value['@container'] === 'string') { - // @assignment - value['@container'] = { [value['@container']]: true }; + // @-assignment + 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; } - // @assignment - value['@container'] = newValue; + // @-assignment + context[key] = { ...value, '@container': newValue }; } } } @@ -263,24 +276,23 @@ 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 - // @assignment - context[key]['@protected'] = true; + context[key] = {...context[key], '@protected': true}; } } else { // Convert string-based term values to object-based values with @protected: true - // @assignment + // @-assignment context[key] = { '@id': value, '@protected': true, }; if (Util.isSimpleTermDefinitionPrefix(value, expandOptions)) { - // @assignment - context[key]['@prefix'] = true + // @-assignment + context[key] = {...context[key], '@prefix': true}; } } } } - // @assignment + // @-assignment delete context['@protected']; } } @@ -301,7 +313,7 @@ Tried mapping ${key} to ${JSON.stringify(keyValue)}`, ERROR_CODES.INVALID_KEYWOR // If the new entry is in string-mode, convert it to object-mode // before checking if it is identical. if (typeof contextAfter[key] === 'string') { - // @assignment + // @-assignment contextAfter[key] = { '@id': contextAfter[key] }; } @@ -310,8 +322,8 @@ 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. - // @assignment - contextAfter[key]['@protected'] = true; + // @-assignment + contextAfter[key] = {...contextAfter[key], '@protected': true}; const valueAfter = canonicalizeJson(contextAfter[key]); // Error if they are not identical @@ -556,10 +568,10 @@ must be one of ${Util.CONTAINERS.join(', ')}`, ERROR_CODES.INVALID_CONTAINER_MAP // Give priority to @base in the parent context if (inheritFromParent && !('@base' in context) && options.parentContext && typeof options.parentContext === 'object' && '@base' in options.parentContext) { - // @assignment + // @-assignment context['@base'] = options.parentContext['@base']; if (options.parentContext['@__baseDocument']) { - // @assignment + // @-assignment context['@__baseDocument'] = true; } } @@ -568,14 +580,14 @@ must be one of ${Util.CONTAINERS.join(', ')}`, ERROR_CODES.INVALID_CONTAINER_MAP if (options.baseIRI && !options.external) { if (!('@base' in context)) { // The context base is the document base - // @assignment + // @-assignment context['@base'] = options.baseIRI; - // @assignment + // @-assignment context['@__baseDocument'] = true; } else if (context['@base'] !== null && typeof context['@base'] === 'string' && !Util.isValidIri( context['@base'])) { // The context base is relative to the document base - // @assignment + // @-assignment context['@base'] = resolve( context['@base'], options.parentContext && options.parentContext['@base'] || options.baseIRI); } @@ -614,9 +626,8 @@ must be one of ${Util.CONTAINERS.join(', ')}`, ERROR_CODES.INVALID_CONTAINER_MAP */ public async parseInnerContexts(context: IJsonLdContextNormalizedRaw, options: IParseOptions) : Promise { - let newContext: IJsonLdContextNormalizedRaw = {...context}; - for (const key of Object.keys(newContext)) { - const value = newContext[key]; + for (const key of Object.keys(context)) { + const value = context[key]; if (value && typeof value === 'object') { if ('@context' in value && value['@context'] !== null && !options.ignoreScopedContexts) { // Simulate a processing based on the parent context to check if there are any (potential errors). @@ -626,7 +637,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 = {...newContext, [key]: {...newContext[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 }); @@ -634,13 +645,13 @@ 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()} } } } - return newContext; + return context; } /** @@ -735,14 +746,13 @@ 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}; // No better way in JS at the moment. if (parentContext && !minimalProcessing) { - parentContext = JSON.parse(JSON.stringify(parentContext)); + parentContext = {...parentContext}; } // According to the JSON-LD spec, @base must be ignored from external contexts. if (external) { - // @assignment delete context['@base']; } @@ -770,7 +780,7 @@ must be one of ${Util.CONTAINERS.join(', ')}`, ERROR_CODES.INVALID_CONTAINER_MAP // Load context importContext = await this.loadImportContext(this.normalizeContextIri(context['@import'], baseIRI)); - // @assignment + // @-assignment delete context['@import']; } else { throw new ErrorCoded('Context importing is not supported in JSON-LD 1.0', @@ -796,14 +806,10 @@ must be one of ${Util.CONTAINERS.join(', ')}`, ERROR_CODES.INVALID_CONTAINER_MAP 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) { - // @assignment 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 - // @assignment - newContext['@vocab'] = newContextWrapped.expandTerm(context['@vocab'], true); - } + newContext['@vocab'] = newContextWrapped.expandTerm(context['@vocab'], true); } } @@ -886,7 +892,7 @@ must be one of ${Util.CONTAINERS.join(', ')}`, ERROR_CODES.INVALID_CONTAINER_MAP */ public async loadImportContext(importContextIri: string): Promise { // Load the context - and do a deep clone since we are about to mutate it - const importContext = JSON.parse(JSON.stringify(await this.load(importContextIri))); + let importContext = await this.load(importContextIri); // Require the context to be a non-array object if (typeof importContext !== 'object' || Array.isArray(importContext)) { @@ -899,6 +905,7 @@ 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 From 3694fc585aa718f09a2e60791fb0981f176a34b0 Mon Sep 17 00:00:00 2001 From: Jesse Wright <63333554+jeswr@users.noreply.github.com> Date: Sun, 29 Oct 2023 00:03:14 +0100 Subject: [PATCH 05/18] perf: remove all deep cloning --- lib/ContextParser.ts | 26 -------------------------- 1 file changed, 26 deletions(-) diff --git a/lib/ContextParser.ts b/lib/ContextParser.ts index 154293a..a8519f5 100644 --- a/lib/ContextParser.ts +++ b/lib/ContextParser.ts @@ -9,13 +9,6 @@ import {Util} from "./Util"; // tslint:disable-next-line:no-var-requires const canonicalizeJson = require('canonicalize'); -const deepFreeze = (obj: any) => { - Object.keys(obj).forEach(prop => { - if (typeof obj[prop] === 'object' && !Object.isFrozen(obj[prop])) deepFreeze(obj[prop]); - }); - return Object.freeze(obj); - // return obj; -}; /** * Parses JSON-LD contexts. @@ -107,7 +100,6 @@ export class ContextParser { throw new ErrorCoded(`Invalid @reverse value, must be absolute IRI or blank node: '${value['@reverse']}'`, ERROR_CODES.INVALID_IRI_MAPPING); } - // @-assignment value = context[key] = {...value, '@id': value['@reverse']}; value['@id'] = value['@reverse']; if (Util.isPotentialKeyword(value['@reverse'])) { @@ -161,7 +153,6 @@ Tried mapping ${key} to ${JSON.stringify(keyValue)}`, ERROR_CODES.INVALID_KEYWOR const value: IPrefixValue = contextRaw[key]; let changed: boolean = false; if (typeof value === 'string') { - // @-assignment contextRaw[key] = context.expandTerm(value, true); changed = changed || value !== contextRaw[key]; } else { @@ -172,7 +163,6 @@ 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') { - // @-assignment contextRaw[key] = { ...contextRaw[key], '@id': context.expandTerm(id, true) }; changed = changed || id !== contextRaw[key]['@id']; } @@ -181,7 +171,6 @@ 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 - // @-assignment contextRaw[key] = { ...contextRaw[key], '@id': newId }; changed = true; } @@ -190,7 +179,6 @@ Tried mapping ${key} to ${JSON.stringify(keyValue)}`, ERROR_CODES.INVALID_KEYWOR && (!value['@container'] || !( value['@container'])['@type']) && canAddIdEntry) { // First check @vocab, then fallback to @base - // @-assignment const expandedType = context.expandTerm(type, !(expandContentTypeToBase && type === contextRaw[key]['@type'])); if (expandedType !== type) { changed = true; @@ -238,19 +226,16 @@ Tried mapping ${key} to ${JSON.stringify(keyValue)}`, ERROR_CODES.INVALID_KEYWOR * @param {IJsonLdContextNormalizedRaw} context A context. */ public containersToHash(context: IJsonLdContextNormalizedRaw) { - // let context = { ...c} for (const key of Object.keys(context)) { const value = context[key]; if (value && typeof value === 'object') { if (typeof value['@container'] === 'string') { - // @-assignment 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; } - // @-assignment context[key] = { ...value, '@container': newValue }; } } @@ -280,19 +265,16 @@ Tried mapping ${key} to ${JSON.stringify(keyValue)}`, ERROR_CODES.INVALID_KEYWOR } } else { // Convert string-based term values to object-based values with @protected: true - // @-assignment context[key] = { '@id': value, '@protected': true, }; if (Util.isSimpleTermDefinitionPrefix(value, expandOptions)) { - // @-assignment context[key] = {...context[key], '@prefix': true}; } } } } - // @-assignment delete context['@protected']; } } @@ -313,7 +295,6 @@ Tried mapping ${key} to ${JSON.stringify(keyValue)}`, ERROR_CODES.INVALID_KEYWOR // If the new entry is in string-mode, convert it to object-mode // before checking if it is identical. if (typeof contextAfter[key] === 'string') { - // @-assignment contextAfter[key] = { '@id': contextAfter[key] }; } @@ -322,7 +303,6 @@ 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. - // @-assignment contextAfter[key] = {...contextAfter[key], '@protected': true}; const valueAfter = canonicalizeJson(contextAfter[key]); @@ -568,10 +548,8 @@ must be one of ${Util.CONTAINERS.join(', ')}`, ERROR_CODES.INVALID_CONTAINER_MAP // Give priority to @base in the parent context if (inheritFromParent && !('@base' in context) && options.parentContext && typeof options.parentContext === 'object' && '@base' in options.parentContext) { - // @-assignment context['@base'] = options.parentContext['@base']; if (options.parentContext['@__baseDocument']) { - // @-assignment context['@__baseDocument'] = true; } } @@ -580,14 +558,11 @@ must be one of ${Util.CONTAINERS.join(', ')}`, ERROR_CODES.INVALID_CONTAINER_MAP if (options.baseIRI && !options.external) { if (!('@base' in context)) { // The context base is the document base - // @-assignment context['@base'] = options.baseIRI; - // @-assignment context['@__baseDocument'] = true; } else if (context['@base'] !== null && typeof context['@base'] === 'string' && !Util.isValidIri( context['@base'])) { // The context base is relative to the document base - // @-assignment context['@base'] = resolve( context['@base'], options.parentContext && options.parentContext['@base'] || options.baseIRI); } @@ -780,7 +755,6 @@ must be one of ${Util.CONTAINERS.join(', ')}`, ERROR_CODES.INVALID_CONTAINER_MAP // Load context importContext = await this.loadImportContext(this.normalizeContextIri(context['@import'], baseIRI)); - // @-assignment delete context['@import']; } else { throw new ErrorCoded('Context importing is not supported in JSON-LD 1.0', From 9d54b88555de9d10c8a32821c3c43841dcbe302d Mon Sep 17 00:00:00 2001 From: Jesse Wright <63333554+jeswr@users.noreply.github.com> Date: Sun, 29 Oct 2023 00:10:44 +0100 Subject: [PATCH 06/18] feat: fix term expansion code --- lib/ContextParser.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lib/ContextParser.ts b/lib/ContextParser.ts index a8519f5..ac363ab 100644 --- a/lib/ContextParser.ts +++ b/lib/ContextParser.ts @@ -179,7 +179,10 @@ Tried mapping ${key} to ${JSON.stringify(keyValue)}`, ERROR_CODES.INVALID_KEYWOR && (!value['@container'] || !( value['@container'])['@type']) && canAddIdEntry) { // First check @vocab, then fallback to @base - const expandedType = context.expandTerm(type, !(expandContentTypeToBase && type === contextRaw[key]['@type'])); + 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 }; From 786f1547e9c26b7a6f0abe0b6bd731fc6b076e88 Mon Sep 17 00:00:00 2001 From: Jesse Wright <63333554+jeswr@users.noreply.github.com> Date: Sun, 29 Oct 2023 00:23:27 +0100 Subject: [PATCH 07/18] Update lib/ContextParser.ts --- lib/ContextParser.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/ContextParser.ts b/lib/ContextParser.ts index ac363ab..107ffcc 100644 --- a/lib/ContextParser.ts +++ b/lib/ContextParser.ts @@ -724,7 +724,7 @@ 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 = {...context}; // No better way in JS at the moment. + context = {...context}; if (parentContext && !minimalProcessing) { parentContext = {...parentContext}; } From 0d2dbdab4dba81583d6f61813a3138acaf3f8d49 Mon Sep 17 00:00:00 2001 From: Jesse Wright <63333554+jeswr@users.noreply.github.com> Date: Sun, 29 Oct 2023 10:56:26 +0000 Subject: [PATCH 08/18] perf: remove unecessary work on parent --- lib/ContextParser.ts | 37 +++++++++++++++++++++---------------- 1 file changed, 21 insertions(+), 16 deletions(-) diff --git a/lib/ContextParser.ts b/lib/ContextParser.ts index 107ffcc..b76e806 100644 --- a/lib/ContextParser.ts +++ b/lib/ContextParser.ts @@ -120,9 +120,9 @@ export class ContextParser { * @param {boolean} expandContentTypeToBase If @type inside the context may be expanded * via @base if @vocab is set to null. */ - public expandPrefixedTerms(context: JsonLdContextNormalized, expandContentTypeToBase: boolean) { + public expandPrefixedTerms(context: JsonLdContextNormalized, expandContentTypeToBase: boolean, keys = Object.keys(context.getContextRaw())) { const contextRaw = context.getContextRaw(); - for (const key of Object.keys(contextRaw)) { + for (const key of keys) { // Only expand allowed keys if (Util.EXPAND_KEYS_BLACKLIST.indexOf(key) < 0 && !Util.isReservedInternalKeyword(key)) { // Error if we try to alias a keyword to something else. @@ -291,7 +291,7 @@ Tried mapping ${key} to ${JSON.stringify(keyValue)}`, ERROR_CODES.INVALID_KEYWOR */ public validateKeywordRedefinitions(contextBefore: IJsonLdContextNormalizedRaw, contextAfter: IJsonLdContextNormalizedRaw, - expandOptions: IExpandOptions) { + expandOptions: IExpandOptions, keys = Object.keys(contextAfter)) { for (const key of Object.keys(contextAfter)) { if (Util.isTermProtected(contextBefore, key)) { // The entry in the context before will always be in object-mode @@ -602,9 +602,9 @@ must be one of ${Util.CONTAINERS.join(', ')}`, ERROR_CODES.INVALID_CONTAINER_MAP * @param {IParseOptions} options Parsing options. * @return {IJsonLdContextNormalizedRaw} The mutated input context. */ - public async parseInnerContexts(context: IJsonLdContextNormalizedRaw, options: IParseOptions) + public async parseInnerContexts(context: IJsonLdContextNormalizedRaw, options: IParseOptions, keys = Object.keys(context)) : Promise { - for (const key of Object.keys(context)) { + for (const key of keys) { const value = context[key]; if (value && typeof value === 'object') { if ('@context' in value && value['@context'] !== null && !options.ignoreScopedContexts) { @@ -639,7 +639,7 @@ must be one of ${Util.CONTAINERS.join(', ')}`, ERROR_CODES.INVALID_CONTAINER_MAP * @return {Promise} A promise resolving to the context. */ public async parse(context: JsonLdContext, - options: IParseOptions = {}): Promise { + options: IParseOptions = {}, ioptions: { skipValidation?: boolean } = {}): Promise { const { baseIRI, parentContext: parentContextInitial, @@ -711,6 +711,8 @@ must be one of ${Util.CONTAINERS.join(', ')}`, ERROR_CODES.INVALID_CONTAINER_MAP external: !!contextIris[i] || options.external, parentContext: accContext.getContextRaw(), remoteContexts: contextIris[i] ? { ...remoteContexts, [contextIris[i]]: true } : remoteContexts, + }, { + skipValidation: i < contexts.length - 1, })), Promise.resolve(new JsonLdContextNormalized(parentContext || {}))); @@ -766,19 +768,25 @@ must be one of ${Util.CONTAINERS.join(', ')}`, ERROR_CODES.INVALID_CONTAINER_MAP } this.applyScopedProtected(importContext, { processingMode }, defaultExpandOptions); + let newContext: IJsonLdContextNormalizedRaw = { ...importContext, ...context }; + + // Handle terms (before protection checks) + this.idifyReverseTerms(newContext); + this.normalize(newContext, { processingMode, normalizeLanguageTags }); + this.applyScopedProtected(newContext, { processingMode }, defaultExpandOptions); + + const keys = Object.keys(newContext); if (typeof parentContext === 'object') { // Merge different parts of the final context in order - this.applyScopedProtected(newContext, { processingMode }, defaultExpandOptions); newContext = { ...parentContext, ...newContext }; } // Parse inner contexts with minimal processing - newContext = await this.parseInnerContexts(newContext, options); + await this.parseInnerContexts(newContext, options, keys); const newContextWrapped = new JsonLdContextNormalized(newContext); - // 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'] === '')) { @@ -787,24 +795,21 @@ must be one of ${Util.CONTAINERS.join(', ')}`, ERROR_CODES.INVALID_CONTAINER_MAP } 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); + } } - // Handle terms (before protection checks) - this.idifyReverseTerms(newContext); + // FIXME: Add keys as a 3rd argument here for performance this.expandPrefixedTerms(newContextWrapped, this.expandContentTypeToBase); // In JSON-LD 1.1, check if we are not redefining any protected keywords if (!ignoreProtection && parentContext && processingMode >= 1.1) { - this.validateKeywordRedefinitions(parentContext, newContext, defaultExpandOptions); + this.validateKeywordRedefinitions(parentContext, newContext, defaultExpandOptions, keys); } - this.normalize(newContext, { processingMode, normalizeLanguageTags }); - this.applyScopedProtected(newContext, { processingMode }, defaultExpandOptions); - if (this.validateContext) { + if (this.validateContext && !ioptions.skipValidation) { this.validate(newContext, { processingMode }); } - return newContextWrapped; } else { throw new ErrorCoded(`Tried parsing a context that is not a string, array or object, but got ${context}`, From cb64a6c6b22f861de74d87bc083951e7500193d1 Mon Sep 17 00:00:00 2001 From: Jesse Wright <63333554+jeswr@users.noreply.github.com> Date: Sun, 29 Oct 2023 11:12:49 +0000 Subject: [PATCH 09/18] chore: fix coverage errors --- lib/ContextParser.ts | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/lib/ContextParser.ts b/lib/ContextParser.ts index b76e806..5351ad7 100644 --- a/lib/ContextParser.ts +++ b/lib/ContextParser.ts @@ -120,7 +120,12 @@ export class ContextParser { * @param {boolean} expandContentTypeToBase If @type inside the context may be expanded * via @base if @vocab is set to null. */ - public expandPrefixedTerms(context: JsonLdContextNormalized, expandContentTypeToBase: boolean, keys = Object.keys(context.getContextRaw())) { + public expandPrefixedTerms( + context: JsonLdContextNormalized, + expandContentTypeToBase: boolean, + /* istanbul ignore next */ + keys = Object.keys(context.getContextRaw() + )) { const contextRaw = context.getContextRaw(); for (const key of keys) { // Only expand allowed keys @@ -291,7 +296,9 @@ Tried mapping ${key} to ${JSON.stringify(keyValue)}`, ERROR_CODES.INVALID_KEYWOR */ public validateKeywordRedefinitions(contextBefore: IJsonLdContextNormalizedRaw, contextAfter: IJsonLdContextNormalizedRaw, - expandOptions: IExpandOptions, keys = Object.keys(contextAfter)) { + expandOptions: IExpandOptions, + /* istanbul ignore next */ + keys = Object.keys(contextAfter)) { for (const key of Object.keys(contextAfter)) { if (Util.isTermProtected(contextBefore, key)) { // The entry in the context before will always be in object-mode @@ -602,7 +609,12 @@ must be one of ${Util.CONTAINERS.join(', ')}`, ERROR_CODES.INVALID_CONTAINER_MAP * @param {IParseOptions} options Parsing options. * @return {IJsonLdContextNormalizedRaw} The mutated input context. */ - public async parseInnerContexts(context: IJsonLdContextNormalizedRaw, options: IParseOptions, keys = Object.keys(context)) + public async parseInnerContexts( + context: IJsonLdContextNormalizedRaw, + options: IParseOptions, + /* istanbul ignore next */ + keys = Object.keys(context) + ) : Promise { for (const key of keys) { const value = context[key]; From 84a1e4199a34caa43e392d91ed340f2c2fc4f2de Mon Sep 17 00:00:00 2001 From: Jesse Wright <63333554+jeswr@users.noreply.github.com> Date: Sun, 29 Oct 2023 12:52:14 +0000 Subject: [PATCH 10/18] perf: use deepEqual instead of canonicalize --- lib/ContextParser.ts | 23 ++++++++++++++++------- package.json | 1 - yarn.lock | 5 ----- 3 files changed, 16 insertions(+), 13 deletions(-) diff --git a/lib/ContextParser.ts b/lib/ContextParser.ts index 5351ad7..0dab67a 100644 --- a/lib/ContextParser.ts +++ b/lib/ContextParser.ts @@ -7,8 +7,21 @@ import {IJsonLdContext, IJsonLdContextNormalizedRaw, IPrefixValue, JsonLdContext import {JsonLdContextNormalized, defaultExpandOptions, IExpandOptions} from "./JsonLdContextNormalized"; import {Util} from "./Util"; -// tslint:disable-next-line:no-var-requires -const canonicalizeJson = require('canonicalize'); +const deepEqual = (object1: any, object2: any): boolean => { + const objKeys1 = Object.keys(object1); + const objKeys2 = Object.keys(object2); + + if (objKeys1.length !== objKeys2.length) return false; + return objKeys1.every((key) => { + const value1 = object1[key]; + const value2 = object2[key]; + return (value1 === value2) || (isObject(value1) && isObject(value2) && deepEqual(value1, value2)); + }); +}; + +const isObject = (object: any) => { + return object != null && typeof object === "object"; +}; /** * Parses JSON-LD contexts. @@ -307,17 +320,13 @@ Tried mapping ${key} to ${JSON.stringify(keyValue)}`, ERROR_CODES.INVALID_KEYWOR if (typeof contextAfter[key] === 'string') { contextAfter[key] = { '@id': contextAfter[key] }; } - - // Convert term values to strings for each comparison - const valueBefore = canonicalizeJson(contextBefore[key]); // 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] = {...contextAfter[key], '@protected': true}; - const valueAfter = canonicalizeJson(contextAfter[key]); // Error if they are not identical - if (valueBefore !== valueAfter) { + if (!deepEqual(contextBefore[key], contextAfter[key])) { throw new ErrorCoded(`Attempted to override the protected keyword ${key} from ${ JSON.stringify(Util.getContextValueId(contextBefore[key]))} to ${ JSON.stringify(Util.getContextValueId(contextAfter[key]))}`, diff --git a/package.json b/package.json index bf3120a..436d1ac 100644 --- a/package.json +++ b/package.json @@ -86,7 +86,6 @@ "dependencies": { "@types/http-link-header": "^1.0.1", "@types/node": "^18.0.0", - "canonicalize": "^1.0.1", "cross-fetch": "^3.0.6", "http-link-header": "^1.0.2", "relative-to-absolute-iri": "^1.0.5" diff --git a/yarn.lock b/yarn.lock index a666a5c..d9336aa 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1137,11 +1137,6 @@ caniuse-lite@^1.0.30001359: resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001363.tgz#26bec2d606924ba318235944e1193304ea7c4f15" integrity sha512-HpQhpzTGGPVMnCjIomjt+jvyUu8vNFo3TaDiZ/RcoTrlOq/5+tC8zHdsbgFB6MxmaY+jCpsH09aD80Bb4Ow3Sg== -canonicalize@^1.0.1: - version "1.0.8" - resolved "https://registry.yarnpkg.com/canonicalize/-/canonicalize-1.0.8.tgz#24d1f1a00ed202faafd9bf8e63352cd4450c6df1" - integrity sha512-0CNTVCLZggSh7bc5VkX5WWPWO+cyZbNd07IHIsSXLia/eAq+r836hgk+8BKoEh7949Mda87VUOitx5OddVj64A== - caseless@~0.12.0: version "0.12.0" resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc" From 0af3349169ad1e8f3e223ee0c23ac0fbaf8b14ef Mon Sep 17 00:00:00 2001 From: Jesse Wright <63333554+jeswr@users.noreply.github.com> Date: Sun, 29 Oct 2023 13:16:47 +0000 Subject: [PATCH 11/18] chore: limit cloning of parent --- lib/ContextParser.ts | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/lib/ContextParser.ts b/lib/ContextParser.ts index 0dab67a..6b32095 100644 --- a/lib/ContextParser.ts +++ b/lib/ContextParser.ts @@ -663,14 +663,13 @@ must be one of ${Util.CONTAINERS.join(', ')}`, ERROR_CODES.INVALID_CONTAINER_MAP options: IParseOptions = {}, ioptions: { skipValidation?: boolean } = {}): Promise { const { baseIRI, - parentContext: parentContextInitial, + parentContext, external, processingMode = ContextParser.DEFAULT_PROCESSING_MODE, normalizeLanguageTags, ignoreProtection, minimalProcessing, } = options; - let parentContext = parentContextInitial; const remoteContexts = options.remoteContexts || {}; // Avoid remote context overflows @@ -748,9 +747,6 @@ 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 = {...context}; - if (parentContext && !minimalProcessing) { - parentContext = {...parentContext}; - } // According to the JSON-LD spec, @base must be ignored from external contexts. if (external) { @@ -1003,4 +999,3 @@ export interface IParseOptions { */ ignoreScopedContexts?: boolean; } - From 1602365156f310b7e5465bfb2a68f8f0c02eeb4a Mon Sep 17 00:00:00 2001 From: Jesse Wright <63333554+jeswr@users.noreply.github.com> Date: Sun, 29 Oct 2023 13:26:10 +0000 Subject: [PATCH 12/18] chore: limit cloning --- lib/ContextParser.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/lib/ContextParser.ts b/lib/ContextParser.ts index 6b32095..7774855 100644 --- a/lib/ContextParser.ts +++ b/lib/ContextParser.ts @@ -786,7 +786,7 @@ must be one of ${Util.CONTAINERS.join(', ')}`, ERROR_CODES.INVALID_CONTAINER_MAP this.applyScopedProtected(importContext, { processingMode }, defaultExpandOptions); - let newContext: IJsonLdContextNormalizedRaw = { ...importContext, ...context }; + const newContext: IJsonLdContextNormalizedRaw = Object.assign(importContext, context); // Handle terms (before protection checks) this.idifyReverseTerms(newContext); @@ -796,7 +796,11 @@ must be one of ${Util.CONTAINERS.join(', ')}`, ERROR_CODES.INVALID_CONTAINER_MAP const keys = Object.keys(newContext); if (typeof parentContext === 'object') { // Merge different parts of the final context in order - newContext = { ...parentContext, ...newContext }; + for (const key in parentContext) { + if (!(key in newContext)) { + newContext[key] = parentContext[key]; + } + } } // Parse inner contexts with minimal processing From bce9e05d40adecdb7c34d26a4bce8d779dfebdbb Mon Sep 17 00:00:00 2001 From: Jesse Wright <63333554+jeswr@users.noreply.github.com> Date: Sun, 29 Oct 2023 13:48:02 +0000 Subject: [PATCH 13/18] perf: reduce redefinition iterations --- lib/ContextParser.ts | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/lib/ContextParser.ts b/lib/ContextParser.ts index 7774855..764bfdc 100644 --- a/lib/ContextParser.ts +++ b/lib/ContextParser.ts @@ -312,18 +312,19 @@ Tried mapping ${key} to ${JSON.stringify(keyValue)}`, ERROR_CODES.INVALID_KEYWOR expandOptions: IExpandOptions, /* istanbul ignore next */ keys = Object.keys(contextAfter)) { - for (const key of Object.keys(contextAfter)) { + for (const key of keys) { if (Util.isTermProtected(contextBefore, key)) { // The entry in the context before will always be in object-mode // If the new entry is in string-mode, convert it to object-mode // before checking if it is identical. if (typeof contextAfter[key] === 'string') { - contextAfter[key] = { '@id': contextAfter[key] }; - } + contextAfter[key] = { '@id': contextAfter[key], '@protected': true }; + } else { // 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] = {...contextAfter[key], '@protected': true}; + contextAfter[key] = {...contextAfter[key], '@protected': true}; + } // Error if they are not identical if (!deepEqual(contextBefore[key], contextAfter[key])) { @@ -794,10 +795,14 @@ must be one of ${Util.CONTAINERS.join(', ')}`, ERROR_CODES.INVALID_CONTAINER_MAP this.applyScopedProtected(newContext, { processingMode }, defaultExpandOptions); const keys = Object.keys(newContext); + + const overlappingKeys: string[] = []; if (typeof parentContext === 'object') { // Merge different parts of the final context in order for (const key in parentContext) { - if (!(key in newContext)) { + if (key in newContext) { + overlappingKeys.push(key); + } else { newContext[key] = parentContext[key]; } } @@ -825,7 +830,7 @@ must be one of ${Util.CONTAINERS.join(', ')}`, ERROR_CODES.INVALID_CONTAINER_MAP // In JSON-LD 1.1, check if we are not redefining any protected keywords if (!ignoreProtection && parentContext && processingMode >= 1.1) { - this.validateKeywordRedefinitions(parentContext, newContext, defaultExpandOptions, keys); + this.validateKeywordRedefinitions(parentContext, newContext, defaultExpandOptions, overlappingKeys); } if (this.validateContext && !ioptions.skipValidation) { From b3ea9de8b881128749e320ba8fff220d41ff55b6 Mon Sep 17 00:00:00 2001 From: Jesse Wright <63333554+jeswr@users.noreply.github.com> Date: Mon, 30 Oct 2023 11:39:11 +0000 Subject: [PATCH 14/18] chore: move deepEqual to Util --- lib/ContextParser.ts | 18 +----------------- lib/Util.ts | 15 +++++++++++++++ 2 files changed, 16 insertions(+), 17 deletions(-) diff --git a/lib/ContextParser.ts b/lib/ContextParser.ts index 764bfdc..215ddb5 100644 --- a/lib/ContextParser.ts +++ b/lib/ContextParser.ts @@ -5,23 +5,7 @@ import {FetchDocumentLoader} from "./FetchDocumentLoader"; import {IDocumentLoader} from "./IDocumentLoader"; import {IJsonLdContext, IJsonLdContextNormalizedRaw, IPrefixValue, JsonLdContext} from "./JsonLdContext"; import {JsonLdContextNormalized, defaultExpandOptions, IExpandOptions} from "./JsonLdContextNormalized"; -import {Util} from "./Util"; - -const deepEqual = (object1: any, object2: any): boolean => { - const objKeys1 = Object.keys(object1); - const objKeys2 = Object.keys(object2); - - if (objKeys1.length !== objKeys2.length) return false; - return objKeys1.every((key) => { - const value1 = object1[key]; - const value2 = object2[key]; - return (value1 === value2) || (isObject(value1) && isObject(value2) && deepEqual(value1, value2)); - }); -}; - -const isObject = (object: any) => { - return object != null && typeof object === "object"; -}; +import {Util,deepEqual} from "./Util"; /** * Parses JSON-LD contexts. diff --git a/lib/Util.ts b/lib/Util.ts index edb41ba..a930e2f 100644 --- a/lib/Util.ts +++ b/lib/Util.ts @@ -248,3 +248,18 @@ export class Util { return key.startsWith('@__'); } } + +export const deepEqual = (object1: any, object2: any): boolean => { + const objKeys1 = Object.keys(object1); + const objKeys2 = Object.keys(object2); + + if (objKeys1.length !== objKeys2.length) return false; + return objKeys1.every((key) => { + const value1 = object1[key]; + const value2 = object2[key]; + return (value1 === value2) || (isObject(value1) && isObject(value2) && deepEqual(value1, value2)); + }); +}; +const isObject = (object: any) => { + return object !== null && typeof object === "object"; +}; From d40a4757aca27970027c10de8fd93db59d1a8c13 Mon Sep 17 00:00:00 2001 From: Jesse Wright <63333554+jeswr@users.noreply.github.com> Date: Mon, 30 Oct 2023 17:10:37 +0000 Subject: [PATCH 15/18] perf: enable key on expandPrefixedTerms --- lib/ContextParser.ts | 35 ++++++++++--------------------- test/ContextParser-test.ts | 43 ++++++++++++++++++++++++++++++++------ 2 files changed, 48 insertions(+), 30 deletions(-) diff --git a/lib/ContextParser.ts b/lib/ContextParser.ts index 215ddb5..56bf8b8 100644 --- a/lib/ContextParser.ts +++ b/lib/ContextParser.ts @@ -117,14 +117,9 @@ export class ContextParser { * @param {boolean} expandContentTypeToBase If @type inside the context may be expanded * via @base if @vocab is set to null. */ - public expandPrefixedTerms( - context: JsonLdContextNormalized, - expandContentTypeToBase: boolean, - /* istanbul ignore next */ - keys = Object.keys(context.getContextRaw() - )) { + public expandPrefixedTerms(context: JsonLdContextNormalized, expandContentTypeToBase: boolean, keys?: string[]) { const contextRaw = context.getContextRaw(); - for (const key of keys) { + for (const key of (keys || Object.keys(contextRaw))) { // Only expand allowed keys if (Util.EXPAND_KEYS_BLACKLIST.indexOf(key) < 0 && !Util.isReservedInternalKeyword(key)) { // Error if we try to alias a keyword to something else. @@ -245,7 +240,6 @@ Tried mapping ${key} to ${JSON.stringify(keyValue)}`, ERROR_CODES.INVALID_KEYWOR } } } - return context; } /** @@ -293,10 +287,9 @@ Tried mapping ${key} to ${JSON.stringify(keyValue)}`, ERROR_CODES.INVALID_KEYWOR */ public validateKeywordRedefinitions(contextBefore: IJsonLdContextNormalizedRaw, contextAfter: IJsonLdContextNormalizedRaw, - expandOptions: IExpandOptions, - /* istanbul ignore next */ - keys = Object.keys(contextAfter)) { - for (const key of keys) { + expandOptions?: IExpandOptions, + keys?: string[]) { + for (const key of (keys ?? Object.keys(contextAfter) )) { if (Util.isTermProtected(contextBefore, key)) { // The entry in the context before will always be in object-mode // If the new entry is in string-mode, convert it to object-mode @@ -603,14 +596,8 @@ must be one of ${Util.CONTAINERS.join(', ')}`, ERROR_CODES.INVALID_CONTAINER_MAP * @param {IParseOptions} options Parsing options. * @return {IJsonLdContextNormalizedRaw} The mutated input context. */ - public async parseInnerContexts( - context: IJsonLdContextNormalizedRaw, - options: IParseOptions, - /* istanbul ignore next */ - keys = Object.keys(context) - ) - : Promise { - for (const key of keys) { + public async parseInnerContexts(context: IJsonLdContextNormalizedRaw, options: IParseOptions, keys?: string[]): Promise { + for (const key of (keys ?? Object.keys(context))) { const value = context[key]; if (value && typeof value === 'object') { if ('@context' in value && value['@context'] !== null && !options.ignoreScopedContexts) { @@ -743,7 +730,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 - context = this.containersToHash(context); + this.containersToHash(context); // Don't perform any other modifications if only minimal processing is needed. if (minimalProcessing) { @@ -809,8 +796,7 @@ must be one of ${Util.CONTAINERS.join(', ')}`, ERROR_CODES.INVALID_CONTAINER_MAP } } - // FIXME: Add keys as a 3rd argument here for performance - this.expandPrefixedTerms(newContextWrapped, this.expandContentTypeToBase); + this.expandPrefixedTerms(newContextWrapped, this.expandContentTypeToBase, keys); // In JSON-LD 1.1, check if we are not redefining any protected keywords if (!ignoreProtection && parentContext && processingMode >= 1.1) { @@ -901,7 +887,8 @@ must be one of ${Util.CONTAINERS.join(', ')}`, ERROR_CODES.INVALID_CONTAINER_MAP // Containers have to be converted into hash values the same way as for the importing context // Otherwise context validation will fail for container values - return this.containersToHash(importContext); + this.containersToHash(importContext); + return importContext; } } diff --git a/test/ContextParser-test.ts b/test/ContextParser-test.ts index 148ffe3..236f189 100644 --- a/test/ContextParser-test.ts +++ b/test/ContextParser-test.ts @@ -3,6 +3,7 @@ import { ERROR_CODES, ErrorCoded, FetchDocumentLoader, + IDocumentLoader, JsonLdContextNormalized, } from "../index"; @@ -114,25 +115,26 @@ describe('ContextParser', () => { }); describe('when instantiated without options', () => { - let parser: any; + let parser: ContextParser; beforeEach(() => { parser = new ContextParser(); }); it('should have a default document loader', async () => { - expect(parser.documentLoader).toBeInstanceOf(FetchDocumentLoader); + expect((parser).documentLoader).toBeInstanceOf(FetchDocumentLoader); }); }); describe('when instantiated with empty options', () => { - let parser: any; + let parser: ContextParser; beforeEach(() => { parser = new ContextParser({}); }); it('should have a default document loader', async () => { + // @ts-expect-error expect(parser.documentLoader).toBeInstanceOf(FetchDocumentLoader); }); }); @@ -151,8 +153,8 @@ describe('ContextParser', () => { }); describe('when instantiated with options and a document loader', () => { - let documentLoader: any; - let parser: any; + let documentLoader: IDocumentLoader; + let parser: ContextParser; beforeEach(() => { documentLoader = new FetchDocumentLoader(); @@ -160,7 +162,7 @@ describe('ContextParser', () => { }); it('should have the given document loader', async () => { - expect(parser.documentLoader).toBe(documentLoader); + expect(( parser).documentLoader).toBe(documentLoader); }); describe('expandPrefixedTerms with expandContentTypeToBase true', () => { @@ -1445,6 +1447,7 @@ Tried mapping @id to {}`, ERROR_CODES.KEYWORD_REDEFINITION)); describe('parse', () => { it('should error when parsing a context with an invalid context entry', () => { + // @ts-expect-error return expect(parser.parse({ '@base': true })).rejects .toEqual(new ErrorCoded('Found an invalid @base IRI: true', ERROR_CODES.INVALID_BASE_IRI)); @@ -1544,6 +1547,7 @@ Tried mapping @id to {}`, ERROR_CODES.KEYWORD_REDEFINITION)); }); it('should parse with a base IRI and not override the inner @base', () => { + // @ts-expect-error return expect(parser.parse({ '@base': 'http://myotherexample.org/' }, 'http://myexample.org/')) .resolves.toEqual(new JsonLdContextNormalized({ '@base': 'http://myotherexample.org/', @@ -1628,10 +1632,12 @@ Tried mapping @id to {}`, ERROR_CODES.KEYWORD_REDEFINITION)); }); it('should cache documents', async () => { + // @ts-expect-error const spy = jest.spyOn(parser.documentLoader, 'load'); await parser.parse('http://example.org/simple.jsonld'); + // @ts-expect-error expect(parser.documentCache['http://example.org/simple.jsonld']).toEqual({ name: "http://xmlns.com/foaf/0.1/name", xsd: "http://www.w3.org/2001/XMLSchema#", @@ -1803,6 +1809,7 @@ Tried mapping @id to {}`, ERROR_CODES.KEYWORD_REDEFINITION)); }); it('should parse an array with an object and a string resolving to an array when cached', () => { + // @ts-expect-error parser.documentCache['http://example.org/simplearray.jsonld'] = [{ nickname: 'http://xmlns.com/foaf/0.1/nick', }]; @@ -3067,18 +3074,21 @@ Tried mapping @id to {}`, ERROR_CODES.KEYWORD_REDEFINITION)); describe('for parsing invalid values', () => { it('should error when parsing true', () => { + // @ts-expect-error return expect(parser.parse(true)).rejects .toEqual(new ErrorCoded('Tried parsing a context that is not a string, array or object, but got true', ERROR_CODES.INVALID_LOCAL_CONTEXT)); }); it('should error when parsing false', () => { + // @ts-expect-error return expect(parser.parse(false)).rejects .toEqual(new ErrorCoded('Tried parsing a context that is not a string, array or object, but got false', ERROR_CODES.INVALID_LOCAL_CONTEXT)); }); it('should error when parsing a number', () => { + // @ts-expect-error return expect(parser.parse(1)).rejects .toEqual(new ErrorCoded('Tried parsing a context that is not a string, array or object, but got 1', ERROR_CODES.INVALID_LOCAL_CONTEXT)); @@ -3524,5 +3534,26 @@ Tried mapping @id to {}`, ERROR_CODES.KEYWORD_REDEFINITION)); }); }); + describe('#validateKeyowrdRedefinitions', () => { + it('should return true when validating over the same context', async () => { + const context = new JsonLdContextNormalized({ + '@base': 'http://base.org/', + '@vocab': 'http://vocab.org/', + 'p': 'http://vocab.org/p', + }); + expect(parser.validateKeywordRedefinitions(context, context)).toBeFalsy(); + }); + }); + + describe('#parseInnerContexts', () => { + it('return the same context when calling parseInnerContexts on a context with no inner contexts', async () => { + const context = new JsonLdContextNormalized({ + '@base': 'http://base.org/', + '@vocab': 'http://vocab.org/', + 'p': 'http://vocab.org/p', + }); + expect(parser.parseInnerContexts(context, {})).resolves.toEqual(context); + }); + }); }); }); From 5277f2ab9f1c99fca68edb8780b62dbea89e8453 Mon Sep 17 00:00:00 2001 From: Jesse Wright <63333554+jeswr@users.noreply.github.com> Date: Mon, 30 Oct 2023 17:15:20 +0000 Subject: [PATCH 16/18] chore: hide internalOptions from API --- lib/ContextParser.ts | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/lib/ContextParser.ts b/lib/ContextParser.ts index 56bf8b8..2e3543e 100644 --- a/lib/ContextParser.ts +++ b/lib/ContextParser.ts @@ -631,8 +631,12 @@ must be one of ${Util.CONTAINERS.join(', ')}`, ERROR_CODES.INVALID_CONTAINER_MAP * @param {IParseOptions} options Optional parsing options. * @return {Promise} A promise resolving to the context. */ + public async parse(context: JsonLdContext, options?: IParseOptions): Promise public async parse(context: JsonLdContext, - options: IParseOptions = {}, ioptions: { skipValidation?: boolean } = {}): Promise { + options: IParseOptions = {}, + // These options are only for internal use on recursive calls and should not be used by + // libraries consuming this function + internalOptions: { skipValidation?: boolean } = {}): Promise { const { baseIRI, parentContext, @@ -703,9 +707,11 @@ must be one of ${Util.CONTAINERS.join(', ')}`, ERROR_CODES.INVALID_CONTAINER_MAP external: !!contextIris[i] || options.external, parentContext: accContext.getContextRaw(), remoteContexts: contextIris[i] ? { ...remoteContexts, [contextIris[i]]: true } : remoteContexts, - }, { - skipValidation: i < contexts.length - 1, - })), + }, + // @ts-expect-error: This third argument causes a type error because we have hidden it from consumers + { + skipValidation: i < contexts.length - 1, + })), Promise.resolve(new JsonLdContextNormalized(parentContext || {}))); // Override the base IRI if provided. @@ -803,7 +809,7 @@ must be one of ${Util.CONTAINERS.join(', ')}`, ERROR_CODES.INVALID_CONTAINER_MAP this.validateKeywordRedefinitions(parentContext, newContext, defaultExpandOptions, overlappingKeys); } - if (this.validateContext && !ioptions.skipValidation) { + if (this.validateContext && !internalOptions.skipValidation) { this.validate(newContext, { processingMode }); } return newContextWrapped; From 3f9d21c4fe3f15d7f04424e802684d17ddd41096 Mon Sep 17 00:00:00 2001 From: Jesse Wright <63333554+jeswr@users.noreply.github.com> Date: Tue, 31 Oct 2023 12:06:01 +0000 Subject: [PATCH 17/18] chore: address review comments --- lib/ContextParser.ts | 8 ++++++-- lib/Util.ts | 36 ++++++++++++++++++++++-------------- test/ContextParser-test.ts | 2 +- 3 files changed, 29 insertions(+), 17 deletions(-) diff --git a/lib/ContextParser.ts b/lib/ContextParser.ts index 2e3543e..fb226c5 100644 --- a/lib/ContextParser.ts +++ b/lib/ContextParser.ts @@ -5,7 +5,7 @@ import {FetchDocumentLoader} from "./FetchDocumentLoader"; import {IDocumentLoader} from "./IDocumentLoader"; import {IJsonLdContext, IJsonLdContextNormalizedRaw, IPrefixValue, JsonLdContext} from "./JsonLdContext"; import {JsonLdContextNormalized, defaultExpandOptions, IExpandOptions} from "./JsonLdContextNormalized"; -import {Util,deepEqual} from "./Util"; +import {Util} from "./Util"; /** * Parses JSON-LD contexts. @@ -116,6 +116,8 @@ export class ContextParser { * @param {IJsonLdContextNormalizedRaw} context A context. * @param {boolean} expandContentTypeToBase If @type inside the context may be expanded * via @base if @vocab is set to null. + * @param {string[]} keys Optional set of keys from the context to expand. If left undefined, all + * keys in the context will be expanded. */ public expandPrefixedTerms(context: JsonLdContextNormalized, expandContentTypeToBase: boolean, keys?: string[]) { const contextRaw = context.getContextRaw(); @@ -284,6 +286,8 @@ Tried mapping ${key} to ${JSON.stringify(keyValue)}`, ERROR_CODES.INVALID_KEYWOR * @param {IJsonLdContextNormalizedRaw} contextBefore The context that may contain some protected terms. * @param {IJsonLdContextNormalizedRaw} contextAfter A new context that is being applied on the first one. * @param {IExpandOptions} expandOptions Options that are needed for any expansions during this validation. + * @param {string[]} keys Optional set of keys from the context to validate. If left undefined, all + * keys defined in contextAfter will be checked. */ public validateKeywordRedefinitions(contextBefore: IJsonLdContextNormalizedRaw, contextAfter: IJsonLdContextNormalizedRaw, @@ -304,7 +308,7 @@ Tried mapping ${key} to ${JSON.stringify(keyValue)}`, ERROR_CODES.INVALID_KEYWOR } // Error if they are not identical - if (!deepEqual(contextBefore[key], contextAfter[key])) { + if (!Util.deepEqual(contextBefore[key], contextAfter[key])) { throw new ErrorCoded(`Attempted to override the protected keyword ${key} from ${ JSON.stringify(Util.getContextValueId(contextBefore[key]))} to ${ JSON.stringify(Util.getContextValueId(contextAfter[key]))}`, diff --git a/lib/Util.ts b/lib/Util.ts index a930e2f..b3f9460 100644 --- a/lib/Util.ts +++ b/lib/Util.ts @@ -247,19 +247,27 @@ export class Util { public static isReservedInternalKeyword(key: string) { return key.startsWith('@__'); } -} -export const deepEqual = (object1: any, object2: any): boolean => { - const objKeys1 = Object.keys(object1); - const objKeys2 = Object.keys(object2); + /** + * Check if two objects are deepEqual to on another. + * @param object1 The first object to test. + * @param object2 The second object to test. + */ + public static deepEqual(object1: any, object2: any): boolean { + const objKeys1 = Object.keys(object1); + const objKeys2 = Object.keys(object2); - if (objKeys1.length !== objKeys2.length) return false; - return objKeys1.every((key) => { - const value1 = object1[key]; - const value2 = object2[key]; - return (value1 === value2) || (isObject(value1) && isObject(value2) && deepEqual(value1, value2)); - }); -}; -const isObject = (object: any) => { - return object !== null && typeof object === "object"; -}; + if (objKeys1.length !== objKeys2.length) return false; + return objKeys1.every((key) => { + const value1 = object1[key]; + const value2 = object2[key]; + return (value1 === value2) || ( + value1 !== null && + value2 !== null && + typeof value1 === "object" && + typeof value2 === "object" && + this.deepEqual(value1, value2) + ); + }); + }; +} diff --git a/test/ContextParser-test.ts b/test/ContextParser-test.ts index 236f189..e477dc2 100644 --- a/test/ContextParser-test.ts +++ b/test/ContextParser-test.ts @@ -3534,7 +3534,7 @@ Tried mapping @id to {}`, ERROR_CODES.KEYWORD_REDEFINITION)); }); }); - describe('#validateKeyowrdRedefinitions', () => { + describe('#validateKeywordRedefinitions', () => { it('should return true when validating over the same context', async () => { const context = new JsonLdContextNormalized({ '@base': 'http://base.org/', From b74c09194467e3ca02b92af90036585c4bb0b96b Mon Sep 17 00:00:00 2001 From: Jesse Wright <63333554+jeswr@users.noreply.github.com> Date: Tue, 31 Oct 2023 12:08:20 +0000 Subject: [PATCH 18/18] chore: add missing tsdoc --- lib/ContextParser.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/ContextParser.ts b/lib/ContextParser.ts index fb226c5..78c9860 100644 --- a/lib/ContextParser.ts +++ b/lib/ContextParser.ts @@ -599,6 +599,8 @@ must be one of ${Util.CONTAINERS.join(', ')}`, ERROR_CODES.INVALID_CONTAINER_MAP * @param {IJsonLdContextNormalizedRaw} context A context. * @param {IParseOptions} options Parsing options. * @return {IJsonLdContextNormalizedRaw} The mutated input context. + * @param {string[]} keys Optional set of keys from the context to parseInnerContexts of. If left undefined, all + * keys in the context will be iterated over. */ public async parseInnerContexts(context: IJsonLdContextNormalizedRaw, options: IParseOptions, keys?: string[]): Promise { for (const key of (keys ?? Object.keys(context))) {