jsonld-streaming-parser
Version:
A fast and lightweight streaming JSON-LD parser
912 lines • 44.2 kB
JavaScript
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
;