n3 2.7.11 → 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/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
- if (this.comments) emitToken('comment', comment[1], '', this._line, comment[0].length);
141
- input = input.slice(comment[0].length);
142
- currentLineLength = input.length;
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) return this._input = input;
148
- if (this.comments) emitToken('comment', input.slice(1), '', this._line, input.length);
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 part of the same separator match as the newline.
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
- separatorLength += following === SPACE || following === TAB ? this._whitespace.exec(input.slice(separatorLength))[0].length : 1;
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)) type = 'IRI', value = match[1];
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} `))) type = 'blank', prefix = '_', value = match[1];
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
  }
@@ -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} `))) type = 'prefixed', prefix = match[1] || '', value = this._unescape(match[2], localNameEscapeReplacements);
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 return this._input = input;
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
- const token = emitToken(type, value, prefix, line, length);
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
- const lines = raw.split(/\r\n|\r|\n/).length - 1;
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 && lines !== 0 || openingLength === 3 && this._lineMode) break;
547
- this._line += lines;
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
- return input.startsWith('\ufeff') ? input.slice(1) : input;
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "n3",
3
- "version": "2.7.11",
3
+ "version": "2.7.12",
4
4
  "description": "Lightning fast, asynchronous, streaming Turtle / N3 / RDF library.",
5
5
  "author": "Ruben Verborgh <ruben.verborgh@gmail.com>",
6
6
  "keywords": [
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
- emitToken('comment', comment[1], '', this._line, comment[0].length);
108
- input = input.slice(comment[0].length);
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
- emitToken('comment', input.slice(1), '', this._line, input.length);
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 part of the same separator match as the newline.
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
- separatorLength += following === SPACE || following === TAB ?
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, inconclusive = false;
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
  }
@@ -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] || '', value = this._unescape(match[2], localNameEscapeReplacements);
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
- const token = emitToken(type, value, prefix, line, length);
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
- const lines = raw.split(/\r\n|\r|\n/).length - 1;
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 && lines !== 0 ||
625
+ if (openingLength === 1 && lineCount !== 0 ||
576
626
  openingLength === 3 && this._lineMode)
577
627
  break;
578
- this._line += lines;
579
- return { value: this._unescape(raw, stringEscapeReplacements), matchLength };
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
- return input.startsWith('\ufeff') ? input.slice(1) : input;
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') {