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