n3 3.0.0-alpha.4 → 3.0.0-alpha.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +32 -0
- package/browser/n3.esm.min.js +11 -11
- package/browser/n3.min.js +11 -11
- package/lib/N3Lexer.js +204 -149
- package/lib/N3Parser.js +54 -27
- package/lib/N3Reasoner.js +1 -1
- package/lib/N3Store.js +46 -5
- package/lib/N3StreamParser.js +4 -0
- package/lib/N3Writer.js +45 -11
- package/lib/Util.js +1 -1
- package/package.json +2 -29
- package/src/N3Lexer.js +228 -162
- package/src/N3Parser.js +61 -27
- package/src/N3Reasoner.js +1 -1
- package/src/N3Store.js +57 -8
- package/src/N3StreamParser.js +2 -0
- package/src/N3Writer.js +47 -15
- package/src/Util.js +1 -1
package/src/N3Parser.js
CHANGED
|
@@ -47,7 +47,16 @@ export default class N3Parser {
|
|
|
47
47
|
this._resolveRelativeIRI = iri => { return null; };
|
|
48
48
|
this._blankNodePrefix = typeof options.blankNodePrefix !== 'string' ? '' :
|
|
49
49
|
options.blankNodePrefix.replace(/^(?!_:)/, '_:');
|
|
50
|
-
|
|
50
|
+
// Map the token types of additional directives (@name and NAME) to their names
|
|
51
|
+
this._directives = null;
|
|
52
|
+
if (options.directives) {
|
|
53
|
+
this._directives = Object.create(null);
|
|
54
|
+
for (const name of options.directives)
|
|
55
|
+
this._directives[`@${name.toLowerCase()}`] = this._directives[name.toUpperCase()] = name;
|
|
56
|
+
}
|
|
57
|
+
this._lexer = options.lexer || new N3Lexer({
|
|
58
|
+
lineMode: isLineMode, n3: isN3, isImpliedBy: this._isImpliedBy, directives: options.directives,
|
|
59
|
+
});
|
|
51
60
|
// Disable explicit quantifiers by default
|
|
52
61
|
this._explicitQuantifiers = !!options.explicitQuantifiers;
|
|
53
62
|
// Scope blank node labels to formulas only, unless explicitly disabled
|
|
@@ -55,6 +64,10 @@ export default class N3Parser {
|
|
|
55
64
|
// Disable parsing of unsupported versions by default
|
|
56
65
|
this._parseUnsupportedVersions = !!options.parseUnsupportedVersions;
|
|
57
66
|
this._version = options.version;
|
|
67
|
+
// Maximum nesting depth of triple terms (default 1024; Infinity disables the limit)
|
|
68
|
+
const maxTripleTermDepth = options.maxTripleTermDepth;
|
|
69
|
+
this._maxTripleTermDepth = maxTripleTermDepth === Infinity ||
|
|
70
|
+
Number.isInteger(maxTripleTermDepth) && maxTripleTermDepth >= 0 ? maxTripleTermDepth : 1024;
|
|
58
71
|
}
|
|
59
72
|
|
|
60
73
|
// ## Static class methods
|
|
@@ -83,12 +96,30 @@ export default class N3Parser {
|
|
|
83
96
|
const path = queryPos < 0 ? baseIRI : baseIRI.substr(0, queryPos);
|
|
84
97
|
this._basePath = baseIRI.indexOf('/') < 0 ? baseIRI :
|
|
85
98
|
path.substr(0, path.lastIndexOf('/') + 1);
|
|
86
|
-
|
|
99
|
+
const base = baseIRI;
|
|
100
|
+
baseIRI = baseIRI.match(/^(?:([a-z][a-z0-9+.-]*:))?(\/\/[^\/?#]*)?/i);
|
|
87
101
|
this._baseRoot = baseIRI[0];
|
|
88
102
|
this._baseScheme = baseIRI[1];
|
|
103
|
+
// If the base has an authority but an empty path,
|
|
104
|
+
// relative IRIs merge under the path '/' (RFC 3986 §5.2.3)
|
|
105
|
+
if (baseIRI[2] !== undefined && (base.length === this._baseRoot.length || base[this._baseRoot.length] === '?'))
|
|
106
|
+
this._basePath = `${this._baseRoot}/`;
|
|
89
107
|
}
|
|
90
108
|
}
|
|
91
109
|
|
|
110
|
+
// ### `_enterTripleTerm` stores the current parsing context when entering a triple term,
|
|
111
|
+
// failing if triple terms nest deeper than the maximum depth
|
|
112
|
+
_enterTripleTerm(type, subject, predicate, token) {
|
|
113
|
+
if (this._tripleTermDepth >= this._maxTripleTermDepth) {
|
|
114
|
+
this._error(`Triple terms nested deeper than ${this._maxTripleTermDepth} level${this._maxTripleTermDepth === 1 ? '' : 's'}`, token);
|
|
115
|
+
return false;
|
|
116
|
+
}
|
|
117
|
+
this._tripleTermDepth++;
|
|
118
|
+
this._saveContext(type, this._graph, subject, predicate, null);
|
|
119
|
+
this._graph = null;
|
|
120
|
+
return true;
|
|
121
|
+
}
|
|
122
|
+
|
|
92
123
|
// ### `_saveContext` stores the current parsing context
|
|
93
124
|
// when entering a new scope (list, blank node, formula)
|
|
94
125
|
_saveContext(type, graph, subject, predicate, object) {
|
|
@@ -207,8 +238,13 @@ export default class N3Parser {
|
|
|
207
238
|
case 'GRAPH':
|
|
208
239
|
if (this._supportsNamedGraphs)
|
|
209
240
|
return this._readNamedGraphLabel;
|
|
210
|
-
// Otherwise, the next token must be a subject
|
|
241
|
+
// Otherwise, the next token must be a subject, unless it is an additional directive
|
|
211
242
|
default:
|
|
243
|
+
if (this._directives !== null && token.type in this._directives && this._graph === null) {
|
|
244
|
+
this._sparqlStyle = token.type[0] !== '@';
|
|
245
|
+
this._directiveCallback(this._directives[token.type]);
|
|
246
|
+
return this._readDeclarationPunctuation;
|
|
247
|
+
}
|
|
212
248
|
return this._readSubject(token);
|
|
213
249
|
}
|
|
214
250
|
}
|
|
@@ -339,13 +375,9 @@ export default class N3Parser {
|
|
|
339
375
|
case '<<(':
|
|
340
376
|
if (!this._n3Mode)
|
|
341
377
|
return this._error('Disallowed triple term as subject', token);
|
|
342
|
-
this.
|
|
343
|
-
this._graph = null;
|
|
344
|
-
return this._readSubject;
|
|
378
|
+
return this._enterTripleTerm('<<(', null, null, token) && this._readSubject;
|
|
345
379
|
case '<<':
|
|
346
|
-
this.
|
|
347
|
-
this._graph = null;
|
|
348
|
-
return this._readSubject;
|
|
380
|
+
return this._enterTripleTerm('<<', null, null, token) && this._readSubject;
|
|
349
381
|
default:
|
|
350
382
|
// Read the subject entity
|
|
351
383
|
if ((this._subject = this._readEntity(token)) === undefined)
|
|
@@ -490,13 +522,9 @@ export default class N3Parser {
|
|
|
490
522
|
this._graph = this._factory.blankNode());
|
|
491
523
|
return this._readInFormulaContext;
|
|
492
524
|
case '<<(':
|
|
493
|
-
this.
|
|
494
|
-
this._graph = null;
|
|
495
|
-
return this._readSubject;
|
|
525
|
+
return this._enterTripleTerm('<<(', this._subject, this._predicate, token) && this._readSubject;
|
|
496
526
|
case '<<':
|
|
497
|
-
this.
|
|
498
|
-
this._graph = null;
|
|
499
|
-
return this._readSubject;
|
|
527
|
+
return this._enterTripleTerm('<<', this._subject, this._predicate, token) && this._readSubject;
|
|
500
528
|
default:
|
|
501
529
|
// Read the object entity
|
|
502
530
|
if ((this._object = this._readEntity(token)) === undefined)
|
|
@@ -716,13 +744,9 @@ export default class N3Parser {
|
|
|
716
744
|
this._subject = null;
|
|
717
745
|
return this._readInFormulaContext;
|
|
718
746
|
case '<<(':
|
|
719
|
-
this._saveContext('<<(', this._graph, null, null, null);
|
|
720
|
-
this._graph = null;
|
|
721
|
-
next = this._readSubject;
|
|
722
|
-
break;
|
|
723
747
|
case '<<':
|
|
724
|
-
this.
|
|
725
|
-
|
|
748
|
+
if (!this._enterTripleTerm(token.type, null, null, token))
|
|
749
|
+
return;
|
|
726
750
|
next = this._readSubject;
|
|
727
751
|
break;
|
|
728
752
|
default:
|
|
@@ -1272,6 +1296,7 @@ export default class N3Parser {
|
|
|
1272
1296
|
const quad = this._createQuad(this._subject, this._predicate, this._object,
|
|
1273
1297
|
this._graph, this._inversePredicate);
|
|
1274
1298
|
this._restoreContext('<<(', token);
|
|
1299
|
+
this._tripleTermDepth--;
|
|
1275
1300
|
|
|
1276
1301
|
// If we're in a list, continue processing that list
|
|
1277
1302
|
const stack = this._contextStack, parent = stack.length && stack[stack.length - 1];
|
|
@@ -1307,6 +1332,7 @@ export default class N3Parser {
|
|
|
1307
1332
|
this._tripleTerm = null;
|
|
1308
1333
|
const reifier = this._readTripleTerm();
|
|
1309
1334
|
this._restoreContext('<<', token);
|
|
1335
|
+
this._tripleTermDepth--;
|
|
1310
1336
|
|
|
1311
1337
|
// // If we're in a list, continue processing that list
|
|
1312
1338
|
const stack = this._contextStack, parent = stack.length && stack[stack.length - 1];
|
|
@@ -1540,8 +1566,13 @@ export default class N3Parser {
|
|
|
1540
1566
|
}
|
|
1541
1567
|
// Resolve root-relative IRIs at the root of the base IRI
|
|
1542
1568
|
case '/':
|
|
1543
|
-
// Resolve scheme-relative IRIs to the scheme
|
|
1544
|
-
|
|
1569
|
+
// Resolve scheme-relative IRIs to the scheme,
|
|
1570
|
+
// keeping the reference's authority intact (RFC 3986 §5.2.4)
|
|
1571
|
+
if (iri[1] !== '/')
|
|
1572
|
+
return this._baseRoot + this._removeDotSegments(iri);
|
|
1573
|
+
// Without a base scheme, a leading ':' marks where the authority starts
|
|
1574
|
+
return this._baseScheme ? this._removeDotSegments(this._baseScheme + iri) :
|
|
1575
|
+
this._removeDotSegments(`:${iri}`).substr(1);
|
|
1545
1576
|
// Resolve all other IRIs at the base IRI's path
|
|
1546
1577
|
default:
|
|
1547
1578
|
// Relative IRIs cannot contain a colon in the first path segment
|
|
@@ -1566,8 +1597,8 @@ export default class N3Parser {
|
|
|
1566
1597
|
if (pathStart < 0) {
|
|
1567
1598
|
// Skip two slashes before the authority
|
|
1568
1599
|
if (iri[++i] === '/' && iri[++i] === '/')
|
|
1569
|
-
// Skip to
|
|
1570
|
-
while ((pathStart = i + 1) < length && iri[pathStart] !== '/')
|
|
1600
|
+
// Skip to the end of the authority
|
|
1601
|
+
while ((pathStart = i + 1) < length && iri[pathStart] !== '/' && iri[pathStart] !== '?' && iri[pathStart] !== '#')
|
|
1571
1602
|
i = pathStart;
|
|
1572
1603
|
}
|
|
1573
1604
|
break;
|
|
@@ -1618,13 +1649,14 @@ export default class N3Parser {
|
|
|
1618
1649
|
parse(input, quadCallback, prefixCallback, versionCallback) {
|
|
1619
1650
|
// The second parameter also accepts an object of named callbacks.
|
|
1620
1651
|
// As a second and third parameter it still accepts a separate quadCallback and prefixCallback for backward compatibility as well
|
|
1621
|
-
let onQuad, onPrefix, onComment, onVersion, onToken, onTokenEnd;
|
|
1652
|
+
let onQuad, onPrefix, onComment, onVersion, onDirective, onToken, onTokenEnd;
|
|
1622
1653
|
if (quadCallback && (quadCallback.onQuad || quadCallback.onPrefix || quadCallback.onComment || quadCallback.onVersion ||
|
|
1623
|
-
quadCallback.onToken || quadCallback.onTokenEnd)) {
|
|
1654
|
+
quadCallback.onDirective || quadCallback.onToken || quadCallback.onTokenEnd)) {
|
|
1624
1655
|
onQuad = quadCallback.onQuad;
|
|
1625
1656
|
onPrefix = quadCallback.onPrefix;
|
|
1626
1657
|
onComment = quadCallback.onComment;
|
|
1627
1658
|
onVersion = quadCallback.onVersion;
|
|
1659
|
+
onDirective = quadCallback.onDirective;
|
|
1628
1660
|
onToken = quadCallback.onToken;
|
|
1629
1661
|
onTokenEnd = quadCallback.onTokenEnd;
|
|
1630
1662
|
}
|
|
@@ -1645,6 +1677,7 @@ export default class N3Parser {
|
|
|
1645
1677
|
this._prefixes[''] = this._resolveIRI('#');
|
|
1646
1678
|
this._prefixCallback = onPrefix || noop;
|
|
1647
1679
|
this._versionCallback = onVersion || noop;
|
|
1680
|
+
this._directiveCallback = onDirective || noop;
|
|
1648
1681
|
this._inversePredicate = false;
|
|
1649
1682
|
this._expectOf = false;
|
|
1650
1683
|
this._quantified = Object.create(null);
|
|
@@ -1652,6 +1685,7 @@ export default class N3Parser {
|
|
|
1652
1685
|
this._prefixChanges = null;
|
|
1653
1686
|
this._quantifiedChanges = null;
|
|
1654
1687
|
this._emptyFormula = false;
|
|
1688
|
+
this._tripleTermDepth = 0;
|
|
1655
1689
|
|
|
1656
1690
|
let readToken = token => {
|
|
1657
1691
|
return this._readCallback = this._readCallback(token);
|
package/src/N3Reasoner.js
CHANGED
|
@@ -156,7 +156,7 @@ export default class N3Reasoner {
|
|
|
156
156
|
}
|
|
157
157
|
|
|
158
158
|
_createRule({ premise, conclusion }) {
|
|
159
|
-
const varMapping =
|
|
159
|
+
const varMapping = Object.create(null);
|
|
160
160
|
|
|
161
161
|
const toId = value => value.termType === 'Variable' ?
|
|
162
162
|
// If the term is a variable, then create an empty object that values can be placed into
|
package/src/N3Store.js
CHANGED
|
@@ -118,6 +118,35 @@ function difference(s1, s2, depth = 4) {
|
|
|
118
118
|
return target;
|
|
119
119
|
}
|
|
120
120
|
|
|
121
|
+
// Returns the key of a term in the entity index.
|
|
122
|
+
// Keys mark the term type by their first character, so the IRI of a named node
|
|
123
|
+
// that starts with such a marker (as relative IRIs can) is wrapped in < and >.
|
|
124
|
+
const markedIRI = /^[?_"[.<]/;
|
|
125
|
+
function entityKey(term) {
|
|
126
|
+
// Strings are term ids, which only need wrapping when they are IRIs starting with <
|
|
127
|
+
if (typeof term === 'string')
|
|
128
|
+
return term.charCodeAt(0) !== 0x3C || term === '<>' ? term : `<${term}>`;
|
|
129
|
+
// IDs of IRIs usually start with a lowercase scheme letter, which never marks a term type
|
|
130
|
+
const id = termToId(term);
|
|
131
|
+
if (id.charCodeAt(0) >= 0x61 || !term)
|
|
132
|
+
return id;
|
|
133
|
+
// Keys of quoted triples are built from the keys of their components
|
|
134
|
+
if (term.termType === 'Quad')
|
|
135
|
+
return JSON.stringify(quadKeyParts(term));
|
|
136
|
+
return term.termType !== 'NamedNode' || !markedIRI.test(term.value) ? id : `<${term.value}>`;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Returns the keys of the components of a quad, nested like the parts of its internal id
|
|
140
|
+
function quadKeyParts(quad) {
|
|
141
|
+
const parts = [nestedKey(quad.subject), nestedKey(quad.predicate), nestedKey(quad.object)];
|
|
142
|
+
if (quad.graph && !isDefaultGraph(quad.graph))
|
|
143
|
+
parts.push(nestedKey(quad.graph));
|
|
144
|
+
return parts;
|
|
145
|
+
}
|
|
146
|
+
function nestedKey(term) {
|
|
147
|
+
return term.termType === 'Quad' ? quadKeyParts(term) : entityKey(term);
|
|
148
|
+
}
|
|
149
|
+
|
|
121
150
|
// ## Constructor
|
|
122
151
|
export class N3EntityIndex {
|
|
123
152
|
constructor(options = {}) {
|
|
@@ -135,6 +164,10 @@ export class N3EntityIndex {
|
|
|
135
164
|
}
|
|
136
165
|
|
|
137
166
|
_termFromId(id) {
|
|
167
|
+
// A key in < and > is a named node
|
|
168
|
+
if (id[0] === '<')
|
|
169
|
+
return this._factory.namedNode(id.substring(1, id.length - 1));
|
|
170
|
+
// A key starting with . is a quoted triple
|
|
138
171
|
if (id[0] === '.') {
|
|
139
172
|
const entities = this._entities;
|
|
140
173
|
const terms = id.split('.');
|
|
@@ -159,18 +192,34 @@ export class N3EntityIndex {
|
|
|
159
192
|
return s && p && o && (isDefaultGraph(term.graph) || (g = this._termToNumericId(term.graph))) &&
|
|
160
193
|
this._ids[g ? `.${s}.${p}.${o}.${g}` : `.${s}.${p}.${o}`];
|
|
161
194
|
}
|
|
162
|
-
return this._ids[
|
|
195
|
+
return typeof term === 'string' ? this._stringToNumericId(term) : this._ids[entityKey(term)];
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Returns the numeric id of a term given as a string id
|
|
199
|
+
_stringToNumericId(term) {
|
|
200
|
+
// The string is the key unless it is an IRI starting with <. Read the first character
|
|
201
|
+
// from the stored key after a match, which is cheaper than from a concatenated string.
|
|
202
|
+
const id = this._ids[term];
|
|
203
|
+
if (id ? this._entities[id].charCodeAt(0) !== 0x3C : term.charCodeAt(0) !== 0x3C)
|
|
204
|
+
return id;
|
|
205
|
+
return term === '<>' ? id : this._ids[`<${term}>`];
|
|
163
206
|
}
|
|
164
207
|
|
|
165
208
|
_termToNewNumericId(term) {
|
|
209
|
+
if (typeof term === 'string')
|
|
210
|
+
return this._stringToNumericId(term) || this._addEntity(entityKey(term));
|
|
166
211
|
// This assumes that no graph term is present - we may wish to error if there is one
|
|
167
212
|
const str = term && term.termType === 'Quad' ?
|
|
168
213
|
`.${this._termToNewNumericId(term.subject)}.${this._termToNewNumericId(term.predicate)}.${this._termToNewNumericId(term.object)}${
|
|
169
214
|
isDefaultGraph(term.graph) ? '' : `.${this._termToNewNumericId(term.graph)}`
|
|
170
215
|
}`
|
|
171
|
-
:
|
|
216
|
+
: entityKey(term);
|
|
217
|
+
|
|
218
|
+
return this._ids[str] || this._addEntity(str);
|
|
219
|
+
}
|
|
172
220
|
|
|
173
|
-
|
|
221
|
+
_addEntity(key) {
|
|
222
|
+
return this._ids[this._entities[++this._id] = key] = this._id;
|
|
174
223
|
}
|
|
175
224
|
|
|
176
225
|
createBlankNode(suggestedName) {
|
|
@@ -1382,7 +1431,7 @@ function intersectMatchPatterns(left, right) {
|
|
|
1382
1431
|
const leftTerm = left[i], rightTerm = right[i];
|
|
1383
1432
|
if (leftTerm === null || leftTerm === undefined)
|
|
1384
1433
|
result[i] = rightTerm;
|
|
1385
|
-
else if (rightTerm === null || rightTerm === undefined ||
|
|
1434
|
+
else if (rightTerm === null || rightTerm === undefined || entityKey(leftTerm) === entityKey(rightTerm))
|
|
1386
1435
|
result[i] = leftTerm;
|
|
1387
1436
|
else
|
|
1388
1437
|
return false;
|
|
@@ -1433,10 +1482,10 @@ class DatasetCoreAndReadableStream extends Readable {
|
|
|
1433
1482
|
_matchesQuad(quad) {
|
|
1434
1483
|
const { subject, predicate, object, graph } = this;
|
|
1435
1484
|
return !this._matchesNothing &&
|
|
1436
|
-
(subject === null || subject === undefined ||
|
|
1437
|
-
(predicate === null || predicate === undefined ||
|
|
1438
|
-
(object === null || object === undefined ||
|
|
1439
|
-
(graph === null || graph === undefined ||
|
|
1485
|
+
(subject === null || subject === undefined || entityKey(subject) === entityKey(quad.subject)) &&
|
|
1486
|
+
(predicate === null || predicate === undefined || entityKey(predicate) === entityKey(quad.predicate)) &&
|
|
1487
|
+
(object === null || object === undefined || entityKey(object) === entityKey(quad.object)) &&
|
|
1488
|
+
(graph === null || graph === undefined || entityKey(graph) === entityKey(quad.graph));
|
|
1440
1489
|
}
|
|
1441
1490
|
|
|
1442
1491
|
// ### `_assertMatchesPattern` rejects a Quad outside this view.
|
package/src/N3StreamParser.js
CHANGED
|
@@ -16,6 +16,8 @@ export default class N3StreamParser extends Transform {
|
|
|
16
16
|
onQuad: (error, quad) => { error && this.emit('error', error) || quad && this.push(quad); },
|
|
17
17
|
// Emit prefixes through the `prefix` event
|
|
18
18
|
onPrefix: (prefix, uri) => { this.emit('prefix', prefix, uri); },
|
|
19
|
+
// Emit additional directives through the `directive` event
|
|
20
|
+
onDirective: name => { this.emit('directive', name); },
|
|
19
21
|
};
|
|
20
22
|
|
|
21
23
|
if (options && options.comments)
|
package/src/N3Writer.js
CHANGED
|
@@ -8,10 +8,17 @@ import { escapeRegex } from './Util';
|
|
|
8
8
|
const DEFAULTGRAPH = N3DataFactory.defaultGraph();
|
|
9
9
|
|
|
10
10
|
const { rdf, xsd } = namespaces;
|
|
11
|
+
const { hasOwnProperty } = Object.prototype;
|
|
11
12
|
|
|
12
|
-
// Characters in
|
|
13
|
+
// Characters that require escaping in Turtle, TriG, and N3,
|
|
14
|
+
// where characters outside the Basic Multilingual Plane are escaped as well
|
|
13
15
|
const escape = /["\\\t\n\r\b\f\u0000-\u0019\ud800-\udbff]/,
|
|
14
16
|
escapeAll = /["\\\t\n\r\b\f\u0000-\u0019]|[\ud800-\udbff][\udc00-\udfff]/g,
|
|
17
|
+
// Characters that require escaping in canonical N-Triples and N-Quads:
|
|
18
|
+
// U+0000–U+001F, `"`, `\`, U+007F, U+FFFE, and U+FFFF.
|
|
19
|
+
// A negated class is as fast to test as a shorter class listing characters.
|
|
20
|
+
canonicalEscape = /[^ !#-\[\]-~\u0080-\uFFFD]/,
|
|
21
|
+
canonicalEscapeAll = /[^ !#-\[\]-~\u0080-\uFFFD]/g,
|
|
15
22
|
escapedCharacters = {
|
|
16
23
|
'\\': '\\\\', '"': '\\"', '\t': '\\t',
|
|
17
24
|
'\n': '\\n', '\r': '\\r', '\b': '\\b', '\f': '\\f',
|
|
@@ -57,8 +64,11 @@ export default class N3Writer {
|
|
|
57
64
|
this._subject = null;
|
|
58
65
|
if (!(/triple|quad/i).test(options.format)) {
|
|
59
66
|
this._lineMode = false;
|
|
67
|
+
this._escape = escape, this._escapeAll = escapeAll, this._characterReplacer = characterReplacer;
|
|
60
68
|
this._graph = DEFAULTGRAPH;
|
|
61
69
|
this._prefixIRIs = Object.create(null);
|
|
70
|
+
// Escaped prefix IRIs and names for the prefix matcher, computed once per prefix
|
|
71
|
+
this._prefixPatterns = Object.create(null);
|
|
62
72
|
if (options.baseIRI) {
|
|
63
73
|
this._baseIri = new BaseIRI(options.baseIRI);
|
|
64
74
|
if (options.writeBase !== false)
|
|
@@ -69,6 +79,9 @@ export default class N3Writer {
|
|
|
69
79
|
else {
|
|
70
80
|
this._lineMode = true;
|
|
71
81
|
this._writeQuad = this._writeQuadLine;
|
|
82
|
+
// N-Triples and N-Quads are written in their canonical form
|
|
83
|
+
this._escape = canonicalEscape, this._escapeAll = canonicalEscapeAll;
|
|
84
|
+
this._characterReplacer = canonicalCharacterReplacer;
|
|
72
85
|
}
|
|
73
86
|
}
|
|
74
87
|
|
|
@@ -161,7 +174,7 @@ export default class N3Writer {
|
|
|
161
174
|
// A blank node or list is represented as-is
|
|
162
175
|
if (entity.termType !== 'NamedNode') {
|
|
163
176
|
// If it is a list head, pretty-print it
|
|
164
|
-
if (this._lists && (entity.value
|
|
177
|
+
if (this._lists && hasOwnProperty.call(this._lists, entity.value))
|
|
165
178
|
entity = this.list(this._lists[entity.value]);
|
|
166
179
|
// Terms from this library already hold their serialization as id
|
|
167
180
|
if (entity instanceof Term)
|
|
@@ -174,10 +187,10 @@ export default class N3Writer {
|
|
|
174
187
|
iri = this._baseIri.toRelative(iri);
|
|
175
188
|
}
|
|
176
189
|
// Escape special characters
|
|
177
|
-
if (
|
|
178
|
-
iri = iri.replace(
|
|
190
|
+
if (this._escape.test(iri))
|
|
191
|
+
iri = iri.replace(this._escapeAll, this._characterReplacer);
|
|
179
192
|
// Try to represent the IRI as prefixed name, unless no prefixes were added
|
|
180
|
-
const prefixMatch = this._hasPrefixes ? this._prefixRegex.exec(iri) : null;
|
|
193
|
+
const prefixMatch = this._hasPrefixes ? (this._prefixRegex || this._createPrefixRegex()).exec(iri) : null;
|
|
181
194
|
return !prefixMatch ? `<${iri}>` : this._prefixIRIs[prefixMatch[1]] + prefixMatch[2];
|
|
182
195
|
}
|
|
183
196
|
|
|
@@ -185,8 +198,8 @@ export default class N3Writer {
|
|
|
185
198
|
_encodeLiteral(literal) {
|
|
186
199
|
// Escape special characters
|
|
187
200
|
let value = literal.value;
|
|
188
|
-
if (
|
|
189
|
-
value = value.replace(
|
|
201
|
+
if (this._escape.test(value))
|
|
202
|
+
value = value.replace(this._escapeAll, this._characterReplacer);
|
|
190
203
|
|
|
191
204
|
// Write a language-tagged literal
|
|
192
205
|
const language = literal.language;
|
|
@@ -257,11 +270,13 @@ export default class N3Writer {
|
|
|
257
270
|
|
|
258
271
|
// ### `_encodeQuad` encodes an RDF-star quad
|
|
259
272
|
_encodeQuad({ subject, predicate, object, graph }) {
|
|
260
|
-
|
|
273
|
+
// Canonical N-Triples and N-Quads put spaces inside the delimiters
|
|
274
|
+
const space = this._lineMode ? ' ' : '';
|
|
275
|
+
return `<<(${space}${
|
|
261
276
|
this._encodeSubject(subject)} ${
|
|
262
277
|
this._encodePredicate(predicate)} ${
|
|
263
278
|
this._encodeObject(object)}${
|
|
264
|
-
isDefaultGraph(graph) ? '' : ` ${this._encodeIriOrBlank(graph)}`})>>`;
|
|
279
|
+
isDefaultGraph(graph) ? '' : ` ${this._encodeIriOrBlank(graph)}`}${space})>>`;
|
|
265
280
|
}
|
|
266
281
|
|
|
267
282
|
// ### `_blockedWrite` replaces `_write` after the writer has been closed
|
|
@@ -315,21 +330,27 @@ export default class N3Writer {
|
|
|
315
330
|
}
|
|
316
331
|
// Store and write the prefix
|
|
317
332
|
this._prefixIRIs[iri] = (prefix += ':');
|
|
333
|
+
this._prefixPatterns[iri] = escapeRegex(iri);
|
|
318
334
|
this._write(`@prefix ${prefix} <${iri}>.\n`);
|
|
319
335
|
}
|
|
320
|
-
// Recreate the prefix matcher
|
|
336
|
+
// Recreate the prefix matcher when it is next needed, so that adding
|
|
337
|
+
// prefixes one by one does not rebuild it for every prefix
|
|
321
338
|
if (hasPrefixes) {
|
|
322
339
|
this._hasPrefixes = true;
|
|
323
|
-
|
|
324
|
-
for (const prefixIRI in this._prefixIRIs)
|
|
325
|
-
IRIlist += IRIlist ? `|${prefixIRI}` : prefixIRI;
|
|
326
|
-
IRIlist = escapeRegex(IRIlist, /[\]\/\(\)\*\+\?\.\\\$]/g, '\\$&');
|
|
327
|
-
this._prefixRegex = new RegExp(`^(${IRIlist})([_a-zA-Z0-9](?:\\.?[\\-_a-zA-Z0-9])*)$`);
|
|
340
|
+
this._prefixRegex = null;
|
|
328
341
|
}
|
|
329
342
|
// End a prefix block with a newline
|
|
330
343
|
this._write(hasPrefixes ? '\n' : '', done);
|
|
331
344
|
}
|
|
332
345
|
|
|
346
|
+
// ### `_createPrefixRegex` creates the matcher for the current prefixes
|
|
347
|
+
_createPrefixRegex() {
|
|
348
|
+
let IRIlist = '';
|
|
349
|
+
for (const prefixIRI in this._prefixPatterns)
|
|
350
|
+
IRIlist += IRIlist ? `|${this._prefixPatterns[prefixIRI]}` : this._prefixPatterns[prefixIRI];
|
|
351
|
+
return this._prefixRegex = new RegExp(`^(${IRIlist})([_a-zA-Z0-9](?:\\.?[\\-_a-zA-Z0-9])*)$`);
|
|
352
|
+
}
|
|
353
|
+
|
|
333
354
|
// ### `blank` creates a blank node with the given content
|
|
334
355
|
blank(predicate, object) {
|
|
335
356
|
let children = predicate, child, length;
|
|
@@ -421,3 +442,14 @@ function characterReplacer(character) {
|
|
|
421
442
|
}
|
|
422
443
|
return result;
|
|
423
444
|
}
|
|
445
|
+
|
|
446
|
+
// Replaces a character by its canonical N-Triples escape:
|
|
447
|
+
// a short escape where one exists, and an upper-case `\uXXXX` escape otherwise
|
|
448
|
+
function canonicalCharacterReplacer(character) {
|
|
449
|
+
let result = escapedCharacters[character];
|
|
450
|
+
if (result === undefined) {
|
|
451
|
+
result = character.charCodeAt(0).toString(16).toUpperCase();
|
|
452
|
+
result = '\\u0000'.substr(0, 6 - result.length) + result;
|
|
453
|
+
}
|
|
454
|
+
return result;
|
|
455
|
+
}
|
package/src/Util.js
CHANGED