n3 2.7.11 → 2.8.0

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/lib/N3Reasoner.js CHANGED
@@ -34,10 +34,17 @@ class N3Reasoner {
34
34
  this._maxPremiseDepth = options.maxPremiseDepth === undefined ? Infinity : options.maxPremiseDepth;
35
35
  }
36
36
  _add(subject, predicate, object, graphItem, c, cb) {
37
- // Only add to the remaining indexes if there is not already a value in the index
38
- if (!this._store._addToIndex(graphItem.subjects, subject, predicate, object)) return;
39
- this._store._addToIndex(graphItem.predicates, predicate, object, subject);
40
- this._store._addToIndex(graphItem.objects, object, subject, predicate);
37
+ const store = this._store;
38
+ // Observing views must see each derivation before indexing
39
+ // (stores from other N3 versions have no observers)
40
+ if (store._observers) {
41
+ if (!store._addQuad(subject, predicate, object, this._graphId)) return;
42
+ } else {
43
+ // Only add to the remaining indexes if there is not already a value in the index
44
+ if (!store._addToIndex(graphItem.subjects, subject, predicate, object)) return;
45
+ store._addToIndex(graphItem.predicates, predicate, object, subject);
46
+ store._addToIndex(graphItem.objects, object, subject, predicate);
47
+ }
41
48
  // Count genuinely new derivations and fail past the budget. The check comes
42
49
  // after all three indexes are updated, so a caught error leaves the store
43
50
  // in a consistent state (the reasoning result is merely incomplete).
@@ -224,6 +231,8 @@ class N3Reasoner {
224
231
  const graphs = this._store._getGraphs();
225
232
  try {
226
233
  for (const graphId in graphs) {
234
+ // Observers compare numeric graph identifiers
235
+ this._graphId = Number(graphId);
227
236
  this._reasonGraphNaive(rules, graphs[graphId]);
228
237
  }
229
238
  } finally {