UNPKG

jsonld-streaming-serializer

Version:
366 lines 14.9 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.JsonLdSerializer = void 0; const jsonld_context_parser_1 = require("jsonld-context-parser"); const SeparatorType_1 = require("./SeparatorType"); const Util_1 = require("./Util"); const readable_stream_1 = require("readable-stream"); /** * A stream transformer that transforms an {@link RDF.Stream} into a JSON-LD (text) stream. */ class JsonLdSerializer extends readable_stream_1.Transform { constructor(options = {}) { super({ objectMode: true }); this.indentation = 0; this.options = options; // Parse the context if (this.options.baseIRI && !this.options.context) { this.options.context = { '@base': this.options.baseIRI }; } if (this.options.context) { this.originalContext = this.options.context; this.context = new jsonld_context_parser_1.ContextParser().parse(this.options.context, { baseIRI: this.options.baseIRI }); } else { this.context = Promise.resolve(new jsonld_context_parser_1.JsonLdContextNormalized({})); } } /** * Parses the given text stream into a quad stream. * @param {NodeJS.EventEmitter} stream A text stream. * @return {NodeJS.EventEmitter} A quad stream. */ import(stream) { const output = new readable_stream_1.PassThrough({ objectMode: true }); stream.on('error', (error) => parsed.emit('error', error)); stream.on('data', (data) => output.push(data)); stream.on('end', () => output.push(null)); const parsed = output.pipe(new JsonLdSerializer(this.options)); return parsed; } /** * Transforms a quad into the text stream. * @param {Quad} quad An RDF quad. * @param {string} encoding An (ignored) encoding. * @param {module:stream.internal.TransformCallback} callback Callback that is invoked when the transformation is done * @private */ _transform(quad, encoding, callback) { this.context.then((context) => { this.transformQuad(quad, context); callback(); }).catch(callback); } /** * Construct a list in an RDF.Term object that can be used * inside a quad's object to write into the serializer * as a list using the @list keyword. * @param {RDF.Quad_Object[]} values A list of values, can be empty. * @return {RDF.Quad_Object} A term that should be used in the object position of the quad that is written to the serializer. */ async list(values) { const context = await this.context; return { '@list': values.map((value) => Util_1.Util.termToValue(value, context, this.options)), }; } /** * Called when the incoming stream is closed. * @param {module:stream.internal.TransformCallback} callback Callback that is invoked when the flushing is done. * @private */ _flush(callback) { // If the stream was empty, ensure that we push the opening array if (!this.opened) { this.pushDocumentStart(); } if (this.lastPredicate) { this.endPredicate(); } if (this.lastSubject) { this.endSubject(); } if (this.lastGraph && this.lastGraph.termType !== 'DefaultGraph') { this.endGraph(); } this.endDocument(); return callback(null, null); } /** * Transforms a quad into the text stream. * @param {Quad} quad An RDF quad. * @param {JsonLdContextNormalized} context A context for compacting. */ transformQuad(quad, context) { // Open the array before the first quad if (!this.opened) { this.pushDocumentStart(); } // Check if the subject equals the last named graph // In that case, we can reuse the already-existing @id node const lastGraphMatchesSubject = this.lastGraph && this.lastGraph.termType !== 'DefaultGraph' && this.lastGraph.equals(quad.subject); // Write graph if (!lastGraphMatchesSubject && (!this.lastGraph || !quad.graph.equals(this.lastGraph))) { // Check if the named graph equals the last subject // In that case, we can reuse the already-existing @id node let lastSubjectMatchesGraph = quad.graph.termType !== 'DefaultGraph' && this.lastSubject && this.lastSubject.equals(quad.graph); if (this.lastGraph) { if (this.lastGraph.termType !== 'DefaultGraph') { // The last graph was named this.endPredicate(); this.endSubject(); this.endGraph(true); lastSubjectMatchesGraph = false; // Special-case to avoid deeper nesting } else { // The last graph was default if (!lastSubjectMatchesGraph) { this.endPredicate(); this.endSubject(true); } else { this.endPredicate(true); this.lastSubject = null; } } } // Push the graph if (quad.graph.termType !== 'DefaultGraph') { if (!lastSubjectMatchesGraph) { this.pushId(quad.graph, true, context); } this.pushSeparator(this.options.space ? SeparatorType_1.SeparatorType.GRAPH_FIELD_NONCOMPACT : SeparatorType_1.SeparatorType.GRAPH_FIELD_COMPACT); this.indentation++; } this.lastGraph = quad.graph; } // Write subject if (!this.lastSubject || !quad.subject.equals(this.lastSubject)) { if (lastGraphMatchesSubject) { this.endPredicate(); this.endSubject(); this.indentation--; this.pushSeparator(SeparatorType_1.SeparatorType.ARRAY_END_COMMA); this.lastGraph = quad.graph; } else { if (this.lastSubject) { this.endPredicate(); this.endSubject(true); } // Open a new node for the new subject this.pushId(quad.subject, true, context); } this.lastSubject = quad.subject; } // Write predicate if (!this.lastPredicate || !quad.predicate.equals(this.lastPredicate)) { if (this.lastPredicate) { this.endPredicate(true); } // Open a new array for the new predicate this.pushPredicate(quad.predicate, context); } // Write the object value this.pushObject(quad.object, context); } pushDocumentStart() { this.opened = true; if (this.originalContext && !this.options.excludeContext) { this.pushSeparator(SeparatorType_1.SeparatorType.OBJECT_START); this.indentation++; this.pushSeparator(SeparatorType_1.SeparatorType.CONTEXT_FIELD); this.pushIndented(JSON.stringify(this.originalContext, null, this.options.space) + ','); this.pushSeparator(this.options.space ? SeparatorType_1.SeparatorType.GRAPH_FIELD_NONCOMPACT : SeparatorType_1.SeparatorType.GRAPH_FIELD_COMPACT); this.indentation++; } else { this.pushSeparator(SeparatorType_1.SeparatorType.ARRAY_START); this.indentation++; } } /** * Push the given term as an @id field. * @param {Term} term An RDF term. * @param startOnNewLine If `{` should start on a new line * @param {JsonLdContextNormalized} context The context. */ pushId(term, startOnNewLine, context) { if (term.termType === 'Quad') { this.pushNestedQuad(term, true, context); } else { const subjectValue = term.termType === 'BlankNode' ? '_:' + term.value : context.compactIri(term.value, false); if (startOnNewLine) { this.pushSeparator(SeparatorType_1.SeparatorType.OBJECT_START); } else { this.push(SeparatorType_1.SeparatorType.OBJECT_START.label); if (this.options.space) { this.push('\n'); } } this.indentation++; this.pushIndented(this.options.space ? `"@id": "${subjectValue}",` : `"@id":"${subjectValue}",`); } } /** * Push the given predicate field. * @param {Term} predicate An RDF term. * @param {JsonLdContextNormalized} context The context. */ pushPredicate(predicate, context) { let property = predicate.value; // Convert rdf:type into @type if not disabled. if (!this.options.useRdfType && property === Util_1.Util.RDF_TYPE) { property = '@type'; this.objectOptions = Object.assign(Object.assign({}, this.options), { compactIds: true, vocab: true }); } // Open array for following objects const compactedProperty = context.compactIri(property, true); this.pushIndented(this.options.space ? `"${compactedProperty}": [` : `"${compactedProperty}":[`); this.indentation++; this.lastPredicate = predicate; } /** * Push the given object value. * @param {Term} object An RDF term. * @param {JsonLdContextNormalized} context The context. */ pushObject(object, context) { // Add a comma if we already had an object for this predicate if (!this.hadObjectForPredicate) { this.hadObjectForPredicate = true; } else { this.pushSeparator(SeparatorType_1.SeparatorType.COMMA); } // Handle nested quad if (object.termType === 'Quad') { const lastLastSubject = this.lastSubject; const lastLastPredicate = this.lastPredicate; this.hadObjectForPredicate = false; this.pushNestedQuad(object, false, context); this.endSubject(false); // Terminate identifier node of nested quad again, since we won't attach additional information to it. this.hadObjectForPredicate = true; this.lastPredicate = lastLastPredicate; this.lastSubject = lastLastSubject; return; } // Convert the object into a value and push it let value; try { if (object['@list']) { value = object; } else { value = Util_1.Util.termToValue(object, context, this.objectOptions || this.options); } } catch (e) { return this.emit('error', e); } this.pushIndented(JSON.stringify(value, null, this.options.space)); } pushNestedQuad(nestedQuad, commaAfterSubject, context) { // Start a nested quad this.pushSeparator(SeparatorType_1.SeparatorType.OBJECT_START); this.indentation++; this.pushIndented(this.options.space ? `"@id": ` : `"@id":`, false); // Print the nested quad if (nestedQuad.graph.termType !== 'DefaultGraph') { this.emit('error', new Error(`Found a nested quad with the non-default graph: ${nestedQuad.graph.value}`)); } this.pushId(nestedQuad.subject, false, context); this.pushPredicate(nestedQuad.predicate, context); this.pushObject(nestedQuad.object, context); this.endPredicate(false); this.endSubject(commaAfterSubject); } endDocument() { this.opened = false; if (this.originalContext && !this.options.excludeContext) { this.indentation--; this.pushSeparator(SeparatorType_1.SeparatorType.ARRAY_END); this.indentation--; this.pushSeparator(SeparatorType_1.SeparatorType.OBJECT_END); } else { this.indentation--; this.pushSeparator(SeparatorType_1.SeparatorType.ARRAY_END); } } /** * Push the end of a predicate and reset the buffers. * @param {boolean} comma If a comma should be appended. */ endPredicate(comma) { // Close the predicate array this.indentation--; this.pushSeparator(comma ? SeparatorType_1.SeparatorType.ARRAY_END_COMMA : SeparatorType_1.SeparatorType.ARRAY_END); // Reset object buffer this.hadObjectForPredicate = false; this.objectOptions = null; // Reset predicate buffer this.lastPredicate = null; } /** * Push the end of a subject and reset the buffers. * @param {boolean} comma If a comma should be appended. */ endSubject(comma) { // Close the last subject's node; this.indentation--; this.pushSeparator(comma ? SeparatorType_1.SeparatorType.OBJECT_END_COMMA : SeparatorType_1.SeparatorType.OBJECT_END); // Reset subject buffer this.lastSubject = null; } /** * Push the end of a graph and reset the buffers. * @param {boolean} comma If a comma should be appended. */ endGraph(comma) { // Close the graph array this.indentation--; this.pushSeparator(SeparatorType_1.SeparatorType.ARRAY_END); // Close the graph id node this.indentation--; this.pushSeparator(comma ? SeparatorType_1.SeparatorType.OBJECT_END_COMMA : SeparatorType_1.SeparatorType.OBJECT_END); // Reset graph buffer this.lastGraph = null; } /** * Puh the given separator. * @param {SeparatorType} type A type of separator. */ pushSeparator(type) { this.pushIndented(type.label); } /** * An indentation-aware variant of {@link #push}. * All strings that are pushed here will be prefixed by {@link #indentation} amount of spaces. * @param {string} data A string. * @param pushNewLine If a newline should be pushed afterwards. */ pushIndented(data, pushNewLine = true) { const prefix = this.getIndentPrefix(); const lines = data.split('\n').map((line) => prefix + line).join('\n'); this.push(lines); if (this.options.space && pushNewLine) { this.push('\n'); } } /** * @return {string} Get the current indentation prefix based on {@link #indentation}. */ getIndentPrefix() { return this.options.space ? this.options.space.repeat(this.indentation) : ''; } } exports.JsonLdSerializer = JsonLdSerializer; //# sourceMappingURL=JsonLdSerializer.js.map