// **N3Writer** writes N3 documents. import namespaces from './IRIs'; import { default as N3DataFactory, Term } from './N3DataFactory'; import { isDefaultGraph } from './N3Util'; const DEFAULTGRAPH = N3DataFactory.defaultGraph(); const { rdf, xsd } = namespaces; // Characters in literals that require escaping const escape = /["\\\t\n\r\b\f\u0000-\u0019\ud800-\udbff]/, escapeAll = /["\\\t\n\r\b\f\u0000-\u0019]|[\ud800-\udbff][\udc00-\udfff]/g, escapedCharacters = { '\\': '\\\\', '"': '\\"', '\t': '\\t', '\n': '\\n', '\r': '\\r', '\b': '\\b', '\f': '\\f', }; // ## Placeholder class to represent already pretty-printed terms class SerializedTerm extends Term { // Pretty-printed nodes are not equal to any other node // (e.g., [] does not equal []) equals(other) { return other === this; } } // ## Constructor export default class N3Writer { constructor(outputStream, options) { // ### `_prefixRegex` matches a prefixed name or IRI that begins with one of the added prefixes this._prefixRegex = /$0^/; // Shift arguments if the first argument is not a stream if (outputStream && typeof outputStream.write !== 'function') options = outputStream, outputStream = null; options = options || {}; this._lists = options.lists; // If no output stream given, send the output as string through the end callback if (!outputStream) { let output = ''; this._outputStream = { write(chunk, encoding, done) { output += chunk; done && done(); }, end: done => { done && done(null, output); }, }; this._endStream = true; } else { this._outputStream = outputStream; this._endStream = options.end === undefined ? true : !!options.end; } // Initialize writer, depending on the format this._subject = null; if (!(/triple|quad/i).test(options.format)) { this._lineMode = false; this._graph = DEFAULTGRAPH; this._prefixIRIs = Object.create(null); options.prefixes && this.addPrefixes(options.prefixes); if (options.baseIRI) { this._baseMatcher = new RegExp(`^${escapeRegex(options.baseIRI) }${options.baseIRI.endsWith('/') ? '' : '[#?]'}`); this._baseLength = options.baseIRI.length; } } else { this._lineMode = true; this._writeQuad = this._writeQuadLine; } } // ## Private methods // ### Whether the current graph is the default graph get _inDefaultGraph() { return DEFAULTGRAPH.equals(this._graph); } // ### `_write` writes the argument to the output stream _write(string, callback) { this._outputStream.write(string, 'utf8', callback); } // ### `_writeQuad` writes the quad to the output stream _writeQuad(subject, predicate, object, graph, done) { try { // Write the graph's label if it has changed if (!graph.equals(this._graph)) { // Close the previous graph and start the new one this._write((this._subject === null ? '' : (this._inDefaultGraph ? '.\n' : '\n}\n')) + (DEFAULTGRAPH.equals(graph) ? '' : `${this._encodeIriOrBlank(graph)} {\n`)); this._graph = graph; this._subject = null; } // Don't repeat the subject if it's the same if (subject.equals(this._subject)) { // Don't repeat the predicate if it's the same if (predicate.equals(this._predicate)) this._write(`, ${this._encodeObject(object)}`, done); // Same subject, different predicate else this._write(`;\n ${ this._encodePredicate(this._predicate = predicate)} ${ this._encodeObject(object)}`, done); } // Different subject; write the whole quad else this._write(`${(this._subject === null ? '' : '.\n') + this._encodeSubject(this._subject = subject)} ${ this._encodePredicate(this._predicate = predicate)} ${ this._encodeObject(object)}`, done); } catch (error) { done && done(error); } } // ### `_writeQuadLine` writes the quad to the output stream as a single line _writeQuadLine(subject, predicate, object, graph, done) { // Write the quad without prefixes delete this._prefixMatch; this._write(this.quadToString(subject, predicate, object, graph), done); } // ### `quadToString` serializes a quad as a string quadToString(subject, predicate, object, graph) { return `${this._encodeSubject(subject)} ${ this._encodeIriOrBlank(predicate)} ${ this._encodeObject(object) }${graph && graph.value ? ` ${this._encodeIriOrBlank(graph)} .\n` : ' .\n'}`; } // ### `quadsToString` serializes an array of quads as a string quadsToString(quads) { return quads.map(t => { return this.quadToString(t.subject, t.predicate, t.object, t.graph); }).join(''); } // ### `_encodeSubject` represents a subject _encodeSubject(entity) { return entity.termType === 'Quad' ? this._encodeQuad(entity) : this._encodeIriOrBlank(entity); } // ### `_encodeIriOrBlank` represents an IRI or blank node _encodeIriOrBlank(entity) { // A blank node or list is represented as-is if (entity.termType !== 'NamedNode') { // If it is a list head, pretty-print it if (this._lists && (entity.value in this._lists)) entity = this.list(this._lists[entity.value]); return 'id' in entity ? entity.id : `_:${entity.value}`; } let iri = entity.value; // Use relative IRIs if requested and possible if (this._baseMatcher && this._baseMatcher.test(iri)) iri = iri.substr(this._baseLength); // Escape special characters if (escape.test(iri)) iri = iri.replace(escapeAll, characterReplacer); // Try to represent the IRI as prefixed name const prefixMatch = this._prefixRegex.exec(iri); return !prefixMatch ? `<${iri}>` : (!prefixMatch[1] ? iri : this._prefixIRIs[prefixMatch[1]] + prefixMatch[2]); } // ### `_encodeLiteral` represents a literal _encodeLiteral(literal) { // Escape special characters let value = literal.value; if (escape.test(value)) value = value.replace(escapeAll, characterReplacer); // Write a language-tagged literal if (literal.language) return `"${value}"@${literal.language}`; // Write dedicated literals per data type if (this._lineMode) { // Only abbreviate strings in N-Triples or N-Quads if (literal.datatype.value === xsd.string) return `"${value}"`; } else { // Use common datatype abbreviations in Turtle or TriG switch (literal.datatype.value) { case xsd.string: return `"${value}"`; case xsd.boolean: if (value === 'true' || value === 'false') return value; break; case xsd.integer: if (/^[+-]?\d+$/.test(value)) return value; break; case xsd.decimal: if (/^[+-]?\d*\.\d+$/.test(value)) return value; break; case xsd.double: if (/^[+-]?(?:\d+\.\d*|\.?\d+)[eE][+-]?\d+$/.test(value)) return value; break; } } // Write a regular datatyped literal return `"${value}"^^${this._encodeIriOrBlank(literal.datatype)}`; } // ### `_encodePredicate` represents a predicate _encodePredicate(predicate) { return predicate.value === rdf.type ? 'a' : this._encodeIriOrBlank(predicate); } // ### `_encodeObject` represents an object _encodeObject(object) { switch (object.termType) { case 'Quad': return this._encodeQuad(object); case 'Literal': return this._encodeLiteral(object); default: return this._encodeIriOrBlank(object); } } // ### `_encodeQuad` encodes an RDF* quad _encodeQuad({ subject, predicate, object, graph }) { return `<<${ this._encodeSubject(subject)} ${ this._encodePredicate(predicate)} ${ this._encodeObject(object)}${ isDefaultGraph(graph) ? '' : ` ${this._encodeIriOrBlank(graph)}`}>>`; } // ### `_blockedWrite` replaces `_write` after the writer has been closed _blockedWrite() { throw new Error('Cannot write because the writer has been closed.'); } // ### `addQuad` adds the quad to the output stream addQuad(subject, predicate, object, graph, done) { // The quad was given as an object, so shift parameters if (object === undefined) this._writeQuad(subject.subject, subject.predicate, subject.object, subject.graph, predicate); // The optional `graph` parameter was not provided else if (typeof graph === 'function') this._writeQuad(subject, predicate, object, DEFAULTGRAPH, graph); // The `graph` parameter was provided else this._writeQuad(subject, predicate, object, graph || DEFAULTGRAPH, done); } // ### `addQuads` adds the quads to the output stream addQuads(quads) { for (let i = 0; i < quads.length; i++) this.addQuad(quads[i]); } // ### `addPrefix` adds the prefix to the output stream addPrefix(prefix, iri, done) { const prefixes = {}; prefixes[prefix] = iri; this.addPrefixes(prefixes, done); } // ### `addPrefixes` adds the prefixes to the output stream addPrefixes(prefixes, done) { // Ignore prefixes if not supported by the serialization if (!this._prefixIRIs) return done && done(); // Write all new prefixes let hasPrefixes = false; for (let prefix in prefixes) { let iri = prefixes[prefix]; if (typeof iri !== 'string') iri = iri.value; hasPrefixes = true; // Finish a possible pending quad if (this._subject !== null) { this._write(this._inDefaultGraph ? '.\n' : '\n}\n'); this._subject = null, this._graph = ''; } // Store and write the prefix this._prefixIRIs[iri] = (prefix += ':'); this._write(`@prefix ${prefix} <${iri}>.\n`); } // Recreate the prefix matcher if (hasPrefixes) { let IRIlist = '', prefixList = ''; for (const prefixIRI in this._prefixIRIs) { IRIlist += IRIlist ? `|${prefixIRI}` : prefixIRI; prefixList += (prefixList ? '|' : '') + this._prefixIRIs[prefixIRI]; } IRIlist = escapeRegex(IRIlist, /[\]\/\(\)\*\+\?\.\\\$]/g, '\\$&'); this._prefixRegex = new RegExp(`^(?:${prefixList})[^\/]*$|` + `^(${IRIlist})([_a-zA-Z][\\-_a-zA-Z0-9]*)$`); } // End a prefix block with a newline this._write(hasPrefixes ? '\n' : '', done); } // ### `blank` creates a blank node with the given content blank(predicate, object) { let children = predicate, child, length; // Empty blank node if (predicate === undefined) children = []; // Blank node passed as blank(Term("predicate"), Term("object")) else if (predicate.termType) children = [{ predicate: predicate, object: object }]; // Blank node passed as blank({ predicate: predicate, object: object }) else if (!('length' in predicate)) children = [predicate]; switch (length = children.length) { // Generate an empty blank node case 0: return new SerializedTerm('[]'); // Generate a non-nested one-triple blank node case 1: child = children[0]; if (!(child.object instanceof SerializedTerm)) return new SerializedTerm(`[ ${this._encodePredicate(child.predicate)} ${ this._encodeObject(child.object)} ]`); // Generate a multi-triple or nested blank node default: let contents = '['; // Write all triples in order for (let i = 0; i < length; i++) { child = children[i]; // Write only the object is the predicate is the same as the previous if (child.predicate.equals(predicate)) contents += `, ${this._encodeObject(child.object)}`; // Otherwise, write the predicate and the object else { contents += `${(i ? ';\n ' : '\n ') + this._encodePredicate(child.predicate)} ${ this._encodeObject(child.object)}`; predicate = child.predicate; } } return new SerializedTerm(`${contents}\n]`); } } // ### `list` creates a list node with the given content list(elements) { const length = elements && elements.length || 0, contents = new Array(length); for (let i = 0; i < length; i++) contents[i] = this._encodeObject(elements[i]); return new SerializedTerm(`(${contents.join(' ')})`); } // ### `end` signals the end of the output stream end(done) { // Finish a possible pending quad if (this._subject !== null) { this._write(this._inDefaultGraph ? '.\n' : '\n}\n'); this._subject = null; } // Disallow further writing this._write = this._blockedWrite; // Try to end the underlying stream, ensuring done is called exactly one time let singleDone = done && ((error, result) => { singleDone = null, done(error, result); }); if (this._endStream) { try { return this._outputStream.end(singleDone); } catch (error) { /* error closing stream */ } } singleDone && singleDone(); } } // Replaces a character by its escaped version function characterReplacer(character) { // Replace a single character by its escaped version let result = escapedCharacters[character]; if (result === undefined) { // Replace a single character with its 4-bit unicode escape sequence if (character.length === 1) { result = character.charCodeAt(0).toString(16); result = '\\u0000'.substr(0, 6 - result.length) + result; } // Replace a surrogate pair with its 8-bit unicode escape sequence else { result = ((character.charCodeAt(0) - 0xD800) * 0x400 + character.charCodeAt(1) + 0x2400).toString(16); result = '\\U00000000'.substr(0, 10 - result.length) + result; } } return result; } function escapeRegex(regex) { return regex.replace(/[\]\/\(\)\*\+\?\.\\\$]/g, '\\$&'); }