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