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/README.md +56 -0
- package/browser/n3.esm.min.js +15 -16
- package/browser/n3.min.js +15 -16
- package/lib/N3Lexer.js +255 -160
- package/lib/N3Parser.js +70 -32
- package/lib/N3Reasoner.js +1 -1
- package/lib/N3Store.js +50 -6
- package/lib/N3StreamParser.js +4 -0
- package/lib/N3Writer.js +65 -19
- package/lib/Util.js +1 -1
- package/package.json +2 -29
- package/src/N3Lexer.js +269 -171
- package/src/N3Parser.js +81 -32
- package/src/N3Reasoner.js +1 -1
- package/src/N3Store.js +61 -9
- package/src/N3StreamParser.js +2 -0
- package/src/N3Writer.js +70 -23
- package/src/Util.js +1 -1
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
|
-
|
|
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
|
-
|
|
45
|
-
this.
|
|
46
|
-
this.
|
|
47
|
-
this.
|
|
48
|
-
this.
|
|
49
|
-
this.
|
|
50
|
-
this.
|
|
51
|
-
this.
|
|
52
|
-
this.
|
|
53
|
-
this.
|
|
54
|
-
this.
|
|
55
|
-
this.
|
|
56
|
-
this.
|
|
57
|
-
this.
|
|
58
|
-
this.
|
|
59
|
-
this.
|
|
60
|
-
this.
|
|
61
|
-
this.
|
|
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
|
-
|
|
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(
|
|
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
|
|
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
|
|
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(
|
|
110
|
-
this.
|
|
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
|
-
|
|
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
|
-
|
|
122
|
-
if (!inputFinished)
|
|
123
|
-
this.
|
|
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,
|
|
128
|
-
|
|
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.
|
|
136
|
-
|
|
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
|
|
186
|
+
execAt(this._whitespace, input, pos + separatorLength)[0].length : 1;
|
|
146
187
|
}
|
|
147
|
-
|
|
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
|
-
|
|
153
|
-
input = input.slice(separatorLength);
|
|
193
|
+
pos += separatorLength;
|
|
154
194
|
break;
|
|
155
195
|
}
|
|
156
196
|
}
|
|
157
|
-
if (input.length
|
|
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
|
-
|
|
160
|
-
|
|
203
|
+
emitToken('eof', '', '', this._line, currentLineLength, 0);
|
|
204
|
+
return this._input = null;
|
|
161
205
|
}
|
|
162
|
-
this.
|
|
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[
|
|
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
|
-
|
|
181
|
-
if (input[
|
|
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
|
|
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
|
|
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
|
|
237
|
-
inputFinished && (match = this._blank
|
|
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
|
|
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
|
|
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
|
|
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
|
|
290
|
-
|
|
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
|
|
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
|
|
336
|
-
inputFinished && (match = this._number
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
461
|
-
inputFinished && (match = this._prefixed
|
|
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 || (
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
return this.
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
619
|
+
if (testAt(nonPrefixedNameChar, input, pos))
|
|
553
620
|
return verb;
|
|
554
|
-
if (this._prefixed
|
|
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
|
|
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[
|
|
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)) >
|
|
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 {
|
|
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(
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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
|
-
//
|
|
723
|
-
if (
|
|
724
|
-
|
|
725
|
-
|
|
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
|
-
//
|
|
728
|
-
if (
|
|
729
|
-
this.
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
this._input
|
|
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 &&
|
|
745
|
-
|
|
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)
|