Skip to content

Commit

Permalink
Merge 0af3349 into e3519d2
Browse files Browse the repository at this point in the history
  • Loading branch information
jeswr committed Oct 29, 2023
2 parents e3519d2 + 0af3349 commit 4481fdb
Show file tree
Hide file tree
Showing 3 changed files with 86 additions and 66 deletions.
146 changes: 86 additions & 60 deletions lib/ContextParser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -93,13 +106,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'] = <string> value['@reverse'];
if (Util.isPotentialKeyword(value['@reverse'])) {
delete value['@reverse'];
Expand All @@ -119,9 +133,14 @@ 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,
/* istanbul ignore next */
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.
Expand Down Expand Up @@ -162,27 +181,30 @@ 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) {
// Add an explicit @id value based on the expanded key value
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;
}
}
if (type && typeof type === 'string' && type !== '@vocab'
&& (!value['@container'] || !(<any> 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) {
Expand All @@ -209,7 +231,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};
}
}
}
}
Expand All @@ -226,16 +251,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;
}

/**
Expand All @@ -256,7 +282,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
Expand All @@ -265,7 +291,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};
}
}
}
Expand All @@ -283,7 +309,9 @@ Tried mapping ${key} to ${JSON.stringify(keyValue)}`, ERROR_CODES.INVALID_KEYWOR
*/
public validateKeywordRedefinitions(contextBefore: IJsonLdContextNormalizedRaw,
contextAfter: IJsonLdContextNormalizedRaw,
expandOptions: IExpandOptions) {
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
Expand All @@ -292,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]['@protected'] = true;
const valueAfter = canonicalizeJson(contextAfter[key]);
contextAfter[key] = {...contextAfter[key], '@protected': true};

// 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]))}`,
Expand Down Expand Up @@ -594,9 +618,14 @@ 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,
/* istanbul ignore next */
keys = Object.keys(context)
)
: Promise<IJsonLdContextNormalizedRaw> {
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) {
Expand All @@ -607,19 +636,17 @@ 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 });
} catch (e) {
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()}
}
}
}
Expand All @@ -633,17 +660,16 @@ must be one of ${Util.CONTAINERS.join(', ')}`, ERROR_CODES.INVALID_CONTAINER_MAP
* @return {Promise<JsonLdContextNormalized>} A promise resolving to the context.
*/
public async parse(context: JsonLdContext,
options: IParseOptions = {}): Promise<JsonLdContextNormalized> {
options: IParseOptions = {}, ioptions: { skipValidation?: boolean } = {}): Promise<JsonLdContextNormalized> {
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
Expand Down Expand Up @@ -705,6 +731,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 || {})));

Expand All @@ -718,10 +746,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 = <IJsonLdContextNormalizedRaw> JSON.parse(JSON.stringify(context)); // No better way in JS at the moment.
if (parentContext && !minimalProcessing) {
parentContext = <IJsonLdContextNormalizedRaw> JSON.parse(JSON.stringify(parentContext));
}
context = <IJsonLdContextNormalizedRaw> {...context};

// According to the JSON-LD spec, @base must be ignored from external contexts.
if (external) {
Expand All @@ -733,7 +758,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) {
Expand All @@ -760,46 +785,48 @@ 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 };
}

const newContextWrapped = new JsonLdContextNormalized(newContext);

// Parse inner contexts with minimal processing
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'] === '')) {
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);

}
}

// 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}`,
Expand All @@ -816,7 +843,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
Expand Down Expand Up @@ -863,8 +890,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<IJsonLdContextNormalizedRaw> {
// 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)) {
Expand All @@ -877,11 +904,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);
}

}
Expand Down Expand Up @@ -972,4 +999,3 @@ export interface IParseOptions {
*/
ignoreScopedContexts?: boolean;
}

1 change: 0 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
5 changes: 0 additions & 5 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down

0 comments on commit 4481fdb

Please sign in to comment.