n3 2.11.8 → 2.12.1

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);
@@ -666,10 +685,29 @@ class N3Lexer {
666
685
  };
667
686
  }
668
687
 
688
+ // ### `_tryTokenizeToEnd` tokenizes as far as possible, reporting failures through the callback
689
+ _tryTokenizeToEnd(callback, inputFinished) {
690
+ // Keep track of errors thrown by the callback, which must reach the caller unchanged
691
+ let callbackError;
692
+ try {
693
+ this._tokenizeToEnd((error, token) => {
694
+ try {
695
+ return callback(error, token);
696
+ } catch (thrown) {
697
+ throw callbackError = thrown;
698
+ }
699
+ }, inputFinished);
700
+ } catch (error) {
701
+ // Matching an extremely long token can exhaust the regular expression stack
702
+ if (error === callbackError || !(error instanceof RangeError)) throw error;
703
+ callback(this._syntaxError(null, `Token too long on line ${this._line}.`));
704
+ }
705
+ }
706
+
669
707
  // ### `_syntaxError` creates a syntax error for the given issue
670
- _syntaxError(issue) {
708
+ _syntaxError(issue, message = `Unexpected "${issue}" on line ${this._line}.`) {
671
709
  this._input = null;
672
- const err = new Error(`Unexpected "${issue}" on line ${this._line}.`);
710
+ const err = new Error(message);
673
711
  err.context = {
674
712
  token: undefined,
675
713
  line: this._line,
@@ -710,13 +748,13 @@ class N3Lexer {
710
748
  this._input = this._readStartingBom(input);
711
749
  // If a callback was passed, asynchronously call it
712
750
  if (typeof callback === 'function') queueMicrotask(() => {
713
- if (this._tokenization === tokenization) this._tokenizeToEnd(callback, true);
751
+ if (this._tokenization === tokenization) this._tryTokenizeToEnd(callback, true);
714
752
  });
715
753
  // If no callback was passed, tokenize synchronously and return
716
754
  else {
717
755
  const tokens = [];
718
756
  let error;
719
- this._tokenizeToEnd((e, t) => e ? error = e : tokens.push(t), true);
757
+ this._tryTokenizeToEnd((e, t) => e ? error = e : tokens.push(t), true);
720
758
  if (error) throw error;
721
759
  return tokens;
722
760
  }
@@ -743,7 +781,7 @@ class N3Lexer {
743
781
  // Tokenize as far as possible. When a previous attempt left a long unfinished token,
744
782
  // wait until the buffered input has doubled, so the token is not rescanned for every chunk.
745
783
  if (this._input.length >= retryLength) {
746
- this._tokenizeToEnd(callback, false);
784
+ this._tryTokenizeToEnd(callback, false);
747
785
  retryLength = this._input !== null && this._input.length > MIN_RESCAN_LENGTH ? 2 * this._input.length : 0;
748
786
  }
749
787
  }
@@ -754,7 +792,7 @@ class N3Lexer {
754
792
  // Decode any incomplete character left at the end
755
793
  const rest = decoder ? decoder.decode() : '';
756
794
  if (rest) this._input = typeof this._input === 'string' ? this._input + rest : rest;
757
- if (typeof this._input === 'string') this._tokenizeToEnd(callback, true);
795
+ if (typeof this._input === 'string') this._tryTokenizeToEnd(callback, true);
758
796
  }
759
797
  });
760
798
  input.on('error', error => {
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}`;
@@ -189,7 +200,11 @@ class N3Writer {
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
202
  const prefixMatch = this._hasPrefixes ? (this._prefixRegex || this._createPrefixRegex()).exec(iri) : null;
192
- return !prefixMatch ? `<${iri}>` : !prefixMatch[1] ? iri : this._prefixIRIs[prefixMatch[1]] + prefixMatch[2];
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
@@ -338,7 +353,7 @@ class N3Writer {
338
353
  IRIlist += IRIlist ? `|${IRIpattern}` : IRIpattern;
339
354
  prefixList += prefixList ? `|${prefixPattern}` : prefixPattern;
340
355
  }
341
- return this._prefixRegex = new RegExp(`^(?:${prefixList})[^/]*$|` + `^(${IRIlist})([_a-zA-Z0-9](?:\\.?[\\-_a-zA-Z0-9])*)$`);
356
+ return this._prefixRegex = new RegExp(`^(${IRIlist})([_a-zA-Z0-9](?:\\.?[\\-_a-zA-Z0-9])*)$|` + `^(?:${prefixList})`);
342
357
  }
343
358
 
344
359
  // ### `blank` creates a blank node with the given content
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "n3",
3
- "version": "2.11.8",
3
+ "version": "2.12.1",
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);
@@ -690,10 +714,32 @@ export default class N3Lexer {
690
714
  return { value: '', matchLength: 0, finalLineLength: 0 };
691
715
  }
692
716
 
717
+ // ### `_tryTokenizeToEnd` tokenizes as far as possible, reporting failures through the callback
718
+ _tryTokenizeToEnd(callback, inputFinished) {
719
+ // Keep track of errors thrown by the callback, which must reach the caller unchanged
720
+ let callbackError;
721
+ try {
722
+ this._tokenizeToEnd((error, token) => {
723
+ try {
724
+ return callback(error, token);
725
+ }
726
+ catch (thrown) {
727
+ throw (callbackError = thrown);
728
+ }
729
+ }, inputFinished);
730
+ }
731
+ catch (error) {
732
+ // Matching an extremely long token can exhaust the regular expression stack
733
+ if (error === callbackError || !(error instanceof RangeError))
734
+ throw error;
735
+ callback(this._syntaxError(null, `Token too long on line ${this._line}.`));
736
+ }
737
+ }
738
+
693
739
  // ### `_syntaxError` creates a syntax error for the given issue
694
- _syntaxError(issue) {
740
+ _syntaxError(issue, message = `Unexpected "${issue}" on line ${this._line}.`) {
695
741
  this._input = null;
696
- const err = new Error(`Unexpected "${issue}" on line ${this._line}.`);
742
+ const err = new Error(message);
697
743
  err.context = {
698
744
  token: undefined,
699
745
  line: this._line,
@@ -736,13 +782,13 @@ export default class N3Lexer {
736
782
  if (typeof callback === 'function')
737
783
  queueMicrotask(() => {
738
784
  if (this._tokenization === tokenization)
739
- this._tokenizeToEnd(callback, true);
785
+ this._tryTokenizeToEnd(callback, true);
740
786
  });
741
787
  // If no callback was passed, tokenize synchronously and return
742
788
  else {
743
789
  const tokens = [];
744
790
  let error;
745
- this._tokenizeToEnd((e, t) => e ? (error = e) : tokens.push(t), true);
791
+ this._tryTokenizeToEnd((e, t) => e ? (error = e) : tokens.push(t), true);
746
792
  if (error) throw error;
747
793
  return tokens;
748
794
  }
@@ -769,7 +815,7 @@ export default class N3Lexer {
769
815
  // Tokenize as far as possible. When a previous attempt left a long unfinished token,
770
816
  // wait until the buffered input has doubled, so the token is not rescanned for every chunk.
771
817
  if (this._input.length >= retryLength) {
772
- this._tokenizeToEnd(callback, false);
818
+ this._tryTokenizeToEnd(callback, false);
773
819
  retryLength = this._input !== null && this._input.length > MIN_RESCAN_LENGTH ?
774
820
  2 * this._input.length : 0;
775
821
  }
@@ -783,7 +829,7 @@ export default class N3Lexer {
783
829
  if (rest)
784
830
  this._input = typeof this._input === 'string' ? this._input + rest : rest;
785
831
  if (typeof this._input === 'string')
786
- this._tokenizeToEnd(callback, true);
832
+ this._tryTokenizeToEnd(callback, true);
787
833
  }
788
834
  });
789
835
  input.on('error', error => {
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)
@@ -190,8 +201,13 @@ export default class N3Writer {
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
203
  const prefixMatch = this._hasPrefixes ? (this._prefixRegex || this._createPrefixRegex()).exec(iri) : null;
193
- return !prefixMatch ? `<${iri}>` :
194
- (!prefixMatch[1] ? iri : this._prefixIRIs[prefixMatch[1]] + prefixMatch[2]);
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
@@ -351,8 +367,8 @@ export default class N3Writer {
351
367
  IRIlist += IRIlist ? `|${IRIpattern}` : IRIpattern;
352
368
  prefixList += prefixList ? `|${prefixPattern}` : prefixPattern;
353
369
  }
354
- return this._prefixRegex = new RegExp(`^(?:${prefixList})[^/]*$|` +
355
- `^(${IRIlist})([_a-zA-Z0-9](?:\\.?[\\-_a-zA-Z0-9])*)$`);
370
+ return this._prefixRegex = new RegExp(`^(${IRIlist})([_a-zA-Z0-9](?:\\.?[\\-_a-zA-Z0-9])*)$|` +
371
+ `^(?:${prefixList})`);
356
372
  }
357
373
 
358
374
  // ### `blank` creates a blank node with the given content