n3 3.0.0-alpha.3 → 3.0.0-alpha.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/N3Lexer.js CHANGED
@@ -1,5 +1,4 @@
1
1
  // **N3Lexer** tokenizes N3 documents.
2
- import { Buffer } from 'buffer';
3
2
  import namespaces from './IRIs';
4
3
 
5
4
  const { xsd } = namespaces;
@@ -18,7 +17,8 @@ const localNameEscapeReplacements = {
18
17
  };
19
18
  const illegalIriChars = /[\x00-\x20<>\\"\{\}\|\^\`]/;
20
19
  // Characters that cannot occur in a prefixed name, not even escaped
21
- const nonPrefixedNameChar = /[\s<>"{}|^`]/;
20
+ // (global, so that testAt searches the rest of the input from a position)
21
+ const nonPrefixedNameChar = /[\s<>"{}|^`]/g;
22
22
 
23
23
  // A valid code point is a Unicode scalar value: at most U+10FFFF and not a surrogate
24
24
  function isValidCodePoint(charCode) {
@@ -35,30 +35,59 @@ const lineModeRegExps = {
35
35
  _whitespace: true,
36
36
  };
37
37
  const invalidRegExp = /$0^/;
38
+ const nonWhitespace = /\S*/y;
39
+
40
+ // Matches a sticky regular expression at the given position of the input
41
+ function execAt(regExp, input, pos) {
42
+ regExp.lastIndex = pos;
43
+ return regExp.exec(input);
44
+ }
45
+ function testAt(regExp, input, pos) {
46
+ regExp.lastIndex = pos;
47
+ return regExp.test(input);
48
+ }
49
+ // Matches the rest of the input followed by a space, as at the end of the input,
50
+ // a token that can contain (but not end with) a dot needs a non-dot character after it
51
+ function execAtEnd(regExp, input, pos) {
52
+ regExp.lastIndex = 0;
53
+ return regExp.exec(`${input.slice(pos)} `);
54
+ }
55
+
56
+ // Whitespace or the start of a comment
57
+ function isSeparatorCode(code) {
58
+ return code === SPACE || code === TAB || code === LF || code === CR || code === HASH;
59
+ }
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
+
64
+ // Unfinished input in a stream up to this length is tokenized again with every chunk
65
+ const MIN_RESCAN_LENGTH = 1024;
38
66
 
39
67
  // ## Constructor
40
68
  export default class N3Lexer {
41
69
  constructor(options) {
42
70
  // ## Regular expressions
43
- // It's slightly faster to have these as properties than as in-scope variables
44
- this._iri = /^<((?:[^ <>{}\\]|\\[uU])+)>[ \t]*/; // IRI with escape sequences; needs sanity check after unescaping
45
- this._unescapedIri = /^<([^\x00-\x20<>\\"\{\}\|\^\`]*)>[ \t]*/; // IRI without escape sequences; no unescaping
46
- this._simpleQuotedString = /^"([^"\\\r\n]*)"(?=[^"])/; // string without escape sequences
47
- this._simpleApostropheString = /^'([^'\\\r\n]*)'(?=[^'])/;
48
- this._langcode = /^@([a-z]+(?:-[a-z0-9]+)*)(?=[^a-z0-9])/i;
49
- this._prefix = /^((?:[A-Za-z\xc0-\xd6\xd8-\xf6\xf8-\u02ff\u0370-\u037d\u037f-\u1fff\u200c\u200d\u2070-\u218f\u2c00-\u2fef\u3001-\ud7ff\uf900-\ufdcf\ufdf0-\ufffd]|[\ud800-\udb7f][\udc00-\udfff])(?:\.?[\-0-9A-Z_a-z\xb7\xc0-\xd6\xd8-\xf6\xf8-\u037d\u037f-\u1fff\u200c\u200d\u203f\u2040\u2070-\u218f\u2c00-\u2fef\u3001-\ud7ff\uf900-\ufdcf\ufdf0-\ufffd]|[\ud800-\udb7f][\udc00-\udfff])*)?:(?=[#\s<])/;
50
- this._prefixed = /^((?:[A-Za-z\xc0-\xd6\xd8-\xf6\xf8-\u02ff\u0370-\u037d\u037f-\u1fff\u200c\u200d\u2070-\u218f\u2c00-\u2fef\u3001-\ud7ff\uf900-\ufdcf\ufdf0-\ufffd]|[\ud800-\udb7f][\udc00-\udfff])(?:\.?[\-0-9A-Z_a-z\xb7\xc0-\xd6\xd8-\xf6\xf8-\u037d\u037f-\u1fff\u200c\u200d\u203f\u2040\u2070-\u218f\u2c00-\u2fef\u3001-\ud7ff\uf900-\ufdcf\ufdf0-\ufffd]|[\ud800-\udb7f][\udc00-\udfff])*)?:((?:(?:[0-9:A-Z_a-z\xc0-\xd6\xd8-\xf6\xf8-\u02ff\u0370-\u037d\u037f-\u1fff\u200c\u200d\u2070-\u218f\u2c00-\u2fef\u3001-\ud7ff\uf900-\ufdcf\ufdf0-\ufffd]|[\ud800-\udb7f][\udc00-\udfff]|%[0-9a-fA-F]{2}|\\[!#-\/;=?\-@_~])(?:(?:[\.\-0-9:A-Z_a-z\xb7\xc0-\xd6\xd8-\xf6\xf8-\u037d\u037f-\u1fff\u200c\u200d\u203f\u2040\u2070-\u218f\u2c00-\u2fef\u3001-\ud7ff\uf900-\ufdcf\ufdf0-\ufffd]|[\ud800-\udb7f][\udc00-\udfff]|%[0-9a-fA-F]{2}|\\[!#-\/;=?\-@_~])*(?:[\-0-9:A-Z_a-z\xb7\xc0-\xd6\xd8-\xf6\xf8-\u037d\u037f-\u1fff\u200c\u200d\u203f\u2040\u2070-\u218f\u2c00-\u2fef\u3001-\ud7ff\uf900-\ufdcf\ufdf0-\ufffd]|[\ud800-\udb7f][\udc00-\udfff]|%[0-9a-fA-F]{2}|\\[!#-\/;=?\-@_~]))?)?)(?:[ \t]+|(?=\.?[,;!\^\s#()\[\]\{\}"'<>]))/;
51
- this._variable = /^\?(?:(?:[A-Z_a-z\xc0-\xd6\xd8-\xf6\xf8-\u02ff\u0370-\u037d\u037f-\u1fff\u200c\u200d\u2070-\u218f\u2c00-\u2fef\u3001-\ud7ff\uf900-\ufdcf\ufdf0-\ufffd]|[\ud800-\udb7f][\udc00-\udfff])(?:[\-0-9:A-Z_a-z\xb7\xc0-\xd6\xd8-\xf6\xf8-\u037d\u037f-\u1fff\u200c\u200d\u203f\u2040\u2070-\u218f\u2c00-\u2fef\u3001-\ud7ff\uf900-\ufdcf\ufdf0-\ufffd]|[\ud800-\udb7f][\udc00-\udfff])*)(?=[.,;!\^\s#()\[\]\{\}"'<>])/;
52
- this._blank = /^_:((?:[0-9A-Z_a-z\xc0-\xd6\xd8-\xf6\xf8-\u02ff\u0370-\u037d\u037f-\u1fff\u200c\u200d\u2070-\u218f\u2c00-\u2fef\u3001-\ud7ff\uf900-\ufdcf\ufdf0-\ufffd]|[\ud800-\udb7f][\udc00-\udfff])(?:\.?[\-0-9A-Z_a-z\xb7\xc0-\xd6\xd8-\xf6\xf8-\u037d\u037f-\u1fff\u200c\u200d\u203f\u2040\u2070-\u218f\u2c00-\u2fef\u3001-\ud7ff\uf900-\ufdcf\ufdf0-\ufffd]|[\ud800-\udb7f][\udc00-\udfff])*)(?:[ \t]+|(?=\.?[,;:!\^\s#()\[\]\{\}"'<>]))/;
53
- this._number = /^[\-+]?(?:(\d+\.\d*|\.?\d+)[eE][\-+]?\d+|(?=\.?\d)\d*(?:(\.)\d+)?)(?=\.?[,;:!\^\s#()\[\]\{\}"'<>])/;
54
- this._boolean = /^(?:true|false)(?=[.,;!\^\s#()\[\]\{\}"'<>])/;
55
- this._atKeyword = /^@[a-z]+(?=[\s#<:])/i;
56
- this._keyword = /^(?:PREFIX|BASE|VERSION|GRAPH)(?=[\s#<])/i;
57
- this._n3Verb = /^(?:has|is|of)(?=[\s#()\[\]\{\}"'<>?_+\-0-9])/;
58
- this._n3Id = /^id(?=[\s#<])/;
59
- this._shortPredicates = /^a(?=[\s#()\[\]\{\}"'<>])/;
60
- this._commentLine = /^[ \t]*#([^\n\r]*)(?:\r\n|\n|\r)([ \t]*)/;
61
- this._whitespace = /^[ \t]+/;
71
+ // It's slightly faster to have these as properties than as in-scope variables.
72
+ // They are sticky, so they only match at the `lastIndex` set by `execAt`.
73
+ this._iri = /<((?:[^ <>{}\\]|\\[uU])+)>[ \t]*/y; // IRI with escape sequences; needs sanity check after unescaping
74
+ this._unescapedIri = /<([^\x00-\x20<>\\"\{\}\|\^\`]*)>[ \t]*/y; // IRI without escape sequences; no unescaping
75
+ this._simpleQuotedString = /"([^"\\\r\n]*)"(?=[^"])/y; // string without escape sequences
76
+ this._simpleApostropheString = /'([^'\\\r\n]*)'(?=[^'])/y;
77
+ this._langcode = /@([a-z]+(?:-[a-z0-9]+)*)(?=[^a-z0-9])/iy;
78
+ this._prefix = /((?:[A-Za-z\xc0-\xd6\xd8-\xf6\xf8-\u02ff\u0370-\u037d\u037f-\u1fff\u200c\u200d\u2070-\u218f\u2c00-\u2fef\u3001-\ud7ff\uf900-\ufdcf\ufdf0-\ufffd]|[\ud800-\udb7f][\udc00-\udfff])(?:\.?[\-0-9A-Z_a-z\xb7\xc0-\xd6\xd8-\xf6\xf8-\u037d\u037f-\u1fff\u200c\u200d\u203f\u2040\u2070-\u218f\u2c00-\u2fef\u3001-\ud7ff\uf900-\ufdcf\ufdf0-\ufffd]|[\ud800-\udb7f][\udc00-\udfff])*)?:(?=[#\s<])/y;
79
+ this._prefixed = /((?:[A-Za-z\xc0-\xd6\xd8-\xf6\xf8-\u02ff\u0370-\u037d\u037f-\u1fff\u200c\u200d\u2070-\u218f\u2c00-\u2fef\u3001-\ud7ff\uf900-\ufdcf\ufdf0-\ufffd]|[\ud800-\udb7f][\udc00-\udfff])(?:\.?[\-0-9A-Z_a-z\xb7\xc0-\xd6\xd8-\xf6\xf8-\u037d\u037f-\u1fff\u200c\u200d\u203f\u2040\u2070-\u218f\u2c00-\u2fef\u3001-\ud7ff\uf900-\ufdcf\ufdf0-\ufffd]|[\ud800-\udb7f][\udc00-\udfff])*)?:((?:(?:[0-9:A-Z_a-z\xc0-\xd6\xd8-\xf6\xf8-\u02ff\u0370-\u037d\u037f-\u1fff\u200c\u200d\u2070-\u218f\u2c00-\u2fef\u3001-\ud7ff\uf900-\ufdcf\ufdf0-\ufffd]|[\ud800-\udb7f][\udc00-\udfff]|%[0-9a-fA-F]{2}|\\[!#-\/;=?\-@_~])(?:(?:[\.\-0-9:A-Z_a-z\xb7\xc0-\xd6\xd8-\xf6\xf8-\u037d\u037f-\u1fff\u200c\u200d\u203f\u2040\u2070-\u218f\u2c00-\u2fef\u3001-\ud7ff\uf900-\ufdcf\ufdf0-\ufffd]|[\ud800-\udb7f][\udc00-\udfff]|%[0-9a-fA-F]{2}|\\[!#-\/;=?\-@_~])*(?:[\-0-9:A-Z_a-z\xb7\xc0-\xd6\xd8-\xf6\xf8-\u037d\u037f-\u1fff\u200c\u200d\u203f\u2040\u2070-\u218f\u2c00-\u2fef\u3001-\ud7ff\uf900-\ufdcf\ufdf0-\ufffd]|[\ud800-\udb7f][\udc00-\udfff]|%[0-9a-fA-F]{2}|\\[!#-\/;=?\-@_~]))?)?)(?:[ \t]+|(?=\.?[,;!\^\s#()\[\]\{\}"'<>]))/y;
80
+ this._variable = /\?(?:(?:[A-Z_a-z\xc0-\xd6\xd8-\xf6\xf8-\u02ff\u0370-\u037d\u037f-\u1fff\u200c\u200d\u2070-\u218f\u2c00-\u2fef\u3001-\ud7ff\uf900-\ufdcf\ufdf0-\ufffd]|[\ud800-\udb7f][\udc00-\udfff])(?:[\-0-9:A-Z_a-z\xb7\xc0-\xd6\xd8-\xf6\xf8-\u037d\u037f-\u1fff\u200c\u200d\u203f\u2040\u2070-\u218f\u2c00-\u2fef\u3001-\ud7ff\uf900-\ufdcf\ufdf0-\ufffd]|[\ud800-\udb7f][\udc00-\udfff])*)(?=[.,;!\^\s#()\[\]\{\}"'<>])/y;
81
+ this._blank = /_:((?:[0-9A-Z_a-z\xc0-\xd6\xd8-\xf6\xf8-\u02ff\u0370-\u037d\u037f-\u1fff\u200c\u200d\u2070-\u218f\u2c00-\u2fef\u3001-\ud7ff\uf900-\ufdcf\ufdf0-\ufffd]|[\ud800-\udb7f][\udc00-\udfff])(?:\.?[\-0-9A-Z_a-z\xb7\xc0-\xd6\xd8-\xf6\xf8-\u037d\u037f-\u1fff\u200c\u200d\u203f\u2040\u2070-\u218f\u2c00-\u2fef\u3001-\ud7ff\uf900-\ufdcf\ufdf0-\ufffd]|[\ud800-\udb7f][\udc00-\udfff])*)(?:[ \t]+|(?=\.?[,;:!\^\s#()\[\]\{\}"'<>]))/y;
82
+ this._number = /[\-+]?(?:(\d+\.\d*|\.?\d+)[eE][\-+]?\d+|(?=\.?\d)\d*(?:(\.)\d+)?)(?=\.?[,;:!\^\s#()\[\]\{\}"'<>])/y;
83
+ this._boolean = /(?:true|false)(?=[.,;!\^\s#()\[\]\{\}"'<>])/y;
84
+ this._atKeyword = /@[a-z]+(?=[\s#<:])/iy;
85
+ this._keyword = /(?:PREFIX|BASE|VERSION|GRAPH)(?=[\s#<])/iy;
86
+ this._n3Verb = /(?:has|is|of)(?=[\s#()\[\]\{\}"'<>?_+\-0-9])/y;
87
+ this._n3Id = /id(?=[\s#<])/y;
88
+ this._shortPredicates = /a(?=[\s#()\[\]\{\}"'<>])/y;
89
+ this._commentLine = /[ \t]*#([^\n\r]*)(?:\r\n|\n|\r)([ \t]*)/y;
90
+ this._whitespace = /[ \t]+/y;
62
91
  options = options || {};
63
92
 
64
93
  // Whether the log:isImpliedBy predicate is supported
@@ -77,6 +106,19 @@ export default class N3Lexer {
77
106
  else {
78
107
  this._n3Mode = options.n3 !== false;
79
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
+ }
80
122
  // Don't output comment tokens by default
81
123
  this.comments = !!options.comments;
82
124
  // Cache the last tested closing position of long literals
@@ -87,98 +129,103 @@ export default class N3Lexer {
87
129
 
88
130
  // ### `_tokenizeToEnd` tokenizes as for as possible, emitting tokens through the callback
89
131
  _tokenizeToEnd(callback, inputFinished) {
90
- // Continue parsing as far as possible; the loop will return eventually
91
- let input = this._input;
132
+ // Continue parsing as far as possible; the loop will return eventually.
133
+ // Rather than slicing off every token, track the position of the remaining input;
134
+ // the regular expressions are sticky, so they match at that position.
135
+ const input = this._input;
136
+ let pos = 0;
92
137
  let currentLineLength = this._linePosition + input.length;
93
138
  while (true) {
94
139
  // Consume one separator line at a time, including its following indentation.
95
140
  while (true) {
96
- let charCode = input.charCodeAt(0), separatorLength = 0;
141
+ let charCode = input.charCodeAt(pos), separatorLength = 0;
97
142
  if (charCode === SPACE || charCode === TAB) {
98
- const next = input.charCodeAt(1);
143
+ const next = input.charCodeAt(pos + 1);
99
144
  separatorLength = next === SPACE || next === TAB ?
100
- this._whitespace.exec(input)[0].length : 1;
101
- charCode = input.charCodeAt(separatorLength);
145
+ execAt(this._whitespace, input, pos)[0].length : 1;
146
+ charCode = input.charCodeAt(pos + separatorLength);
102
147
  }
103
148
  if (charCode === HASH) {
104
- const comment = this._commentLine.exec(input);
149
+ const comment = execAt(this._commentLine, input, pos);
105
150
  if (comment) {
106
151
  const commentLength = comment[0].length;
107
152
  // Keep a trailing CR buffered in case the next chunk starts with LF.
108
- if (!inputFinished && commentLength === input.length &&
109
- input.charCodeAt(commentLength - 1) === CR) {
110
- this._linePosition = currentLineLength - input.length;
111
- return this._input = input;
112
- }
153
+ if (!inputFinished && pos + commentLength === input.length &&
154
+ input.charCodeAt(input.length - 1) === CR)
155
+ return this._suspend(input, pos, currentLineLength);
113
156
  if (this.comments)
114
- emitComment(comment[1], this._line, separatorLength);
115
- input = input.slice(commentLength);
116
- currentLineLength = input.length + comment[2].length;
157
+ emitComment(comment[1], this._line, currentLineLength - (input.length - pos) + separatorLength);
158
+ pos += commentLength;
159
+ currentLineLength = input.length - pos + comment[2].length;
117
160
  this._line++;
118
161
  }
119
162
  else {
120
163
  // A comment without a line ending stays buffered until EOF.
121
- input = input.slice(separatorLength);
122
- if (!inputFinished) {
123
- this._linePosition = currentLineLength - input.length;
124
- return this._input = input;
125
- }
164
+ pos += separatorLength;
165
+ if (!inputFinished)
166
+ return this._suspend(input, pos, currentLineLength);
126
167
  if (this.comments)
127
- emitComment(input.slice(1), this._line, 0);
128
- input = '';
168
+ emitComment(input.slice(pos + 1), this._line, currentLineLength - (input.length - pos));
169
+ pos = input.length;
129
170
  break;
130
171
  }
131
172
  }
132
173
  else if (charCode === LF || charCode === CR) {
133
174
  // A CR at the end of a chunk may still be followed by LF.
134
- if (!inputFinished && charCode === CR && separatorLength + 1 === input.length) {
135
- this._linePosition = currentLineLength - input.length;
136
- return this._input = input;
137
- }
138
- separatorLength += charCode === CR && input.charCodeAt(separatorLength + 1) === LF ? 2 : 1;
175
+ if (!inputFinished && charCode === CR && pos + separatorLength + 1 === input.length)
176
+ return this._suspend(input, pos, currentLineLength);
177
+ separatorLength += charCode === CR && input.charCodeAt(pos + separatorLength + 1) === LF ? 2 : 1;
139
178
  // Indentation is consumed with the newline, but belongs to the next line's columns.
140
179
  let indentationLength = 0;
141
- const next = input.charCodeAt(separatorLength);
180
+ const next = input.charCodeAt(pos + separatorLength);
142
181
  if (next === SPACE || next === TAB) {
143
- const following = input.charCodeAt(separatorLength + 1);
182
+ const following = input.charCodeAt(pos + separatorLength + 1);
144
183
  indentationLength = following === SPACE || following === TAB ?
145
- this._whitespace.exec(input.slice(separatorLength))[0].length : 1;
184
+ execAt(this._whitespace, input, pos + separatorLength)[0].length : 1;
146
185
  }
147
- input = input.slice(separatorLength + indentationLength);
148
- currentLineLength = input.length + indentationLength;
186
+ pos += separatorLength + indentationLength;
187
+ currentLineLength = input.length - pos + indentationLength;
149
188
  this._line++;
150
189
  }
151
190
  else {
152
- if (separatorLength !== 0)
153
- input = input.slice(separatorLength);
191
+ pos += separatorLength;
154
192
  break;
155
193
  }
156
194
  }
157
- if (input.length === 0) {
195
+ if (pos >= input.length) {
196
+ // A datatype marker needs a type
197
+ if (inputFinished && this._previousMarker === '^^')
198
+ return reportSyntaxError(this, input, pos);
199
+ this._linePosition = currentLineLength;
158
200
  if (inputFinished) {
159
- input = null;
160
- emitToken('eof', '', '', this._line, 0);
201
+ emitToken('eof', '', '', this._line, currentLineLength, 0);
202
+ return this._input = null;
161
203
  }
162
- this._linePosition = currentLineLength;
163
- return this._input = input;
204
+ return this._input = '';
164
205
  }
165
206
 
166
207
  // Look for specific token types based on the first character
167
- const line = this._line, firstChar = input[0];
208
+ const line = this._line, firstChar = input[pos];
168
209
  let type = '', value = '', prefix = '',
169
210
  match = null, matchLength = 0, lexicalLength = 0,
170
211
  finalLineLength = 0, inconclusive = false;
171
212
  switch (firstChar) {
172
213
  case '^':
214
+ // A datatype marker separated from its type cannot be followed by another marker
215
+ if (this._previousMarker === '^^')
216
+ return reportSyntaxError(this, input, pos);
173
217
  // We need at least 3 tokens lookahead to distinguish ^^<IRI> and ^^pre:fixed
174
- if (input.length < 3)
218
+ if (input.length - pos < 3)
175
219
  break;
176
220
  // Try to match a type
177
- else if (input[1] === '^') {
221
+ else if (input[pos + 1] === '^') {
178
222
  this._previousMarker = '^^';
179
223
  // Move to type IRI or prefixed name
180
- input = input.slice(2);
181
- if (input[0] !== '<') {
224
+ pos += 2;
225
+ if (input[pos] !== '<') {
226
+ // Whitespace and comments may separate the marker from the type
227
+ if (isSeparatorCode(input.charCodeAt(pos)))
228
+ continue; // eslint-disable-line no-continue
182
229
  inconclusive = true;
183
230
  break;
184
231
  }
@@ -194,38 +241,38 @@ export default class N3Lexer {
194
241
  // Fall through in case the type is an IRI
195
242
  case '<':
196
243
  // Try to find a full IRI without escape sequences
197
- if (match = this._unescapedIri.exec(input)) {
244
+ if (match = execAt(this._unescapedIri, input, pos)) {
198
245
  type = 'IRI', value = match[1];
199
246
  lexicalLength = match[1].length + 2;
200
247
  }
201
248
  // Try to find a full IRI with escape sequences
202
- else if (match = this._iri.exec(input)) {
249
+ else if (match = execAt(this._iri, input, pos)) {
203
250
  value = this._unescape(match[1], stringEscapeReplacements);
204
251
  if (value === null || illegalIriChars.test(value))
205
- return reportSyntaxError(this);
252
+ return reportSyntaxError(this, input, pos);
206
253
  type = 'IRI';
207
254
  lexicalLength = match[1].length + 2;
208
255
  }
209
256
  // Try to find a triple term
210
- else if (input.length > 2 && input[1] === '<' && input[2] === '(')
257
+ else if (input.length - pos > 2 && input[pos + 1] === '<' && input[pos + 2] === '(')
211
258
  type = '<<(', matchLength = 3;
212
259
  // Try to find a reified triple
213
- else if (!this._lineMode && input.length > (inputFinished ? 1 : 2) && input[1] === '<')
260
+ else if (!this._lineMode && input.length - pos > (inputFinished ? 1 : 2) && input[pos + 1] === '<')
214
261
  type = '<<', matchLength = 2;
215
262
  // Try to find a backwards implication arrow
216
- else if (this._n3Mode && input.length > 1 && input[1] === '=') {
263
+ else if (this._n3Mode && input.length - pos > 1 && input[pos + 1] === '=') {
217
264
  matchLength = 2;
218
265
  if (this._isImpliedBy) type = 'abbreviation', value = '<';
219
266
  else type = 'inverse', value = '>';
220
267
  }
221
268
  // Try to find an inverted predicate marker
222
- else if (this._n3Mode && input.length > 1 && input[1] === '-')
269
+ else if (this._n3Mode && input.length - pos > 1 && input[pos + 1] === '-')
223
270
  type = 'inversePredicate', matchLength = 2;
224
271
  break;
225
272
 
226
273
  case '>':
227
274
  // Try to find a reified triple
228
- if (input.length > 1 && input[1] === '>')
275
+ if (input.length - pos > 1 && input[pos + 1] === '>')
229
276
  type = '>>', matchLength = 2;
230
277
  break;
231
278
 
@@ -233,8 +280,8 @@ export default class N3Lexer {
233
280
  // Try to find a blank node. Since it can contain (but not end with) a dot,
234
281
  // we always need a non-dot character before deciding it is a blank node.
235
282
  // Therefore, try inserting a space if we're at the end of the input.
236
- if ((match = this._blank.exec(input)) ||
237
- inputFinished && (match = this._blank.exec(`${input} `))) {
283
+ if ((match = execAt(this._blank, input, pos)) ||
284
+ inputFinished && (match = execAtEnd(this._blank, input, pos))) {
238
285
  type = 'blank', prefix = '_', value = match[1];
239
286
  lexicalLength = match[1].length + 2;
240
287
  }
@@ -242,13 +289,13 @@ export default class N3Lexer {
242
289
 
243
290
  case '"':
244
291
  // Try to find a literal without escape sequences
245
- if (match = this._simpleQuotedString.exec(input))
292
+ if (match = execAt(this._simpleQuotedString, input, pos))
246
293
  value = match[1];
247
294
  // Try to find a literal wrapped in three pairs of quotes
248
295
  else {
249
- ({ value, matchLength, finalLineLength } = this._parseLiteral(input));
296
+ ({ value, matchLength, finalLineLength } = this._parseLiteral(input, pos));
250
297
  if (value === null)
251
- return reportSyntaxError(this);
298
+ return reportSyntaxError(this, input, pos);
252
299
  }
253
300
  if (match !== null || matchLength !== 0) {
254
301
  type = 'literal';
@@ -259,13 +306,13 @@ export default class N3Lexer {
259
306
  case "'":
260
307
  if (!this._lineMode) {
261
308
  // Try to find a literal without escape sequences
262
- if (match = this._simpleApostropheString.exec(input))
309
+ if (match = execAt(this._simpleApostropheString, input, pos))
263
310
  value = match[1];
264
311
  // Try to find a literal wrapped in three pairs of quotes
265
312
  else {
266
- ({ value, matchLength, finalLineLength } = this._parseLiteral(input));
313
+ ({ value, matchLength, finalLineLength } = this._parseLiteral(input, pos));
267
314
  if (value === null)
268
- return reportSyntaxError(this);
315
+ return reportSyntaxError(this, input, pos);
269
316
  }
270
317
  if (match !== null || matchLength !== 0) {
271
318
  type = 'literal';
@@ -276,7 +323,7 @@ export default class N3Lexer {
276
323
 
277
324
  case '?':
278
325
  // Try to find a variable
279
- if (this._n3Mode && (match = this._variable.exec(input)))
326
+ if (this._n3Mode && (match = execAt(this._variable, input, pos)))
280
327
  type = 'var', value = match[0];
281
328
  break;
282
329
 
@@ -286,20 +333,21 @@ export default class N3Lexer {
286
333
  // input is not finished, another subtag may still arrive in a later chunk and
287
334
  // the match would be premature; wait for more input in that case.
288
335
  // A double dash starts a direction code, which cannot extend the language code.
289
- if (this._previousMarker === 'literal' && (match = this._langcode.exec(input)) && match[1] !== 'version') {
290
- if (!inputFinished && input[match[0].length] === '-' && input[match[0].length + 1] !== '-')
336
+ if (this._previousMarker === 'literal' && (match = execAt(this._langcode, input, pos)) && match[1] !== 'version') {
337
+ const end = pos + match[0].length;
338
+ if (!inputFinished && input[end] === '-' && input[end + 1] !== '-')
291
339
  match = null;
292
340
  else
293
341
  type = 'langcode', value = match[1];
294
342
  }
295
343
  // Try to find a keyword
296
- else if (match = this._atKeyword.exec(input))
344
+ else if (match = execAt(this._atKeyword, input, pos))
297
345
  type = match[0];
298
346
  break;
299
347
 
300
348
  case '.':
301
349
  // Try to find a dot as punctuation
302
- if (input.length === 1 ? inputFinished : (input[1] < '0' || input[1] > '9')) {
350
+ if (input.length - pos === 1 ? inputFinished : (input[pos + 1] < '0' || input[pos + 1] > '9')) {
303
351
  type = '.';
304
352
  matchLength = 1;
305
353
  break;
@@ -318,12 +366,12 @@ export default class N3Lexer {
318
366
  case '9':
319
367
  case '+':
320
368
  case '-':
321
- if (input[1] === '-') {
369
+ if (input[pos + 1] === '-') {
322
370
  // Try to find a direction code
323
371
  if (this._previousMarker === 'langcode') {
324
- if (input.startsWith('--ltr'))
372
+ if (input.startsWith('--ltr', pos))
325
373
  type = 'dircode', value = 'ltr', matchLength = 5;
326
- else if (input.startsWith('--rtl'))
374
+ else if (input.startsWith('--rtl', pos))
327
375
  type = 'dircode', value = 'rtl', matchLength = 5;
328
376
  }
329
377
  break;
@@ -332,8 +380,8 @@ export default class N3Lexer {
332
380
  // Try to find a number. Since it can contain (but not end with) a dot,
333
381
  // we always need a non-dot character before deciding it is a number.
334
382
  // Therefore, try inserting a space if we're at the end of the input.
335
- if (match = this._number.exec(input) ||
336
- inputFinished && (match = this._number.exec(`${input} `))) {
383
+ if (match = execAt(this._number, input, pos) ||
384
+ inputFinished && (match = execAtEnd(this._number, input, pos))) {
337
385
  type = 'literal', value = match[0];
338
386
  prefix = (typeof match[1] === 'string' ? xsd.double :
339
387
  (typeof match[2] === 'string' ? xsd.decimal : xsd.integer));
@@ -349,7 +397,7 @@ export default class N3Lexer {
349
397
  case 'V':
350
398
  case 'v':
351
399
  // Try to find a SPARQL-style keyword
352
- if (match = this._keyword.exec(input))
400
+ if (match = execAt(this._keyword, input, pos))
353
401
  type = match[0].toUpperCase();
354
402
  else
355
403
  inconclusive = true;
@@ -358,7 +406,7 @@ export default class N3Lexer {
358
406
  case 'f':
359
407
  case 't':
360
408
  // Try to match a boolean
361
- if (this._boolean.test(input))
409
+ if (testAt(this._boolean, input, pos))
362
410
  type = 'literal', value = firstChar === 't' ? 'true' : 'false', prefix = xsd.boolean, matchLength = value.length;
363
411
  else
364
412
  inconclusive = true;
@@ -366,7 +414,7 @@ export default class N3Lexer {
366
414
 
367
415
  case 'a':
368
416
  // Try to find an abbreviated predicate
369
- if (this._shortPredicates.test(input))
417
+ if (testAt(this._shortPredicates, input, pos))
370
418
  type = 'abbreviation', value = 'a', matchLength = 1;
371
419
  else
372
420
  inconclusive = true;
@@ -375,7 +423,7 @@ export default class N3Lexer {
375
423
  case 'h':
376
424
  case 'o':
377
425
  // Try to find an N3 verb keyword
378
- if (this._n3Mode && (match = this._matchN3Verb(input, inputFinished)))
426
+ if (this._n3Mode && (match = this._matchN3Verb(input, pos, inputFinished)))
379
427
  type = match[0];
380
428
  else
381
429
  inconclusive = true;
@@ -383,9 +431,9 @@ export default class N3Lexer {
383
431
 
384
432
  case 'i':
385
433
  // Try to find an IRI property list identifier or N3 verb keyword
386
- if (this._n3Mode && this._n3Id.test(input))
434
+ if (this._n3Mode && testAt(this._n3Id, input, pos))
387
435
  type = 'id', matchLength = 2;
388
- else if (this._n3Mode && (match = this._matchN3Verb(input, inputFinished)))
436
+ else if (this._n3Mode && (match = this._matchN3Verb(input, pos, inputFinished)))
389
437
  type = match[0];
390
438
  else
391
439
  inconclusive = true;
@@ -393,9 +441,9 @@ export default class N3Lexer {
393
441
 
394
442
  case '=':
395
443
  // Try to find an implication arrow or equals sign
396
- if (this._n3Mode && input.length > 1) {
444
+ if (this._n3Mode && input.length - pos > 1) {
397
445
  type = 'abbreviation';
398
- if (input[1] !== '>')
446
+ if (input[pos + 1] !== '>')
399
447
  matchLength = 1, value = '=';
400
448
  else
401
449
  matchLength = 2, value = '>';
@@ -406,12 +454,12 @@ export default class N3Lexer {
406
454
  if (!this._n3Mode)
407
455
  break;
408
456
  case ')':
409
- if (!inputFinished && (input.length === 1 || (input.length === 2 && input[1] === '>'))) {
457
+ if (!inputFinished && (input.length - pos === 1 || (input.length - pos === 2 && input[pos + 1] === '>'))) {
410
458
  // Don't consume yet, as it *could* become a triple term end.
411
459
  break;
412
460
  }
413
461
  // Try to find a triple term
414
- if (input.length > 2 && input[1] === '>' && input[2] === '>') {
462
+ if (input.length - pos > 2 && input[pos + 1] === '>' && input[pos + 2] === '>') {
415
463
  type = ')>>', matchLength = 3;
416
464
  break;
417
465
  }
@@ -429,9 +477,9 @@ export default class N3Lexer {
429
477
  break;
430
478
  case '{':
431
479
  // We need at least 2 tokens lookahead to distinguish "{|" and "{ "
432
- if (!this._lineMode && input.length >= 2) {
480
+ if (!this._lineMode && input.length - pos >= 2) {
433
481
  // Try to find a quoted triple annotation start
434
- if (input[1] === '|')
482
+ if (input[pos + 1] === '|')
435
483
  type = '{|', matchLength = 2;
436
484
  else
437
485
  type = firstChar, matchLength = 1;
@@ -440,7 +488,7 @@ export default class N3Lexer {
440
488
  case '|':
441
489
  // We need 2 tokens lookahead to parse "|}"
442
490
  // Try to find a quoted triple annotation end
443
- if (input.length >= 2 && input[1] === '}')
491
+ if (input.length - pos >= 2 && input[pos + 1] === '}')
444
492
  type = '|}', matchLength = 2;
445
493
  break;
446
494
 
@@ -452,13 +500,20 @@ export default class N3Lexer {
452
500
  if (inconclusive) {
453
501
  // Try to find a prefix
454
502
  if ((this._previousMarker === '@prefix' || this._previousMarker === 'PREFIX') &&
455
- (match = this._prefix.exec(input)))
503
+ (match = execAt(this._prefix, input, pos)))
456
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();
457
512
  // Try to find a prefixed name. Since it can contain (but not end with) a dot,
458
513
  // we always need a non-dot character before deciding it is a prefixed name.
459
514
  // Therefore, try inserting a space if we're at the end of the input.
460
- else if ((match = this._prefixed.exec(input)) ||
461
- inputFinished && (match = this._prefixed.exec(`${input} `))) {
515
+ else if ((match = execAt(this._prefixed, input, pos)) ||
516
+ inputFinished && (match = execAtEnd(this._prefixed, input, pos))) {
462
517
  type = 'prefixed', prefix = match[1] || '';
463
518
  value = this._unescape(match[2], localNameEscapeReplacements);
464
519
  lexicalLength = prefix.length + match[2].length + 1;
@@ -479,86 +534,91 @@ export default class N3Lexer {
479
534
  // We could be in streaming mode, and then we just wait for more input to arrive.
480
535
  // Otherwise, a syntax error has occurred in the input.
481
536
  // One exception: error on an unaccounted linebreak (= not inside a triple-quoted literal).
482
- if (inputFinished || (!/^'''|^"""/.test(input) && /\n|\r/.test(input)))
483
- return reportSyntaxError(this);
484
- else {
485
- this._linePosition = currentLineLength - input.length;
486
- return this._input = input;
487
- }
537
+ if (inputFinished || (!input.startsWith("'''", pos) && !input.startsWith('"""', pos) &&
538
+ /\n|\r/.test(input.slice(pos))))
539
+ return reportSyntaxError(this, input, pos);
540
+ else
541
+ return this._suspend(input, pos, currentLineLength);
488
542
  }
489
543
 
490
544
  // Emit the parsed token
491
545
  // Consumption includes separator whitespace; lexicalLength excludes it
492
- // and any synthetic EOF space. slice below clamps consumption to the input.
546
+ // and any synthetic EOF space. Consumption is clamped to the input below.
493
547
  const length = matchLength || match[0].length;
548
+ const start = currentLineLength - (input.length - pos);
494
549
  let token;
495
550
  if (finalLineLength) {
496
551
  token = {
497
- type, value, prefix, line,
498
- start: currentLineLength - input.length,
552
+ type, value, prefix, line, start,
499
553
  end: finalLineLength, endLine: this._line,
500
554
  };
501
555
  callback(null, token);
502
556
  }
503
557
  else
504
- token = emitToken(type, value, prefix, line, lexicalLength || length);
558
+ token = emitToken(type, value, prefix, line, start, lexicalLength || length);
505
559
  this.previousToken = token;
506
- 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;
507
562
 
508
563
  // Advance to next part to tokenize
509
- input = input.slice(length);
564
+ pos = Math.min(pos + length, input.length);
510
565
  if (finalLineLength)
511
- currentLineLength = input.length + finalLineLength;
566
+ currentLineLength = input.length - pos + finalLineLength;
512
567
  }
513
568
 
514
569
  // Emits a comment at its exact position within matched whitespace.
515
- function emitComment(value, line, offset) {
516
- const start = currentLineLength - input.length + offset;
570
+ function emitComment(value, line, start) {
517
571
  callback(null, {
518
572
  type: 'comment', value, prefix: '', line,
519
573
  start, end: start + value.length + 1,
520
574
  });
521
575
  }
522
576
  // Emits the token through the callback
523
- function emitToken(type, value, prefix, line, length) {
524
- const start = input ? currentLineLength - input.length : currentLineLength;
525
- const end = start + length;
526
- const token = { type, value, prefix, line, start, end };
577
+ function emitToken(type, value, prefix, line, start, length) {
578
+ const token = { type, value, prefix, line, start, end: start + length };
527
579
  callback(null, token);
528
580
  return token;
529
581
  }
530
582
  // Signals the syntax error through the callback
531
- function reportSyntaxError(self) { callback(self._syntaxError(/^\S*/.exec(input)[0])); }
583
+ function reportSyntaxError(self, input, pos) {
584
+ callback(self._syntaxError(execAt(nonWhitespace, input, pos)[0]));
585
+ }
586
+ }
587
+
588
+ // ### `_suspend` keeps the unconsumed input until more input arrives
589
+ _suspend(input, pos, currentLineLength) {
590
+ this._linePosition = currentLineLength - (input.length - pos);
591
+ return this._input = input.slice(pos);
532
592
  }
533
593
 
534
594
  // ### `_matchN3Verb` matches an N3 verb unless the input is a longer prefixed name
535
- _matchN3Verb(input, inputFinished) {
536
- const verb = this._n3Verb.exec(input);
595
+ _matchN3Verb(input, pos, inputFinished) {
596
+ const verb = execAt(this._n3Verb, input, pos);
537
597
  if (!verb)
538
598
  return null;
539
599
 
540
600
  // Most verb boundaries cannot be part of a prefix, so keep the common path fast.
541
- const next = input[verb[0].length];
601
+ const next = input[pos + verb[0].length];
542
602
  if (next !== '-' && next !== '_' && (next < '0' || next > '9'))
543
603
  return verb;
544
604
 
545
605
  // A prefix can start with a verb and continue with characters that are also
546
606
  // valid verb boundaries. Prefer the longer prefixed name when it is complete.
547
- if (this._prefixed.exec(input))
607
+ if (execAt(this._prefixed, input, pos))
548
608
  return null;
549
609
  // Appending to the input only matters when a prefixed name could run up to
550
610
  // its end, which a character that cannot occur in prefixed names rules out.
551
611
  // This avoids copying the rest of the document for every such verb.
552
- if (nonPrefixedNameChar.test(input))
612
+ if (testAt(nonPrefixedNameChar, input, pos))
553
613
  return verb;
554
- if (this._prefixed.exec(`${input} `))
614
+ if (execAtEnd(this._prefixed, input, pos))
555
615
  return null;
556
616
 
557
617
  // If a stream chunk ends partway through such a prefix, wait for the colon
558
618
  // instead of prematurely emitting the verb. Appending ": " lets the prefix
559
619
  // grammar determine whether all input seen so far can be a complete prefix.
560
620
  if (!inputFinished) {
561
- const prefix = this._prefix.exec(`${input}: `);
621
+ const prefix = execAt(this._prefix, `${input.slice(pos)}: `, 0);
562
622
  if (prefix)
563
623
  return null;
564
624
  }
@@ -612,20 +672,20 @@ export default class N3Lexer {
612
672
  return result + item.slice(start);
613
673
  }
614
674
 
615
- // ### `_parseLiteral` parses a literal into an unescaped value
616
- _parseLiteral(input) {
675
+ // ### `_parseLiteral` parses a literal at the given position into an unescaped value
676
+ _parseLiteral(input, pos) {
617
677
  // Ensure we have enough lookahead to identify triple-quoted strings
618
- if (input.length >= 3) {
678
+ if (input.length - pos >= 3) {
619
679
  // The caller has already identified a single or double quote.
620
- const quote = input[0];
621
- const openingLength = input[1] === quote && input[2] === quote ? 3 : 1;
680
+ const quote = input[pos];
681
+ const openingLength = input[pos + 1] === quote && input[pos + 2] === quote ? 3 : 1;
622
682
  let opening = quote;
623
683
  if (openingLength === 3)
624
684
  opening = quote === '"' ? '"""' : "'''";
625
685
 
626
686
  // Find the next candidate closing quotes
627
- let closingPos = Math.max(this._literalClosingPos, openingLength);
628
- while ((closingPos = input.indexOf(opening, closingPos)) > 0) {
687
+ let closingPos = pos + Math.max(this._literalClosingPos, openingLength);
688
+ while ((closingPos = input.indexOf(opening, closingPos)) > pos) {
629
689
  // Count backslashes right before the closing quotes
630
690
  let backslashCount = 0;
631
691
  while (input[closingPos - backslashCount - 1] === '\\')
@@ -635,10 +695,10 @@ export default class N3Lexer {
635
695
  // means these are actual, non-escaped closing quotes
636
696
  if (backslashCount % 2 === 0) {
637
697
  // Extract and unescape the value
638
- const raw = input.substring(openingLength, closingPos),
698
+ const raw = input.substring(pos + openingLength, closingPos),
639
699
  lines = raw.split(/\r\n|\r|\n/),
640
700
  lineCount = lines.length - 1;
641
- const matchLength = closingPos + openingLength;
701
+ const matchLength = closingPos - pos + openingLength;
642
702
  // Only triple-quoted strings can be multi-line
643
703
  if (openingLength === 1 && lineCount !== 0 ||
644
704
  openingLength === 3 && this._lineMode)
@@ -649,7 +709,7 @@ export default class N3Lexer {
649
709
  }
650
710
  closingPos++;
651
711
  }
652
- this._literalClosingPos = input.length - openingLength + 1;
712
+ this._literalClosingPos = input.length - pos - openingLength + 1;
653
713
  }
654
714
  return { value: '', matchLength: 0, finalLineLength: 0 };
655
715
  }
@@ -713,36 +773,42 @@ export default class N3Lexer {
713
773
  }
714
774
  // Otherwise, the input must be a stream
715
775
  else {
716
- this._pendingBuffer = null;
776
+ let decoder, retryLength = 0;
717
777
  if (typeof input.setEncoding === 'function')
718
778
  input.setEncoding('utf8');
719
779
  // Adds the data chunk to the buffer and parses as far as possible
720
780
  input.on('data', data => {
721
781
  if (this._tokenization === tokenization && this._input !== null && data.length !== 0) {
722
- // Prepend any previous pending writes
723
- if (this._pendingBuffer) {
724
- data = Buffer.concat([this._pendingBuffer, data]);
725
- this._pendingBuffer = null;
726
- }
727
- // Hold if the buffer ends in an incomplete unicode sequence
728
- if (data[data.length - 1] & 0x80) {
729
- this._pendingBuffer = data;
782
+ // Decode bytes, keeping an incomplete trailing character for the next chunk
783
+ if (typeof data !== 'string') {
784
+ decoder = decoder || new TextDecoder('utf-8', { ignoreBOM: true });
785
+ if (!(data = decoder.decode(data, { stream: true })))
786
+ return;
730
787
  }
731
- // Otherwise, tokenize as far as possible
732
- else {
733
- // Only read a BOM at the start
734
- if (typeof this._input === 'undefined')
735
- this._input = this._readStartingBom(typeof data === 'string' ? data : data.toString());
736
- else
737
- this._input += data;
788
+ // Only read a BOM at the start
789
+ if (typeof this._input === 'undefined')
790
+ this._input = this._readStartingBom(data);
791
+ else
792
+ this._input += data;
793
+ // Tokenize as far as possible. When a previous attempt left a long unfinished token,
794
+ // wait until the buffered input has doubled, so the token is not rescanned for every chunk.
795
+ if (this._input.length >= retryLength) {
738
796
  this._tokenizeToEnd(callback, false);
797
+ retryLength = this._input !== null && this._input.length > MIN_RESCAN_LENGTH ?
798
+ 2 * this._input.length : 0;
739
799
  }
740
800
  }
741
801
  });
742
802
  // Parses until the end
743
803
  input.on('end', () => {
744
- if (this._tokenization === tokenization && typeof this._input === 'string')
745
- this._tokenizeToEnd(callback, true);
804
+ if (this._tokenization === tokenization && this._input !== null) {
805
+ // Decode any incomplete character left at the end
806
+ const rest = decoder ? decoder.decode() : '';
807
+ if (rest)
808
+ this._input = typeof this._input === 'string' ? this._input + rest : rest;
809
+ if (typeof this._input === 'string')
810
+ this._tokenizeToEnd(callback, true);
811
+ }
746
812
  });
747
813
  input.on('error', error => {
748
814
  if (this._tokenization === tokenization)