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/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') {
package/src/N3Reasoner.js CHANGED
@@ -23,10 +23,18 @@ export default class N3Reasoner {
23
23
  }
24
24
 
25
25
  _add(subject, predicate, object, graphItem, c, cb) {
26
- // Only add to the remaining indexes if there is not already a value in the index
27
- if (!this._store._addToIndex(graphItem.subjects, subject, predicate, object)) return;
28
- this._store._addToIndex(graphItem.predicates, predicate, object, subject);
29
- this._store._addToIndex(graphItem.objects, object, subject, predicate);
26
+ const store = this._store;
27
+ // Observing views must see each derivation before indexing
28
+ // (stores from other N3 versions have no observers)
29
+ if (store._observers) {
30
+ if (!store._addQuad(subject, predicate, object, this._graphId)) return;
31
+ }
32
+ else {
33
+ // Only add to the remaining indexes if there is not already a value in the index
34
+ if (!store._addToIndex(graphItem.subjects, subject, predicate, object)) return;
35
+ store._addToIndex(graphItem.predicates, predicate, object, subject);
36
+ store._addToIndex(graphItem.objects, object, subject, predicate);
37
+ }
30
38
  // Count genuinely new derivations and fail past the budget. The check comes
31
39
  // after all three indexes are updated, so a caught error leaves the store
32
40
  // in a consistent state (the reasoning result is merely incomplete).
@@ -228,6 +236,8 @@ export default class N3Reasoner {
228
236
  const graphs = this._store._getGraphs();
229
237
  try {
230
238
  for (const graphId in graphs) {
239
+ // Observers compare numeric graph identifiers
240
+ this._graphId = Number(graphId);
231
241
  this._reasonGraphNaive(rules, graphs[graphId]);
232
242
  }
233
243
  }