n3 2.11.7 → 2.12.0

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/N3Lexer.js CHANGED
@@ -93,6 +93,9 @@ function isSeparatorCode(code) {
93
93
  return code === SPACE || code === TAB || code === LF || code === CR || code === HASH;
94
94
  }
95
95
 
96
+ // Words with a fixed meaning in the grammar, which cannot name an additional directive
97
+ const reservedWords = /^(?:prefix|base|version|graph|forsome|forall|iri|a|true|false|has|is|of|id)$/i;
98
+
96
99
  // Unfinished input in a stream up to this length is tokenized again with every chunk
97
100
  const MIN_RESCAN_LENGTH = 1024;
98
101
 
@@ -137,6 +140,18 @@ class N3Lexer {
137
140
  else {
138
141
  this._n3Mode = options.n3 !== false;
139
142
  }
143
+ // Recognize additional directive keywords, such as MESSAGE
144
+ // (the @-form of a directive is always tokenized as an @-keyword)
145
+ this._directive = null;
146
+ if (options.directives && options.directives.length !== 0) {
147
+ for (const name of options.directives) {
148
+ if (!/^[a-z]+$/i.test(name) || reservedWords.test(name)) throw new Error(`Invalid directive name: "${name}"`);
149
+ }
150
+ this._directive = new RegExp(`(?:${options.directives.join('|')})(?=[\\s#<])`, 'iy');
151
+ this._directiveMaxLength = Math.max(...options.directives.map(name => name.length));
152
+ // The first characters of directive names, so other words skip the regular expression
153
+ this._directiveStarts = options.directives.map(name => name[0].toLowerCase() + name[0].toUpperCase()).join('');
154
+ }
140
155
  // Don't output comment tokens by default
141
156
  this.comments = !!options.comments;
142
157
  // Cache the last tested closing position of long literals
@@ -457,6 +472,9 @@ class N3Lexer {
457
472
  if (inconclusive) {
458
473
  // Try to find a prefix
459
474
  if ((this._previousMarker === '@prefix' || this._previousMarker === 'PREFIX') && (match = execAt(this._prefix, input, pos))) type = 'prefix', value = match[1] || '';
475
+ // Try to find an additional directive keyword
476
+ // (at the end of the input, only a short final word can be one)
477
+ else if (this._directive !== null && this._directiveStarts.includes(firstChar) && ((match = execAt(this._directive, input, pos)) || inputFinished && input.length - pos <= this._directiveMaxLength && (match = execAtEnd(this._directive, input, pos)))) type = match[0].toUpperCase();
460
478
  // Try to find a prefixed name. Since it can contain (but not end with) a dot,
461
479
  // we always need a non-dot character before deciding it is a prefixed name.
462
480
  // Therefore, try inserting a space if we're at the end of the input.
@@ -508,7 +526,8 @@ class N3Lexer {
508
526
  callback(null, token);
509
527
  } else token = emitToken(type, value, prefix, line, start, lexicalLength || length);
510
528
  this.previousToken = token;
511
- this._previousMarker = type;
529
+ // The string of a version declaration cannot take a language tag, so a following @keyword is a keyword
530
+ this._previousMarker = type === 'literal' && (this._previousMarker === 'VERSION' || this._previousMarker === '@version') ? 'version' : type;
512
531
 
513
532
  // Advance to next part to tokenize
514
533
  pos = Math.min(pos + length, input.length);
package/lib/N3Parser.js CHANGED
@@ -53,10 +53,17 @@ class N3Parser {
53
53
  return null;
54
54
  };
55
55
  this._blankNodePrefix = typeof options.blankNodePrefix !== 'string' ? '' : options.blankNodePrefix.replace(/^(?!_:)/, '_:');
56
+ // Map the token types of additional directives (@name and NAME) to their names
57
+ this._directives = null;
58
+ if (options.directives) {
59
+ this._directives = Object.create(null);
60
+ for (const name of options.directives) this._directives[`@${name.toLowerCase()}`] = this._directives[name.toUpperCase()] = name;
61
+ }
56
62
  this._lexer = options.lexer || new _N3Lexer.default({
57
63
  lineMode: isLineMode,
58
64
  n3: isN3,
59
- isImpliedBy: this._isImpliedBy
65
+ isImpliedBy: this._isImpliedBy,
66
+ directives: options.directives
60
67
  });
61
68
  // Disable explicit quantifiers by default
62
69
  this._explicitQuantifiers = !!options.explicitQuantifiers;
@@ -238,8 +245,13 @@ class N3Parser {
238
245
  }
239
246
  case 'GRAPH':
240
247
  if (this._supportsNamedGraphs) return this._readNamedGraphLabel;
241
- // Otherwise, the next token must be a subject
248
+ // Otherwise, the next token must be a subject, unless it is an additional directive
242
249
  default:
250
+ if (this._directives !== null && token.type in this._directives && this._graph === null) {
251
+ this._sparqlStyle = token.type[0] !== '@';
252
+ this._directiveCallback(this._directives[token.type]);
253
+ return this._readDeclarationPunctuation;
254
+ }
243
255
  return this._readSubject(token);
244
256
  }
245
257
  }
@@ -1514,12 +1526,13 @@ class N3Parser {
1514
1526
  parse(input, quadCallback, prefixCallback, versionCallback) {
1515
1527
  // The second parameter also accepts an object of named callbacks.
1516
1528
  // As a second and third parameter it still accepts a separate quadCallback and prefixCallback for backward compatibility as well
1517
- let onQuad, onPrefix, onComment, onVersion, onToken, onTokenEnd;
1518
- if (quadCallback && (quadCallback.onQuad || quadCallback.onPrefix || quadCallback.onComment || quadCallback.onVersion || quadCallback.onToken || quadCallback.onTokenEnd)) {
1529
+ let onQuad, onPrefix, onComment, onVersion, onDirective, onToken, onTokenEnd;
1530
+ if (quadCallback && (quadCallback.onQuad || quadCallback.onPrefix || quadCallback.onComment || quadCallback.onVersion || quadCallback.onDirective || quadCallback.onToken || quadCallback.onTokenEnd)) {
1519
1531
  onQuad = quadCallback.onQuad;
1520
1532
  onPrefix = quadCallback.onPrefix;
1521
1533
  onComment = quadCallback.onComment;
1522
1534
  onVersion = quadCallback.onVersion;
1535
+ onDirective = quadCallback.onDirective;
1523
1536
  onToken = quadCallback.onToken;
1524
1537
  onTokenEnd = quadCallback.onTokenEnd;
1525
1538
  } else {
@@ -1537,6 +1550,7 @@ class N3Parser {
1537
1550
  if (this._n3Mode && this._implicitEmptyPrefix && this._base) this._prefixes[''] = this._resolveIRI('#');
1538
1551
  this._prefixCallback = onPrefix || noop;
1539
1552
  this._versionCallback = onVersion || noop;
1553
+ this._directiveCallback = onDirective || noop;
1540
1554
  this._inversePredicate = false;
1541
1555
  this._expectOf = false;
1542
1556
  this._quantified = Object.create(null);
@@ -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,6 +18,9 @@ const {
18
18
  rdf,
19
19
  xsd
20
20
  } = _IRIs.default;
21
+ const {
22
+ hasOwnProperty
23
+ } = Object.prototype;
21
24
 
22
25
  // Characters that require escaping in Turtle, TriG, and N3,
23
26
  // where characters outside the Basic Multilingual Plane are escaped as well
@@ -38,6 +41,14 @@ const escape = /["\\\t\n\r\b\f\u0000-\u0019\ud800-\udbff]/,
38
41
  '\f': '\\f'
39
42
  };
40
43
 
44
+ // A local name (PN_LOCAL) as written in a prefixed name, without backslash escapes,
45
+ // so that it denotes the same characters as the IRI it is taken from.
46
+ // Characters outside the Basic Multilingual Plane are escaped before matching, so they never occur.
47
+ const localNameStart = 'A-Za-z\\u00C0-\\u00D6\\u00D8-\\u00F6\\u00F8-\\u02FF\\u0370-\\u037D\\u037F-\\u1FFF' + '\\u200C\\u200D\\u2070-\\u218F\\u2C00-\\u2FEF\\u3001-\\uD7FF\\uF900-\\uFDCF\\uFDF0-\\uFFFD_:0-9',
48
+ localNameChar = `${localNameStart}\\-\\u00B7\\u0300-\\u036F\\u203F\\u2040`,
49
+ percent = '%[0-9A-Fa-f]{2}',
50
+ localName = new RegExp(`^(?:(?:[${localNameStart}]|${percent})` + `(?:(?:[${localNameChar}.]|${percent})*(?:[${localNameChar}]|${percent}))?)?$`);
51
+
41
52
  // ## Placeholder class to represent already pretty-printed terms
42
53
  class SerializedTerm extends _N3DataFactory.Term {
43
54
  // Pretty-printed nodes are not equal to any other node
@@ -175,7 +186,7 @@ class N3Writer {
175
186
  // A blank node or list is represented as-is
176
187
  if (entity.termType !== 'NamedNode') {
177
188
  // If it is a list head, pretty-print it
178
- if (this._lists && entity.value in this._lists) entity = this.list(this._lists[entity.value]);
189
+ if (this._lists && hasOwnProperty.call(this._lists, entity.value)) entity = this.list(this._lists[entity.value]);
179
190
  // Terms from this library already hold their serialization as id
180
191
  if (entity instanceof _N3DataFactory.Term) return entity.id;
181
192
  return entity.termType === 'Variable' ? `?${entity.value}` : `_:${entity.value}`;
@@ -188,8 +199,12 @@ class N3Writer {
188
199
  // Escape special characters
189
200
  if (this._escape.test(iri)) iri = iri.replace(this._escapeAll, this._characterReplacer);
190
201
  // Try to represent the IRI as prefixed name, unless no prefixes were added
191
- const prefixMatch = this._hasPrefixes ? this._prefixRegex.exec(iri) : null;
192
- return !prefixMatch ? `<${iri}>` : !prefixMatch[1] ? iri : this._prefixIRIs[prefixMatch[1]] + prefixMatch[2];
202
+ const prefixMatch = this._hasPrefixes ? (this._prefixRegex || this._createPrefixRegex()).exec(iri) : null;
203
+ if (!prefixMatch) return `<${iri}>`;
204
+ // An IRI that starts with a registered prefix IRI
205
+ if (prefixMatch[1]) return this._prefixIRIs[prefixMatch[1]] + prefixMatch[2];
206
+ // An IRI that already looks like a prefixed name, if the rest is a valid local name
207
+ return localName.test(iri.slice(prefixMatch[0].length)) ? iri : `<${iri}>`;
193
208
  }
194
209
 
195
210
  // ### `_encodeLiteral` represents a literal
@@ -319,22 +334,28 @@ class N3Writer {
319
334
  this._prefixPatterns[iri] = [(0, _Util.escapeRegex)(iri), (0, _Util.escapeRegex)(prefix)];
320
335
  this._write(`@prefix ${prefix} <${iri}>.\n`);
321
336
  }
322
- // Recreate the prefix matcher
337
+ // Recreate the prefix matcher when it is next needed, so that adding
338
+ // prefixes one by one does not rebuild it for every prefix
323
339
  if (hasPrefixes) {
324
340
  this._hasPrefixes = true;
325
- let IRIlist = '',
326
- prefixList = '';
327
- for (const prefixIRI in this._prefixPatterns) {
328
- const [IRIpattern, prefixPattern] = this._prefixPatterns[prefixIRI];
329
- IRIlist += IRIlist ? `|${IRIpattern}` : IRIpattern;
330
- prefixList += prefixList ? `|${prefixPattern}` : prefixPattern;
331
- }
332
- this._prefixRegex = new RegExp(`^(?:${prefixList})[^/]*$|` + `^(${IRIlist})([_a-zA-Z0-9](?:\\.?[\\-_a-zA-Z0-9])*)$`);
341
+ this._prefixRegex = null;
333
342
  }
334
343
  // End a prefix block with a newline
335
344
  this._write(hasPrefixes ? '\n' : '', done);
336
345
  }
337
346
 
347
+ // ### `_createPrefixRegex` creates the matcher for the current prefixes
348
+ _createPrefixRegex() {
349
+ let IRIlist = '',
350
+ prefixList = '';
351
+ for (const prefixIRI in this._prefixPatterns) {
352
+ const [IRIpattern, prefixPattern] = this._prefixPatterns[prefixIRI];
353
+ IRIlist += IRIlist ? `|${IRIpattern}` : IRIpattern;
354
+ prefixList += prefixList ? `|${prefixPattern}` : prefixPattern;
355
+ }
356
+ return this._prefixRegex = new RegExp(`^(${IRIlist})([_a-zA-Z0-9](?:\\.?[\\-_a-zA-Z0-9])*)$|` + `^(?:${prefixList})`);
357
+ }
358
+
338
359
  // ### `blank` creates a blank node with the given content
339
360
  blank(predicate, object) {
340
361
  let children = predicate,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "n3",
3
- "version": "2.11.7",
3
+ "version": "2.12.0",
4
4
  "description": "Lightning fast, asynchronous, streaming Turtle / N3 / RDF library.",
5
5
  "author": "Ruben Verborgh <ruben.verborgh@gmail.com>",
6
6
  "keywords": [
package/src/N3Lexer.js CHANGED
@@ -58,6 +58,9 @@ function isSeparatorCode(code) {
58
58
  return code === SPACE || code === TAB || code === LF || code === CR || code === HASH;
59
59
  }
60
60
 
61
+ // Words with a fixed meaning in the grammar, which cannot name an additional directive
62
+ const reservedWords = /^(?:prefix|base|version|graph|forsome|forall|iri|a|true|false|has|is|of|id)$/i;
63
+
61
64
  // Unfinished input in a stream up to this length is tokenized again with every chunk
62
65
  const MIN_RESCAN_LENGTH = 1024;
63
66
 
@@ -103,6 +106,19 @@ export default class N3Lexer {
103
106
  else {
104
107
  this._n3Mode = options.n3 !== false;
105
108
  }
109
+ // Recognize additional directive keywords, such as MESSAGE
110
+ // (the @-form of a directive is always tokenized as an @-keyword)
111
+ this._directive = null;
112
+ if (options.directives && options.directives.length !== 0) {
113
+ for (const name of options.directives) {
114
+ if (!/^[a-z]+$/i.test(name) || reservedWords.test(name))
115
+ throw new Error(`Invalid directive name: "${name}"`);
116
+ }
117
+ this._directive = new RegExp(`(?:${options.directives.join('|')})(?=[\\s#<])`, 'iy');
118
+ this._directiveMaxLength = Math.max(...options.directives.map(name => name.length));
119
+ // The first characters of directive names, so other words skip the regular expression
120
+ this._directiveStarts = options.directives.map(name => name[0].toLowerCase() + name[0].toUpperCase()).join('');
121
+ }
106
122
  // Don't output comment tokens by default
107
123
  this.comments = !!options.comments;
108
124
  // Cache the last tested closing position of long literals
@@ -486,6 +502,13 @@ export default class N3Lexer {
486
502
  if ((this._previousMarker === '@prefix' || this._previousMarker === 'PREFIX') &&
487
503
  (match = execAt(this._prefix, input, pos)))
488
504
  type = 'prefix', value = match[1] || '';
505
+ // Try to find an additional directive keyword
506
+ // (at the end of the input, only a short final word can be one)
507
+ else if (this._directive !== null && this._directiveStarts.includes(firstChar) &&
508
+ ((match = execAt(this._directive, input, pos)) ||
509
+ inputFinished && input.length - pos <= this._directiveMaxLength &&
510
+ (match = execAtEnd(this._directive, input, pos))))
511
+ type = match[0].toUpperCase();
489
512
  // Try to find a prefixed name. Since it can contain (but not end with) a dot,
490
513
  // we always need a non-dot character before deciding it is a prefixed name.
491
514
  // Therefore, try inserting a space if we're at the end of the input.
@@ -534,7 +557,8 @@ export default class N3Lexer {
534
557
  else
535
558
  token = emitToken(type, value, prefix, line, start, lexicalLength || length);
536
559
  this.previousToken = token;
537
- this._previousMarker = type;
560
+ // The string of a version declaration cannot take a language tag, so a following @keyword is a keyword
561
+ this._previousMarker = type === 'literal' && (this._previousMarker === 'VERSION' || this._previousMarker === '@version') ? 'version' : type;
538
562
 
539
563
  // Advance to next part to tokenize
540
564
  pos = Math.min(pos + length, input.length);
package/src/N3Parser.js CHANGED
@@ -46,7 +46,16 @@ export default class N3Parser {
46
46
  this._resolveRelativeIRI = iri => { return null; };
47
47
  this._blankNodePrefix = typeof options.blankNodePrefix !== 'string' ? '' :
48
48
  options.blankNodePrefix.replace(/^(?!_:)/, '_:');
49
- this._lexer = options.lexer || new N3Lexer({ lineMode: isLineMode, n3: isN3, isImpliedBy: this._isImpliedBy });
49
+ // Map the token types of additional directives (@name and NAME) to their names
50
+ this._directives = null;
51
+ if (options.directives) {
52
+ this._directives = Object.create(null);
53
+ for (const name of options.directives)
54
+ this._directives[`@${name.toLowerCase()}`] = this._directives[name.toUpperCase()] = name;
55
+ }
56
+ this._lexer = options.lexer || new N3Lexer({
57
+ lineMode: isLineMode, n3: isN3, isImpliedBy: this._isImpliedBy, directives: options.directives,
58
+ });
50
59
  // Disable explicit quantifiers by default
51
60
  this._explicitQuantifiers = !!options.explicitQuantifiers;
52
61
  // Disable formula-only blank node scoping by default
@@ -228,8 +237,13 @@ export default class N3Parser {
228
237
  case 'GRAPH':
229
238
  if (this._supportsNamedGraphs)
230
239
  return this._readNamedGraphLabel;
231
- // Otherwise, the next token must be a subject
240
+ // Otherwise, the next token must be a subject, unless it is an additional directive
232
241
  default:
242
+ if (this._directives !== null && token.type in this._directives && this._graph === null) {
243
+ this._sparqlStyle = token.type[0] !== '@';
244
+ this._directiveCallback(this._directives[token.type]);
245
+ return this._readDeclarationPunctuation;
246
+ }
233
247
  return this._readSubject(token);
234
248
  }
235
249
  }
@@ -1634,13 +1648,14 @@ export default class N3Parser {
1634
1648
  parse(input, quadCallback, prefixCallback, versionCallback) {
1635
1649
  // The second parameter also accepts an object of named callbacks.
1636
1650
  // As a second and third parameter it still accepts a separate quadCallback and prefixCallback for backward compatibility as well
1637
- let onQuad, onPrefix, onComment, onVersion, onToken, onTokenEnd;
1651
+ let onQuad, onPrefix, onComment, onVersion, onDirective, onToken, onTokenEnd;
1638
1652
  if (quadCallback && (quadCallback.onQuad || quadCallback.onPrefix || quadCallback.onComment || quadCallback.onVersion ||
1639
- quadCallback.onToken || quadCallback.onTokenEnd)) {
1653
+ quadCallback.onDirective || quadCallback.onToken || quadCallback.onTokenEnd)) {
1640
1654
  onQuad = quadCallback.onQuad;
1641
1655
  onPrefix = quadCallback.onPrefix;
1642
1656
  onComment = quadCallback.onComment;
1643
1657
  onVersion = quadCallback.onVersion;
1658
+ onDirective = quadCallback.onDirective;
1644
1659
  onToken = quadCallback.onToken;
1645
1660
  onTokenEnd = quadCallback.onTokenEnd;
1646
1661
  }
@@ -1661,6 +1676,7 @@ export default class N3Parser {
1661
1676
  this._prefixes[''] = this._resolveIRI('#');
1662
1677
  this._prefixCallback = onPrefix || noop;
1663
1678
  this._versionCallback = onVersion || noop;
1679
+ this._directiveCallback = onDirective || noop;
1664
1680
  this._inversePredicate = false;
1665
1681
  this._expectOf = false;
1666
1682
  this._quantified = Object.create(null);
@@ -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,6 +8,7 @@ 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
13
  // Characters that require escaping in Turtle, TriG, and N3,
13
14
  // where characters outside the Basic Multilingual Plane are escaped as well
@@ -23,6 +24,16 @@ const escape = /["\\\t\n\r\b\f\u0000-\u0019\ud800-\udbff]/,
23
24
  '\n': '\\n', '\r': '\\r', '\b': '\\b', '\f': '\\f',
24
25
  };
25
26
 
27
+ // A local name (PN_LOCAL) as written in a prefixed name, without backslash escapes,
28
+ // so that it denotes the same characters as the IRI it is taken from.
29
+ // Characters outside the Basic Multilingual Plane are escaped before matching, so they never occur.
30
+ const localNameStart = 'A-Za-z\\u00C0-\\u00D6\\u00D8-\\u00F6\\u00F8-\\u02FF\\u0370-\\u037D\\u037F-\\u1FFF' +
31
+ '\\u200C\\u200D\\u2070-\\u218F\\u2C00-\\u2FEF\\u3001-\\uD7FF\\uF900-\\uFDCF\\uFDF0-\\uFFFD_:0-9',
32
+ localNameChar = `${localNameStart}\\-\\u00B7\\u0300-\\u036F\\u203F\\u2040`,
33
+ percent = '%[0-9A-Fa-f]{2}',
34
+ localName = new RegExp(`^(?:(?:[${localNameStart}]|${percent})` +
35
+ `(?:(?:[${localNameChar}.]|${percent})*(?:[${localNameChar}]|${percent}))?)?$`);
36
+
26
37
  // ## Placeholder class to represent already pretty-printed terms
27
38
  class SerializedTerm extends Term {
28
39
  // Pretty-printed nodes are not equal to any other node
@@ -173,7 +184,7 @@ export default class N3Writer {
173
184
  // A blank node or list is represented as-is
174
185
  if (entity.termType !== 'NamedNode') {
175
186
  // If it is a list head, pretty-print it
176
- if (this._lists && (entity.value in this._lists))
187
+ if (this._lists && hasOwnProperty.call(this._lists, entity.value))
177
188
  entity = this.list(this._lists[entity.value]);
178
189
  // Terms from this library already hold their serialization as id
179
190
  if (entity instanceof Term)
@@ -189,9 +200,14 @@ export default class N3Writer {
189
200
  if (this._escape.test(iri))
190
201
  iri = iri.replace(this._escapeAll, this._characterReplacer);
191
202
  // Try to represent the IRI as prefixed name, unless no prefixes were added
192
- const prefixMatch = this._hasPrefixes ? this._prefixRegex.exec(iri) : null;
193
- return !prefixMatch ? `<${iri}>` :
194
- (!prefixMatch[1] ? iri : this._prefixIRIs[prefixMatch[1]] + prefixMatch[2]);
203
+ const prefixMatch = this._hasPrefixes ? (this._prefixRegex || this._createPrefixRegex()).exec(iri) : null;
204
+ if (!prefixMatch)
205
+ return `<${iri}>`;
206
+ // An IRI that starts with a registered prefix IRI
207
+ if (prefixMatch[1])
208
+ return this._prefixIRIs[prefixMatch[1]] + prefixMatch[2];
209
+ // An IRI that already looks like a prefixed name, if the rest is a valid local name
210
+ return localName.test(iri.slice(prefixMatch[0].length)) ? iri : `<${iri}>`;
195
211
  }
196
212
 
197
213
  // ### `_encodeLiteral` represents a literal
@@ -333,22 +349,28 @@ export default class N3Writer {
333
349
  this._prefixPatterns[iri] = [escapeRegex(iri), escapeRegex(prefix)];
334
350
  this._write(`@prefix ${prefix} <${iri}>.\n`);
335
351
  }
336
- // Recreate the prefix matcher
352
+ // Recreate the prefix matcher when it is next needed, so that adding
353
+ // prefixes one by one does not rebuild it for every prefix
337
354
  if (hasPrefixes) {
338
355
  this._hasPrefixes = true;
339
- let IRIlist = '', prefixList = '';
340
- for (const prefixIRI in this._prefixPatterns) {
341
- const [IRIpattern, prefixPattern] = this._prefixPatterns[prefixIRI];
342
- IRIlist += IRIlist ? `|${IRIpattern}` : IRIpattern;
343
- prefixList += prefixList ? `|${prefixPattern}` : prefixPattern;
344
- }
345
- this._prefixRegex = new RegExp(`^(?:${prefixList})[^/]*$|` +
346
- `^(${IRIlist})([_a-zA-Z0-9](?:\\.?[\\-_a-zA-Z0-9])*)$`);
356
+ this._prefixRegex = null;
347
357
  }
348
358
  // End a prefix block with a newline
349
359
  this._write(hasPrefixes ? '\n' : '', done);
350
360
  }
351
361
 
362
+ // ### `_createPrefixRegex` creates the matcher for the current prefixes
363
+ _createPrefixRegex() {
364
+ let IRIlist = '', prefixList = '';
365
+ for (const prefixIRI in this._prefixPatterns) {
366
+ const [IRIpattern, prefixPattern] = this._prefixPatterns[prefixIRI];
367
+ IRIlist += IRIlist ? `|${IRIpattern}` : IRIpattern;
368
+ prefixList += prefixList ? `|${prefixPattern}` : prefixPattern;
369
+ }
370
+ return this._prefixRegex = new RegExp(`^(${IRIlist})([_a-zA-Z0-9](?:\\.?[\\-_a-zA-Z0-9])*)$|` +
371
+ `^(?:${prefixList})`);
372
+ }
373
+
352
374
  // ### `blank` creates a blank node with the given content
353
375
  blank(predicate, object) {
354
376
  let children = predicate, child, length;