n3 2.7.10 → 2.7.12
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/browser/n3.esm.min.js +9 -9
- package/browser/n3.min.js +9 -9
- package/lib/N3Lexer.js +105 -28
- package/package.json +1 -1
- package/src/N3Lexer.js +96 -33
package/lib/N3Lexer.js
CHANGED
|
@@ -92,7 +92,7 @@ class N3Lexer {
|
|
|
92
92
|
this._n3Verb = /^(?:has|is|of)(?=[\s#()\[\]\{\}"'<>?_+\-0-9])/;
|
|
93
93
|
this._n3Id = /^id(?=[\s#<])/;
|
|
94
94
|
this._shortPredicates = /^a(?=[\s#()\[\]\{\}"'<>])/;
|
|
95
|
-
this._commentLine = /^[ \t]*#([^\n\r]*)(?:\r\n|\n|\r)[ \t]
|
|
95
|
+
this._commentLine = /^[ \t]*#([^\n\r]*)(?:\r\n|\n|\r)([ \t]*)/;
|
|
96
96
|
this._whitespace = /^[ \t]+/;
|
|
97
97
|
options = options || {};
|
|
98
98
|
|
|
@@ -123,7 +123,7 @@ class N3Lexer {
|
|
|
123
123
|
_tokenizeToEnd(callback, inputFinished) {
|
|
124
124
|
// Continue parsing as far as possible; the loop will return eventually
|
|
125
125
|
let input = this._input;
|
|
126
|
-
let currentLineLength = input.length;
|
|
126
|
+
let currentLineLength = this._linePosition + input.length;
|
|
127
127
|
while (true) {
|
|
128
128
|
// Consume one separator line at a time, including its following indentation.
|
|
129
129
|
while (true) {
|
|
@@ -137,28 +137,43 @@ class N3Lexer {
|
|
|
137
137
|
if (charCode === HASH) {
|
|
138
138
|
const comment = this._commentLine.exec(input);
|
|
139
139
|
if (comment) {
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
140
|
+
const commentLength = comment[0].length;
|
|
141
|
+
// Keep a trailing CR buffered in case the next chunk starts with LF.
|
|
142
|
+
if (!inputFinished && commentLength === input.length && input.charCodeAt(commentLength - 1) === CR) {
|
|
143
|
+
this._linePosition = currentLineLength - input.length;
|
|
144
|
+
return this._input = input;
|
|
145
|
+
}
|
|
146
|
+
if (this.comments) emitComment(comment[1], this._line, separatorLength);
|
|
147
|
+
input = input.slice(commentLength);
|
|
148
|
+
currentLineLength = input.length + comment[2].length;
|
|
143
149
|
this._line++;
|
|
144
150
|
} else {
|
|
145
151
|
// A comment without a line ending stays buffered until EOF.
|
|
146
152
|
input = input.slice(separatorLength);
|
|
147
|
-
if (!inputFinished)
|
|
148
|
-
|
|
153
|
+
if (!inputFinished) {
|
|
154
|
+
this._linePosition = currentLineLength - input.length;
|
|
155
|
+
return this._input = input;
|
|
156
|
+
}
|
|
157
|
+
if (this.comments) emitComment(input.slice(1), this._line, 0);
|
|
149
158
|
input = '';
|
|
150
159
|
break;
|
|
151
160
|
}
|
|
152
161
|
} else if (charCode === LF || charCode === CR) {
|
|
162
|
+
// A CR at the end of a chunk may still be followed by LF.
|
|
163
|
+
if (!inputFinished && charCode === CR && separatorLength + 1 === input.length) {
|
|
164
|
+
this._linePosition = currentLineLength - input.length;
|
|
165
|
+
return this._input = input;
|
|
166
|
+
}
|
|
153
167
|
separatorLength += charCode === CR && input.charCodeAt(separatorLength + 1) === LF ? 2 : 1;
|
|
154
|
-
// Indentation is
|
|
168
|
+
// Indentation is consumed with the newline, but belongs to the next line's columns.
|
|
169
|
+
let indentationLength = 0;
|
|
155
170
|
const next = input.charCodeAt(separatorLength);
|
|
156
171
|
if (next === SPACE || next === TAB) {
|
|
157
172
|
const following = input.charCodeAt(separatorLength + 1);
|
|
158
|
-
|
|
173
|
+
indentationLength = following === SPACE || following === TAB ? this._whitespace.exec(input.slice(separatorLength))[0].length : 1;
|
|
159
174
|
}
|
|
160
|
-
input = input.slice(separatorLength);
|
|
161
|
-
currentLineLength = input.length;
|
|
175
|
+
input = input.slice(separatorLength + indentationLength);
|
|
176
|
+
currentLineLength = input.length + indentationLength;
|
|
162
177
|
this._line++;
|
|
163
178
|
} else {
|
|
164
179
|
if (separatorLength !== 0) input = input.slice(separatorLength);
|
|
@@ -170,6 +185,7 @@ class N3Lexer {
|
|
|
170
185
|
input = null;
|
|
171
186
|
emitToken('eof', '', '', this._line, 0);
|
|
172
187
|
}
|
|
188
|
+
this._linePosition = currentLineLength;
|
|
173
189
|
return this._input = input;
|
|
174
190
|
}
|
|
175
191
|
|
|
@@ -181,6 +197,8 @@ class N3Lexer {
|
|
|
181
197
|
prefix = '',
|
|
182
198
|
match = null,
|
|
183
199
|
matchLength = 0,
|
|
200
|
+
lexicalLength = 0,
|
|
201
|
+
finalLineLength = 0,
|
|
184
202
|
inconclusive = false;
|
|
185
203
|
switch (firstChar) {
|
|
186
204
|
case '^':
|
|
@@ -207,12 +225,16 @@ class N3Lexer {
|
|
|
207
225
|
// Fall through in case the type is an IRI
|
|
208
226
|
case '<':
|
|
209
227
|
// Try to find a full IRI without escape sequences
|
|
210
|
-
if (match = this._unescapedIri.exec(input))
|
|
228
|
+
if (match = this._unescapedIri.exec(input)) {
|
|
229
|
+
type = 'IRI', value = match[1];
|
|
230
|
+
lexicalLength = match[1].length + 2;
|
|
231
|
+
}
|
|
211
232
|
// Try to find a full IRI with escape sequences
|
|
212
233
|
else if (match = this._iri.exec(input)) {
|
|
213
234
|
value = this._unescape(match[1], stringEscapeReplacements);
|
|
214
235
|
if (value === null || illegalIriChars.test(value)) return reportSyntaxError(this);
|
|
215
236
|
type = 'IRI';
|
|
237
|
+
lexicalLength = match[1].length + 2;
|
|
216
238
|
}
|
|
217
239
|
// Try to find a triple term
|
|
218
240
|
else if (input.length > 2 && input[1] === '<' && input[2] === '(') type = '<<(', matchLength = 3;
|
|
@@ -234,7 +256,10 @@ class N3Lexer {
|
|
|
234
256
|
// Try to find a blank node. Since it can contain (but not end with) a dot,
|
|
235
257
|
// we always need a non-dot character before deciding it is a blank node.
|
|
236
258
|
// Therefore, try inserting a space if we're at the end of the input.
|
|
237
|
-
if ((match = this._blank.exec(input)) || inputFinished && (match = this._blank.exec(`${input} `)))
|
|
259
|
+
if ((match = this._blank.exec(input)) || inputFinished && (match = this._blank.exec(`${input} `))) {
|
|
260
|
+
type = 'blank', prefix = '_', value = match[1];
|
|
261
|
+
lexicalLength = match[1].length + 2;
|
|
262
|
+
}
|
|
238
263
|
break;
|
|
239
264
|
case '"':
|
|
240
265
|
// Try to find a literal without escape sequences
|
|
@@ -243,7 +268,8 @@ class N3Lexer {
|
|
|
243
268
|
else {
|
|
244
269
|
({
|
|
245
270
|
value,
|
|
246
|
-
matchLength
|
|
271
|
+
matchLength,
|
|
272
|
+
finalLineLength
|
|
247
273
|
} = this._parseLiteral(input));
|
|
248
274
|
if (value === null) return reportSyntaxError(this);
|
|
249
275
|
}
|
|
@@ -260,7 +286,8 @@ class N3Lexer {
|
|
|
260
286
|
else {
|
|
261
287
|
({
|
|
262
288
|
value,
|
|
263
|
-
matchLength
|
|
289
|
+
matchLength,
|
|
290
|
+
finalLineLength
|
|
264
291
|
} = this._parseLiteral(input));
|
|
265
292
|
if (value === null) return reportSyntaxError(this);
|
|
266
293
|
}
|
|
@@ -337,11 +364,11 @@ class N3Lexer {
|
|
|
337
364
|
case 'f':
|
|
338
365
|
case 't':
|
|
339
366
|
// Try to match a boolean
|
|
340
|
-
if (
|
|
367
|
+
if (this._boolean.test(input)) type = 'literal', value = firstChar === 't' ? 'true' : 'false', prefix = xsd.boolean, matchLength = value.length;else inconclusive = true;
|
|
341
368
|
break;
|
|
342
369
|
case 'a':
|
|
343
370
|
// Try to find an abbreviated predicate
|
|
344
|
-
if (
|
|
371
|
+
if (this._shortPredicates.test(input)) type = 'abbreviation', value = 'a', matchLength = 1;else inconclusive = true;
|
|
345
372
|
break;
|
|
346
373
|
case 'h':
|
|
347
374
|
case 'o':
|
|
@@ -350,7 +377,7 @@ class N3Lexer {
|
|
|
350
377
|
break;
|
|
351
378
|
case 'i':
|
|
352
379
|
// Try to find an IRI property list identifier or N3 verb keyword
|
|
353
|
-
if (this._n3Mode &&
|
|
380
|
+
if (this._n3Mode && this._n3Id.test(input)) type = 'id', matchLength = 2;else if (this._n3Mode && (match = this._matchN3Verb(input, inputFinished))) type = match[0];else inconclusive = true;
|
|
354
381
|
break;
|
|
355
382
|
case '=':
|
|
356
383
|
// Try to find an implication arrow or equals sign
|
|
@@ -406,7 +433,11 @@ class N3Lexer {
|
|
|
406
433
|
// Try to find a prefixed name. Since it can contain (but not end with) a dot,
|
|
407
434
|
// we always need a non-dot character before deciding it is a prefixed name.
|
|
408
435
|
// Therefore, try inserting a space if we're at the end of the input.
|
|
409
|
-
else if ((match = this._prefixed.exec(input)) || inputFinished && (match = this._prefixed.exec(`${input} `)))
|
|
436
|
+
else if ((match = this._prefixed.exec(input)) || inputFinished && (match = this._prefixed.exec(`${input} `))) {
|
|
437
|
+
type = 'prefixed', prefix = match[1] || '';
|
|
438
|
+
value = this._unescape(match[2], localNameEscapeReplacements);
|
|
439
|
+
lexicalLength = prefix.length + match[2].length + 1;
|
|
440
|
+
}
|
|
410
441
|
}
|
|
411
442
|
|
|
412
443
|
// A type token is special: it can only be emitted after an IRI or prefixed name is read
|
|
@@ -428,19 +459,49 @@ class N3Lexer {
|
|
|
428
459
|
// We could be in streaming mode, and then we just wait for more input to arrive.
|
|
429
460
|
// Otherwise, a syntax error has occurred in the input.
|
|
430
461
|
// One exception: error on an unaccounted linebreak (= not inside a triple-quoted literal).
|
|
431
|
-
if (inputFinished || !/^'''|^"""/.test(input) && /\n|\r/.test(input)) return reportSyntaxError(this);else
|
|
462
|
+
if (inputFinished || !/^'''|^"""/.test(input) && /\n|\r/.test(input)) return reportSyntaxError(this);else {
|
|
463
|
+
this._linePosition = currentLineLength - input.length;
|
|
464
|
+
return this._input = input;
|
|
465
|
+
}
|
|
432
466
|
}
|
|
433
467
|
|
|
434
468
|
// Emit the parsed token
|
|
469
|
+
// Consumption includes separator whitespace; lexicalLength excludes it
|
|
470
|
+
// and any synthetic EOF space. slice below clamps consumption to the input.
|
|
435
471
|
const length = matchLength || match[0].length;
|
|
436
|
-
|
|
472
|
+
let token;
|
|
473
|
+
if (finalLineLength) {
|
|
474
|
+
token = {
|
|
475
|
+
type,
|
|
476
|
+
value,
|
|
477
|
+
prefix,
|
|
478
|
+
line,
|
|
479
|
+
start: currentLineLength - input.length,
|
|
480
|
+
end: finalLineLength,
|
|
481
|
+
endLine: this._line
|
|
482
|
+
};
|
|
483
|
+
callback(null, token);
|
|
484
|
+
} else token = emitToken(type, value, prefix, line, lexicalLength || length);
|
|
437
485
|
this.previousToken = token;
|
|
438
486
|
this._previousMarker = type;
|
|
439
487
|
|
|
440
488
|
// Advance to next part to tokenize
|
|
441
489
|
input = input.slice(length);
|
|
490
|
+
if (finalLineLength) currentLineLength = input.length + finalLineLength;
|
|
442
491
|
}
|
|
443
492
|
|
|
493
|
+
// Emits a comment at its exact position within matched whitespace.
|
|
494
|
+
function emitComment(value, line, offset) {
|
|
495
|
+
const start = currentLineLength - input.length + offset;
|
|
496
|
+
callback(null, {
|
|
497
|
+
type: 'comment',
|
|
498
|
+
value,
|
|
499
|
+
prefix: '',
|
|
500
|
+
line,
|
|
501
|
+
start,
|
|
502
|
+
end: start + value.length + 1
|
|
503
|
+
});
|
|
504
|
+
}
|
|
444
505
|
// Emits the token through the callback
|
|
445
506
|
function emitToken(type, value, prefix, line, length) {
|
|
446
507
|
const start = input ? currentLineLength - input.length : currentLineLength;
|
|
@@ -539,15 +600,18 @@ class N3Lexer {
|
|
|
539
600
|
// means these are actual, non-escaped closing quotes
|
|
540
601
|
if (backslashCount % 2 === 0) {
|
|
541
602
|
// Extract and unescape the value
|
|
542
|
-
const raw = input.substring(openingLength, closingPos)
|
|
543
|
-
|
|
603
|
+
const raw = input.substring(openingLength, closingPos),
|
|
604
|
+
lines = raw.split(/\r\n|\r|\n/),
|
|
605
|
+
lineCount = lines.length - 1;
|
|
544
606
|
const matchLength = closingPos + openingLength;
|
|
545
607
|
// Only triple-quoted strings can be multi-line
|
|
546
|
-
if (openingLength === 1 &&
|
|
547
|
-
this._line +=
|
|
608
|
+
if (openingLength === 1 && lineCount !== 0 || openingLength === 3 && this._lineMode) break;
|
|
609
|
+
this._line += lineCount;
|
|
610
|
+
const finalLineLength = lineCount === 0 ? 0 : lines[lines.length - 1].length + openingLength;
|
|
548
611
|
return {
|
|
549
612
|
value: this._unescape(raw, stringEscapeReplacements),
|
|
550
|
-
matchLength
|
|
613
|
+
matchLength,
|
|
614
|
+
finalLineLength
|
|
551
615
|
};
|
|
552
616
|
}
|
|
553
617
|
closingPos++;
|
|
@@ -556,7 +620,8 @@ class N3Lexer {
|
|
|
556
620
|
}
|
|
557
621
|
return {
|
|
558
622
|
value: '',
|
|
559
|
-
matchLength: 0
|
|
623
|
+
matchLength: 0,
|
|
624
|
+
finalLineLength: 0
|
|
560
625
|
};
|
|
561
626
|
}
|
|
562
627
|
|
|
@@ -574,18 +639,30 @@ class N3Lexer {
|
|
|
574
639
|
|
|
575
640
|
// ### Strips off any starting UTF BOM mark.
|
|
576
641
|
_readStartingBom(input) {
|
|
577
|
-
|
|
642
|
+
if (input.startsWith('\ufeff')) {
|
|
643
|
+
this._linePosition = 1;
|
|
644
|
+
return input.slice(1);
|
|
645
|
+
}
|
|
646
|
+
return input;
|
|
578
647
|
}
|
|
579
648
|
|
|
580
649
|
// ## Public methods
|
|
581
650
|
|
|
582
651
|
// ### `tokenize` starts the transformation of an N3 document into an array of tokens.
|
|
583
652
|
// The input can be a string or a stream.
|
|
653
|
+
// Token ranges use one-based lines and zero-based, end-exclusive UTF-16 columns.
|
|
654
|
+
// Separator whitespace counts towards the next token's start, outside either range.
|
|
655
|
+
// Multiline tokens also have endLine; their end column is relative to that line.
|
|
584
656
|
tokenize(input, callback) {
|
|
585
657
|
// Deferred tokenization and stream events can outlive their invocation.
|
|
586
658
|
// Ignore them once a later call takes ownership of the lexer state.
|
|
587
659
|
const tokenization = this._tokenization = {};
|
|
588
660
|
this._line = 1;
|
|
661
|
+
this._linePosition = 0;
|
|
662
|
+
this._previousMarker = undefined;
|
|
663
|
+
this.previousToken = undefined;
|
|
664
|
+
this._literalClosingPos = 0;
|
|
665
|
+
this._input = undefined;
|
|
589
666
|
|
|
590
667
|
// If the input is a string, continuously emit tokens through the callback until the end
|
|
591
668
|
if (typeof input === 'string') {
|
package/package.json
CHANGED
package/src/N3Lexer.js
CHANGED
|
@@ -57,7 +57,7 @@ export default class N3Lexer {
|
|
|
57
57
|
this._n3Verb = /^(?:has|is|of)(?=[\s#()\[\]\{\}"'<>?_+\-0-9])/;
|
|
58
58
|
this._n3Id = /^id(?=[\s#<])/;
|
|
59
59
|
this._shortPredicates = /^a(?=[\s#()\[\]\{\}"'<>])/;
|
|
60
|
-
this._commentLine = /^[ \t]*#([^\n\r]*)(?:\r\n|\n|\r)[ \t]
|
|
60
|
+
this._commentLine = /^[ \t]*#([^\n\r]*)(?:\r\n|\n|\r)([ \t]*)/;
|
|
61
61
|
this._whitespace = /^[ \t]+/;
|
|
62
62
|
options = options || {};
|
|
63
63
|
|
|
@@ -89,7 +89,7 @@ export default class N3Lexer {
|
|
|
89
89
|
_tokenizeToEnd(callback, inputFinished) {
|
|
90
90
|
// Continue parsing as far as possible; the loop will return eventually
|
|
91
91
|
let input = this._input;
|
|
92
|
-
let currentLineLength = input.length;
|
|
92
|
+
let currentLineLength = this._linePosition + input.length;
|
|
93
93
|
while (true) {
|
|
94
94
|
// Consume one separator line at a time, including its following indentation.
|
|
95
95
|
while (true) {
|
|
@@ -103,34 +103,49 @@ export default class N3Lexer {
|
|
|
103
103
|
if (charCode === HASH) {
|
|
104
104
|
const comment = this._commentLine.exec(input);
|
|
105
105
|
if (comment) {
|
|
106
|
+
const commentLength = comment[0].length;
|
|
107
|
+
// 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
|
+
}
|
|
106
113
|
if (this.comments)
|
|
107
|
-
|
|
108
|
-
input = input.slice(
|
|
109
|
-
currentLineLength = input.length;
|
|
114
|
+
emitComment(comment[1], this._line, separatorLength);
|
|
115
|
+
input = input.slice(commentLength);
|
|
116
|
+
currentLineLength = input.length + comment[2].length;
|
|
110
117
|
this._line++;
|
|
111
118
|
}
|
|
112
119
|
else {
|
|
113
120
|
// A comment without a line ending stays buffered until EOF.
|
|
114
121
|
input = input.slice(separatorLength);
|
|
115
|
-
if (!inputFinished)
|
|
122
|
+
if (!inputFinished) {
|
|
123
|
+
this._linePosition = currentLineLength - input.length;
|
|
116
124
|
return this._input = input;
|
|
125
|
+
}
|
|
117
126
|
if (this.comments)
|
|
118
|
-
|
|
127
|
+
emitComment(input.slice(1), this._line, 0);
|
|
119
128
|
input = '';
|
|
120
129
|
break;
|
|
121
130
|
}
|
|
122
131
|
}
|
|
123
132
|
else if (charCode === LF || charCode === CR) {
|
|
133
|
+
// 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
|
+
}
|
|
124
138
|
separatorLength += charCode === CR && input.charCodeAt(separatorLength + 1) === LF ? 2 : 1;
|
|
125
|
-
// Indentation is
|
|
139
|
+
// Indentation is consumed with the newline, but belongs to the next line's columns.
|
|
140
|
+
let indentationLength = 0;
|
|
126
141
|
const next = input.charCodeAt(separatorLength);
|
|
127
142
|
if (next === SPACE || next === TAB) {
|
|
128
143
|
const following = input.charCodeAt(separatorLength + 1);
|
|
129
|
-
|
|
144
|
+
indentationLength = following === SPACE || following === TAB ?
|
|
130
145
|
this._whitespace.exec(input.slice(separatorLength))[0].length : 1;
|
|
131
146
|
}
|
|
132
|
-
input = input.slice(separatorLength);
|
|
133
|
-
currentLineLength = input.length;
|
|
147
|
+
input = input.slice(separatorLength + indentationLength);
|
|
148
|
+
currentLineLength = input.length + indentationLength;
|
|
134
149
|
this._line++;
|
|
135
150
|
}
|
|
136
151
|
else {
|
|
@@ -144,13 +159,15 @@ export default class N3Lexer {
|
|
|
144
159
|
input = null;
|
|
145
160
|
emitToken('eof', '', '', this._line, 0);
|
|
146
161
|
}
|
|
162
|
+
this._linePosition = currentLineLength;
|
|
147
163
|
return this._input = input;
|
|
148
164
|
}
|
|
149
165
|
|
|
150
166
|
// Look for specific token types based on the first character
|
|
151
167
|
const line = this._line, firstChar = input[0];
|
|
152
168
|
let type = '', value = '', prefix = '',
|
|
153
|
-
match = null, matchLength = 0,
|
|
169
|
+
match = null, matchLength = 0, lexicalLength = 0,
|
|
170
|
+
finalLineLength = 0, inconclusive = false;
|
|
154
171
|
switch (firstChar) {
|
|
155
172
|
case '^':
|
|
156
173
|
// We need at least 3 tokens lookahead to distinguish ^^<IRI> and ^^pre:fixed
|
|
@@ -177,14 +194,17 @@ export default class N3Lexer {
|
|
|
177
194
|
// Fall through in case the type is an IRI
|
|
178
195
|
case '<':
|
|
179
196
|
// Try to find a full IRI without escape sequences
|
|
180
|
-
if (match = this._unescapedIri.exec(input))
|
|
197
|
+
if (match = this._unescapedIri.exec(input)) {
|
|
181
198
|
type = 'IRI', value = match[1];
|
|
199
|
+
lexicalLength = match[1].length + 2;
|
|
200
|
+
}
|
|
182
201
|
// Try to find a full IRI with escape sequences
|
|
183
202
|
else if (match = this._iri.exec(input)) {
|
|
184
203
|
value = this._unescape(match[1], stringEscapeReplacements);
|
|
185
204
|
if (value === null || illegalIriChars.test(value))
|
|
186
205
|
return reportSyntaxError(this);
|
|
187
206
|
type = 'IRI';
|
|
207
|
+
lexicalLength = match[1].length + 2;
|
|
188
208
|
}
|
|
189
209
|
// Try to find a triple term
|
|
190
210
|
else if (input.length > 2 && input[1] === '<' && input[2] === '(')
|
|
@@ -214,8 +234,10 @@ export default class N3Lexer {
|
|
|
214
234
|
// we always need a non-dot character before deciding it is a blank node.
|
|
215
235
|
// Therefore, try inserting a space if we're at the end of the input.
|
|
216
236
|
if ((match = this._blank.exec(input)) ||
|
|
217
|
-
inputFinished && (match = this._blank.exec(`${input} `)))
|
|
237
|
+
inputFinished && (match = this._blank.exec(`${input} `))) {
|
|
218
238
|
type = 'blank', prefix = '_', value = match[1];
|
|
239
|
+
lexicalLength = match[1].length + 2;
|
|
240
|
+
}
|
|
219
241
|
break;
|
|
220
242
|
|
|
221
243
|
case '"':
|
|
@@ -224,7 +246,7 @@ export default class N3Lexer {
|
|
|
224
246
|
value = match[1];
|
|
225
247
|
// Try to find a literal wrapped in three pairs of quotes
|
|
226
248
|
else {
|
|
227
|
-
({ value, matchLength } = this._parseLiteral(input));
|
|
249
|
+
({ value, matchLength, finalLineLength } = this._parseLiteral(input));
|
|
228
250
|
if (value === null)
|
|
229
251
|
return reportSyntaxError(this);
|
|
230
252
|
}
|
|
@@ -241,7 +263,7 @@ export default class N3Lexer {
|
|
|
241
263
|
value = match[1];
|
|
242
264
|
// Try to find a literal wrapped in three pairs of quotes
|
|
243
265
|
else {
|
|
244
|
-
({ value, matchLength } = this._parseLiteral(input));
|
|
266
|
+
({ value, matchLength, finalLineLength } = this._parseLiteral(input));
|
|
245
267
|
if (value === null)
|
|
246
268
|
return reportSyntaxError(this);
|
|
247
269
|
}
|
|
@@ -336,16 +358,16 @@ export default class N3Lexer {
|
|
|
336
358
|
case 'f':
|
|
337
359
|
case 't':
|
|
338
360
|
// Try to match a boolean
|
|
339
|
-
if (
|
|
340
|
-
type = 'literal', value =
|
|
361
|
+
if (this._boolean.test(input))
|
|
362
|
+
type = 'literal', value = firstChar === 't' ? 'true' : 'false', prefix = xsd.boolean, matchLength = value.length;
|
|
341
363
|
else
|
|
342
364
|
inconclusive = true;
|
|
343
365
|
break;
|
|
344
366
|
|
|
345
367
|
case 'a':
|
|
346
368
|
// Try to find an abbreviated predicate
|
|
347
|
-
if (
|
|
348
|
-
type = 'abbreviation', value = 'a';
|
|
369
|
+
if (this._shortPredicates.test(input))
|
|
370
|
+
type = 'abbreviation', value = 'a', matchLength = 1;
|
|
349
371
|
else
|
|
350
372
|
inconclusive = true;
|
|
351
373
|
break;
|
|
@@ -361,8 +383,8 @@ export default class N3Lexer {
|
|
|
361
383
|
|
|
362
384
|
case 'i':
|
|
363
385
|
// Try to find an IRI property list identifier or N3 verb keyword
|
|
364
|
-
if (this._n3Mode &&
|
|
365
|
-
type = 'id';
|
|
386
|
+
if (this._n3Mode && this._n3Id.test(input))
|
|
387
|
+
type = 'id', matchLength = 2;
|
|
366
388
|
else if (this._n3Mode && (match = this._matchN3Verb(input, inputFinished)))
|
|
367
389
|
type = match[0];
|
|
368
390
|
else
|
|
@@ -436,8 +458,11 @@ export default class N3Lexer {
|
|
|
436
458
|
// we always need a non-dot character before deciding it is a prefixed name.
|
|
437
459
|
// Therefore, try inserting a space if we're at the end of the input.
|
|
438
460
|
else if ((match = this._prefixed.exec(input)) ||
|
|
439
|
-
inputFinished && (match = this._prefixed.exec(`${input} `)))
|
|
440
|
-
type = 'prefixed', prefix = match[1] || ''
|
|
461
|
+
inputFinished && (match = this._prefixed.exec(`${input} `))) {
|
|
462
|
+
type = 'prefixed', prefix = match[1] || '';
|
|
463
|
+
value = this._unescape(match[2], localNameEscapeReplacements);
|
|
464
|
+
lexicalLength = prefix.length + match[2].length + 1;
|
|
465
|
+
}
|
|
441
466
|
}
|
|
442
467
|
|
|
443
468
|
// A type token is special: it can only be emitted after an IRI or prefixed name is read
|
|
@@ -456,20 +481,44 @@ export default class N3Lexer {
|
|
|
456
481
|
// One exception: error on an unaccounted linebreak (= not inside a triple-quoted literal).
|
|
457
482
|
if (inputFinished || (!/^'''|^"""/.test(input) && /\n|\r/.test(input)))
|
|
458
483
|
return reportSyntaxError(this);
|
|
459
|
-
else
|
|
484
|
+
else {
|
|
485
|
+
this._linePosition = currentLineLength - input.length;
|
|
460
486
|
return this._input = input;
|
|
487
|
+
}
|
|
461
488
|
}
|
|
462
489
|
|
|
463
490
|
// Emit the parsed token
|
|
491
|
+
// Consumption includes separator whitespace; lexicalLength excludes it
|
|
492
|
+
// and any synthetic EOF space. slice below clamps consumption to the input.
|
|
464
493
|
const length = matchLength || match[0].length;
|
|
465
|
-
|
|
494
|
+
let token;
|
|
495
|
+
if (finalLineLength) {
|
|
496
|
+
token = {
|
|
497
|
+
type, value, prefix, line,
|
|
498
|
+
start: currentLineLength - input.length,
|
|
499
|
+
end: finalLineLength, endLine: this._line,
|
|
500
|
+
};
|
|
501
|
+
callback(null, token);
|
|
502
|
+
}
|
|
503
|
+
else
|
|
504
|
+
token = emitToken(type, value, prefix, line, lexicalLength || length);
|
|
466
505
|
this.previousToken = token;
|
|
467
506
|
this._previousMarker = type;
|
|
468
507
|
|
|
469
508
|
// Advance to next part to tokenize
|
|
470
509
|
input = input.slice(length);
|
|
510
|
+
if (finalLineLength)
|
|
511
|
+
currentLineLength = input.length + finalLineLength;
|
|
471
512
|
}
|
|
472
513
|
|
|
514
|
+
// Emits a comment at its exact position within matched whitespace.
|
|
515
|
+
function emitComment(value, line, offset) {
|
|
516
|
+
const start = currentLineLength - input.length + offset;
|
|
517
|
+
callback(null, {
|
|
518
|
+
type: 'comment', value, prefix: '', line,
|
|
519
|
+
start, end: start + value.length + 1,
|
|
520
|
+
});
|
|
521
|
+
}
|
|
473
522
|
// Emits the token through the callback
|
|
474
523
|
function emitToken(type, value, prefix, line, length) {
|
|
475
524
|
const start = input ? currentLineLength - input.length : currentLineLength;
|
|
@@ -568,21 +617,23 @@ export default class N3Lexer {
|
|
|
568
617
|
// means these are actual, non-escaped closing quotes
|
|
569
618
|
if (backslashCount % 2 === 0) {
|
|
570
619
|
// Extract and unescape the value
|
|
571
|
-
const raw = input.substring(openingLength, closingPos)
|
|
572
|
-
|
|
620
|
+
const raw = input.substring(openingLength, closingPos),
|
|
621
|
+
lines = raw.split(/\r\n|\r|\n/),
|
|
622
|
+
lineCount = lines.length - 1;
|
|
573
623
|
const matchLength = closingPos + openingLength;
|
|
574
624
|
// Only triple-quoted strings can be multi-line
|
|
575
|
-
if (openingLength === 1 &&
|
|
625
|
+
if (openingLength === 1 && lineCount !== 0 ||
|
|
576
626
|
openingLength === 3 && this._lineMode)
|
|
577
627
|
break;
|
|
578
|
-
this._line +=
|
|
579
|
-
|
|
628
|
+
this._line += lineCount;
|
|
629
|
+
const finalLineLength = lineCount === 0 ? 0 : lines[lines.length - 1].length + openingLength;
|
|
630
|
+
return { value: this._unescape(raw, stringEscapeReplacements), matchLength, finalLineLength };
|
|
580
631
|
}
|
|
581
632
|
closingPos++;
|
|
582
633
|
}
|
|
583
634
|
this._literalClosingPos = input.length - openingLength + 1;
|
|
584
635
|
}
|
|
585
|
-
return { value: '', matchLength: 0 };
|
|
636
|
+
return { value: '', matchLength: 0, finalLineLength: 0 };
|
|
586
637
|
}
|
|
587
638
|
|
|
588
639
|
// ### `_syntaxError` creates a syntax error for the given issue
|
|
@@ -599,18 +650,30 @@ export default class N3Lexer {
|
|
|
599
650
|
|
|
600
651
|
// ### Strips off any starting UTF BOM mark.
|
|
601
652
|
_readStartingBom(input) {
|
|
602
|
-
|
|
653
|
+
if (input.startsWith('\ufeff')) {
|
|
654
|
+
this._linePosition = 1;
|
|
655
|
+
return input.slice(1);
|
|
656
|
+
}
|
|
657
|
+
return input;
|
|
603
658
|
}
|
|
604
659
|
|
|
605
660
|
// ## Public methods
|
|
606
661
|
|
|
607
662
|
// ### `tokenize` starts the transformation of an N3 document into an array of tokens.
|
|
608
663
|
// The input can be a string or a stream.
|
|
664
|
+
// Token ranges use one-based lines and zero-based, end-exclusive UTF-16 columns.
|
|
665
|
+
// Separator whitespace counts towards the next token's start, outside either range.
|
|
666
|
+
// Multiline tokens also have endLine; their end column is relative to that line.
|
|
609
667
|
tokenize(input, callback) {
|
|
610
668
|
// Deferred tokenization and stream events can outlive their invocation.
|
|
611
669
|
// Ignore them once a later call takes ownership of the lexer state.
|
|
612
670
|
const tokenization = this._tokenization = {};
|
|
613
671
|
this._line = 1;
|
|
672
|
+
this._linePosition = 0;
|
|
673
|
+
this._previousMarker = undefined;
|
|
674
|
+
this.previousToken = undefined;
|
|
675
|
+
this._literalClosingPos = 0;
|
|
676
|
+
this._input = undefined;
|
|
614
677
|
|
|
615
678
|
// If the input is a string, continuously emit tokens through the callback until the end
|
|
616
679
|
if (typeof input === 'string') {
|