n3 3.0.0-alpha.4 → 3.0.0-alpha.6

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/lib/N3Parser.js CHANGED
@@ -11,6 +11,10 @@ function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e
11
11
  // **N3Parser** parses N3 documents.
12
12
 
13
13
  let blankNodePrefix = 0;
14
+ // Detects `.` and `..` path segments in an IRI
15
+ const dotSegments = /(^|\/)\.\.?($|[/#?])/;
16
+ // Detects `.` and `..` segments in the path of an IRI, ignoring its query and fragment
17
+ const pathDotSegments = /^[^?#]*(?:^|\/)\.\.?(?:$|[/#?])/;
14
18
 
15
19
  // ## Constructor
16
20
  class N3Parser {
@@ -54,10 +58,17 @@ class N3Parser {
54
58
  return null;
55
59
  };
56
60
  this._blankNodePrefix = typeof options.blankNodePrefix !== 'string' ? '' : options.blankNodePrefix.replace(/^(?!_:)/, '_:');
61
+ // Map the token types of additional directives (@name and NAME) to their names
62
+ this._directives = null;
63
+ if (options.directives) {
64
+ this._directives = Object.create(null);
65
+ for (const name of options.directives) this._directives[`@${name.toLowerCase()}`] = this._directives[name.toUpperCase()] = name;
66
+ }
57
67
  this._lexer = options.lexer || new _N3Lexer.default({
58
68
  lineMode: isLineMode,
59
69
  n3: isN3,
60
- isImpliedBy: this._isImpliedBy
70
+ isImpliedBy: this._isImpliedBy,
71
+ directives: options.directives
61
72
  });
62
73
  // Disable explicit quantifiers by default
63
74
  this._explicitQuantifiers = !!options.explicitQuantifiers;
@@ -66,6 +77,9 @@ class N3Parser {
66
77
  // Disable parsing of unsupported versions by default
67
78
  this._parseUnsupportedVersions = !!options.parseUnsupportedVersions;
68
79
  this._version = options.version;
80
+ // Maximum nesting depth of triple terms (default 1024; Infinity disables the limit)
81
+ const maxTripleTermDepth = options.maxTripleTermDepth;
82
+ this._maxTripleTermDepth = maxTripleTermDepth === Infinity || Number.isInteger(maxTripleTermDepth) && maxTripleTermDepth >= 0 ? maxTripleTermDepth : 1024;
69
83
  }
70
84
 
71
85
  // ## Static class methods
@@ -82,6 +96,7 @@ class N3Parser {
82
96
  if (!baseIRI) {
83
97
  this._base = '';
84
98
  this._basePath = '';
99
+ this._basePathHasDotSegments = false;
85
100
  } else {
86
101
  // Remove fragment if present
87
102
  const fragmentPos = baseIRI.indexOf('#');
@@ -91,12 +106,31 @@ class N3Parser {
91
106
  const queryPos = baseIRI.indexOf('?');
92
107
  const path = queryPos < 0 ? baseIRI : baseIRI.substr(0, queryPos);
93
108
  this._basePath = baseIRI.indexOf('/') < 0 ? baseIRI : path.substr(0, path.lastIndexOf('/') + 1);
94
- baseIRI = baseIRI.match(/^(?:([a-z][a-z0-9+.-]*:))?(?:\/\/[^\/]*)?/i);
109
+ const base = baseIRI;
110
+ baseIRI = baseIRI.match(/^(?:([a-z][a-z0-9+.-]*:))?(\/\/[^\/?#]*)?/i);
95
111
  this._baseRoot = baseIRI[0];
96
112
  this._baseScheme = baseIRI[1];
113
+ // If the base has an authority but an empty path,
114
+ // relative IRIs merge under the path '/' (RFC 3986 §5.2.3)
115
+ if (baseIRI[2] !== undefined && (base.length === this._baseRoot.length || base[this._baseRoot.length] === '?')) this._basePath = `${this._baseRoot}/`;
116
+ // Check once whether resolving against the base path needs to remove dot segments
117
+ this._basePathHasDotSegments = dotSegments.test(this._basePath);
97
118
  }
98
119
  }
99
120
 
121
+ // ### `_enterTripleTerm` stores the current parsing context when entering a triple term,
122
+ // failing if triple terms nest deeper than the maximum depth
123
+ _enterTripleTerm(type, subject, predicate, token) {
124
+ if (this._tripleTermDepth >= this._maxTripleTermDepth) {
125
+ this._error(`Triple terms nested deeper than ${this._maxTripleTermDepth} level${this._maxTripleTermDepth === 1 ? '' : 's'}`, token);
126
+ return false;
127
+ }
128
+ this._tripleTermDepth++;
129
+ this._saveContext(type, this._graph, subject, predicate, null);
130
+ this._graph = null;
131
+ return true;
132
+ }
133
+
100
134
  // ### `_saveContext` stores the current parsing context
101
135
  // when entering a new scope (list, blank node, formula)
102
136
  _saveContext(type, graph, subject, predicate, object) {
@@ -127,7 +161,7 @@ class N3Parser {
127
161
  // Prefix and base declarations are scoped to their formula,
128
162
  // so record prefix changes to undo them when the formula ends
129
163
  if (type === 'formula') {
130
- context.base = [this._base, this._basePath, this._baseRoot, this._baseScheme];
164
+ context.base = [this._base, this._basePath, this._baseRoot, this._baseScheme, this._basePathHasDotSegments];
131
165
  this._prefixChanges = [];
132
166
  }
133
167
  this._contextStack.push(context);
@@ -167,7 +201,7 @@ class N3Parser {
167
201
  if (this._n3Mode) {
168
202
  this._inversePredicate = context.inverse;
169
203
  this._expectOf = context.expectOf;
170
- if (type === 'formula') [this._base, this._basePath, this._baseRoot, this._baseScheme] = context.base;
204
+ if (type === 'formula') [this._base, this._basePath, this._baseRoot, this._baseScheme, this._basePathHasDotSegments] = context.base;
171
205
  if (this._prefixChanges !== context.prefixChanges) {
172
206
  undoChanges(this._prefixes, this._prefixChanges);
173
207
  this._prefixChanges = context.prefixChanges;
@@ -219,8 +253,13 @@ class N3Parser {
219
253
  }
220
254
  case 'GRAPH':
221
255
  if (this._supportsNamedGraphs) return this._readNamedGraphLabel;
222
- // Otherwise, the next token must be a subject
256
+ // Otherwise, the next token must be a subject, unless it is an additional directive
223
257
  default:
258
+ if (this._directives !== null && token.type in this._directives && this._graph === null) {
259
+ this._sparqlStyle = token.type[0] !== '@';
260
+ this._directiveCallback(this._directives[token.type]);
261
+ return this._readDeclarationPunctuation;
262
+ }
224
263
  return this._readSubject(token);
225
264
  }
226
265
  }
@@ -339,13 +378,9 @@ class N3Parser {
339
378
  }
340
379
  case '<<(':
341
380
  if (!this._n3Mode) return this._error('Disallowed triple term as subject', token);
342
- this._saveContext('<<(', this._graph, null, null, null);
343
- this._graph = null;
344
- return this._readSubject;
381
+ return this._enterTripleTerm('<<(', null, null, token) && this._readSubject;
345
382
  case '<<':
346
- this._saveContext('<<', this._graph, null, null, null);
347
- this._graph = null;
348
- return this._readSubject;
383
+ return this._enterTripleTerm('<<', null, null, token) && this._readSubject;
349
384
  default:
350
385
  // Read the subject entity
351
386
  if ((this._subject = this._readEntity(token)) === undefined) return;
@@ -468,13 +503,9 @@ class N3Parser {
468
503
  this._saveContext('formula', this._graph, this._subject, this._predicate, this._graph = this._factory.blankNode());
469
504
  return this._readInFormulaContext;
470
505
  case '<<(':
471
- this._saveContext('<<(', this._graph, this._subject, this._predicate, null);
472
- this._graph = null;
473
- return this._readSubject;
506
+ return this._enterTripleTerm('<<(', this._subject, this._predicate, token) && this._readSubject;
474
507
  case '<<':
475
- this._saveContext('<<', this._graph, this._subject, this._predicate, null);
476
- this._graph = null;
477
- return this._readSubject;
508
+ return this._enterTripleTerm('<<', this._subject, this._predicate, token) && this._readSubject;
478
509
  default:
479
510
  // Read the object entity
480
511
  if ((this._object = this._readEntity(token)) === undefined) return;
@@ -669,13 +700,8 @@ class N3Parser {
669
700
  this._subject = null;
670
701
  return this._readInFormulaContext;
671
702
  case '<<(':
672
- this._saveContext('<<(', this._graph, null, null, null);
673
- this._graph = null;
674
- next = this._readSubject;
675
- break;
676
703
  case '<<':
677
- this._saveContext('<<', this._graph, null, null, null);
678
- this._graph = null;
704
+ if (!this._enterTripleTerm(token.type, null, null, token)) return;
679
705
  next = this._readSubject;
680
706
  break;
681
707
  default:
@@ -1007,7 +1033,8 @@ class N3Parser {
1007
1033
  // ### `_readVersion` reads version string declaration
1008
1034
  _readVersion(token) {
1009
1035
  if (token.type !== 'literal') return this._error('Expected literal to follow version declaration', token);
1010
- if (token.end - token.start !== token.value.length + 2) return this._error('Version declarations must use single quotes', token);
1036
+ // Only short strings are allowed, so no numbers or booleans (which have a datatype prefix)
1037
+ if (token.prefix !== '' || token.tripleQuoted) return this._error('Version declarations must use single quotes', token);
1011
1038
  this._versionCallback(token.value);
1012
1039
  if (!this._isValidVersion(token.value)) return this._error(`Detected unsupported version: "${token.value}"`, token);
1013
1040
  return this._readDeclarationPunctuation;
@@ -1167,6 +1194,7 @@ class N3Parser {
1167
1194
  // Read the quad and restore the previous context
1168
1195
  const quad = this._createQuad(this._subject, this._predicate, this._object, this._graph, this._inversePredicate);
1169
1196
  this._restoreContext('<<(', token);
1197
+ this._tripleTermDepth--;
1170
1198
 
1171
1199
  // If we're in a list, continue processing that list
1172
1200
  const stack = this._contextStack,
@@ -1202,6 +1230,7 @@ class N3Parser {
1202
1230
  this._tripleTerm = null;
1203
1231
  const reifier = this._readTripleTerm();
1204
1232
  this._restoreContext('<<', token);
1233
+ this._tripleTermDepth--;
1205
1234
 
1206
1235
  // // If we're in a list, continue processing that list
1207
1236
  const stack = this._contextStack,
@@ -1426,19 +1455,25 @@ class N3Parser {
1426
1455
  }
1427
1456
  // Resolve root-relative IRIs at the root of the base IRI
1428
1457
  case '/':
1429
- // Resolve scheme-relative IRIs to the scheme
1430
- return (iri[1] === '/' ? this._baseScheme : this._baseRoot) + this._removeDotSegments(iri);
1458
+ // Resolve scheme-relative IRIs to the scheme,
1459
+ // keeping the reference's authority intact (RFC 3986 §5.2.4)
1460
+ if (iri[1] !== '/') return this._baseRoot + this._removeDotSegments(iri);
1461
+ // Without a base scheme, a leading ':' marks where the authority starts
1462
+ return this._baseScheme ? this._removeDotSegments(this._baseScheme + iri) : this._removeDotSegments(`:${iri}`).substr(1);
1431
1463
  // Resolve all other IRIs at the base IRI's path
1432
1464
  default:
1433
1465
  // Relative IRIs cannot contain a colon in the first path segment
1434
- return /^[^/:]*:/.test(iri) ? null : this._removeDotSegments(this._basePath + iri);
1466
+ if (/^[^/:]*:/.test(iri)) return null;
1467
+ // Only scan the joined IRI for dot segments if either part can contain them,
1468
+ // as the base path can be long and the joined IRI would need to be copied
1469
+ return this._basePathHasDotSegments || pathDotSegments.test(iri) ? this._removeDotSegments(this._basePath + iri) : this._basePath + iri;
1435
1470
  }
1436
1471
  }
1437
1472
 
1438
1473
  // ### `_removeDotSegments` resolves './' and '../' path segments in an IRI as per RFC3986
1439
1474
  _removeDotSegments(iri) {
1440
1475
  // Don't modify the IRI if it does not contain any dot segments
1441
- if (!/(^|\/)\.\.?($|[/#?])/.test(iri)) return iri;
1476
+ if (!dotSegments.test(iri)) return iri;
1442
1477
 
1443
1478
  // Start with an imaginary slash before the IRI in order to resolve trailing './' and '../'
1444
1479
  const length = iri.length;
@@ -1454,8 +1489,8 @@ class N3Parser {
1454
1489
  if (pathStart < 0) {
1455
1490
  // Skip two slashes before the authority
1456
1491
  if (iri[++i] === '/' && iri[++i] === '/')
1457
- // Skip to slash after the authority
1458
- while ((pathStart = i + 1) < length && iri[pathStart] !== '/') i = pathStart;
1492
+ // Skip to the end of the authority
1493
+ while ((pathStart = i + 1) < length && iri[pathStart] !== '/' && iri[pathStart] !== '?' && iri[pathStart] !== '#') i = pathStart;
1459
1494
  }
1460
1495
  break;
1461
1496
  // Don't modify a query string or fragment
@@ -1503,12 +1538,13 @@ class N3Parser {
1503
1538
  parse(input, quadCallback, prefixCallback, versionCallback) {
1504
1539
  // The second parameter also accepts an object of named callbacks.
1505
1540
  // As a second and third parameter it still accepts a separate quadCallback and prefixCallback for backward compatibility as well
1506
- let onQuad, onPrefix, onComment, onVersion, onToken, onTokenEnd;
1507
- if (quadCallback && (quadCallback.onQuad || quadCallback.onPrefix || quadCallback.onComment || quadCallback.onVersion || quadCallback.onToken || quadCallback.onTokenEnd)) {
1541
+ let onQuad, onPrefix, onComment, onVersion, onDirective, onToken, onTokenEnd;
1542
+ if (quadCallback && (quadCallback.onQuad || quadCallback.onPrefix || quadCallback.onComment || quadCallback.onVersion || quadCallback.onDirective || quadCallback.onToken || quadCallback.onTokenEnd)) {
1508
1543
  onQuad = quadCallback.onQuad;
1509
1544
  onPrefix = quadCallback.onPrefix;
1510
1545
  onComment = quadCallback.onComment;
1511
1546
  onVersion = quadCallback.onVersion;
1547
+ onDirective = quadCallback.onDirective;
1512
1548
  onToken = quadCallback.onToken;
1513
1549
  onTokenEnd = quadCallback.onTokenEnd;
1514
1550
  } else {
@@ -1526,6 +1562,7 @@ class N3Parser {
1526
1562
  if (this._n3Mode && this._implicitEmptyPrefix && this._base) this._prefixes[''] = this._resolveIRI('#');
1527
1563
  this._prefixCallback = onPrefix || noop;
1528
1564
  this._versionCallback = onVersion || noop;
1565
+ this._directiveCallback = onDirective || noop;
1529
1566
  this._inversePredicate = false;
1530
1567
  this._expectOf = false;
1531
1568
  this._quantified = Object.create(null);
@@ -1533,6 +1570,7 @@ class N3Parser {
1533
1570
  this._prefixChanges = null;
1534
1571
  this._quantifiedChanges = null;
1535
1572
  this._emptyFormula = false;
1573
+ this._tripleTermDepth = 0;
1536
1574
  let readToken = token => {
1537
1575
  return this._readCallback = this._readCallback(token);
1538
1576
  };
package/lib/N3Reasoner.js CHANGED
@@ -154,7 +154,7 @@ class N3Reasoner {
154
154
  premise,
155
155
  conclusion
156
156
  }) {
157
- const varMapping = {};
157
+ const varMapping = Object.create(null);
158
158
  const toId = value => value.termType === 'Variable' ?
159
159
  // If the term is a variable, then create an empty object that values can be placed into
160
160
  varMapping[value.value] = varMapping[value.value] || {} :
package/lib/N3Store.js CHANGED
@@ -116,6 +116,31 @@ function difference(s1, s2, depth = 4) {
116
116
  return target;
117
117
  }
118
118
 
119
+ // Returns the key of a term in the entity index.
120
+ // Keys mark the term type by their first character, so the IRI of a named node
121
+ // that starts with such a marker (as relative IRIs can) is wrapped in < and >.
122
+ const markedIRI = /^[?_"[.<]/;
123
+ function entityKey(term) {
124
+ // Strings are term ids, which only need wrapping when they are IRIs starting with <
125
+ if (typeof term === 'string') return term.charCodeAt(0) !== 0x3C || term === '<>' ? term : `<${term}>`;
126
+ // IDs of IRIs usually start with a lowercase scheme letter, which never marks a term type
127
+ const id = (0, _N3DataFactory.termToId)(term);
128
+ if (id.charCodeAt(0) >= 0x61 || !term) return id;
129
+ // Keys of quoted triples are built from the keys of their components
130
+ if (term.termType === 'Quad') return JSON.stringify(quadKeyParts(term));
131
+ return term.termType !== 'NamedNode' || !markedIRI.test(term.value) ? id : `<${term.value}>`;
132
+ }
133
+
134
+ // Returns the keys of the components of a quad, nested like the parts of its internal id
135
+ function quadKeyParts(quad) {
136
+ const parts = [nestedKey(quad.subject), nestedKey(quad.predicate), nestedKey(quad.object)];
137
+ if (quad.graph && !(0, _N3Util.isDefaultGraph)(quad.graph)) parts.push(nestedKey(quad.graph));
138
+ return parts;
139
+ }
140
+ function nestedKey(term) {
141
+ return term.termType === 'Quad' ? quadKeyParts(term) : entityKey(term);
142
+ }
143
+
119
144
  // ## Constructor
120
145
  class N3EntityIndex {
121
146
  constructor(options = {}) {
@@ -132,6 +157,9 @@ class N3EntityIndex {
132
157
  this._factory = options.factory || _N3DataFactory.default;
133
158
  }
134
159
  _termFromId(id) {
160
+ // A key in < and > is a named node
161
+ if (id[0] === '<') return this._factory.namedNode(id.substring(1, id.length - 1));
162
+ // A key starting with . is a quoted triple
135
163
  if (id[0] === '.') {
136
164
  const entities = this._entities;
137
165
  const terms = id.split('.');
@@ -148,12 +176,25 @@ class N3EntityIndex {
148
176
  let g;
149
177
  return s && p && o && ((0, _N3Util.isDefaultGraph)(term.graph) || (g = this._termToNumericId(term.graph))) && this._ids[g ? `.${s}.${p}.${o}.${g}` : `.${s}.${p}.${o}`];
150
178
  }
151
- return this._ids[(0, _N3DataFactory.termToId)(term)];
179
+ return typeof term === 'string' ? this._stringToNumericId(term) : this._ids[entityKey(term)];
180
+ }
181
+
182
+ // Returns the numeric id of a term given as a string id
183
+ _stringToNumericId(term) {
184
+ // The string is the key unless it is an IRI starting with <. Read the first character
185
+ // from the stored key after a match, which is cheaper than from a concatenated string.
186
+ const id = this._ids[term];
187
+ if (id ? this._entities[id].charCodeAt(0) !== 0x3C : term.charCodeAt(0) !== 0x3C) return id;
188
+ return term === '<>' ? id : this._ids[`<${term}>`];
152
189
  }
153
190
  _termToNewNumericId(term) {
191
+ if (typeof term === 'string') return this._stringToNumericId(term) || this._addEntity(entityKey(term));
154
192
  // This assumes that no graph term is present - we may wish to error if there is one
155
- const str = term && term.termType === 'Quad' ? `.${this._termToNewNumericId(term.subject)}.${this._termToNewNumericId(term.predicate)}.${this._termToNewNumericId(term.object)}${(0, _N3Util.isDefaultGraph)(term.graph) ? '' : `.${this._termToNewNumericId(term.graph)}`}` : (0, _N3DataFactory.termToId)(term);
156
- return this._ids[str] || (this._ids[this._entities[++this._id] = str] = this._id);
193
+ const str = term && term.termType === 'Quad' ? `.${this._termToNewNumericId(term.subject)}.${this._termToNewNumericId(term.predicate)}.${this._termToNewNumericId(term.object)}${(0, _N3Util.isDefaultGraph)(term.graph) ? '' : `.${this._termToNewNumericId(term.graph)}`}` : entityKey(term);
194
+ return this._ids[str] || this._addEntity(str);
195
+ }
196
+ _addEntity(key) {
197
+ return this._ids[this._entities[++this._id] = key] = this._id;
157
198
  }
158
199
  createBlankNode(suggestedName) {
159
200
  let name, index;
@@ -576,8 +617,11 @@ class N3Store {
576
617
  }
577
618
 
578
619
  // ### `removeQuads` removes multiple quads from the store
620
+ // returns `true` if all quads were removed, `false` if some were not found
579
621
  removeQuads(quads) {
580
- for (let i = 0; i < quads.length; i++) this.removeQuad(quads[i]);
622
+ let removed = true;
623
+ for (let i = 0; i < quads.length; i++) removed = this.removeQuad(quads[i]) && removed;
624
+ return removed;
581
625
  }
582
626
 
583
627
  // ### `remove` removes a stream of quads from the store
@@ -1305,7 +1349,7 @@ function intersectMatchPatterns(left, right) {
1305
1349
  for (let i = 0; i < 4; i++) {
1306
1350
  const leftTerm = left[i],
1307
1351
  rightTerm = right[i];
1308
- if (leftTerm === null || leftTerm === undefined) result[i] = rightTerm;else if (rightTerm === null || rightTerm === undefined || (0, _N3DataFactory.termToId)(leftTerm) === (0, _N3DataFactory.termToId)(rightTerm)) result[i] = leftTerm;else return false;
1352
+ if (leftTerm === null || leftTerm === undefined) result[i] = rightTerm;else if (rightTerm === null || rightTerm === undefined || entityKey(leftTerm) === entityKey(rightTerm)) result[i] = leftTerm;else return false;
1309
1353
  }
1310
1354
  return result;
1311
1355
  }
@@ -1368,7 +1412,7 @@ class DatasetCoreAndReadableStream extends _readableStream.Readable {
1368
1412
  object,
1369
1413
  graph
1370
1414
  } = this;
1371
- return !this._matchesNothing && (subject === null || subject === undefined || (0, _N3DataFactory.termToId)(subject) === (0, _N3DataFactory.termToId)(quad.subject)) && (predicate === null || predicate === undefined || (0, _N3DataFactory.termToId)(predicate) === (0, _N3DataFactory.termToId)(quad.predicate)) && (object === null || object === undefined || (0, _N3DataFactory.termToId)(object) === (0, _N3DataFactory.termToId)(quad.object)) && (graph === null || graph === undefined || (0, _N3DataFactory.termToId)(graph) === (0, _N3DataFactory.termToId)(quad.graph));
1415
+ return !this._matchesNothing && (subject === null || subject === undefined || entityKey(subject) === entityKey(quad.subject)) && (predicate === null || predicate === undefined || entityKey(predicate) === entityKey(quad.predicate)) && (object === null || object === undefined || entityKey(object) === entityKey(quad.object)) && (graph === null || graph === undefined || entityKey(graph) === entityKey(quad.graph));
1372
1416
  }
1373
1417
 
1374
1418
  // ### `_assertMatchesPattern` rejects a Quad outside this view.
@@ -28,6 +28,10 @@ class N3StreamParser extends _readableStream.Transform {
28
28
  // Emit prefixes through the `prefix` event
29
29
  onPrefix: (prefix, uri) => {
30
30
  this.emit('prefix', prefix, uri);
31
+ },
32
+ // Emit additional directives through the `directive` event
33
+ onDirective: name => {
34
+ this.emit('directive', name);
31
35
  }
32
36
  };
33
37
  if (options && options.comments) callbacks.onComment = comment => {
package/lib/N3Writer.js CHANGED
@@ -18,10 +18,19 @@ const {
18
18
  rdf,
19
19
  xsd
20
20
  } = _IRIs.default;
21
+ const {
22
+ hasOwnProperty
23
+ } = Object.prototype;
21
24
 
22
- // Characters in literals that require escaping
25
+ // Characters that require escaping in Turtle, TriG, and N3,
26
+ // where characters outside the Basic Multilingual Plane are escaped as well
23
27
  const escape = /["\\\t\n\r\b\f\u0000-\u0019\ud800-\udbff]/,
24
28
  escapeAll = /["\\\t\n\r\b\f\u0000-\u0019]|[\ud800-\udbff][\udc00-\udfff]/g,
29
+ // Characters that require escaping in canonical N-Triples and N-Quads:
30
+ // U+0000–U+001F, `"`, `\`, U+007F, U+FFFE, and U+FFFF.
31
+ // A negated class is as fast to test as a shorter class listing characters.
32
+ canonicalEscape = /[^ !#-\[\]-~\u0080-\uFFFD]/,
33
+ canonicalEscapeAll = /[^ !#-\[\]-~\u0080-\uFFFD]/g,
25
34
  escapedCharacters = {
26
35
  '\\': '\\\\',
27
36
  '"': '\\"',
@@ -32,6 +41,9 @@ const escape = /["\\\t\n\r\b\f\u0000-\u0019\ud800-\udbff]/,
32
41
  '\f': '\\f'
33
42
  };
34
43
 
44
+ // Characters that a version label cannot contain
45
+ const invalidVersionLabel = /["\\\u0000-\u001f\u007f]|[\ud800-\udbff](?![\udc00-\udfff])|(?:^|[^\ud800-\udbff])[\udc00-\udfff]/;
46
+
35
47
  // ## Placeholder class to represent already pretty-printed terms
36
48
  class SerializedTerm extends _N3DataFactory.Term {
37
49
  // Pretty-printed nodes are not equal to any other node
@@ -71,12 +83,20 @@ class N3Writer {
71
83
  this._endStream = options.end === undefined ? true : !!options.end;
72
84
  }
73
85
 
86
+ // A version label is written as-is, so it cannot contain characters that need escaping,
87
+ // nor unpaired surrogates (which cannot be encoded)
88
+ if (options.version && invalidVersionLabel.test(options.version)) throw new Error(`Invalid version label: ${JSON.stringify(options.version)}`);
89
+
74
90
  // Initialize writer, depending on the format
75
91
  this._subject = null;
76
92
  if (!/triple|quad/i.test(options.format)) {
77
93
  this._lineMode = false;
94
+ this._escape = escape, this._escapeAll = escapeAll, this._characterReplacer = characterReplacer;
78
95
  this._graph = DEFAULTGRAPH;
96
+ if (options.version) this._write(`@version "${options.version}".\n`);
79
97
  this._prefixIRIs = Object.create(null);
98
+ // Escaped prefix IRIs and names for the prefix matcher, computed once per prefix
99
+ this._prefixPatterns = Object.create(null);
80
100
  if (options.baseIRI) {
81
101
  this._baseIri = new _BaseIRI.default(options.baseIRI);
82
102
  if (options.writeBase !== false) this._write(`@base <${options.baseIRI}>.\n`);
@@ -85,6 +105,10 @@ class N3Writer {
85
105
  } else {
86
106
  this._lineMode = true;
87
107
  this._writeQuad = this._writeQuadLine;
108
+ // N-Triples and N-Quads are written in their canonical form
109
+ this._escape = canonicalEscape, this._escapeAll = canonicalEscapeAll;
110
+ this._characterReplacer = canonicalCharacterReplacer;
111
+ if (options.version) this._write(`VERSION "${options.version}"\n`);
88
112
  }
89
113
  }
90
114
 
@@ -100,6 +124,15 @@ class N3Writer {
100
124
  this._outputStream.write(string, 'utf8', callback);
101
125
  }
102
126
 
127
+ // ### `_endStatement` finishes a pending statement and closes an open graph block
128
+ _endStatement() {
129
+ if (this._subject !== null) {
130
+ this._write(this._inDefaultGraph ? '.\n' : '\n}\n');
131
+ this._subject = null;
132
+ this._graph = DEFAULTGRAPH;
133
+ }
134
+ }
135
+
103
136
  // ### `_writeQuad` writes the quad to the output stream
104
137
  _writeQuad(subject, predicate, object, graph, done) {
105
138
  try {
@@ -163,7 +196,7 @@ class N3Writer {
163
196
  // A blank node or list is represented as-is
164
197
  if (entity.termType !== 'NamedNode') {
165
198
  // If it is a list head, pretty-print it
166
- if (this._lists && entity.value in this._lists) entity = this.list(this._lists[entity.value]);
199
+ if (this._lists && hasOwnProperty.call(this._lists, entity.value)) entity = this.list(this._lists[entity.value]);
167
200
  // Terms from this library already hold their serialization as id
168
201
  if (entity instanceof _N3DataFactory.Term) return entity.id;
169
202
  return entity.termType === 'Variable' ? `?${entity.value}` : `_:${entity.value}`;
@@ -174,9 +207,9 @@ class N3Writer {
174
207
  iri = this._baseIri.toRelative(iri);
175
208
  }
176
209
  // Escape special characters
177
- if (escape.test(iri)) iri = iri.replace(escapeAll, characterReplacer);
210
+ if (this._escape.test(iri)) iri = iri.replace(this._escapeAll, this._characterReplacer);
178
211
  // Try to represent the IRI as prefixed name, unless no prefixes were added
179
- const prefixMatch = this._hasPrefixes ? this._prefixRegex.exec(iri) : null;
212
+ const prefixMatch = this._hasPrefixes ? (this._prefixRegex || this._createPrefixRegex()).exec(iri) : null;
180
213
  return !prefixMatch ? `<${iri}>` : this._prefixIRIs[prefixMatch[1]] + prefixMatch[2];
181
214
  }
182
215
 
@@ -184,7 +217,7 @@ class N3Writer {
184
217
  _encodeLiteral(literal) {
185
218
  // Escape special characters
186
219
  let value = literal.value;
187
- if (escape.test(value)) value = value.replace(escapeAll, characterReplacer);
220
+ if (this._escape.test(value)) value = value.replace(this._escapeAll, this._characterReplacer);
188
221
 
189
222
  // Write a language-tagged literal
190
223
  const language = literal.language;
@@ -254,7 +287,9 @@ class N3Writer {
254
287
  object,
255
288
  graph
256
289
  }) {
257
- return `<<(${this._encodeSubject(subject)} ${this._encodePredicate(predicate)} ${this._encodeObject(object)}${(0, _N3Util.isDefaultGraph)(graph) ? '' : ` ${this._encodeIriOrBlank(graph)}`})>>`;
290
+ // Canonical N-Triples and N-Quads put spaces inside the delimiters
291
+ const space = this._lineMode ? ' ' : '';
292
+ return `<<(${space}${this._encodeSubject(subject)} ${this._encodePredicate(predicate)} ${this._encodeObject(object)}${(0, _N3Util.isDefaultGraph)(graph) ? '' : ` ${this._encodeIriOrBlank(graph)}`}${space})>>`;
258
293
  }
259
294
 
260
295
  // ### `_blockedWrite` replaces `_write` after the writer has been closed
@@ -296,26 +331,29 @@ class N3Writer {
296
331
  if (typeof iri !== 'string') iri = iri.value;
297
332
  hasPrefixes = true;
298
333
  // Finish a possible pending quad
299
- if (this._subject !== null) {
300
- this._write(this._inDefaultGraph ? '.\n' : '\n}\n');
301
- this._subject = null, this._graph = '';
302
- }
334
+ this._endStatement();
303
335
  // Store and write the prefix
304
336
  this._prefixIRIs[iri] = prefix += ':';
337
+ this._prefixPatterns[iri] = (0, _Util.escapeRegex)(iri);
305
338
  this._write(`@prefix ${prefix} <${iri}>.\n`);
306
339
  }
307
- // Recreate the prefix matcher
340
+ // Recreate the prefix matcher when it is next needed, so that adding
341
+ // prefixes one by one does not rebuild it for every prefix
308
342
  if (hasPrefixes) {
309
343
  this._hasPrefixes = true;
310
- let IRIlist = '';
311
- for (const prefixIRI in this._prefixIRIs) IRIlist += IRIlist ? `|${prefixIRI}` : prefixIRI;
312
- IRIlist = (0, _Util.escapeRegex)(IRIlist, /[\]\/\(\)\*\+\?\.\\\$]/g, '\\$&');
313
- this._prefixRegex = new RegExp(`^(${IRIlist})([_a-zA-Z0-9](?:\\.?[\\-_a-zA-Z0-9])*)$`);
344
+ this._prefixRegex = null;
314
345
  }
315
346
  // End a prefix block with a newline
316
347
  this._write(hasPrefixes ? '\n' : '', done);
317
348
  }
318
349
 
350
+ // ### `_createPrefixRegex` creates the matcher for the current prefixes
351
+ _createPrefixRegex() {
352
+ let IRIlist = '';
353
+ for (const prefixIRI in this._prefixPatterns) IRIlist += IRIlist ? `|${this._prefixPatterns[prefixIRI]}` : this._prefixPatterns[prefixIRI];
354
+ return this._prefixRegex = new RegExp(`^(${IRIlist})([_a-zA-Z0-9](?:\\.?[\\-_a-zA-Z0-9])*)$`);
355
+ }
356
+
319
357
  // ### `blank` creates a blank node with the given content
320
358
  blank(predicate, object) {
321
359
  let children = predicate,
@@ -367,10 +405,7 @@ class N3Writer {
367
405
  // ### `end` signals the end of the output stream
368
406
  end(done) {
369
407
  // Finish a possible pending quad
370
- if (this._subject !== null) {
371
- this._write(this._inDefaultGraph ? '.\n' : '\n}\n');
372
- this._subject = null;
373
- }
408
+ this._endStatement();
374
409
  // Disallow further writing
375
410
  this._write = this._blockedWrite;
376
411
 
@@ -405,4 +440,15 @@ function characterReplacer(character) {
405
440
  }
406
441
  }
407
442
  return result;
443
+ }
444
+
445
+ // Replaces a character by its canonical N-Triples escape:
446
+ // a short escape where one exists, and an upper-case `\uXXXX` escape otherwise
447
+ function canonicalCharacterReplacer(character) {
448
+ let result = escapedCharacters[character];
449
+ if (result === undefined) {
450
+ result = character.charCodeAt(0).toString(16).toUpperCase();
451
+ result = '\\u0000'.substr(0, 6 - result.length) + result;
452
+ }
453
+ return result;
408
454
  }
package/lib/Util.js CHANGED
@@ -5,5 +5,5 @@ Object.defineProperty(exports, "__esModule", {
5
5
  });
6
6
  exports.escapeRegex = escapeRegex;
7
7
  function escapeRegex(regex) {
8
- return regex.replace(/[\]\/\(\)\*\+\?\.\\\$]/g, '\\$&');
8
+ return regex.replace(/[.*+?^${}()|[\]\\/-]/g, '\\$&');
9
9
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "n3",
3
- "version": "3.0.0-alpha.4",
3
+ "version": "3.0.0-alpha.6",
4
4
  "description": "Lightning fast, asynchronous, streaming Turtle / N3 / RDF library.",
5
5
  "author": "Ruben Verborgh <ruben.verborgh@gmail.com>",
6
6
  "keywords": [
@@ -60,44 +60,17 @@
60
60
  "test": "jest",
61
61
  "lint": "eslint src perf test spec",
62
62
  "prepare": "npm run build",
63
- "spec": "npm run spec-1-1 && npm run spec-1-2 && npm run spec-n3",
64
- "spec-1-1": "npm run spec-1-1-turtle && npm run spec-1-1-ntriples && npm run spec-1-1-nquads && npm run spec-1-1-trig",
63
+ "spec": "node spec/run.js",
65
64
  "spec-1-1-earl": "npm run spec-1-1-earl-turtle && npm run spec-1-1-earl-ntriples && npm run spec-1-1-earl-nquads && npm run spec-1-1-earl-trig",
66
- "spec-1-1-ntriples": "rdf-test-suite spec/parser.js https://w3c.github.io/rdf-tests/rdf/rdf11/rdf-n-triples/manifest.ttl -i '{ \"format\": \"n-triples\" }' -c .rdf-test-suite-cache/",
67
- "spec-1-1-nquads": "rdf-test-suite spec/parser.js https://w3c.github.io/rdf-tests/rdf/rdf11/rdf-n-quads/manifest.ttl -i '{ \"format\": \"n-quads\" }' -c .rdf-test-suite-cache/",
68
- "spec-1-1-turtle": "rdf-test-suite spec/parser.js https://w3c.github.io/rdf-tests/rdf/rdf11/rdf-turtle/manifest.ttl -i '{ \"format\": \"turtle\" }' -c .rdf-test-suite-cache/ --skip \"(bad-numeric-escape|bareword_decimal)\"",
69
- "spec-1-1-trig": "rdf-test-suite spec/parser.js https://w3c.github.io/rdf-tests/rdf/rdf11/rdf-trig/manifest.ttl -i '{ \"format\": \"trig\" }' -c .rdf-test-suite-cache/ --skip \"(bad-numeric-escape|IRI-resolution-01)\"",
70
65
  "spec-1-1-earl-ntriples": "rdf-test-suite spec/parser.js https://w3c.github.io/rdf-tests/rdf/rdf11/rdf-n-triples/manifest.ttl -i '{ \"format\": \"n-triples\" }' -c .rdf-test-suite-cache/ -o earl -p spec/earl-meta.json > spec/earl-ntriples.ttl",
71
66
  "spec-1-1-earl-nquads": "rdf-test-suite spec/parser.js https://w3c.github.io/rdf-tests/rdf/rdf11/rdf-n-quads/manifest.ttl -i '{ \"format\": \"n-quads\" }' -c .rdf-test-suite-cache/ -o earl -p spec/earl-meta.json > spec/earl-nquads.ttl",
72
67
  "spec-1-1-earl-turtle": "rdf-test-suite spec/parser.js https://w3c.github.io/rdf-tests/rdf/rdf11/rdf-turtle/manifest.ttl -i '{ \"format\": \"turtle\" }' -c .rdf-test-suite-cache/ -o earl -p spec/earl-meta.json > spec/earl-turtle.ttl",
73
68
  "spec-1-1-earl-trig": "rdf-test-suite spec/parser.js https://w3c.github.io/rdf-tests/rdf/rdf11/rdf-trig/manifest.ttl -i '{ \"format\": \"trig\" }' -c .rdf-test-suite-cache/ -o earl -p spec/earl-meta.json > spec/earl-trig.ttl",
74
- "spec-1-2": "npm run spec-1-2-turtle && npm run spec-1-2-ntriples && npm run spec-1-2-nquads && npm run spec-1-2-trig && npm run spec-1-2-turtle-eval && npm run spec-1-2-trig-eval",
75
69
  "spec-1-2-earl": "npm run spec-1-2-earl-turtle && npm run spec-1-2-earl-ntriples && npm run spec-1-2-earl-nquads && npm run spec-1-2-earl-trig",
76
- "spec-1-2-ntriples": "rdf-test-suite spec/parser.js https://w3c.github.io/rdf-tests/rdf/rdf12/rdf-n-triples/syntax/manifest.ttl -i '{ \"format\": \"n-triples\" }' -c .rdf-test-suite-cache/",
77
- "spec-1-2-nquads": "rdf-test-suite spec/parser.js https://w3c.github.io/rdf-tests/rdf/rdf12/rdf-n-quads/syntax/manifest.ttl -i '{ \"format\": \"n-quads\" }' -c .rdf-test-suite-cache/",
78
- "spec-1-2-turtle": "rdf-test-suite spec/parser.js https://w3c.github.io/rdf-tests/rdf/rdf12/rdf-turtle/syntax/manifest.ttl -i '{ \"format\": \"turtle\" }' -c .rdf-test-suite-cache/",
79
- "spec-1-2-trig": "rdf-test-suite spec/parser.js https://w3c.github.io/rdf-tests/rdf/rdf12/rdf-trig/syntax/manifest.ttl -i '{ \"format\": \"trig\" }' -c .rdf-test-suite-cache/",
80
- "spec-1-2-turtle-eval": "rdf-test-suite spec/parser.js https://w3c.github.io/rdf-tests/rdf/rdf12/rdf-turtle/eval/manifest.ttl -i '{ \"format\": \"turtle\" }' -c .rdf-test-suite-cache/",
81
- "spec-1-2-trig-eval": "rdf-test-suite spec/parser.js https://w3c.github.io/rdf-tests/rdf/rdf12/rdf-trig/eval/manifest.ttl -i '{ \"format\": \"trig\" }' -c .rdf-test-suite-cache/",
82
70
  "spec-1-2-earl-ntriples": "rdf-test-suite spec/parser.js https://w3c.github.io/rdf-tests/rdf/rdf12/rdf-n-triples/syntax/manifest.ttl -i '{ \"format\": \"n-triples\" }' -c .rdf-test-suite-cache/ -o earl -p spec/earl-meta.json > spec/earl-ntriples.ttl",
83
71
  "spec-1-2-earl-nquads": "rdf-test-suite spec/parser.js https://w3c.github.io/rdf-tests/rdf/rdf12/rdf-n-quads/syntax/manifest.ttl -i '{ \"format\": \"n-quads\" }' -c .rdf-test-suite-cache/ -o earl -p spec/earl-meta.json > spec/earl-nquads.ttl",
84
72
  "spec-1-2-earl-turtle": "rdf-test-suite spec/parser.js https://w3c.github.io/rdf-tests/rdf/rdf12/rdf-turtle/syntax/manifest.ttl -i '{ \"format\": \"turtle\" }' -c .rdf-test-suite-cache/ -o earl -p spec/earl-meta.json > spec/earl-turtle.ttl",
85
73
  "spec-1-2-earl-trig": "rdf-test-suite spec/parser.js https://w3c.github.io/rdf-tests/rdf/rdf12/rdf-trig/syntax/manifest.ttl -i '{ \"format\": \"trig\" }' -c .rdf-test-suite-cache/ -o earl -p spec/earl-meta.json > spec/earl-trig.ttl",
86
- "spec-writer": "npm run spec-writer-1-1-turtle && npm run spec-writer-1-1-ntriples && npm run spec-writer-1-1-nquads && npm run spec-writer-1-1-trig && npm run spec-writer-1-2-turtle && npm run spec-writer-1-2-ntriples && npm run spec-writer-1-2-nquads && npm run spec-writer-1-2-trig && npm run spec-writer-1-2-turtle-eval && npm run spec-writer-1-2-trig-eval",
87
- "spec-writer-1-1-turtle": "rdf-test-suite spec/writer.js https://w3c.github.io/rdf-tests/rdf/rdf11/rdf-turtle/manifest.ttl -i '{ \"format\": \"turtle\" }' -c .rdf-test-suite-cache/ --skip \"(bad-numeric-escape|bareword_decimal)\"",
88
- "spec-writer-1-1-ntriples": "rdf-test-suite spec/writer.js https://w3c.github.io/rdf-tests/rdf/rdf11/rdf-n-triples/manifest.ttl -i '{ \"format\": \"n-triples\" }' -c .rdf-test-suite-cache/",
89
- "spec-writer-1-1-nquads": "rdf-test-suite spec/writer.js https://w3c.github.io/rdf-tests/rdf/rdf11/rdf-n-quads/manifest.ttl -i '{ \"format\": \"n-quads\" }' -c .rdf-test-suite-cache/",
90
- "spec-writer-1-1-trig": "rdf-test-suite spec/writer.js https://w3c.github.io/rdf-tests/rdf/rdf11/rdf-trig/manifest.ttl -i '{ \"format\": \"trig\" }' -c .rdf-test-suite-cache/ --skip \"(bad-numeric-escape|IRI-resolution-01)\"",
91
- "spec-writer-1-2-turtle": "rdf-test-suite spec/writer.js https://w3c.github.io/rdf-tests/rdf/rdf12/rdf-turtle/syntax/manifest.ttl -i '{ \"format\": \"turtle\" }' -c .rdf-test-suite-cache/",
92
- "spec-writer-1-2-ntriples": "rdf-test-suite spec/writer.js https://w3c.github.io/rdf-tests/rdf/rdf12/rdf-n-triples/syntax/manifest.ttl -i '{ \"format\": \"n-triples\" }' -c .rdf-test-suite-cache/",
93
- "spec-writer-1-2-nquads": "rdf-test-suite spec/writer.js https://w3c.github.io/rdf-tests/rdf/rdf12/rdf-n-quads/syntax/manifest.ttl -i '{ \"format\": \"n-quads\" }' -c .rdf-test-suite-cache/",
94
- "spec-writer-1-2-trig": "rdf-test-suite spec/writer.js https://w3c.github.io/rdf-tests/rdf/rdf12/rdf-trig/syntax/manifest.ttl -i '{ \"format\": \"trig\" }' -c .rdf-test-suite-cache/",
95
- "spec-writer-1-2-turtle-eval": "rdf-test-suite spec/writer.js https://w3c.github.io/rdf-tests/rdf/rdf12/rdf-turtle/eval/manifest.ttl -i '{ \"format\": \"turtle\" }' -c .rdf-test-suite-cache/",
96
- "spec-writer-1-2-trig-eval": "rdf-test-suite spec/writer.js https://w3c.github.io/rdf-tests/rdf/rdf12/rdf-trig/eval/manifest.ttl -i '{ \"format\": \"trig\" }' -c .rdf-test-suite-cache/",
97
- "spec-n3": "npm run spec-n3-parser && npm run spec-n3-extended",
98
- "spec-n3-fetch": "sh scripts/fetch-n3-tests.sh",
99
- "spec-n3-parser": "npm run spec-n3-fetch && rdf-test-suite spec/parser.js https://w3c-cg.github.io/N3/tests/N3Tests/manifest-parser.ttl -i '{ \"format\": \"text/n3\" }' -m 'https://w3c-cg.github.io/N3/tests/~.n3-tests/tests/'",
100
- "spec-n3-extended": "npm run spec-n3-fetch && rdf-test-suite spec/parser.js https://w3c-cg.github.io/N3/tests/N3Tests/manifest-extended.ttl -i '{ \"format\": \"text/n3\" }' -m 'https://w3c-cg.github.io/N3/tests/~.n3-tests/tests/'",
101
74
  "spec-clean": "rm -rf .rdf-test-suite-cache/ .n3-tests/",
102
75
  "docs": "cd src && docco *.js -o ../docs && cd .."
103
76
  },