jsonld-streaming-parser
Version:
A fast and lightweight streaming JSON-LD parser
348 lines • 15.9 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.ParsingContext = void 0;
const jsonld_context_parser_1 = require("jsonld-context-parser");
const ErrorCoded_1 = require("jsonld-context-parser/lib/ErrorCoded");
const ContextTree_1 = require("./ContextTree");
const JsonLdParser_1 = require("./JsonLdParser");
/**
* Data holder for parsing information.
*/
class ParsingContext {
constructor(options) {
// Initialize settings
this.contextParser = new jsonld_context_parser_1.ContextParser({ documentLoader: options.documentLoader, skipValidation: options.skipContextValidation });
this.streamingProfile = !!options.streamingProfile;
this.baseIRI = options.baseIRI;
this.produceGeneralizedRdf = !!options.produceGeneralizedRdf;
this.allowSubjectList = !!options.allowSubjectList;
this.processingMode = options.processingMode || JsonLdParser_1.JsonLdParser.DEFAULT_PROCESSING_MODE;
this.strictValues = !!options.strictValues;
this.validateValueIndexes = !!options.validateValueIndexes;
this.defaultGraph = options.defaultGraph;
this.rdfDirection = options.rdfDirection;
this.normalizeLanguageTags = options.normalizeLanguageTags;
this.streamingProfileAllowOutOfOrderPlainType = options.streamingProfileAllowOutOfOrderPlainType;
this.rdfstar = options.rdfstar !== false;
this.rdfstarReverseInEmbedded = options.rdfstarReverseInEmbedded;
this.topLevelProperties = false;
this.activeProcessingMode = parseFloat(this.processingMode);
// Initialize stacks
this.processingStack = [];
this.processingType = [];
this.emittedStack = [];
this.idStack = [];
this.graphStack = [];
this.graphContainerTermStack = [];
this.listPointerStack = [];
this.contextTree = new ContextTree_1.ContextTree();
this.literalStack = [];
this.validationStack = [];
this.unaliasedKeywordCacheStack = [];
this.jsonLiteralStack = [];
this.unidentifiedValuesBuffer = [];
this.unidentifiedGraphsBuffer = [];
this.annotationsBuffer = [];
this.pendingContainerFlushBuffers = [];
this.parser = options.parser;
if (options.context) {
this.rootContext = this.parseContext(options.context);
this.rootContext.then((context) => this.validateContext(context));
}
else {
this.rootContext = Promise.resolve(new jsonld_context_parser_1.JsonLdContextNormalized(this.baseIRI ? { '@base': this.baseIRI, '@__baseDocument': true } : {}));
}
}
/**
* Parse the given context with the configured options.
* @param {JsonLdContext} context A context to parse.
* @param {JsonLdContextNormalized} parentContext An optional parent context.
* @param {boolean} ignoreProtection If @protected term checks should be ignored.
* @return {Promise<JsonLdContextNormalized>} A promise resolving to the parsed context.
*/
async parseContext(context, parentContext, ignoreProtection) {
return this.contextParser.parse(context, {
baseIRI: this.baseIRI,
ignoreProtection,
normalizeLanguageTags: this.normalizeLanguageTags,
parentContext,
processingMode: this.activeProcessingMode,
});
}
/**
* Check if the given context is valid.
* If not, an error will be thrown.
* @param {JsonLdContextNormalized} context A context.
*/
validateContext(context) {
const activeVersion = context.getContextRaw()['@version'];
if (activeVersion) {
if (this.activeProcessingMode && activeVersion > this.activeProcessingMode) {
throw new ErrorCoded_1.ErrorCoded(`Unsupported JSON-LD version '${activeVersion}' under active processing mode ${this.activeProcessingMode}.`, ErrorCoded_1.ERROR_CODES.PROCESSING_MODE_CONFLICT);
}
else {
if (this.activeProcessingMode && activeVersion < this.activeProcessingMode) {
throw new ErrorCoded_1.ErrorCoded(`Invalid JSON-LD version ${activeVersion} under active processing mode ${this.activeProcessingMode}.`, ErrorCoded_1.ERROR_CODES.INVALID_VERSION_VALUE);
}
this.activeProcessingMode = activeVersion;
}
}
}
/**
* Get the context at the given path.
* @param {keys} keys The path of keys to get the context at.
* @param {number} offset The path offset, defaults to 1.
* @return {Promise<JsonLdContextNormalized>} A promise resolving to a context.
*/
async getContext(keys, offset = 1) {
const keysOriginal = keys;
// Ignore array keys at the end
while (typeof keys[keys.length - 1] === 'number') {
keys = keys.slice(0, keys.length - 1);
}
// Handle offset on keys
if (offset) {
keys = keys.slice(0, -offset);
}
// Determine the closest context
const contextData = await this.getContextPropagationAware(keys);
const context = contextData.context;
// Process property-scoped contexts (high-to-low)
let contextRaw = context.getContextRaw();
for (let i = contextData.depth; i < keysOriginal.length - offset; i++) {
const key = keysOriginal[i];
const contextKeyEntry = contextRaw[key];
if (contextKeyEntry && typeof contextKeyEntry === 'object' && '@context' in contextKeyEntry) {
const scopedContext = (await this.parseContext(contextKeyEntry, contextRaw, true)).getContextRaw();
const propagate = !(key in scopedContext)
|| scopedContext[key]['@context']['@propagate']; // Propagation is true by default
if (propagate !== false || i === keysOriginal.length - 1 - offset) {
contextRaw = Object.assign({}, scopedContext);
// Clean up final context
delete contextRaw['@propagate'];
contextRaw[key] = Object.assign({}, contextRaw[key]);
if ('@id' in contextKeyEntry) {
contextRaw[key]['@id'] = contextKeyEntry['@id'];
}
delete contextRaw[key]['@context'];
if (propagate !== false) {
this.contextTree.setContext(keysOriginal.slice(0, i + offset), Promise.resolve(new jsonld_context_parser_1.JsonLdContextNormalized(contextRaw)));
}
}
}
}
return new jsonld_context_parser_1.JsonLdContextNormalized(contextRaw);
}
/**
* Get the context at the given path.
* Non-propagating contexts will be skipped,
* unless the context at that exact depth is retrieved.
*
* This ONLY takes into account context propagation logic,
* so this should usually not be called directly,
* call {@link #getContext} instead.
*
* @param keys The path of keys to get the context at.
* @return {Promise<{ context: JsonLdContextNormalized, depth: number }>} A context and its depth.
*/
async getContextPropagationAware(keys) {
const originalDepth = keys.length;
let contextData = null;
let hasApplicablePropertyScopedContext;
do {
hasApplicablePropertyScopedContext = false;
if (contextData && '@__propagateFallback' in contextData.context.getContextRaw()) {
// If a propagation fallback context has been set,
// fallback to that context and retry for the same depth.
contextData.context = new jsonld_context_parser_1.JsonLdContextNormalized(contextData.context.getContextRaw()['@__propagateFallback']);
}
else {
if (contextData) {
// If we had a previous iteration, jump to the parent of context depth.
// We must do this because once we get here, last context had propagation disabled,
// so we check its first parent instead.
keys = keys.slice(0, contextData.depth - 1);
}
contextData = await this.contextTree.getContext(keys) || { context: await this.rootContext, depth: 0 };
}
// Allow non-propagating contexts to propagate one level deeper
// if it defines a property-scoped context that is applicable for the current key.
// @see https://w3c.github.io/json-ld-api/tests/toRdf-manifest#tc012
const lastKey = keys[keys.length - 1];
if (lastKey in contextData.context.getContextRaw()) {
const lastKeyValue = contextData.context.getContextRaw()[lastKey];
if (lastKeyValue && typeof lastKeyValue === 'object' && '@context' in lastKeyValue) {
hasApplicablePropertyScopedContext = true;
}
}
} while (contextData.depth > 0 // Root context has a special case
&& contextData.context.getContextRaw()['@propagate'] === false // Stop loop if propagation is true
&& contextData.depth !== originalDepth // Stop loop if requesting exact depth of non-propagating
&& !hasApplicablePropertyScopedContext);
// Special case for root context that does not allow propagation.
// Fallback to empty context in that case.
if (contextData.depth === 0
&& contextData.context.getContextRaw()['@propagate'] === false
&& contextData.depth !== originalDepth) {
contextData.context = new jsonld_context_parser_1.JsonLdContextNormalized({});
}
return contextData;
}
/**
* Start a new job for parsing the given value.
* @param {any[]} keys The stack of keys.
* @param value The value to parse.
* @param {number} depth The depth to parse at.
* @param {boolean} lastDepthCheck If the lastDepth check should be done for buffer draining.
* @return {Promise<void>} A promise resolving when the job is done.
*/
async newOnValueJob(keys, value, depth, lastDepthCheck) {
await this.parser.newOnValueJob(keys, value, depth, lastDepthCheck);
}
/**
* Flush the pending container flush buffers
* @return {boolean} If any pending buffers were flushed.
*/
async handlePendingContainerFlushBuffers() {
if (this.pendingContainerFlushBuffers.length > 0) {
for (const pendingFlushBuffer of this.pendingContainerFlushBuffers) {
await this.parser.flushBuffer(pendingFlushBuffer.depth, pendingFlushBuffer.keys);
this.parser.flushStacks(pendingFlushBuffer.depth);
}
this.pendingContainerFlushBuffers.splice(0, this.pendingContainerFlushBuffers.length);
return true;
}
else {
return false;
}
}
/**
* Emit the given quad into the output stream.
* @param {number} depth The depth the quad was generated at.
* @param {Quad} quad A quad to emit.
*/
emitQuad(depth, quad) {
if (depth === 1) {
this.topLevelProperties = true;
}
this.parser.push(quad);
}
/**
* Emit the given error into the output stream.
* @param {Error} error An error to emit.
*/
emitError(error) {
this.parser.emit('error', error);
}
/**
* Emit the given context into the output stream under the 'context' event.
* @param {JsonLdContext} context A context to emit.
*/
emitContext(context) {
this.parser.emit('context', context);
}
/**
* Safely get or create the depth value of {@link ParsingContext.unidentifiedValuesBuffer}.
* @param {number} depth A depth.
* @return {{predicate: Term; object: Term; reverse: boolean}[]} An element of
* {@link ParsingContext.unidentifiedValuesBuffer}.
*/
getUnidentifiedValueBufferSafe(depth) {
let buffer = this.unidentifiedValuesBuffer[depth];
if (!buffer) {
buffer = [];
this.unidentifiedValuesBuffer[depth] = buffer;
}
return buffer;
}
/**
* Safely get or create the depth value of {@link ParsingContext.unidentifiedGraphsBuffer}.
* @param {number} depth A depth.
* @return {{predicate: Term; object: Term; reverse: boolean}[]} An element of
* {@link ParsingContext.unidentifiedGraphsBuffer}.
*/
getUnidentifiedGraphBufferSafe(depth) {
let buffer = this.unidentifiedGraphsBuffer[depth];
if (!buffer) {
buffer = [];
this.unidentifiedGraphsBuffer[depth] = buffer;
}
return buffer;
}
/**
* Safely get or create the depth value of {@link ParsingContext.annotationsBuffer}.
* @param {number} depth A depth.
* @return {} An element of {@link ParsingContext.annotationsBuffer}.
*/
getAnnotationsBufferSafe(depth) {
let buffer = this.annotationsBuffer[depth];
if (!buffer) {
buffer = [];
this.annotationsBuffer[depth] = buffer;
}
return buffer;
}
/**
* @return IExpandOptions The expand options for the active processing mode.
*/
getExpandOptions() {
return ParsingContext.EXPAND_OPTIONS[this.activeProcessingMode];
}
/**
* Shift the stack at the given offset to the given depth.
*
* This will override anything in the stack at `depth`,
* and this will remove anything at `depth + depthOffset`
*
* @param depth The target depth.
* @param depthOffset The origin depth, relative to `depth`.
*/
shiftStack(depth, depthOffset) {
// Copy the id stack value up one level so that the next job can access the id.
const deeperIdStack = this.idStack[depth + depthOffset];
if (deeperIdStack) {
this.idStack[depth] = deeperIdStack;
this.emittedStack[depth] = true;
delete this.idStack[depth + depthOffset];
}
// Shorten key stack
if (this.pendingContainerFlushBuffers.length) {
for (const buffer of this.pendingContainerFlushBuffers) {
if (buffer.depth >= depth + depthOffset) {
buffer.depth -= depthOffset;
buffer.keys.splice(depth, depthOffset);
}
}
}
// Splice stacks
if (this.unidentifiedValuesBuffer[depth + depthOffset]) {
this.unidentifiedValuesBuffer[depth] = this.unidentifiedValuesBuffer[depth + depthOffset];
delete this.unidentifiedValuesBuffer[depth + depthOffset];
}
if (this.annotationsBuffer[depth + depthOffset - 1]) {
if (!this.annotationsBuffer[depth - 1]) {
this.annotationsBuffer[depth - 1] = [];
}
this.annotationsBuffer[depth - 1] = [
...this.annotationsBuffer[depth - 1],
...this.annotationsBuffer[depth + depthOffset - 1],
];
delete this.annotationsBuffer[depth + depthOffset - 1];
}
// TODO: also do the same for other stacks
}
}
exports.ParsingContext = ParsingContext;
ParsingContext.EXPAND_OPTIONS = {
1.0: {
allowPrefixForcing: false,
allowPrefixNonGenDelims: false,
allowVocabRelativeToBase: false,
},
1.1: {
allowPrefixForcing: true,
allowPrefixNonGenDelims: false,
allowVocabRelativeToBase: true,
},
};
//# sourceMappingURL=ParsingContext.js.map