n3 2.11.0 → 2.11.2

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