384 lines
14 KiB
JavaScript
384 lines
14 KiB
JavaScript
"use strict";
|
|
|
|
Object.defineProperty(exports, "__esModule", {
|
|
value: true
|
|
});
|
|
exports.default = void 0;
|
|
var _IRIs = _interopRequireDefault(require("./IRIs"));
|
|
var _N3DataFactory = _interopRequireWildcard(require("./N3DataFactory"));
|
|
var _N3Util = require("./N3Util");
|
|
var _BaseIRI = _interopRequireDefault(require("./BaseIRI"));
|
|
var _Util = require("./Util");
|
|
function _interopRequireWildcard(e, t) { if ("function" == typeof WeakMap) var r = new WeakMap(), n = new WeakMap(); return (_interopRequireWildcard = function (e, t) { if (!t && e && e.__esModule) return e; var o, i, f = { __proto__: null, default: e }; if (null === e || "object" != typeof e && "function" != typeof e) return f; if (o = t ? n : r) { if (o.has(e)) return o.get(e); o.set(e, f); } for (const t in e) "default" !== t && {}.hasOwnProperty.call(e, t) && ((i = (o = Object.defineProperty) && Object.getOwnPropertyDescriptor(e, t)) && (i.get || i.set) ? o(f, t, i) : f[t] = e[t]); return f; })(e, t); }
|
|
function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
|
|
// **N3Writer** writes N3 documents.
|
|
|
|
const DEFAULTGRAPH = _N3DataFactory.default.defaultGraph();
|
|
const {
|
|
rdf,
|
|
xsd
|
|
} = _IRIs.default;
|
|
|
|
// 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 _N3DataFactory.Term {
|
|
// Pretty-printed nodes are not equal to any other node
|
|
// (e.g., [] does not equal [])
|
|
equals(other) {
|
|
return other === this;
|
|
}
|
|
}
|
|
|
|
// ## Constructor
|
|
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._baseIri = new _BaseIRI.default(options.baseIRI);
|
|
}
|
|
} 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) {
|
|
let quadsString = '';
|
|
for (const quad of quads) quadsString += this.quadToString(quad.subject, quad.predicate, quad.object, quad.graph);
|
|
return quadsString;
|
|
}
|
|
|
|
// ### `_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._baseIri) {
|
|
iri = this._baseIri.toRelative(iri);
|
|
}
|
|
// 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-star quad
|
|
_encodeQuad({
|
|
subject,
|
|
predicate,
|
|
object,
|
|
graph
|
|
}) {
|
|
return `<<${this._encodeSubject(subject)} ${this._encodePredicate(predicate)} ${this._encodeObject(object)}${(0, _N3Util.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 = (0, _Util.escapeRegex)(IRIlist, /[\]\/\(\)\*\+\?\.\\\$]/g, '\\$&');
|
|
this._prefixRegex = new RegExp(`^(?:${prefixList})[^\/]*$|` + `^(${IRIlist})([_a-zA-Z0-9][\\-_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
|
|
exports.default = N3Writer;
|
|
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;
|
|
} |