jsonld-streaming-serializer
Version:
A fast and lightweight streaming JSON-LD serializer
366 lines • 14.9 kB
JavaScript
"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