-
Notifications
You must be signed in to change notification settings - Fork 141
Expand file tree
/
Copy pathN3Writer.js
More file actions
397 lines (361 loc) · 13.7 KB
/
N3Writer.js
File metadata and controls
397 lines (361 loc) · 13.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
// **N3Writer** writes N3 documents.
import namespaces from './IRIs';
import { default as N3DataFactory, Term } from './N3DataFactory';
import { isDefaultGraph } from './N3Util';
import BaseIRI from './BaseIRI';
import { escapeRegex } from './Util';
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._baseIri = new BaseIRI(options.baseIRI, { absoluteIris: options.absoluteIris });
}
}
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)}${
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-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
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;
}