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/src/N3Parser.js CHANGED
@@ -4,6 +4,10 @@ import N3DataFactory from './N3DataFactory';
4
4
  import namespaces from './IRIs';
5
5
 
6
6
  let blankNodePrefix = 0;
7
+ // Detects `.` and `..` path segments in an IRI
8
+ const dotSegments = /(^|\/)\.\.?($|[/#?])/;
9
+ // Detects `.` and `..` segments in the path of an IRI, ignoring its query and fragment
10
+ const pathDotSegments = /^[^?#]*(?:^|\/)\.\.?(?:$|[/#?])/;
7
11
 
8
12
  // ## Constructor
9
13
  export default class N3Parser {
@@ -47,7 +51,16 @@ export default class N3Parser {
47
51
  this._resolveRelativeIRI = iri => { return null; };
48
52
  this._blankNodePrefix = typeof options.blankNodePrefix !== 'string' ? '' :
49
53
  options.blankNodePrefix.replace(/^(?!_:)/, '_:');
50
- this._lexer = options.lexer || new N3Lexer({ lineMode: isLineMode, n3: isN3, isImpliedBy: this._isImpliedBy });
54
+ // Map the token types of additional directives (@name and NAME) to their names
55
+ this._directives = null;
56
+ if (options.directives) {
57
+ this._directives = Object.create(null);
58
+ for (const name of options.directives)
59
+ this._directives[`@${name.toLowerCase()}`] = this._directives[name.toUpperCase()] = name;
60
+ }
61
+ this._lexer = options.lexer || new N3Lexer({
62
+ lineMode: isLineMode, n3: isN3, isImpliedBy: this._isImpliedBy, directives: options.directives,
63
+ });
51
64
  // Disable explicit quantifiers by default
52
65
  this._explicitQuantifiers = !!options.explicitQuantifiers;
53
66
  // Scope blank node labels to formulas only, unless explicitly disabled
@@ -55,6 +68,10 @@ export default class N3Parser {
55
68
  // Disable parsing of unsupported versions by default
56
69
  this._parseUnsupportedVersions = !!options.parseUnsupportedVersions;
57
70
  this._version = options.version;
71
+ // Maximum nesting depth of triple terms (default 1024; Infinity disables the limit)
72
+ const maxTripleTermDepth = options.maxTripleTermDepth;
73
+ this._maxTripleTermDepth = maxTripleTermDepth === Infinity ||
74
+ Number.isInteger(maxTripleTermDepth) && maxTripleTermDepth >= 0 ? maxTripleTermDepth : 1024;
58
75
  }
59
76
 
60
77
  // ## Static class methods
@@ -71,6 +88,7 @@ export default class N3Parser {
71
88
  if (!baseIRI) {
72
89
  this._base = '';
73
90
  this._basePath = '';
91
+ this._basePathHasDotSegments = false;
74
92
  }
75
93
  else {
76
94
  // Remove fragment if present
@@ -83,12 +101,32 @@ export default class N3Parser {
83
101
  const path = queryPos < 0 ? baseIRI : baseIRI.substr(0, queryPos);
84
102
  this._basePath = baseIRI.indexOf('/') < 0 ? baseIRI :
85
103
  path.substr(0, path.lastIndexOf('/') + 1);
86
- baseIRI = baseIRI.match(/^(?:([a-z][a-z0-9+.-]*:))?(?:\/\/[^\/]*)?/i);
104
+ const base = baseIRI;
105
+ baseIRI = baseIRI.match(/^(?:([a-z][a-z0-9+.-]*:))?(\/\/[^\/?#]*)?/i);
87
106
  this._baseRoot = baseIRI[0];
88
107
  this._baseScheme = baseIRI[1];
108
+ // If the base has an authority but an empty path,
109
+ // relative IRIs merge under the path '/' (RFC 3986 §5.2.3)
110
+ if (baseIRI[2] !== undefined && (base.length === this._baseRoot.length || base[this._baseRoot.length] === '?'))
111
+ this._basePath = `${this._baseRoot}/`;
112
+ // Check once whether resolving against the base path needs to remove dot segments
113
+ this._basePathHasDotSegments = dotSegments.test(this._basePath);
89
114
  }
90
115
  }
91
116
 
117
+ // ### `_enterTripleTerm` stores the current parsing context when entering a triple term,
118
+ // failing if triple terms nest deeper than the maximum depth
119
+ _enterTripleTerm(type, subject, predicate, token) {
120
+ if (this._tripleTermDepth >= this._maxTripleTermDepth) {
121
+ this._error(`Triple terms nested deeper than ${this._maxTripleTermDepth} level${this._maxTripleTermDepth === 1 ? '' : 's'}`, token);
122
+ return false;
123
+ }
124
+ this._tripleTermDepth++;
125
+ this._saveContext(type, this._graph, subject, predicate, null);
126
+ this._graph = null;
127
+ return true;
128
+ }
129
+
92
130
  // ### `_saveContext` stores the current parsing context
93
131
  // when entering a new scope (list, blank node, formula)
94
132
  _saveContext(type, graph, subject, predicate, object) {
@@ -110,7 +148,8 @@ export default class N3Parser {
110
148
  // Prefix and base declarations are scoped to their formula,
111
149
  // so record prefix changes to undo them when the formula ends
112
150
  if (type === 'formula') {
113
- context.base = [this._base, this._basePath, this._baseRoot, this._baseScheme];
151
+ context.base = [this._base, this._basePath, this._baseRoot, this._baseScheme,
152
+ this._basePathHasDotSegments];
114
153
  this._prefixChanges = [];
115
154
  }
116
155
  this._contextStack.push(context);
@@ -152,7 +191,8 @@ export default class N3Parser {
152
191
  this._inversePredicate = context.inverse;
153
192
  this._expectOf = context.expectOf;
154
193
  if (type === 'formula')
155
- [this._base, this._basePath, this._baseRoot, this._baseScheme] = context.base;
194
+ [this._base, this._basePath, this._baseRoot, this._baseScheme,
195
+ this._basePathHasDotSegments] = context.base;
156
196
  if (this._prefixChanges !== context.prefixChanges) {
157
197
  undoChanges(this._prefixes, this._prefixChanges);
158
198
  this._prefixChanges = context.prefixChanges;
@@ -207,8 +247,13 @@ export default class N3Parser {
207
247
  case 'GRAPH':
208
248
  if (this._supportsNamedGraphs)
209
249
  return this._readNamedGraphLabel;
210
- // Otherwise, the next token must be a subject
250
+ // Otherwise, the next token must be a subject, unless it is an additional directive
211
251
  default:
252
+ if (this._directives !== null && token.type in this._directives && this._graph === null) {
253
+ this._sparqlStyle = token.type[0] !== '@';
254
+ this._directiveCallback(this._directives[token.type]);
255
+ return this._readDeclarationPunctuation;
256
+ }
212
257
  return this._readSubject(token);
213
258
  }
214
259
  }
@@ -339,13 +384,9 @@ export default class N3Parser {
339
384
  case '<<(':
340
385
  if (!this._n3Mode)
341
386
  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;
387
+ return this._enterTripleTerm('<<(', null, null, token) && this._readSubject;
345
388
  case '<<':
346
- this._saveContext('<<', this._graph, null, null, null);
347
- this._graph = null;
348
- return this._readSubject;
389
+ return this._enterTripleTerm('<<', null, null, token) && this._readSubject;
349
390
  default:
350
391
  // Read the subject entity
351
392
  if ((this._subject = this._readEntity(token)) === undefined)
@@ -490,13 +531,9 @@ export default class N3Parser {
490
531
  this._graph = this._factory.blankNode());
491
532
  return this._readInFormulaContext;
492
533
  case '<<(':
493
- this._saveContext('<<(', this._graph, this._subject, this._predicate, null);
494
- this._graph = null;
495
- return this._readSubject;
534
+ return this._enterTripleTerm('<<(', this._subject, this._predicate, token) && this._readSubject;
496
535
  case '<<':
497
- this._saveContext('<<', this._graph, this._subject, this._predicate, null);
498
- this._graph = null;
499
- return this._readSubject;
536
+ return this._enterTripleTerm('<<', this._subject, this._predicate, token) && this._readSubject;
500
537
  default:
501
538
  // Read the object entity
502
539
  if ((this._object = this._readEntity(token)) === undefined)
@@ -716,13 +753,9 @@ export default class N3Parser {
716
753
  this._subject = null;
717
754
  return this._readInFormulaContext;
718
755
  case '<<(':
719
- this._saveContext('<<(', this._graph, null, null, null);
720
- this._graph = null;
721
- next = this._readSubject;
722
- break;
723
756
  case '<<':
724
- this._saveContext('<<', this._graph, null, null, null);
725
- this._graph = null;
757
+ if (!this._enterTripleTerm(token.type, null, null, token))
758
+ return;
726
759
  next = this._readSubject;
727
760
  break;
728
761
  default:
@@ -1088,7 +1121,8 @@ export default class N3Parser {
1088
1121
  _readVersion(token) {
1089
1122
  if (token.type !== 'literal')
1090
1123
  return this._error('Expected literal to follow version declaration', token);
1091
- if ((token.end - token.start) !== token.value.length + 2)
1124
+ // Only short strings are allowed, so no numbers or booleans (which have a datatype prefix)
1125
+ if (token.prefix !== '' || token.tripleQuoted)
1092
1126
  return this._error('Version declarations must use single quotes', token);
1093
1127
  this._versionCallback(token.value);
1094
1128
  if (!this._isValidVersion(token.value))
@@ -1272,6 +1306,7 @@ export default class N3Parser {
1272
1306
  const quad = this._createQuad(this._subject, this._predicate, this._object,
1273
1307
  this._graph, this._inversePredicate);
1274
1308
  this._restoreContext('<<(', token);
1309
+ this._tripleTermDepth--;
1275
1310
 
1276
1311
  // If we're in a list, continue processing that list
1277
1312
  const stack = this._contextStack, parent = stack.length && stack[stack.length - 1];
@@ -1307,6 +1342,7 @@ export default class N3Parser {
1307
1342
  this._tripleTerm = null;
1308
1343
  const reifier = this._readTripleTerm();
1309
1344
  this._restoreContext('<<', token);
1345
+ this._tripleTermDepth--;
1310
1346
 
1311
1347
  // // If we're in a list, continue processing that list
1312
1348
  const stack = this._contextStack, parent = stack.length && stack[stack.length - 1];
@@ -1540,19 +1576,29 @@ export default class N3Parser {
1540
1576
  }
1541
1577
  // Resolve root-relative IRIs at the root of the base IRI
1542
1578
  case '/':
1543
- // Resolve scheme-relative IRIs to the scheme
1544
- return (iri[1] === '/' ? this._baseScheme : this._baseRoot) + this._removeDotSegments(iri);
1579
+ // Resolve scheme-relative IRIs to the scheme,
1580
+ // keeping the reference's authority intact (RFC 3986 §5.2.4)
1581
+ if (iri[1] !== '/')
1582
+ return this._baseRoot + this._removeDotSegments(iri);
1583
+ // Without a base scheme, a leading ':' marks where the authority starts
1584
+ return this._baseScheme ? this._removeDotSegments(this._baseScheme + iri) :
1585
+ this._removeDotSegments(`:${iri}`).substr(1);
1545
1586
  // Resolve all other IRIs at the base IRI's path
1546
1587
  default:
1547
1588
  // Relative IRIs cannot contain a colon in the first path segment
1548
- return (/^[^/:]*:/.test(iri)) ? null : this._removeDotSegments(this._basePath + iri);
1589
+ if (/^[^/:]*:/.test(iri))
1590
+ return null;
1591
+ // Only scan the joined IRI for dot segments if either part can contain them,
1592
+ // as the base path can be long and the joined IRI would need to be copied
1593
+ return this._basePathHasDotSegments || pathDotSegments.test(iri) ?
1594
+ this._removeDotSegments(this._basePath + iri) : this._basePath + iri;
1549
1595
  }
1550
1596
  }
1551
1597
 
1552
1598
  // ### `_removeDotSegments` resolves './' and '../' path segments in an IRI as per RFC3986
1553
1599
  _removeDotSegments(iri) {
1554
1600
  // Don't modify the IRI if it does not contain any dot segments
1555
- if (!/(^|\/)\.\.?($|[/#?])/.test(iri))
1601
+ if (!dotSegments.test(iri))
1556
1602
  return iri;
1557
1603
 
1558
1604
  // Start with an imaginary slash before the IRI in order to resolve trailing './' and '../'
@@ -1566,8 +1612,8 @@ export default class N3Parser {
1566
1612
  if (pathStart < 0) {
1567
1613
  // Skip two slashes before the authority
1568
1614
  if (iri[++i] === '/' && iri[++i] === '/')
1569
- // Skip to slash after the authority
1570
- while ((pathStart = i + 1) < length && iri[pathStart] !== '/')
1615
+ // Skip to the end of the authority
1616
+ while ((pathStart = i + 1) < length && iri[pathStart] !== '/' && iri[pathStart] !== '?' && iri[pathStart] !== '#')
1571
1617
  i = pathStart;
1572
1618
  }
1573
1619
  break;
@@ -1618,13 +1664,14 @@ export default class N3Parser {
1618
1664
  parse(input, quadCallback, prefixCallback, versionCallback) {
1619
1665
  // The second parameter also accepts an object of named callbacks.
1620
1666
  // 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;
1667
+ let onQuad, onPrefix, onComment, onVersion, onDirective, onToken, onTokenEnd;
1622
1668
  if (quadCallback && (quadCallback.onQuad || quadCallback.onPrefix || quadCallback.onComment || quadCallback.onVersion ||
1623
- quadCallback.onToken || quadCallback.onTokenEnd)) {
1669
+ quadCallback.onDirective || quadCallback.onToken || quadCallback.onTokenEnd)) {
1624
1670
  onQuad = quadCallback.onQuad;
1625
1671
  onPrefix = quadCallback.onPrefix;
1626
1672
  onComment = quadCallback.onComment;
1627
1673
  onVersion = quadCallback.onVersion;
1674
+ onDirective = quadCallback.onDirective;
1628
1675
  onToken = quadCallback.onToken;
1629
1676
  onTokenEnd = quadCallback.onTokenEnd;
1630
1677
  }
@@ -1645,6 +1692,7 @@ export default class N3Parser {
1645
1692
  this._prefixes[''] = this._resolveIRI('#');
1646
1693
  this._prefixCallback = onPrefix || noop;
1647
1694
  this._versionCallback = onVersion || noop;
1695
+ this._directiveCallback = onDirective || noop;
1648
1696
  this._inversePredicate = false;
1649
1697
  this._expectOf = false;
1650
1698
  this._quantified = Object.create(null);
@@ -1652,6 +1700,7 @@ export default class N3Parser {
1652
1700
  this._prefixChanges = null;
1653
1701
  this._quantifiedChanges = null;
1654
1702
  this._emptyFormula = false;
1703
+ this._tripleTermDepth = 0;
1655
1704
 
1656
1705
  let readToken = token => {
1657
1706
  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[termToId(term)];
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
- : termToId(term);
216
+ : entityKey(term);
217
+
218
+ return this._ids[str] || this._addEntity(str);
219
+ }
172
220
 
173
- return this._ids[str] || (this._ids[this._entities[++this._id] = str] = this._id);
221
+ _addEntity(key) {
222
+ return this._ids[this._entities[++this._id] = key] = this._id;
174
223
  }
175
224
 
176
225
  createBlankNode(suggestedName) {
@@ -600,9 +649,12 @@ export default class N3Store {
600
649
  }
601
650
 
602
651
  // ### `removeQuads` removes multiple quads from the store
652
+ // returns `true` if all quads were removed, `false` if some were not found
603
653
  removeQuads(quads) {
654
+ let removed = true;
604
655
  for (let i = 0; i < quads.length; i++)
605
- this.removeQuad(quads[i]);
656
+ removed = this.removeQuad(quads[i]) && removed;
657
+ return removed;
606
658
  }
607
659
 
608
660
  // ### `remove` removes a stream of quads from the store
@@ -1382,7 +1434,7 @@ function intersectMatchPatterns(left, right) {
1382
1434
  const leftTerm = left[i], rightTerm = right[i];
1383
1435
  if (leftTerm === null || leftTerm === undefined)
1384
1436
  result[i] = rightTerm;
1385
- else if (rightTerm === null || rightTerm === undefined || termToId(leftTerm) === termToId(rightTerm))
1437
+ else if (rightTerm === null || rightTerm === undefined || entityKey(leftTerm) === entityKey(rightTerm))
1386
1438
  result[i] = leftTerm;
1387
1439
  else
1388
1440
  return false;
@@ -1433,10 +1485,10 @@ class DatasetCoreAndReadableStream extends Readable {
1433
1485
  _matchesQuad(quad) {
1434
1486
  const { subject, predicate, object, graph } = this;
1435
1487
  return !this._matchesNothing &&
1436
- (subject === null || subject === undefined || termToId(subject) === termToId(quad.subject)) &&
1437
- (predicate === null || predicate === undefined || termToId(predicate) === termToId(quad.predicate)) &&
1438
- (object === null || object === undefined || termToId(object) === termToId(quad.object)) &&
1439
- (graph === null || graph === undefined || termToId(graph) === termToId(quad.graph));
1488
+ (subject === null || subject === undefined || entityKey(subject) === entityKey(quad.subject)) &&
1489
+ (predicate === null || predicate === undefined || entityKey(predicate) === entityKey(quad.predicate)) &&
1490
+ (object === null || object === undefined || entityKey(object) === entityKey(quad.object)) &&
1491
+ (graph === null || graph === undefined || entityKey(graph) === entityKey(quad.graph));
1440
1492
  }
1441
1493
 
1442
1494
  // ### `_assertMatchesPattern` rejects a Quad outside this view.
@@ -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,15 +8,25 @@ 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 literals that require escaping
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',
18
25
  };
19
26
 
27
+ // Characters that a version label cannot contain
28
+ const invalidVersionLabel = /["\\\u0000-\u001f\u007f]|[\ud800-\udbff](?![\udc00-\udfff])|(?:^|[^\ud800-\udbff])[\udc00-\udfff]/;
29
+
20
30
  // ## Placeholder class to represent already pretty-printed terms
21
31
  class SerializedTerm extends Term {
22
32
  // Pretty-printed nodes are not equal to any other node
@@ -53,12 +63,22 @@ export default class N3Writer {
53
63
  this._endStream = options.end === undefined ? true : !!options.end;
54
64
  }
55
65
 
66
+ // A version label is written as-is, so it cannot contain characters that need escaping,
67
+ // nor unpaired surrogates (which cannot be encoded)
68
+ if (options.version && invalidVersionLabel.test(options.version))
69
+ throw new Error(`Invalid version label: ${JSON.stringify(options.version)}`);
70
+
56
71
  // Initialize writer, depending on the format
57
72
  this._subject = null;
58
73
  if (!(/triple|quad/i).test(options.format)) {
59
74
  this._lineMode = false;
75
+ this._escape = escape, this._escapeAll = escapeAll, this._characterReplacer = characterReplacer;
60
76
  this._graph = DEFAULTGRAPH;
77
+ if (options.version)
78
+ this._write(`@version "${options.version}".\n`);
61
79
  this._prefixIRIs = Object.create(null);
80
+ // Escaped prefix IRIs and names for the prefix matcher, computed once per prefix
81
+ this._prefixPatterns = Object.create(null);
62
82
  if (options.baseIRI) {
63
83
  this._baseIri = new BaseIRI(options.baseIRI);
64
84
  if (options.writeBase !== false)
@@ -69,6 +89,11 @@ export default class N3Writer {
69
89
  else {
70
90
  this._lineMode = true;
71
91
  this._writeQuad = this._writeQuadLine;
92
+ // N-Triples and N-Quads are written in their canonical form
93
+ this._escape = canonicalEscape, this._escapeAll = canonicalEscapeAll;
94
+ this._characterReplacer = canonicalCharacterReplacer;
95
+ if (options.version)
96
+ this._write(`VERSION "${options.version}"\n`);
72
97
  }
73
98
  }
74
99
 
@@ -84,6 +109,15 @@ export default class N3Writer {
84
109
  this._outputStream.write(string, 'utf8', callback);
85
110
  }
86
111
 
112
+ // ### `_endStatement` finishes a pending statement and closes an open graph block
113
+ _endStatement() {
114
+ if (this._subject !== null) {
115
+ this._write(this._inDefaultGraph ? '.\n' : '\n}\n');
116
+ this._subject = null;
117
+ this._graph = DEFAULTGRAPH;
118
+ }
119
+ }
120
+
87
121
  // ### `_writeQuad` writes the quad to the output stream
88
122
  _writeQuad(subject, predicate, object, graph, done) {
89
123
  try {
@@ -161,7 +195,7 @@ export default class N3Writer {
161
195
  // A blank node or list is represented as-is
162
196
  if (entity.termType !== 'NamedNode') {
163
197
  // If it is a list head, pretty-print it
164
- if (this._lists && (entity.value in this._lists))
198
+ if (this._lists && hasOwnProperty.call(this._lists, entity.value))
165
199
  entity = this.list(this._lists[entity.value]);
166
200
  // Terms from this library already hold their serialization as id
167
201
  if (entity instanceof Term)
@@ -174,10 +208,10 @@ export default class N3Writer {
174
208
  iri = this._baseIri.toRelative(iri);
175
209
  }
176
210
  // Escape special characters
177
- if (escape.test(iri))
178
- iri = iri.replace(escapeAll, characterReplacer);
211
+ if (this._escape.test(iri))
212
+ iri = iri.replace(this._escapeAll, this._characterReplacer);
179
213
  // Try to represent the IRI as prefixed name, unless no prefixes were added
180
- const prefixMatch = this._hasPrefixes ? this._prefixRegex.exec(iri) : null;
214
+ const prefixMatch = this._hasPrefixes ? (this._prefixRegex || this._createPrefixRegex()).exec(iri) : null;
181
215
  return !prefixMatch ? `<${iri}>` : this._prefixIRIs[prefixMatch[1]] + prefixMatch[2];
182
216
  }
183
217
 
@@ -185,8 +219,8 @@ export default class N3Writer {
185
219
  _encodeLiteral(literal) {
186
220
  // Escape special characters
187
221
  let value = literal.value;
188
- if (escape.test(value))
189
- value = value.replace(escapeAll, characterReplacer);
222
+ if (this._escape.test(value))
223
+ value = value.replace(this._escapeAll, this._characterReplacer);
190
224
 
191
225
  // Write a language-tagged literal
192
226
  const language = literal.language;
@@ -257,11 +291,13 @@ export default class N3Writer {
257
291
 
258
292
  // ### `_encodeQuad` encodes an RDF-star quad
259
293
  _encodeQuad({ subject, predicate, object, graph }) {
260
- return `<<(${
294
+ // Canonical N-Triples and N-Quads put spaces inside the delimiters
295
+ const space = this._lineMode ? ' ' : '';
296
+ return `<<(${space}${
261
297
  this._encodeSubject(subject)} ${
262
298
  this._encodePredicate(predicate)} ${
263
299
  this._encodeObject(object)}${
264
- isDefaultGraph(graph) ? '' : ` ${this._encodeIriOrBlank(graph)}`})>>`;
300
+ isDefaultGraph(graph) ? '' : ` ${this._encodeIriOrBlank(graph)}`}${space})>>`;
265
301
  }
266
302
 
267
303
  // ### `_blockedWrite` replaces `_write` after the writer has been closed
@@ -309,27 +345,30 @@ export default class N3Writer {
309
345
  iri = iri.value;
310
346
  hasPrefixes = true;
311
347
  // Finish a possible pending quad
312
- if (this._subject !== null) {
313
- this._write(this._inDefaultGraph ? '.\n' : '\n}\n');
314
- this._subject = null, this._graph = '';
315
- }
348
+ this._endStatement();
316
349
  // Store and write the prefix
317
350
  this._prefixIRIs[iri] = (prefix += ':');
351
+ this._prefixPatterns[iri] = escapeRegex(iri);
318
352
  this._write(`@prefix ${prefix} <${iri}>.\n`);
319
353
  }
320
- // Recreate the prefix matcher
354
+ // Recreate the prefix matcher when it is next needed, so that adding
355
+ // prefixes one by one does not rebuild it for every prefix
321
356
  if (hasPrefixes) {
322
357
  this._hasPrefixes = true;
323
- let IRIlist = '';
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])*)$`);
358
+ this._prefixRegex = null;
328
359
  }
329
360
  // End a prefix block with a newline
330
361
  this._write(hasPrefixes ? '\n' : '', done);
331
362
  }
332
363
 
364
+ // ### `_createPrefixRegex` creates the matcher for the current prefixes
365
+ _createPrefixRegex() {
366
+ let IRIlist = '';
367
+ for (const prefixIRI in this._prefixPatterns)
368
+ IRIlist += IRIlist ? `|${this._prefixPatterns[prefixIRI]}` : this._prefixPatterns[prefixIRI];
369
+ return this._prefixRegex = new RegExp(`^(${IRIlist})([_a-zA-Z0-9](?:\\.?[\\-_a-zA-Z0-9])*)$`);
370
+ }
371
+
333
372
  // ### `blank` creates a blank node with the given content
334
373
  blank(predicate, object) {
335
374
  let children = predicate, child, length;
@@ -385,10 +424,7 @@ export default class N3Writer {
385
424
  // ### `end` signals the end of the output stream
386
425
  end(done) {
387
426
  // Finish a possible pending quad
388
- if (this._subject !== null) {
389
- this._write(this._inDefaultGraph ? '.\n' : '\n}\n');
390
- this._subject = null;
391
- }
427
+ this._endStatement();
392
428
  // Disallow further writing
393
429
  this._write = this._blockedWrite;
394
430
 
@@ -421,3 +457,14 @@ function characterReplacer(character) {
421
457
  }
422
458
  return result;
423
459
  }
460
+
461
+ // Replaces a character by its canonical N-Triples escape:
462
+ // a short escape where one exists, and an upper-case `\uXXXX` escape otherwise
463
+ function canonicalCharacterReplacer(character) {
464
+ let result = escapedCharacters[character];
465
+ if (result === undefined) {
466
+ result = character.charCodeAt(0).toString(16).toUpperCase();
467
+ result = '\\u0000'.substr(0, 6 - result.length) + result;
468
+ }
469
+ return result;
470
+ }
package/src/Util.js CHANGED
@@ -1,3 +1,3 @@
1
1
  export function escapeRegex(regex) {
2
- return regex.replace(/[\]\/\(\)\*\+\?\.\\\$]/g, '\\$&');
2
+ return regex.replace(/[.*+?^${}()|[\]\\/-]/g, '\\$&');
3
3
  }