n3 3.0.0-alpha.4 → 3.0.0-alpha.6

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