UNPKG

jsonld-streaming-parser

Version:
912 lines 44.2 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.Util = void 0; const jsonld_context_parser_1 = require("jsonld-context-parser"); const rdf_data_factory_1 = require("rdf-data-factory"); const EntryHandlerContainer_1 = require("./entryhandler/EntryHandlerContainer"); // tslint:disable-next-line:no-var-requires const canonicalizeJson = require('canonicalize'); /** * Utility functions and methods. */ class Util { constructor(options) { this.parsingContext = options.parsingContext; this.dataFactory = options.dataFactory || new rdf_data_factory_1.DataFactory(); this.rdfFirst = this.dataFactory.namedNode(Util.RDF + 'first'); this.rdfRest = this.dataFactory.namedNode(Util.RDF + 'rest'); this.rdfNil = this.dataFactory.namedNode(Util.RDF + 'nil'); this.rdfType = this.dataFactory.namedNode(Util.RDF + 'type'); this.rdfJson = this.dataFactory.namedNode(Util.RDF + 'JSON'); } /** * Helper function to get the value of a context entry, * or fallback to a certain value. * @param {JsonLdContextNormalized} context A JSON-LD context. * @param {string} contextKey A pre-defined JSON-LD key in context entries. * @param {string} key A context entry key. * @param {string} fallback A fallback value for when the given contextKey * could not be found in the value with the given key. * @return {string} The value of the given contextKey in the entry behind key in the given context, * or the given fallback value. */ static getContextValue(context, contextKey, key, fallback) { const entry = context.getContextRaw()[key]; if (!entry) { return fallback; } const type = entry[contextKey]; return type === undefined ? fallback : type; } /** * Get the container type of the given key in the context. * * Should any context-scoping bugs should occur related to this in the future, * it may be required to increase the offset from the depth at which the context is retrieved by one (to 2). * This is because containers act 2 levels deep. * * @param {JsonLdContextNormalized} context A JSON-LD context. * @param {string} key A context entry key. * @return {string} The container type. */ static getContextValueContainer(context, key) { return Util.getContextValue(context, '@container', key, { '@set': true }); } /** * Get the value type of the given key in the context. * @param {JsonLdContextNormalized} context A JSON-LD context. * @param {string} key A context entry key. * @return {string} The node type. */ static getContextValueType(context, key) { const valueType = Util.getContextValue(context, '@type', key, null); if (valueType === '@none') { return null; } return valueType; } /** * Get the language of the given key in the context. * @param {JsonLdContextNormalized} context A JSON-LD context. * @param {string} key A context entry key. * @return {string} The node type. */ static getContextValueLanguage(context, key) { return Util.getContextValue(context, '@language', key, context.getContextRaw()['@language'] || null); } /** * Get the direction of the given key in the context. * @param {JsonLdContextNormalized} context A JSON-LD context. * @param {string} key A context entry key. * @return {string} The node type. */ static getContextValueDirection(context, key) { return Util.getContextValue(context, '@direction', key, context.getContextRaw()['@direction'] || null); } /** * Check if the given key in the context is a reversed property. * @param {JsonLdContextNormalized} context A JSON-LD context. * @param {string} key A context entry key. * @return {boolean} If the context value has a @reverse key. */ static isContextValueReverse(context, key) { return !!Util.getContextValue(context, '@reverse', key, null); } /** * Get the @index of the given key in the context. * @param {JsonLdContextNormalized} context A JSON-LD context. * @param {string} key A context entry key. * @return {string} The index. */ static getContextValueIndex(context, key) { return Util.getContextValue(context, '@index', key, context.getContextRaw()['@index'] || null); } /** * Check if the given key refers to a reversed property. * @param {JsonLdContextNormalized} context A JSON-LD context. * @param {string} key The property key. * @param {string} parentKey The parent key. * @return {boolean} If the property must be reversed. */ static isPropertyReverse(context, key, parentKey) { // '!==' is needed because reversed properties in a @reverse container should cancel each other out. return parentKey === '@reverse' !== Util.isContextValueReverse(context, key); } /** * Check if the given key exists inside an embedded node as direct child. * @param {string} parentKey The parent key. * @return {boolean} If the property is embedded. */ static isPropertyInEmbeddedNode(parentKey) { return parentKey === '@id'; } /** * Check if the given key exists inside an annotation object as direct child. * @param {string} parentKey The parent key. * @return {boolean} If the property is an annotation. */ static isPropertyInAnnotationObject(parentKey) { return parentKey === '@annotation'; } /** * Check if the given IRI is valid. * @param {string} iri A potential IRI. * @return {boolean} If the given IRI is valid. */ static isValidIri(iri) { return iri !== null && jsonld_context_parser_1.Util.isValidIri(iri); } /** * Check if the given first array (needle) is a prefix of the given second array (haystack). * @param needle An array to check if it is a prefix. * @param haystack An array to look in. */ static isPrefixArray(needle, haystack) { if (needle.length > haystack.length) { return false; } for (let i = 0; i < needle.length; i++) { if (needle[i] !== haystack[i]) { return false; } } return true; } /** * Make sure that @id-@index pairs are equal over all array values. * Reject otherwise. * @param {any[]} value An array value. * @return {Promise<void>} A promise rejecting if conflicts are present. */ async validateValueIndexes(value) { if (this.parsingContext.validateValueIndexes) { const indexHashes = {}; for (const entry of value) { if (entry && typeof entry === 'object') { const id = entry['@id']; const index = entry['@index']; if (id && index) { const existingIndexValue = indexHashes[id]; if (existingIndexValue && existingIndexValue !== index) { throw new jsonld_context_parser_1.ErrorCoded(`Conflicting @index value for ${id}`, jsonld_context_parser_1.ERROR_CODES.CONFLICTING_INDEXES); } indexHashes[id] = index; } } } } } /** * Convert a given JSON value to an RDF term. * @param {JsonLdContextNormalized} context A JSON-LD context. * @param {string} key The current JSON key. * @param value A JSON value. * @param {number} depth The depth the value is at. * @param {string[]} keys The path of keys. * @return {Promise<RDF.Term[]>} An RDF term array. */ async valueToTerm(context, key, value, depth, keys) { // Skip further processing if we have an @type: @json if (Util.getContextValueType(context, key) === '@json') { return [this.dataFactory.literal(this.valueToJsonString(value), this.rdfJson)]; } const type = typeof value; switch (type) { case 'object': // Skip if we have a null or undefined object if (value === null || value === undefined) { return []; } // Special case for arrays if (Array.isArray(value)) { // We handle arrays at value level so we can emit earlier, so this is handled already when we get here. // Empty context-based lists are emitted at this place, because our streaming algorithm doesn't detect those. if ('@list' in Util.getContextValueContainer(context, key)) { if (value.length === 0) { return [this.rdfNil]; } else { return this.parsingContext.idStack[depth + 1] || []; } } await this.validateValueIndexes(value); return []; } // Handle property-scoped contexts context = await this.getContextSelfOrPropertyScoped(context, key); // Handle local context in the value if ('@context' in value) { context = await this.parsingContext.parseContext(value['@context'], (await this.parsingContext.getContext(keys, 0)).getContextRaw()); } // In all other cases, we have a hash value = await this.unaliasKeywords(value, keys, depth, context); // Un-alias potential keywords in this hash if ('@value' in value) { let val; let valueLanguage; let valueDirection; let valueType; let valueIndex; // We don't use the index, but we need to check its type for spec-compliance for (key in value) { const subValue = value[key]; switch (key) { case '@value': val = subValue; break; case '@language': valueLanguage = subValue; break; case '@direction': valueDirection = subValue; break; case '@type': valueType = subValue; break; case '@index': valueIndex = subValue; break; case '@annotation': // This keyword is allowed, but is processed like normal nodes break; default: throw new jsonld_context_parser_1.ErrorCoded(`Unknown value entry '${key}' in @value: ${JSON.stringify(value)}`, jsonld_context_parser_1.ERROR_CODES.INVALID_VALUE_OBJECT); } } // Skip further processing if we have an @type: @json if (await this.unaliasKeyword(valueType, keys, depth, true, context) === '@json') { return [this.dataFactory.literal(this.valueToJsonString(val), this.rdfJson)]; } // Validate @value if (val === null) { return []; } if (typeof val === 'object') { throw new jsonld_context_parser_1.ErrorCoded(`The value of an '@value' can not be an object, got '${JSON.stringify(val)}'`, jsonld_context_parser_1.ERROR_CODES.INVALID_VALUE_OBJECT_VALUE); } // Validate @index if (this.parsingContext.validateValueIndexes && valueIndex && typeof valueIndex !== 'string') { throw new jsonld_context_parser_1.ErrorCoded(`The value of an '@index' must be a string, got '${JSON.stringify(valueIndex)}'`, jsonld_context_parser_1.ERROR_CODES.INVALID_INDEX_VALUE); } // Validate @language and @direction if (valueLanguage) { if (typeof val !== 'string') { throw new jsonld_context_parser_1.ErrorCoded(`When an '@language' is set, the value of '@value' must be a string, got '${JSON.stringify(val)}'`, jsonld_context_parser_1.ERROR_CODES.INVALID_LANGUAGE_TAGGED_VALUE); } if (!jsonld_context_parser_1.ContextParser.validateLanguage(valueLanguage, this.parsingContext.strictValues, jsonld_context_parser_1.ERROR_CODES.INVALID_LANGUAGE_TAGGED_STRING)) { return []; } // Language tags are always normalized to lowercase in 1.0. if (this.parsingContext.normalizeLanguageTags || this.parsingContext.activeProcessingMode === 1.0) { valueLanguage = valueLanguage.toLowerCase(); } } if (valueDirection) { if (typeof val !== 'string') { throw new Error(`When an '@direction' is set, the value of '@value' must be a string, got '${JSON.stringify(val)}'`); } if (!jsonld_context_parser_1.ContextParser.validateDirection(valueDirection, this.parsingContext.strictValues)) { return []; } } // Check @language and @direction if (valueLanguage && valueDirection) { if (valueType) { throw new jsonld_context_parser_1.ErrorCoded(`Can not have '@language', '@direction' and '@type' in a value: '${JSON .stringify(value)}'`, jsonld_context_parser_1.ERROR_CODES.INVALID_VALUE_OBJECT); } return this.nullableTermToArray(this .createLanguageDirectionLiteral(depth, val, valueLanguage, valueDirection)); } else if (valueLanguage) { // Check @language if (valueType) { throw new jsonld_context_parser_1.ErrorCoded(`Can not have both '@language' and '@type' in a value: '${JSON.stringify(value)}'`, jsonld_context_parser_1.ERROR_CODES.INVALID_VALUE_OBJECT); } return [this.dataFactory.literal(val, valueLanguage)]; } else if (valueDirection) { // Check @direction if (valueType) { throw new jsonld_context_parser_1.ErrorCoded(`Can not have both '@direction' and '@type' in a value: '${JSON.stringify(value)}'`, jsonld_context_parser_1.ERROR_CODES.INVALID_VALUE_OBJECT); } return this.nullableTermToArray(this .createLanguageDirectionLiteral(depth, val, valueLanguage, valueDirection)); } else if (valueType) { // Validate @type if (typeof valueType !== 'string') { throw new jsonld_context_parser_1.ErrorCoded(`The value of an '@type' must be a string, got '${JSON.stringify(valueType)}'`, jsonld_context_parser_1.ERROR_CODES.INVALID_TYPED_VALUE); } const typeTerm = this.createVocabOrBaseTerm(context, valueType); if (!typeTerm) { throw new jsonld_context_parser_1.ErrorCoded(`Invalid '@type' value, got '${JSON.stringify(valueType)}'`, jsonld_context_parser_1.ERROR_CODES.INVALID_TYPED_VALUE); } if (typeTerm.termType !== 'NamedNode') { throw new jsonld_context_parser_1.ErrorCoded(`Illegal value type (${typeTerm.termType}): ${valueType}`, jsonld_context_parser_1.ERROR_CODES.INVALID_TYPED_VALUE); } return [this.dataFactory.literal(val, typeTerm)]; } // We don't pass the context, because context-based things like @language should be ignored return await this.valueToTerm(new jsonld_context_parser_1.JsonLdContextNormalized({}), key, val, depth, keys); } else if ('@set' in value) { // No other entries are allow in this value if (Object.keys(value).length > 1) { throw new jsonld_context_parser_1.ErrorCoded(`Found illegal neighbouring entries next to @set for key: '${key}'`, jsonld_context_parser_1.ERROR_CODES.INVALID_SET_OR_LIST_OBJECT); } // No need to do anything here, this is handled at the deeper level. return []; } else if ('@list' in value) { // No other entries are allowed in this value if (Object.keys(value).length > 1) { throw new jsonld_context_parser_1.ErrorCoded(`Found illegal neighbouring entries next to @list for key: '${key}'`, jsonld_context_parser_1.ERROR_CODES.INVALID_SET_OR_LIST_OBJECT); } const listValue = value["@list"]; // We handle lists at value level so we can emit earlier, so this is handled already when we get here. // Empty anonymous lists are emitted at this place, because our streaming algorithm doesn't detect those. if (Array.isArray(listValue)) { if (listValue.length === 0) { return [this.rdfNil]; } else { return this.parsingContext.idStack[depth + 1] || []; } } else { // We only have a single list element here, so emit this directly as single element return await this.valueToTerm(await this.parsingContext.getContext(keys), key, listValue, depth - 1, keys.slice(0, -1)); } } else if ('@reverse' in value && typeof value['@reverse'] === 'boolean') { // We handle reverse properties at value level so we can emit earlier, // so this is handled already when we get here. return []; } else if ('@graph' in Util.getContextValueContainer(await this.parsingContext.getContext(keys), key)) { // We are processing a graph container const graphContainerEntries = this.parsingContext.graphContainerTermStack[depth + 1]; return graphContainerEntries ? Object.values(graphContainerEntries) : [this.dataFactory.blankNode()]; } else if ("@id" in value) { // Use deeper context if the value node contains other properties next to @id. if (Object.keys(value).length > 1) { context = await this.parsingContext.getContext(keys, 0); } // Handle local context in the value if ('@context' in value) { context = await this.parsingContext.parseContext(value['@context'], context.getContextRaw()); } if (value["@type"] === '@vocab') { return this.nullableTermToArray(this.createVocabOrBaseTerm(context, value["@id"])); } else { const valueId = value["@id"]; let valueTerm; if (typeof valueId === 'object') { if (this.parsingContext.rdfstar) { valueTerm = this.parsingContext.idStack[depth + 1][0]; } else { throw new jsonld_context_parser_1.ErrorCoded(`Found illegal @id '${value}'`, jsonld_context_parser_1.ERROR_CODES.INVALID_ID_VALUE); } } else { valueTerm = this.resourceToTerm(context, valueId); } return this.nullableTermToArray(valueTerm); } } else { // Only make a blank node if at least one triple was emitted at the value's level. if (this.parsingContext.emittedStack[depth + 1] || (value && typeof value === 'object' && Object.keys(value).length === 0)) { return (this.parsingContext.idStack[depth + 1] || (this.parsingContext.idStack[depth + 1] = [this.dataFactory.blankNode()])); } else { return []; } } case 'string': return this.nullableTermToArray(this.stringValueToTerm(depth, await this.getContextSelfOrPropertyScoped(context, key), key, value, null)); case 'boolean': return this.nullableTermToArray(this.stringValueToTerm(depth, await this.getContextSelfOrPropertyScoped(context, key), key, Boolean(value).toString(), this.dataFactory.namedNode(Util.XSD_BOOLEAN))); case 'number': return this.nullableTermToArray(this.stringValueToTerm(depth, await this.getContextSelfOrPropertyScoped(context, key), key, value, this.dataFactory.namedNode(value % 1 === 0 && value < 1e21 ? Util.XSD_INTEGER : Util.XSD_DOUBLE))); default: this.parsingContext.emitError(new Error(`Could not determine the RDF type of a ${type}`)); return []; } } /** * If the context defines a property-scoped context for the given key, * that context will be returned. * Otherwise, the given context will be returned as-is. * * This should be used for valueToTerm cases that are not objects. * @param context A context. * @param key A JSON key. */ async getContextSelfOrPropertyScoped(context, key) { const contextKeyEntry = context.getContextRaw()[key]; if (contextKeyEntry && typeof contextKeyEntry === 'object' && '@context' in contextKeyEntry) { context = await this.parsingContext.parseContext(contextKeyEntry, context.getContextRaw(), true); } return context; } /** * If the given term is null, return an empty array, otherwise return an array with the single given term. * @param term A term. */ nullableTermToArray(term) { return term ? [term] : []; } /** * Convert a given JSON key to an RDF predicate term, * based on @vocab. * @param {JsonLdContextNormalized} context A JSON-LD context. * @param key A JSON key. * @return {RDF.NamedNode} An RDF named node. */ predicateToTerm(context, key) { const expanded = context.expandTerm(key, true, this.parsingContext.getExpandOptions()); // Immediately return if the predicate was disabled in the context if (!expanded) { return null; } // Check if the predicate is a blank node if (expanded[0] === '_' && expanded[1] === ':') { if (this.parsingContext.produceGeneralizedRdf) { return this.dataFactory.blankNode(expanded.substr(2)); } else { return null; } } // Check if the predicate is a valid IRI if (Util.isValidIri(expanded)) { return this.dataFactory.namedNode(expanded); } else { if (expanded && this.parsingContext.strictValues) { this.parsingContext.emitError(new jsonld_context_parser_1.ErrorCoded(`Invalid predicate IRI: ${expanded}`, jsonld_context_parser_1.ERROR_CODES.INVALID_IRI_MAPPING)); } else { return null; } } return null; } /** * Convert a given JSON key to an RDF resource term or blank node, * based on @base. * @param {JsonLdContextNormalized} context A JSON-LD context. * @param key A JSON key. * @return {RDF.NamedNode} An RDF named node or null. */ resourceToTerm(context, key) { if (key.startsWith('_:')) { return this.dataFactory.blankNode(key.substr(2)); } const iri = context.expandTerm(key, false, this.parsingContext.getExpandOptions()); if (!Util.isValidIri(iri)) { if (iri && this.parsingContext.strictValues) { this.parsingContext.emitError(new Error(`Invalid resource IRI: ${iri}`)); } else { return null; } } return this.dataFactory.namedNode(iri); } /** * Convert a given JSON key to an RDF resource term. * It will do this based on the @vocab, * and fallback to @base. * @param {JsonLdContextNormalized} context A JSON-LD context. * @param key A JSON key. * @return {RDF.NamedNode} An RDF named node or null. */ createVocabOrBaseTerm(context, key) { if (key.startsWith('_:')) { return this.dataFactory.blankNode(key.substr(2)); } const expandOptions = this.parsingContext.getExpandOptions(); let expanded = context.expandTerm(key, true, expandOptions); if (expanded === key) { expanded = context.expandTerm(key, false, expandOptions); } if (!Util.isValidIri(expanded)) { if (expanded && this.parsingContext.strictValues && !expanded.startsWith('@')) { this.parsingContext.emitError(new Error(`Invalid term IRI: ${expanded}`)); } else { return null; } } return this.dataFactory.namedNode(expanded); } /** * Ensure that the given value becomes a string. * @param {string | number} value A string or number. * @param {NamedNode} datatype The intended datatype. * @return {string} The returned string. */ intToString(value, datatype) { if (typeof value === 'number') { if (Number.isFinite(value)) { const isInteger = value % 1 === 0; if (isInteger && (!datatype || datatype.value !== Util.XSD_DOUBLE)) { return Number(value).toString(); } else { return value.toExponential(15).replace(/(\d)0*e\+?/, '$1E'); } } else { return value > 0 ? 'INF' : '-INF'; } } else { return value; } } /** * Convert a given JSON string value to an RDF term. * @param {number} depth The current stack depth. * @param {JsonLdContextNormalized} context A JSON-LD context. * @param {string} key The current JSON key. * @param {string} value A JSON value. * @param {NamedNode} defaultDatatype The default datatype for the given value. * @return {RDF.Term} An RDF term or null. */ stringValueToTerm(depth, context, key, value, defaultDatatype) { // Check the datatype from the context const contextType = Util.getContextValueType(context, key); if (contextType) { if (contextType === '@id') { if (!defaultDatatype) { return this.resourceToTerm(context, this.intToString(value, defaultDatatype)); } } else if (contextType === '@vocab') { if (!defaultDatatype) { return this.createVocabOrBaseTerm(context, this.intToString(value, defaultDatatype)); } } else { defaultDatatype = this.dataFactory.namedNode(contextType); } } // If we don't find such a datatype, check the language from the context if (!defaultDatatype) { const contextLanguage = Util.getContextValueLanguage(context, key); const contextDirection = Util.getContextValueDirection(context, key); if (contextDirection) { return this.createLanguageDirectionLiteral(depth, this.intToString(value, defaultDatatype), contextLanguage, contextDirection); } else { return this.dataFactory.literal(this.intToString(value, defaultDatatype), contextLanguage); } } // If all else fails, make a literal based on the default content type return this.dataFactory.literal(this.intToString(value, defaultDatatype), defaultDatatype); } /** * Create a literal for the given value with the given language and direction. * Auxiliary quads may be emitted. * @param {number} depth The current stack depth. * @param {string} value A string value. * @param {string} language A language tag. * @param {string} direction A direction. * @return {Term} An RDF term. */ createLanguageDirectionLiteral(depth, value, language, direction) { if (this.parsingContext.rdfDirection === 'i18n-datatype') { // Create a datatyped literal, by encoding the language and direction into https://www.w3.org/ns/i18n#. if (!language) { language = ''; } return this.dataFactory.literal(value, this.dataFactory.namedNode(`https://www.w3.org/ns/i18n#${language}_${direction}`)); } else if (this.parsingContext.rdfDirection === 'compound-literal') { // Reify the literal. const valueNode = this.dataFactory.blankNode(); const graph = this.getDefaultGraph(); this.parsingContext.emitQuad(depth, this.dataFactory.quad(valueNode, this.dataFactory.namedNode(Util.RDF + 'value'), this.dataFactory.literal(value), graph)); if (language) { this.parsingContext.emitQuad(depth, this.dataFactory.quad(valueNode, this.dataFactory.namedNode(Util.RDF + 'language'), this.dataFactory.literal(language), graph)); } this.parsingContext.emitQuad(depth, this.dataFactory.quad(valueNode, this.dataFactory.namedNode(Util.RDF + 'direction'), this.dataFactory.literal(direction), graph)); return valueNode; } else { return this.dataFactory.literal(value, { language: language || '', direction: direction }); } } /** * Stringify the given JSON object to a canonical JSON string. * @param value Any valid JSON value. * @return {string} A canonical JSON string. */ valueToJsonString(value) { return canonicalizeJson(value); } /** * If the key is not a keyword, try to check if it is an alias for a keyword, * and if so, un-alias it. * @param {string} key A key, can be falsy. * @param {string[]} keys The path of keys. * @param {number} depth The depth to * @param {boolean} disableCache If the cache should be disabled * @param {JsonLdContextNormalized} context A context to unalias with, * will fallback to retrieving the context for the given keys. * @return {Promise<string>} A promise resolving to the key itself, or another key. */ async unaliasKeyword(key, keys, depth, disableCache, context) { // Numbers can not be an alias if (Number.isInteger(key)) { return key; } // Try to grab from cache if it was already un-aliased before. if (!disableCache) { const cachedUnaliasedKeyword = this.parsingContext.unaliasedKeywordCacheStack[depth]; if (cachedUnaliasedKeyword) { return cachedUnaliasedKeyword; } } if (!jsonld_context_parser_1.Util.isPotentialKeyword(key)) { context = context || await this.parsingContext.getContext(keys); let unliased = context.getContextRaw()[key]; if (unliased && typeof unliased === 'object') { unliased = unliased['@id']; } if (jsonld_context_parser_1.Util.isValidKeyword(unliased)) { key = unliased; } } return disableCache ? key : (this.parsingContext.unaliasedKeywordCacheStack[depth] = key); } /** * Unalias the keyword of the parent. * This adds a safety check if no parent exist. * @param {any[]} keys A stack of keys. * @param {number} depth The current depth. * @return {Promise<any>} A promise resolving to the parent key, or another key. */ async unaliasKeywordParent(keys, depth) { return await this.unaliasKeyword(depth > 0 && keys[depth - 1], keys, depth - 1); } /** * Un-alias all keywords in the given hash. * @param {{[p: string]: any}} hash A hash object. * @param {string[]} keys The path of keys. * @param {number} depth The depth. * @param {JsonLdContextNormalized} context A context to unalias with, * will fallback to retrieving the context for the given keys. * @return {Promise<{[p: string]: any}>} A promise resolving to the new hash. */ async unaliasKeywords(hash, keys, depth, context) { const newHash = {}; for (const key in hash) { newHash[await this.unaliasKeyword(key, keys, depth + 1, true, context)] = hash[key]; } return newHash; } /** * Check if we are processing a literal (including JSON literals) at the given depth. * This will also check higher levels, * because if a parent is a literal, * then the deeper levels are definitely a literal as well. * @param {any[]} keys The keys. * @param {number} depth The depth. * @return {boolean} If we are processing a literal. */ async isLiteral(keys, depth) { for (let i = depth; i >= 0; i--) { if (await this.unaliasKeyword(keys[i], keys, i) === '@annotation') { // Literals may have annotations, which require processing of inner nodes. return false; } if (this.parsingContext.literalStack[i] || this.parsingContext.jsonLiteralStack[i]) { return true; } } return false; } /** * Check how many parents should be skipped for checking the @graph for the given node. * * @param {number} depth The depth of the node. * @param {any[]} keys An array of keys. * @return {number} The graph depth offset. */ async getDepthOffsetGraph(depth, keys) { for (let i = depth - 1; i > 0; i--) { if (await this.unaliasKeyword(keys[i], keys, i) === '@graph') { // Skip further processing if we are already in an @graph-@id or @graph-@index container const containers = (await EntryHandlerContainer_1.EntryHandlerContainer.getContainerHandler(this.parsingContext, keys, i)).containers; if (EntryHandlerContainer_1.EntryHandlerContainer.isComplexGraphContainer(containers)) { return -1; } return depth - i - 1; } } return -1; } /** * Check if the given subject is of a valid type. * This should be called when applying @reverse'd properties. * @param {Term} subject A subject. */ validateReverseSubject(subject) { if (subject.termType === 'Literal') { throw new jsonld_context_parser_1.ErrorCoded(`Found illegal literal in subject position: ${subject.value}`, jsonld_context_parser_1.ERROR_CODES.INVALID_REVERSE_PROPERTY_VALUE); } } /** * Get the default graph. * @return {Term} An RDF term. */ getDefaultGraph() { return this.parsingContext.defaultGraph || this.dataFactory.defaultGraph(); } /** * Get the current graph, while taking into account a graph that can be defined via @container: @graph. * If not within a graph container, the default graph will be returned. * @param keys The current keys. * @param depth The current depth. */ async getGraphContainerValue(keys, depth) { // Default to default graph let graph = this.getDefaultGraph(); // Check if we are in an @container: @graph. const { containers, depth: depthContainer } = await EntryHandlerContainer_1.EntryHandlerContainer .getContainerHandler(this.parsingContext, keys, depth); if ('@graph' in containers) { // Get the graph from the stack. const graphContainerIndex = EntryHandlerContainer_1.EntryHandlerContainer.getContainerGraphIndex(containers, depthContainer, keys); const entry = this.parsingContext.graphContainerTermStack[depthContainer]; graph = entry ? entry[graphContainerIndex] : null; // Set the graph in the stack if none has been set yet. if (!graph) { let graphId = null; if ('@id' in containers) { const keyUnaliased = await this.getContainerKey(keys[depthContainer], keys, depthContainer); if (keyUnaliased !== null) { graphId = await this.resourceToTerm(await this.parsingContext.getContext(keys), keyUnaliased); } } if (!graphId) { graphId = this.dataFactory.blankNode(); } if (!this.parsingContext.graphContainerTermStack[depthContainer]) { this.parsingContext.graphContainerTermStack[depthContainer] = {}; } graph = this.parsingContext.graphContainerTermStack[depthContainer][graphContainerIndex] = graphId; } } return graph; } /** * Get the properties depth for retrieving properties. * * Typically, the properties depth will be identical to the given depth. * * The following exceptions apply: * * When the parent is @reverse, the depth is decremented by one. * * When @nest parents are found, the depth is decremented by the number of @nest parents. * If in combination with the exceptions above an intermediary array is discovered, * the depth is also decremented by this number of arrays. * * @param keys The current key chain. * @param depth The current depth. */ async getPropertiesDepth(keys, depth) { let lastValidDepth = depth; for (let i = depth - 1; i > 0; i--) { if (typeof keys[i] !== 'number') { // Skip array keys const parentKey = await this.unaliasKeyword(keys[i], keys, i); if (parentKey === '@reverse') { return i; } else if (parentKey === '@nest') { lastValidDepth = i; } else { return lastValidDepth; } } } return lastValidDepth; } /** * Get the key for the current container entry. * @param key A key, can be falsy. * @param keys The key chain. * @param depth The current depth to get the key from. * @return Promise resolving to the key. * Null will be returned for @none entries, with aliasing taken into account. */ async getContainerKey(key, keys, depth) { const keyUnaliased = await this.unaliasKeyword(key, keys, depth); return keyUnaliased === '@none' ? null : keyUnaliased; } /** * Check if no reverse properties are present in embedded nodes. * @param key The current key. * @param reverse If a reverse property is active. * @param isEmbedded If we're in an embedded node. */ validateReverseInEmbeddedNode(key, reverse, isEmbedded) { if (isEmbedded && reverse && !this.parsingContext.rdfstarReverseInEmbedded) { throw new jsonld_context_parser_1.ErrorCoded(`Illegal reverse property in embedded node in ${key}`, jsonld_context_parser_1.ERROR_CODES.INVALID_EMBEDDED_NODE); } } /** * Emit a quad, with checks. * @param depth The current depth. * @param subject S * @param predicate P * @param object O * @param graph G * @param reverse If a reverse property is active. * @param isEmbedded If we're in an embedded node. */ emitQuadChecked(depth, subject, predicate, object, graph, reverse, isEmbedded) { // Create a quad let quad; if (reverse) { this.validateReverseSubject(object); quad = this.dataFactory.quad(object, predicate, subject, graph); } else { quad = this.dataFactory.quad(subject, predicate, object, graph); } // Emit the quad, unless it was created in an embedded node if (isEmbedded) { // Embedded nodes don't inherit the active graph if (quad.graph.termType !== 'DefaultGraph') { quad = this.dataFactory.quad(quad.subject, quad.predicate, quad.object); } // Multiple embedded nodes are not allowed if (this.parsingContext.idStack[depth - 1]) { throw new jsonld_context_parser_1.ErrorCoded(`Illegal multiple properties in an embedded node`, jsonld_context_parser_1.ERROR_CODES.INVALID_EMBEDDED_NODE); } this.parsingContext.idStack[depth - 1] = [quad]; } else { this.parsingContext.emitQuad(depth, quad); } // Flush annotations const annotationsBuffer = this.parsingContext.annotationsBuffer[depth]; if (annotationsBuffer) { for (const annotation of annotationsBuffer) { this.emitAnnotation(depth, quad, annotation); } delete this.parsingContext.annotationsBuffer[depth]; } } // This is a separate function to enable recursion emitAnnotation(depth, quad, annotation) { // Construct annotation quad let annotationQuad; if (annotation.reverse) { this.validateReverseSubject(annotation.object); annotationQuad = this.dataFactory.quad(annotation.object, annotation.predicate, quad); } else { annotationQuad = this.dataFactory.quad(quad, annotation.predicate, annotation.object); } // Emit annotated quad this.parsingContext.emitQuad(depth, annotationQuad); // Also emit nested annotations for (const nestedAnnotation of annotation.nestedAnnotations) { this.emitAnnotation(depth, annotationQuad, nestedAnnotation); } } } exports.Util = Util; Util.XSD = 'http://www.w3.org/2001/XMLSchema#'; Util.XSD_BOOLEAN = Util.XSD + 'boolean'; Util.XSD_INTEGER = Util.XSD + 'integer'; Util.XSD_DOUBLE = Util.XSD + 'double'; Util.RDF = 'http://www.w3.org/1999/02/22-rdf-syntax-ns#'; //# sourceMappingURL=Util.js.map