n3 2.11.1 → 2.11.3

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
@@ -18,7 +18,8 @@ const localNameEscapeReplacements = {
18
18
  };
19
19
  const illegalIriChars = /[\x00-\x20<>\\"\{\}\|\^\`]/;
20
20
  // Characters that cannot occur in a prefixed name, not even escaped
21
- const nonPrefixedNameChar = /[\s<>"{}|^`]/;
21
+ // (global, so that testAt searches the rest of the input from a position)
22
+ const nonPrefixedNameChar = /[\s<>"{}|^`]/g;
22
23
 
23
24
  // A valid code point is a Unicode scalar value: at most U+10FFFF and not a surrogate
24
25
  function isValidCodePoint(charCode) {
@@ -35,30 +36,48 @@ const lineModeRegExps = {
35
36
  _whitespace: true,
36
37
  };
37
38
  const invalidRegExp = /$0^/;
39
+ const nonWhitespace = /\S*/y;
40
+
41
+ // Matches a sticky regular expression at the given position of the input
42
+ function execAt(regExp, input, pos) {
43
+ regExp.lastIndex = pos;
44
+ return regExp.exec(input);
45
+ }
46
+ function testAt(regExp, input, pos) {
47
+ regExp.lastIndex = pos;
48
+ return regExp.test(input);
49
+ }
50
+ // Matches the rest of the input followed by a space, as at the end of the input,
51
+ // a token that can contain (but not end with) a dot needs a non-dot character after it
52
+ function execAtEnd(regExp, input, pos) {
53
+ regExp.lastIndex = 0;
54
+ return regExp.exec(`${input.slice(pos)} `);
55
+ }
38
56
 
39
57
  // ## Constructor
40
58
  export default class N3Lexer {
41
59
  constructor(options) {
42
60
  // ## 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]+/;
61
+ // It's slightly faster to have these as properties than as in-scope variables.
62
+ // They are sticky, so they only match at the `lastIndex` set by `execAt`.
63
+ this._iri = /<((?:[^ <>{}\\]|\\[uU])+)>[ \t]*/y; // IRI with escape sequences; needs sanity check after unescaping
64
+ this._unescapedIri = /<([^\x00-\x20<>\\"\{\}\|\^\`]*)>[ \t]*/y; // IRI without escape sequences; no unescaping
65
+ this._simpleQuotedString = /"([^"\\\r\n]*)"(?=[^"])/y; // string without escape sequences
66
+ this._simpleApostropheString = /'([^'\\\r\n]*)'(?=[^'])/y;
67
+ this._langcode = /@([a-z]+(?:-[a-z0-9]+)*)(?=[^a-z0-9])/iy;
68
+ 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;
69
+ 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;
70
+ 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;
71
+ 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;
72
+ this._number = /[\-+]?(?:(\d+\.\d*|\.?\d+)[eE][\-+]?\d+|(?=\.?\d)\d*(?:(\.)\d+)?)(?=\.?[,;:!\^\s#()\[\]\{\}"'<>])/y;
73
+ this._boolean = /(?:true|false)(?=[.,;!\^\s#()\[\]\{\}"'<>])/y;
74
+ this._atKeyword = /@[a-z]+(?=[\s#<:])/iy;
75
+ this._keyword = /(?:PREFIX|BASE|VERSION|GRAPH)(?=[\s#<])/iy;
76
+ this._n3Verb = /(?:has|is|of)(?=[\s#()\[\]\{\}"'<>?_+\-0-9])/y;
77
+ this._n3Id = /id(?=[\s#<])/y;
78
+ this._shortPredicates = /a(?=[\s#()\[\]\{\}"'<>])/y;
79
+ this._commentLine = /[ \t]*#([^\n\r]*)(?:\r\n|\n|\r)([ \t]*)/y;
80
+ this._whitespace = /[ \t]+/y;
62
81
  options = options || {};
63
82
 
64
83
  // Whether the log:isImpliedBy predicate is supported
@@ -87,98 +106,94 @@ export default class N3Lexer {
87
106
 
88
107
  // ### `_tokenizeToEnd` tokenizes as for as possible, emitting tokens through the callback
89
108
  _tokenizeToEnd(callback, inputFinished) {
90
- // Continue parsing as far as possible; the loop will return eventually
91
- let input = this._input;
109
+ // Continue parsing as far as possible; the loop will return eventually.
110
+ // Rather than slicing off every token, track the position of the remaining input;
111
+ // the regular expressions are sticky, so they match at that position.
112
+ const input = this._input;
113
+ let pos = 0;
92
114
  let currentLineLength = this._linePosition + input.length;
93
115
  while (true) {
94
116
  // Consume one separator line at a time, including its following indentation.
95
117
  while (true) {
96
- let charCode = input.charCodeAt(0), separatorLength = 0;
118
+ let charCode = input.charCodeAt(pos), separatorLength = 0;
97
119
  if (charCode === SPACE || charCode === TAB) {
98
- const next = input.charCodeAt(1);
120
+ const next = input.charCodeAt(pos + 1);
99
121
  separatorLength = next === SPACE || next === TAB ?
100
- this._whitespace.exec(input)[0].length : 1;
101
- charCode = input.charCodeAt(separatorLength);
122
+ execAt(this._whitespace, input, pos)[0].length : 1;
123
+ charCode = input.charCodeAt(pos + separatorLength);
102
124
  }
103
125
  if (charCode === HASH) {
104
- const comment = this._commentLine.exec(input);
126
+ const comment = execAt(this._commentLine, input, pos);
105
127
  if (comment) {
106
128
  const commentLength = comment[0].length;
107
129
  // 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
- }
130
+ if (!inputFinished && pos + commentLength === input.length &&
131
+ input.charCodeAt(input.length - 1) === CR)
132
+ return this._suspend(input, pos, currentLineLength);
113
133
  if (this.comments)
114
- emitComment(comment[1], this._line, separatorLength);
115
- input = input.slice(commentLength);
116
- currentLineLength = input.length + comment[2].length;
134
+ emitComment(comment[1], this._line, currentLineLength - (input.length - pos) + separatorLength);
135
+ pos += commentLength;
136
+ currentLineLength = input.length - pos + comment[2].length;
117
137
  this._line++;
118
138
  }
119
139
  else {
120
140
  // 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
- }
141
+ pos += separatorLength;
142
+ if (!inputFinished)
143
+ return this._suspend(input, pos, currentLineLength);
126
144
  if (this.comments)
127
- emitComment(input.slice(1), this._line, 0);
128
- input = '';
145
+ emitComment(input.slice(pos + 1), this._line, currentLineLength - (input.length - pos));
146
+ pos = input.length;
129
147
  break;
130
148
  }
131
149
  }
132
150
  else if (charCode === LF || charCode === CR) {
133
151
  // 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;
152
+ if (!inputFinished && charCode === CR && pos + separatorLength + 1 === input.length)
153
+ return this._suspend(input, pos, currentLineLength);
154
+ separatorLength += charCode === CR && input.charCodeAt(pos + separatorLength + 1) === LF ? 2 : 1;
139
155
  // Indentation is consumed with the newline, but belongs to the next line's columns.
140
156
  let indentationLength = 0;
141
- const next = input.charCodeAt(separatorLength);
157
+ const next = input.charCodeAt(pos + separatorLength);
142
158
  if (next === SPACE || next === TAB) {
143
- const following = input.charCodeAt(separatorLength + 1);
159
+ const following = input.charCodeAt(pos + separatorLength + 1);
144
160
  indentationLength = following === SPACE || following === TAB ?
145
- this._whitespace.exec(input.slice(separatorLength))[0].length : 1;
161
+ execAt(this._whitespace, input, pos + separatorLength)[0].length : 1;
146
162
  }
147
- input = input.slice(separatorLength + indentationLength);
148
- currentLineLength = input.length + indentationLength;
163
+ pos += separatorLength + indentationLength;
164
+ currentLineLength = input.length - pos + indentationLength;
149
165
  this._line++;
150
166
  }
151
167
  else {
152
- if (separatorLength !== 0)
153
- input = input.slice(separatorLength);
168
+ pos += separatorLength;
154
169
  break;
155
170
  }
156
171
  }
157
- if (input.length === 0) {
172
+ if (pos >= input.length) {
173
+ this._linePosition = currentLineLength;
158
174
  if (inputFinished) {
159
- input = null;
160
- emitToken('eof', '', '', this._line, 0);
175
+ emitToken('eof', '', '', this._line, currentLineLength, 0);
176
+ return this._input = null;
161
177
  }
162
- this._linePosition = currentLineLength;
163
- return this._input = input;
178
+ return this._input = '';
164
179
  }
165
180
 
166
181
  // Look for specific token types based on the first character
167
- const line = this._line, firstChar = input[0];
182
+ const line = this._line, firstChar = input[pos];
168
183
  let type = '', value = '', prefix = '',
169
184
  match = null, matchLength = 0, lexicalLength = 0,
170
185
  finalLineLength = 0, inconclusive = false;
171
186
  switch (firstChar) {
172
187
  case '^':
173
188
  // We need at least 3 tokens lookahead to distinguish ^^<IRI> and ^^pre:fixed
174
- if (input.length < 3)
189
+ if (input.length - pos < 3)
175
190
  break;
176
191
  // Try to match a type
177
- else if (input[1] === '^') {
192
+ else if (input[pos + 1] === '^') {
178
193
  this._previousMarker = '^^';
179
194
  // Move to type IRI or prefixed name
180
- input = input.slice(2);
181
- if (input[0] !== '<') {
195
+ pos += 2;
196
+ if (input[pos] !== '<') {
182
197
  inconclusive = true;
183
198
  break;
184
199
  }
@@ -194,38 +209,38 @@ export default class N3Lexer {
194
209
  // Fall through in case the type is an IRI
195
210
  case '<':
196
211
  // Try to find a full IRI without escape sequences
197
- if (match = this._unescapedIri.exec(input)) {
212
+ if (match = execAt(this._unescapedIri, input, pos)) {
198
213
  type = 'IRI', value = match[1];
199
214
  lexicalLength = match[1].length + 2;
200
215
  }
201
216
  // Try to find a full IRI with escape sequences
202
- else if (match = this._iri.exec(input)) {
217
+ else if (match = execAt(this._iri, input, pos)) {
203
218
  value = this._unescape(match[1], stringEscapeReplacements);
204
219
  if (value === null || illegalIriChars.test(value))
205
- return reportSyntaxError(this);
220
+ return reportSyntaxError(this, input, pos);
206
221
  type = 'IRI';
207
222
  lexicalLength = match[1].length + 2;
208
223
  }
209
224
  // Try to find a triple term
210
- else if (input.length > 2 && input[1] === '<' && input[2] === '(')
225
+ else if (input.length - pos > 2 && input[pos + 1] === '<' && input[pos + 2] === '(')
211
226
  type = '<<(', matchLength = 3;
212
227
  // Try to find a reified triple
213
- else if (!this._lineMode && input.length > (inputFinished ? 1 : 2) && input[1] === '<')
228
+ else if (!this._lineMode && input.length - pos > (inputFinished ? 1 : 2) && input[pos + 1] === '<')
214
229
  type = '<<', matchLength = 2;
215
230
  // Try to find a backwards implication arrow
216
- else if (this._n3Mode && input.length > 1 && input[1] === '=') {
231
+ else if (this._n3Mode && input.length - pos > 1 && input[pos + 1] === '=') {
217
232
  matchLength = 2;
218
233
  if (this._isImpliedBy) type = 'abbreviation', value = '<';
219
234
  else type = 'inverse', value = '>';
220
235
  }
221
236
  // Try to find an inverted predicate marker
222
- else if (this._n3Mode && input.length > 1 && input[1] === '-')
237
+ else if (this._n3Mode && input.length - pos > 1 && input[pos + 1] === '-')
223
238
  type = 'inversePredicate', matchLength = 2;
224
239
  break;
225
240
 
226
241
  case '>':
227
242
  // Try to find a reified triple
228
- if (input.length > 1 && input[1] === '>')
243
+ if (input.length - pos > 1 && input[pos + 1] === '>')
229
244
  type = '>>', matchLength = 2;
230
245
  break;
231
246
 
@@ -233,8 +248,8 @@ export default class N3Lexer {
233
248
  // Try to find a blank node. Since it can contain (but not end with) a dot,
234
249
  // we always need a non-dot character before deciding it is a blank node.
235
250
  // 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} `))) {
251
+ if ((match = execAt(this._blank, input, pos)) ||
252
+ inputFinished && (match = execAtEnd(this._blank, input, pos))) {
238
253
  type = 'blank', prefix = '_', value = match[1];
239
254
  lexicalLength = match[1].length + 2;
240
255
  }
@@ -242,13 +257,13 @@ export default class N3Lexer {
242
257
 
243
258
  case '"':
244
259
  // Try to find a literal without escape sequences
245
- if (match = this._simpleQuotedString.exec(input))
260
+ if (match = execAt(this._simpleQuotedString, input, pos))
246
261
  value = match[1];
247
262
  // Try to find a literal wrapped in three pairs of quotes
248
263
  else {
249
- ({ value, matchLength, finalLineLength } = this._parseLiteral(input));
264
+ ({ value, matchLength, finalLineLength } = this._parseLiteral(input, pos));
250
265
  if (value === null)
251
- return reportSyntaxError(this);
266
+ return reportSyntaxError(this, input, pos);
252
267
  }
253
268
  if (match !== null || matchLength !== 0) {
254
269
  type = 'literal';
@@ -259,13 +274,13 @@ export default class N3Lexer {
259
274
  case "'":
260
275
  if (!this._lineMode) {
261
276
  // Try to find a literal without escape sequences
262
- if (match = this._simpleApostropheString.exec(input))
277
+ if (match = execAt(this._simpleApostropheString, input, pos))
263
278
  value = match[1];
264
279
  // Try to find a literal wrapped in three pairs of quotes
265
280
  else {
266
- ({ value, matchLength, finalLineLength } = this._parseLiteral(input));
281
+ ({ value, matchLength, finalLineLength } = this._parseLiteral(input, pos));
267
282
  if (value === null)
268
- return reportSyntaxError(this);
283
+ return reportSyntaxError(this, input, pos);
269
284
  }
270
285
  if (match !== null || matchLength !== 0) {
271
286
  type = 'literal';
@@ -276,7 +291,7 @@ export default class N3Lexer {
276
291
 
277
292
  case '?':
278
293
  // Try to find a variable
279
- if (this._n3Mode && (match = this._variable.exec(input)))
294
+ if (this._n3Mode && (match = execAt(this._variable, input, pos)))
280
295
  type = 'var', value = match[0];
281
296
  break;
282
297
 
@@ -286,20 +301,21 @@ export default class N3Lexer {
286
301
  // input is not finished, another subtag may still arrive in a later chunk and
287
302
  // the match would be premature; wait for more input in that case.
288
303
  // 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] !== '-')
304
+ if (this._previousMarker === 'literal' && (match = execAt(this._langcode, input, pos)) && match[1] !== 'version') {
305
+ const end = pos + match[0].length;
306
+ if (!inputFinished && input[end] === '-' && input[end + 1] !== '-')
291
307
  match = null;
292
308
  else
293
309
  type = 'langcode', value = match[1];
294
310
  }
295
311
  // Try to find a keyword
296
- else if (match = this._atKeyword.exec(input))
312
+ else if (match = execAt(this._atKeyword, input, pos))
297
313
  type = match[0];
298
314
  break;
299
315
 
300
316
  case '.':
301
317
  // Try to find a dot as punctuation
302
- if (input.length === 1 ? inputFinished : (input[1] < '0' || input[1] > '9')) {
318
+ if (input.length - pos === 1 ? inputFinished : (input[pos + 1] < '0' || input[pos + 1] > '9')) {
303
319
  type = '.';
304
320
  matchLength = 1;
305
321
  break;
@@ -318,12 +334,12 @@ export default class N3Lexer {
318
334
  case '9':
319
335
  case '+':
320
336
  case '-':
321
- if (input[1] === '-') {
337
+ if (input[pos + 1] === '-') {
322
338
  // Try to find a direction code
323
339
  if (this._previousMarker === 'langcode') {
324
- if (input.startsWith('--ltr'))
340
+ if (input.startsWith('--ltr', pos))
325
341
  type = 'dircode', value = 'ltr', matchLength = 5;
326
- else if (input.startsWith('--rtl'))
342
+ else if (input.startsWith('--rtl', pos))
327
343
  type = 'dircode', value = 'rtl', matchLength = 5;
328
344
  }
329
345
  break;
@@ -332,8 +348,8 @@ export default class N3Lexer {
332
348
  // Try to find a number. Since it can contain (but not end with) a dot,
333
349
  // we always need a non-dot character before deciding it is a number.
334
350
  // 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} `))) {
351
+ if (match = execAt(this._number, input, pos) ||
352
+ inputFinished && (match = execAtEnd(this._number, input, pos))) {
337
353
  type = 'literal', value = match[0];
338
354
  prefix = (typeof match[1] === 'string' ? xsd.double :
339
355
  (typeof match[2] === 'string' ? xsd.decimal : xsd.integer));
@@ -349,7 +365,7 @@ export default class N3Lexer {
349
365
  case 'V':
350
366
  case 'v':
351
367
  // Try to find a SPARQL-style keyword
352
- if (match = this._keyword.exec(input))
368
+ if (match = execAt(this._keyword, input, pos))
353
369
  type = match[0].toUpperCase();
354
370
  else
355
371
  inconclusive = true;
@@ -358,7 +374,7 @@ export default class N3Lexer {
358
374
  case 'f':
359
375
  case 't':
360
376
  // Try to match a boolean
361
- if (this._boolean.test(input))
377
+ if (testAt(this._boolean, input, pos))
362
378
  type = 'literal', value = firstChar === 't' ? 'true' : 'false', prefix = xsd.boolean, matchLength = value.length;
363
379
  else
364
380
  inconclusive = true;
@@ -366,7 +382,7 @@ export default class N3Lexer {
366
382
 
367
383
  case 'a':
368
384
  // Try to find an abbreviated predicate
369
- if (this._shortPredicates.test(input))
385
+ if (testAt(this._shortPredicates, input, pos))
370
386
  type = 'abbreviation', value = 'a', matchLength = 1;
371
387
  else
372
388
  inconclusive = true;
@@ -375,7 +391,7 @@ export default class N3Lexer {
375
391
  case 'h':
376
392
  case 'o':
377
393
  // Try to find an N3 verb keyword
378
- if (this._n3Mode && (match = this._matchN3Verb(input, inputFinished)))
394
+ if (this._n3Mode && (match = this._matchN3Verb(input, pos, inputFinished)))
379
395
  type = match[0];
380
396
  else
381
397
  inconclusive = true;
@@ -383,9 +399,9 @@ export default class N3Lexer {
383
399
 
384
400
  case 'i':
385
401
  // Try to find an IRI property list identifier or N3 verb keyword
386
- if (this._n3Mode && this._n3Id.test(input))
402
+ if (this._n3Mode && testAt(this._n3Id, input, pos))
387
403
  type = 'id', matchLength = 2;
388
- else if (this._n3Mode && (match = this._matchN3Verb(input, inputFinished)))
404
+ else if (this._n3Mode && (match = this._matchN3Verb(input, pos, inputFinished)))
389
405
  type = match[0];
390
406
  else
391
407
  inconclusive = true;
@@ -393,9 +409,9 @@ export default class N3Lexer {
393
409
 
394
410
  case '=':
395
411
  // Try to find an implication arrow or equals sign
396
- if (this._n3Mode && input.length > 1) {
412
+ if (this._n3Mode && input.length - pos > 1) {
397
413
  type = 'abbreviation';
398
- if (input[1] !== '>')
414
+ if (input[pos + 1] !== '>')
399
415
  matchLength = 1, value = '=';
400
416
  else
401
417
  matchLength = 2, value = '>';
@@ -406,12 +422,12 @@ export default class N3Lexer {
406
422
  if (!this._n3Mode)
407
423
  break;
408
424
  case ')':
409
- if (!inputFinished && (input.length === 1 || (input.length === 2 && input[1] === '>'))) {
425
+ if (!inputFinished && (input.length - pos === 1 || (input.length - pos === 2 && input[pos + 1] === '>'))) {
410
426
  // Don't consume yet, as it *could* become a triple term end.
411
427
  break;
412
428
  }
413
429
  // Try to find a triple term
414
- if (input.length > 2 && input[1] === '>' && input[2] === '>') {
430
+ if (input.length - pos > 2 && input[pos + 1] === '>' && input[pos + 2] === '>') {
415
431
  type = ')>>', matchLength = 3;
416
432
  break;
417
433
  }
@@ -429,9 +445,9 @@ export default class N3Lexer {
429
445
  break;
430
446
  case '{':
431
447
  // We need at least 2 tokens lookahead to distinguish "{|" and "{ "
432
- if (!this._lineMode && input.length >= 2) {
448
+ if (!this._lineMode && input.length - pos >= 2) {
433
449
  // Try to find a quoted triple annotation start
434
- if (input[1] === '|')
450
+ if (input[pos + 1] === '|')
435
451
  type = '{|', matchLength = 2;
436
452
  else
437
453
  type = firstChar, matchLength = 1;
@@ -440,7 +456,7 @@ export default class N3Lexer {
440
456
  case '|':
441
457
  // We need 2 tokens lookahead to parse "|}"
442
458
  // Try to find a quoted triple annotation end
443
- if (input.length >= 2 && input[1] === '}')
459
+ if (input.length - pos >= 2 && input[pos + 1] === '}')
444
460
  type = '|}', matchLength = 2;
445
461
  break;
446
462
 
@@ -452,13 +468,13 @@ export default class N3Lexer {
452
468
  if (inconclusive) {
453
469
  // Try to find a prefix
454
470
  if ((this._previousMarker === '@prefix' || this._previousMarker === 'PREFIX') &&
455
- (match = this._prefix.exec(input)))
471
+ (match = execAt(this._prefix, input, pos)))
456
472
  type = 'prefix', value = match[1] || '';
457
473
  // Try to find a prefixed name. Since it can contain (but not end with) a dot,
458
474
  // we always need a non-dot character before deciding it is a prefixed name.
459
475
  // 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} `))) {
476
+ else if ((match = execAt(this._prefixed, input, pos)) ||
477
+ inputFinished && (match = execAtEnd(this._prefixed, input, pos))) {
462
478
  type = 'prefixed', prefix = match[1] || '';
463
479
  value = this._unescape(match[2], localNameEscapeReplacements);
464
480
  lexicalLength = prefix.length + match[2].length + 1;
@@ -479,86 +495,90 @@ export default class N3Lexer {
479
495
  // We could be in streaming mode, and then we just wait for more input to arrive.
480
496
  // Otherwise, a syntax error has occurred in the input.
481
497
  // 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
- }
498
+ if (inputFinished || (!input.startsWith("'''", pos) && !input.startsWith('"""', pos) &&
499
+ /\n|\r/.test(input.slice(pos))))
500
+ return reportSyntaxError(this, input, pos);
501
+ else
502
+ return this._suspend(input, pos, currentLineLength);
488
503
  }
489
504
 
490
505
  // Emit the parsed token
491
506
  // Consumption includes separator whitespace; lexicalLength excludes it
492
- // and any synthetic EOF space. slice below clamps consumption to the input.
507
+ // and any synthetic EOF space. Consumption is clamped to the input below.
493
508
  const length = matchLength || match[0].length;
509
+ const start = currentLineLength - (input.length - pos);
494
510
  let token;
495
511
  if (finalLineLength) {
496
512
  token = {
497
- type, value, prefix, line,
498
- start: currentLineLength - input.length,
513
+ type, value, prefix, line, start,
499
514
  end: finalLineLength, endLine: this._line,
500
515
  };
501
516
  callback(null, token);
502
517
  }
503
518
  else
504
- token = emitToken(type, value, prefix, line, lexicalLength || length);
519
+ token = emitToken(type, value, prefix, line, start, lexicalLength || length);
505
520
  this.previousToken = token;
506
521
  this._previousMarker = type;
507
522
 
508
523
  // Advance to next part to tokenize
509
- input = input.slice(length);
524
+ pos = Math.min(pos + length, input.length);
510
525
  if (finalLineLength)
511
- currentLineLength = input.length + finalLineLength;
526
+ currentLineLength = input.length - pos + finalLineLength;
512
527
  }
513
528
 
514
529
  // Emits a comment at its exact position within matched whitespace.
515
- function emitComment(value, line, offset) {
516
- const start = currentLineLength - input.length + offset;
530
+ function emitComment(value, line, start) {
517
531
  callback(null, {
518
532
  type: 'comment', value, prefix: '', line,
519
533
  start, end: start + value.length + 1,
520
534
  });
521
535
  }
522
536
  // 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 };
537
+ function emitToken(type, value, prefix, line, start, length) {
538
+ const token = { type, value, prefix, line, start, end: start + length };
527
539
  callback(null, token);
528
540
  return token;
529
541
  }
530
542
  // Signals the syntax error through the callback
531
- function reportSyntaxError(self) { callback(self._syntaxError(/^\S*/.exec(input)[0])); }
543
+ function reportSyntaxError(self, input, pos) {
544
+ callback(self._syntaxError(execAt(nonWhitespace, input, pos)[0]));
545
+ }
546
+ }
547
+
548
+ // ### `_suspend` keeps the unconsumed input until more input arrives
549
+ _suspend(input, pos, currentLineLength) {
550
+ this._linePosition = currentLineLength - (input.length - pos);
551
+ return this._input = input.slice(pos);
532
552
  }
533
553
 
534
554
  // ### `_matchN3Verb` matches an N3 verb unless the input is a longer prefixed name
535
- _matchN3Verb(input, inputFinished) {
536
- const verb = this._n3Verb.exec(input);
555
+ _matchN3Verb(input, pos, inputFinished) {
556
+ const verb = execAt(this._n3Verb, input, pos);
537
557
  if (!verb)
538
558
  return null;
539
559
 
540
560
  // Most verb boundaries cannot be part of a prefix, so keep the common path fast.
541
- const next = input[verb[0].length];
561
+ const next = input[pos + verb[0].length];
542
562
  if (next !== '-' && next !== '_' && (next < '0' || next > '9'))
543
563
  return verb;
544
564
 
545
565
  // A prefix can start with a verb and continue with characters that are also
546
566
  // valid verb boundaries. Prefer the longer prefixed name when it is complete.
547
- if (this._prefixed.exec(input))
567
+ if (execAt(this._prefixed, input, pos))
548
568
  return null;
549
569
  // Appending to the input only matters when a prefixed name could run up to
550
570
  // its end, which a character that cannot occur in prefixed names rules out.
551
571
  // This avoids copying the rest of the document for every such verb.
552
- if (nonPrefixedNameChar.test(input))
572
+ if (testAt(nonPrefixedNameChar, input, pos))
553
573
  return verb;
554
- if (this._prefixed.exec(`${input} `))
574
+ if (execAtEnd(this._prefixed, input, pos))
555
575
  return null;
556
576
 
557
577
  // If a stream chunk ends partway through such a prefix, wait for the colon
558
578
  // instead of prematurely emitting the verb. Appending ": " lets the prefix
559
579
  // grammar determine whether all input seen so far can be a complete prefix.
560
580
  if (!inputFinished) {
561
- const prefix = this._prefix.exec(`${input}: `);
581
+ const prefix = execAt(this._prefix, `${input.slice(pos)}: `, 0);
562
582
  if (prefix)
563
583
  return null;
564
584
  }
@@ -612,20 +632,20 @@ export default class N3Lexer {
612
632
  return result + item.slice(start);
613
633
  }
614
634
 
615
- // ### `_parseLiteral` parses a literal into an unescaped value
616
- _parseLiteral(input) {
635
+ // ### `_parseLiteral` parses a literal at the given position into an unescaped value
636
+ _parseLiteral(input, pos) {
617
637
  // Ensure we have enough lookahead to identify triple-quoted strings
618
- if (input.length >= 3) {
638
+ if (input.length - pos >= 3) {
619
639
  // 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;
640
+ const quote = input[pos];
641
+ const openingLength = input[pos + 1] === quote && input[pos + 2] === quote ? 3 : 1;
622
642
  let opening = quote;
623
643
  if (openingLength === 3)
624
644
  opening = quote === '"' ? '"""' : "'''";
625
645
 
626
646
  // Find the next candidate closing quotes
627
- let closingPos = Math.max(this._literalClosingPos, openingLength);
628
- while ((closingPos = input.indexOf(opening, closingPos)) > 0) {
647
+ let closingPos = pos + Math.max(this._literalClosingPos, openingLength);
648
+ while ((closingPos = input.indexOf(opening, closingPos)) > pos) {
629
649
  // Count backslashes right before the closing quotes
630
650
  let backslashCount = 0;
631
651
  while (input[closingPos - backslashCount - 1] === '\\')
@@ -635,10 +655,10 @@ export default class N3Lexer {
635
655
  // means these are actual, non-escaped closing quotes
636
656
  if (backslashCount % 2 === 0) {
637
657
  // Extract and unescape the value
638
- const raw = input.substring(openingLength, closingPos),
658
+ const raw = input.substring(pos + openingLength, closingPos),
639
659
  lines = raw.split(/\r\n|\r|\n/),
640
660
  lineCount = lines.length - 1;
641
- const matchLength = closingPos + openingLength;
661
+ const matchLength = closingPos - pos + openingLength;
642
662
  // Only triple-quoted strings can be multi-line
643
663
  if (openingLength === 1 && lineCount !== 0 ||
644
664
  openingLength === 3 && this._lineMode)
@@ -649,7 +669,7 @@ export default class N3Lexer {
649
669
  }
650
670
  closingPos++;
651
671
  }
652
- this._literalClosingPos = input.length - openingLength + 1;
672
+ this._literalClosingPos = input.length - pos - openingLength + 1;
653
673
  }
654
674
  return { value: '', matchLength: 0, finalLineLength: 0 };
655
675
  }