n3 2.13.4 → 2.13.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/lib/N3Lexer.js CHANGED
@@ -15,7 +15,33 @@ const SPACE = 0x20,
15
15
  TAB = 0x09,
16
16
  LF = 0x0A,
17
17
  CR = 0x0D,
18
- HASH = 0x23;
18
+ HASH = 0x23,
19
+ DOT = 0x2E,
20
+ COLON = 0x3A,
21
+ ZERO = 0x30,
22
+ NINE = 0x39,
23
+ PERCENT = 0x25,
24
+ BACKSLASH = 0x5C;
25
+
26
+ // Whitespace as matched by `\s`
27
+ function isWhitespace(charCode) {
28
+ return charCode === SPACE || charCode >= TAB && charCode <= CR || charCode >= 0xA0 && (charCode === 0xA0 || charCode === 0x1680 || charCode >= 0x2000 && charCode <= 0x200A || charCode === 0x2028 || charCode === 0x2029 || charCode === 0x202F || charCode === 0x205F || charCode === 0x3000 || charCode === 0xFEFF);
29
+ }
30
+ // Characters that can directly follow a name: whitespace and punctuation
31
+ // (the lookahead `[,;!\^\s#()\[\]\{\}"'<>]` of the former regular expressions)
32
+ const asciiDelimiters = new Uint8Array(0x80);
33
+ for (const char of ',;!^#()[]{}"\'<> \t\n\v\f\r') asciiDelimiters[char.charCodeAt(0)] = 1;
34
+ function isDelimiter(charCode) {
35
+ return charCode < 0x80 ? asciiDelimiters[charCode] === 1 : isWhitespace(charCode);
36
+ }
37
+ // Whether a name can end before the given position: it must be followed by
38
+ // a delimiter, optionally after a dot. At the end of finished input, it can always end.
39
+ function canEndName(input, pos, inputFinished) {
40
+ let charCode = input.charCodeAt(pos);
41
+ if (charCode === DOT) charCode = input.charCodeAt(++pos);
42
+ if (pos >= input.length) return inputFinished;
43
+ return isDelimiter(charCode);
44
+ }
19
45
 
20
46
  // Fixed escape sequences allowed in string literals (ECHAR)
21
47
  const stringEscapeReplacements = {
@@ -52,9 +78,87 @@ const localNameEscapeReplacements = {
52
78
  '%': '%'
53
79
  };
54
80
  const illegalIriChars = /[\x00-\x20<>\\"\{\}\|\^\`]/;
55
- // Characters that cannot occur in a prefixed name, not even escaped
56
- // (global, so that testAt searches the rest of the input from a position)
57
- const nonPrefixedNameChar = /[\s<>"{}|^`]/g;
81
+
82
+ // Character classes of names, as bit flags for ASCII characters
83
+ const PREFIX_START = 1,
84
+ // PN_CHARS_BASE
85
+ LOCAL_START = 2,
86
+ // PN_CHARS_U, digits, and colon
87
+ NAME_CHAR = 4,
88
+ // PN_CHARS
89
+ LOCAL_CHAR = 8,
90
+ // PN_CHARS and colon
91
+ LOCAL_ESCAPE = 16; // characters that can be escaped in local names (PN_LOCAL_ESC)
92
+ const asciiNameClasses = new Uint8Array(0x80);
93
+ for (let charCode = 0; charCode < 0x80; charCode++) {
94
+ const char = String.fromCharCode(charCode);
95
+ const letter = char >= 'A' && char <= 'Z' || char >= 'a' && char <= 'z';
96
+ const nameChar = letter || char === '_' || char === '-' || char >= '0' && char <= '9';
97
+ asciiNameClasses[charCode] = (letter ? PREFIX_START : 0) | (nameChar && char !== '-' || char === ':' ? LOCAL_START : 0) | (nameChar ? NAME_CHAR : 0) | (nameChar || char === ':' ? LOCAL_CHAR : 0) | (char in localNameEscapeReplacements ? LOCAL_ESCAPE : 0);
98
+ }
99
+ // Returns the length (0, 1, or 2 code units) of the character at the given position
100
+ // if it is in the given name character class
101
+ function nameCharLength(input, pos, charClass) {
102
+ const charCode = input.charCodeAt(pos);
103
+ if (charCode < 0x80) return (asciiNameClasses[charCode] & charClass) !== 0 ? 1 : 0;
104
+ // Characters from U+10000 to U+EFFFF consist of a surrogate pair
105
+ if (charCode >= 0xD800 && charCode <= 0xDB7F) {
106
+ const low = input.charCodeAt(pos + 1);
107
+ return low >= 0xDC00 && low <= 0xDFFF ? 2 : 0;
108
+ }
109
+ // PN_CHARS has some characters that PN_CHARS_BASE does not
110
+ if (charClass >= NAME_CHAR && (charCode === 0xB7 || charCode >= 0x300 && charCode <= 0x36F || charCode === 0x203F || charCode === 0x2040)) return 1;
111
+ // PN_CHARS_BASE
112
+ return charCode >= 0xC0 && charCode <= 0x1FFF && charCode !== 0xD7 && charCode !== 0xF7 && (charCode < 0x300 || charCode >= 0x370 && charCode !== 0x37E) || charCode >= 0x200C && charCode <= 0x200D || charCode >= 0x2070 && charCode <= 0x218F || charCode >= 0x2C00 && charCode <= 0x2FEF || charCode >= 0x3001 && charCode <= 0xD7FF || charCode >= 0xF900 && charCode <= 0xFDCF || charCode >= 0xFDF0 && charCode <= 0xFFFD ? 1 : 0;
113
+ }
114
+ function isLocalEscape(charCode) {
115
+ return charCode < 0x80 && (asciiNameClasses[charCode] & LOCAL_ESCAPE) !== 0;
116
+ }
117
+ function isHexDigit(charCode) {
118
+ return charCode >= ZERO && charCode <= NINE || charCode >= 0x41 && charCode <= 0x46 || charCode >= 0x61 && charCode <= 0x66;
119
+ }
120
+ // Returns the end of the prefix (PN_PREFIX) at the given position,
121
+ // which can contain single dots, but not start or end with one
122
+ function skipPrefix(input, pos) {
123
+ let length = nameCharLength(input, pos, PREFIX_START);
124
+ while (length !== 0) {
125
+ pos += length;
126
+ const next = input.charCodeAt(pos) === DOT ? pos + 1 : pos;
127
+ // Most names are ASCII, so look those up without a call
128
+ const charCode = input.charCodeAt(next);
129
+ length = charCode < 0x80 ? (asciiNameClasses[charCode] & NAME_CHAR) !== 0 ? 1 : 0 : nameCharLength(input, next, NAME_CHAR);
130
+ if (length !== 0) pos = next;
131
+ }
132
+ return pos;
133
+ }
134
+ // Returns the end of the local name (PN_LOCAL) at the given position,
135
+ // which can contain dots, but not start or end with one
136
+ function skipLocalName(input, pos) {
137
+ let end = pos,
138
+ charClass = LOCAL_START;
139
+ while (true) {
140
+ // Most names are ASCII, so look those up without a call
141
+ const charCode = input.charCodeAt(pos);
142
+ let length = charCode < 0x80 ? (asciiNameClasses[charCode] & charClass) !== 0 ? 1 : 0 : nameCharLength(input, pos, charClass);
143
+ // Percent-encoded character (PERCENT)
144
+ if (length === 0 && charCode === PERCENT && isHexDigit(input.charCodeAt(pos + 1)) && isHexDigit(input.charCodeAt(pos + 2))) length = 3;
145
+ // Escaped character (PN_LOCAL_ESC)
146
+ else if (length === 0 && charCode === BACKSLASH && isLocalEscape(input.charCodeAt(pos + 1))) length = 2;
147
+ if (length !== 0) {
148
+ end = pos += length;
149
+ charClass = LOCAL_CHAR;
150
+ }
151
+ // Dots are allowed after the first character, but not at the end
152
+ else if (charCode === DOT && charClass === LOCAL_CHAR) pos++;else return end;
153
+ }
154
+ }
155
+ // Returns the end of the prefixed name at the given position, or -1 if there is none
156
+ function skipPrefixedName(input, pos, inputFinished) {
157
+ const colon = skipPrefix(input, pos);
158
+ if (input.charCodeAt(colon) !== COLON) return -1;
159
+ const end = skipLocalName(input, colon + 1);
160
+ return canEndName(input, end, inputFinished) ? end : -1;
161
+ }
58
162
 
59
163
  // A valid code point is a Unicode scalar value: at most U+10FFFF and not a surrogate
60
164
  function isValidCodePoint(charCode) {
@@ -110,8 +214,6 @@ class N3Lexer {
110
214
  this._simpleQuotedString = /"([^"\\\r\n]*)"(?=[^"])/y; // string without escape sequences
111
215
  this._simpleApostropheString = /'([^'\\\r\n]*)'(?=[^'])/y;
112
216
  this._langcode = /@([a-z]+(?:-[a-z0-9]+)*)(?=[^a-z0-9])/iy;
113
- 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;
114
- 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;
115
217
  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;
116
218
  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;
117
219
  this._number = /[\-+]?(?:(\d+\.\d*|\.?\d+)[eE][\-+]?\d+|(?=\.?\d)\d*(?:(\.)\d+)?)(?=\.?[,;:!\^\s#()\[\]\{\}"'<>])/y;
@@ -476,17 +578,22 @@ class N3Lexer {
476
578
  // Some first characters do not allow an immediate decision, so inspect more
477
579
  if (inconclusive) {
478
580
  // Try to find a prefix
479
- if ((this._previousMarker === '@prefix' || this._previousMarker === 'PREFIX') && (match = execAt(this._prefix, input, pos))) type = 'prefix', value = match[1] || '';
581
+ let end;
582
+ if ((this._previousMarker === '@prefix' || this._previousMarker === 'PREFIX') && input.charCodeAt(end = skipPrefix(input, pos)) === COLON && end + 1 < input.length && (input[end + 1] === '#' || input[end + 1] === '<' || isWhitespace(input.charCodeAt(end + 1)))) {
583
+ type = 'prefix', value = input.slice(pos, end);
584
+ matchLength = end + 1 - pos;
585
+ }
480
586
  // Try to find an additional directive keyword
481
587
  // (at the end of the input, only a short final word can be one)
482
588
  else if (this._directive !== null && this._directiveStarts.includes(firstChar) && ((match = execAt(this._directive, input, pos)) || inputFinished && input.length - pos <= this._directiveMaxLength && (match = execAtEnd(this._directive, input, pos)))) type = match[0].toUpperCase();
483
589
  // Try to find a prefixed name. Since it can contain (but not end with) a dot,
484
- // we always need a non-dot character before deciding it is a prefixed name.
485
- // Therefore, try inserting a space if we're at the end of the input.
486
- else if ((match = execAt(this._prefixed, input, pos)) || inputFinished && (match = execAtEnd(this._prefixed, input, pos))) {
487
- type = 'prefixed', prefix = match[1] || '';
488
- value = this._unescape(match[2], localNameEscapeReplacements);
489
- lexicalLength = prefix.length + match[2].length + 1;
590
+ // we always need a non-dot character before deciding it is a prefixed name,
591
+ // except at the end of the input.
592
+ else if (!this._lineMode && (end = skipPrefixedName(input, pos, inputFinished)) >= 0) {
593
+ const colon = input.indexOf(':', pos);
594
+ type = 'prefixed', prefix = input.slice(pos, colon);
595
+ value = this._unescape(input.slice(colon + 1, end), localNameEscapeReplacements);
596
+ matchLength = end - pos;
490
597
  }
491
598
  }
492
599
 
@@ -595,25 +702,17 @@ class N3Lexer {
595
702
  if (!verb) return null;
596
703
 
597
704
  // Most verb boundaries cannot be part of a prefix, so keep the common path fast.
705
+ // U+1680 and U+FEFF are whitespace to the regular expression but name characters.
598
706
  const next = input[pos + verb[0].length];
599
- if (next !== '-' && next !== '_' && (next < '0' || next > '9')) return verb;
707
+ if (next !== '-' && next !== '_' && (next < '0' || next > '9') && next !== '\u1680' && next !== '\ufeff') return verb;
600
708
 
601
709
  // A prefix can start with a verb and continue with characters that are also
602
710
  // valid verb boundaries. Prefer the longer prefixed name when it is complete.
603
- if (execAt(this._prefixed, input, pos)) return null;
604
- // Appending to the input only matters when a prefixed name could run up to
605
- // its end, which a character that cannot occur in prefixed names rules out.
606
- // This avoids copying the rest of the document for every such verb.
607
- if (testAt(nonPrefixedNameChar, input, pos)) return verb;
608
- if (execAtEnd(this._prefixed, input, pos)) return null;
609
-
610
- // If a stream chunk ends partway through such a prefix, wait for the colon
611
- // instead of prematurely emitting the verb. Appending ": " lets the prefix
612
- // grammar determine whether all input seen so far can be a complete prefix.
613
- if (!inputFinished) {
614
- const prefix = execAt(this._prefix, `${input.slice(pos)}: `, 0);
615
- if (prefix) return null;
616
- }
711
+ if (skipPrefixedName(input, pos, true) >= 0) return null;
712
+
713
+ // If a stream chunk ends partway through such a prefix,
714
+ // wait for the colon instead of prematurely emitting the verb.
715
+ if (!inputFinished && skipPrefix(input, pos) === input.length) return null;
617
716
  return verb;
618
717
  }
619
718
 
package/lib/N3Reasoner.js CHANGED
@@ -44,6 +44,7 @@ class N3Reasoner {
44
44
  if (!store._addToIndex(graphItem.subjects, subject, predicate, object)) return;
45
45
  store._addToIndex(graphItem.predicates, predicate, object, subject);
46
46
  store._addToIndex(graphItem.objects, object, subject, predicate);
47
+ this._indexed++;
47
48
  }
48
49
  // Count genuinely new derivations and fail past the budget. The check comes
49
50
  // after all three indexes are updated, so a caught error leaves the store
@@ -176,7 +177,7 @@ class N3Reasoner {
176
177
  };
177
178
  }
178
179
  reason(rules) {
179
- this._derivations = 0;
180
+ this._derivations = this._indexed = 0;
180
181
  if (!Array.isArray(rules)) {
181
182
  rules = getRulesFromDataset(rules);
182
183
  }
@@ -236,9 +237,9 @@ class N3Reasoner {
236
237
  this._reasonGraphNaive(rules, graphs[graphId]);
237
238
  }
238
239
  } finally {
239
- // Invalidate the cached size even if a derivation budget was exceeded,
240
- // so a caught budget error leaves the store fully consistent.
241
- this._store._size = null;
240
+ // Count the quads added directly to the indexes, even if a derivation
241
+ // budget was exceeded, so a caught budget error leaves the store consistent
242
+ if (this._store._size !== null) this._store._size += this._indexed;
242
243
  }
243
244
  }
244
245
  }
package/lib/N3Store.js CHANGED
@@ -31,90 +31,6 @@ function hasInIndex(index0, key0, key1, key2) {
31
31
  const index2 = index1 && index1[key1];
32
32
  return !!index2 && key2 in index2;
33
33
  }
34
- function merge(target, source, depth = 4) {
35
- let size = target[SIZE] || 0;
36
- for (const key in source) {
37
- if (!(key in target)) {
38
- size++;
39
- target[key] = depth === 0 ? null : merge(Object.create(null), source[key], depth - 1);
40
- }
41
- // Merge into the existing object in place,
42
- // as graph objects are frozen and cannot be reassigned
43
- else if (depth !== 0) merge(target[key], source[key], depth - 1);
44
- }
45
- // Depth 2 is the level of the `subjects`, `predicates`, and `objects` indexes.
46
- if (depth <= 2) target[SIZE] = size;
47
- return target;
48
- }
49
-
50
- /**
51
- * Determines the intersection of the `_graphs` index s1 and s2.
52
- * s1 and s2 *must* belong to Stores that share an `_entityIndex`.
53
- *
54
- * False is returned when there is no intersection; this should
55
- * *not* be set as the value for an index.
56
- */
57
- function intersect(s1, s2, depth = 4) {
58
- let target = false,
59
- size = 0;
60
- if (depth <= 2 && s2[SIZE] < s1[SIZE]) [s1, s2] = [s2, s1];
61
- for (const key in s1) {
62
- if (key in s2) {
63
- const intersection = depth === 0 ? null : intersect(s1[key], s2[key], depth - 1);
64
- if (intersection !== false) {
65
- target = target || Object.create(null);
66
- target[key] = intersection;
67
- size++;
68
- }
69
- // Depth 3 is the 'subjects', 'predicates' and 'objects' keys.
70
- // If the 'subjects' index is empty, so will the 'predicates' and 'objects' index.
71
- else if (depth === 3) {
72
- return false;
73
- }
74
- }
75
- }
76
-
77
- // Depth 2 is the level of the `subjects`, `predicates`, and `objects` indexes.
78
- if (depth <= 2 && target) target[SIZE] = size;
79
- return target;
80
- }
81
-
82
- /**
83
- * Determines the difference of the `_graphs` index s1 and s2.
84
- * s1 and s2 *must* belong to Stores that share an `_entityIndex`.
85
- *
86
- * False is returned when there is no difference; this should
87
- * *not* be set as the value for an index.
88
- */
89
- function difference(s1, s2, depth = 4) {
90
- let target = false,
91
- size = 0;
92
- for (const key in s1) {
93
- // When the key is not in the index, then none of the triples defined by s1[key] are
94
- // in s2 and so we want to copy them over to the resultant store.
95
- if (!(key in s2)) {
96
- target = target || Object.create(null);
97
- target[key] = depth === 0 ? null : merge({}, s1[key], depth - 1);
98
- size++;
99
- } else if (depth !== 0) {
100
- const diff = difference(s1[key], s2[key], depth - 1);
101
- if (diff !== false) {
102
- target = target || Object.create(null);
103
- target[key] = diff;
104
- size++;
105
- }
106
- // Depth 3 is the 'subjects', 'predicates' and 'objects' keys.
107
- // If the 'subjects' index is empty, so will the 'predicates' and 'objects' index.
108
- else if (depth === 3) {
109
- return false;
110
- }
111
- }
112
- }
113
-
114
- // Depth 2 is the level of the `subjects`, `predicates`, and `objects` indexes.
115
- if (depth <= 2 && target) target[SIZE] = size;
116
- return target;
117
- }
118
34
 
119
35
  // Returns the key of a term in the entity index.
120
36
  // Keys mark the term type by their first character, so the IRI of a named node
@@ -216,14 +132,21 @@ class N3EntityIndex {
216
132
  }
217
133
  }
218
134
 
219
- // ## Constructor
135
+ // Returns the size of a store from this copy of N3 if it is known without counting,
136
+ // or null otherwise
220
137
  exports.N3EntityIndex = N3EntityIndex;
138
+ function knownSize(dataset) {
139
+ return dataset instanceof N3Store ? dataset._size : null;
140
+ }
141
+
142
+ // ## Constructor
221
143
  class N3Store {
222
144
  constructor(quads, options) {
223
145
  // The number of quads is initially zero
224
146
  this._size = 0;
225
147
  // `_graphs` contains subject, predicate, and object indexes per graph
226
148
  this._graphs = Object.create(null);
149
+ this._graphCount = 0;
227
150
  // `_observers` contains weak references to views notified before every mutation
228
151
  this._observers = null;
229
152
 
@@ -248,16 +171,10 @@ class N3Store {
248
171
 
249
172
  // ### `size` returns the number of quads in the store
250
173
  get size() {
251
- // Return the quad count if if was cached
252
- let size = this._size;
253
- if (size !== null) return size;
254
-
255
- // Calculate the number of quads by counting to the deepest level
256
- size = 0;
257
- const graphs = this._graphs;
258
- let subjects, subject;
259
- for (const graphKey in graphs) for (const subjectKey in subjects = graphs[graphKey].subjects) for (const predicateKey in subject = subjects[subjectKey]) size += subject[predicateKey][SIZE];
260
- return this._size = size;
174
+ // The quad count is kept up to date by every change,
175
+ // except by reasoners from earlier versions of N3, which set it to null
176
+ if (this._size === null) this._size = this.countQuads();
177
+ return this._size;
261
178
  }
262
179
 
263
180
  // ## Private methods
@@ -524,6 +441,7 @@ class N3Store {
524
441
  // Freezing a graph helps subsequent `add` performance,
525
442
  // and properties will never be modified anyway
526
443
  Object.freeze(graphItem);
444
+ this._graphCount++;
527
445
  }
528
446
 
529
447
  // Notify observers before inserting a new quad so snapshots retain their prior contents
@@ -534,12 +452,68 @@ class N3Store {
534
452
  if (!this._addToIndex(graphItem.subjects, subject, predicate, object)) return false;
535
453
  this._addToIndex(graphItem.predicates, predicate, object, subject);
536
454
  this._addToIndex(graphItem.objects, object, subject, predicate);
537
-
538
- // The cached quad count is now invalid
539
- this._size = null;
455
+ if (this._size !== null) this._size++;
540
456
  return true;
541
457
  }
542
458
 
459
+ // ### `_addFromIndex` adds the quads in the given graph indexes of a store
460
+ // that shares this store's entity index, so no terms need to be converted.
461
+ // With `otherGraphs`, it only adds the quads that are not in those indexes.
462
+ // Index keys are strings, so the ids are converted back to the numbers that
463
+ // `addQuad` uses: forwarded views compare the ids they are notified with to
464
+ // their own numeric ids.
465
+ _addFromIndex(graphs, otherGraphs = null) {
466
+ for (const graphKey in graphs) {
467
+ const subjects = graphs[graphKey].subjects,
468
+ other = otherGraphs && otherGraphs[graphKey];
469
+ const otherSubjects = other ? other.subjects : null;
470
+ for (const subjectKey in subjects) {
471
+ const subject = Number(subjectKey),
472
+ predicates = subjects[subjectKey];
473
+ const otherPredicates = otherSubjects && otherSubjects[subjectKey];
474
+ for (const predicateKey in predicates) {
475
+ const predicate = Number(predicateKey),
476
+ objects = predicates[predicateKey];
477
+ const otherObjects = otherPredicates && otherPredicates[predicateKey];
478
+ for (const objectKey in objects) {
479
+ if (!otherObjects || !(objectKey in otherObjects)) this._addQuad(subject, predicate, Number(objectKey), Number(graphKey));
480
+ }
481
+ }
482
+ }
483
+ }
484
+ }
485
+
486
+ // ### `_addIntersectionFromIndex` adds the quads that are in both of the
487
+ // given graph indexes, walking the graphs of the first and, below them,
488
+ // whichever index has fewer keys
489
+ _addIntersectionFromIndex(graphs, otherGraphs) {
490
+ for (const graphKey in graphs) {
491
+ const other = otherGraphs[graphKey];
492
+ if (other) {
493
+ let subjects = graphs[graphKey].subjects,
494
+ otherSubjects = other.subjects;
495
+ if (otherSubjects[SIZE] < subjects[SIZE]) [subjects, otherSubjects] = [otherSubjects, subjects];
496
+ for (const subjectKey in subjects) {
497
+ let predicates = subjects[subjectKey],
498
+ otherPredicates = otherSubjects[subjectKey];
499
+ if (otherPredicates) {
500
+ if (otherPredicates[SIZE] < predicates[SIZE]) [predicates, otherPredicates] = [otherPredicates, predicates];
501
+ for (const predicateKey in predicates) {
502
+ let objects = predicates[predicateKey],
503
+ otherObjects = otherPredicates[predicateKey];
504
+ if (otherObjects) {
505
+ if (otherObjects[SIZE] < objects[SIZE]) [objects, otherObjects] = [otherObjects, objects];
506
+ for (const objectKey in objects) {
507
+ if (objectKey in otherObjects) this._addQuad(Number(subjectKey), Number(predicateKey), Number(objectKey), Number(graphKey));
508
+ }
509
+ }
510
+ }
511
+ }
512
+ }
513
+ }
514
+ }
515
+ }
516
+
543
517
  // ### `addQuads` adds multiple quads to the store
544
518
  addQuads(quads) {
545
519
  for (let i = 0; i < quads.length; i++) this.addQuad(quads[i]);
@@ -612,7 +586,10 @@ class N3Store {
612
586
  if (this._size !== null) this._size--;
613
587
 
614
588
  // Remove the graph if it is empty
615
- if (graphItem.subjects[SIZE] === 0) delete graphs[graph];
589
+ if (graphItem.subjects[SIZE] === 0) {
590
+ delete graphs[graph];
591
+ this._graphCount--;
592
+ }
616
593
  return true;
617
594
  }
618
595
 
@@ -734,9 +711,12 @@ class N3Store {
734
711
  } else if (predicate) {
735
712
  // If only predicate and possibly object are given, the predicate index will be the fastest
736
713
  count += this._countInIndex(content.predicates, predicateId, objectId, subjectId);
737
- } else {
738
- // If only object is possibly given, the object index will be the fastest
714
+ } else if (object) {
715
+ // If only object is given, the object index will be the fastest
739
716
  count += this._countInIndex(content.objects, objectId, subjectId, predicateId);
717
+ } else {
718
+ // Without a pattern, the subject index sums the fewest leaf counts
719
+ count += this._countInIndex(content.subjects);
740
720
  }
741
721
  }
742
722
  }
@@ -1020,13 +1000,8 @@ class N3Store {
1020
1000
  addAll(quads) {
1021
1001
  if (quads instanceof DatasetCoreAndReadableStream) quads = quads.filtered;
1022
1002
  if (Array.isArray(quads)) this.addQuads(quads);
1023
- // Index merging bypasses observer notifications
1024
- else if (this._observers === null && quads instanceof N3Store && quads._entityIndex === this._entityIndex) {
1025
- if (quads._size !== 0) {
1026
- this._graphs = merge(this._graphs, quads._graphs);
1027
- this._size = null; // Invalidate the cached size
1028
- }
1029
- } else {
1003
+ // A store with the same entity index can be copied by identifier
1004
+ else if (quads instanceof N3Store && quads._entityIndex === this._entityIndex) this._addFromIndex(quads._graphs);else {
1030
1005
  for (const quad of quads) this.add(quad);
1031
1006
  }
1032
1007
  return this;
@@ -1041,18 +1016,27 @@ class N3Store {
1041
1016
  contains(other) {
1042
1017
  if (other instanceof DatasetCoreAndReadableStream) other = other.filtered;
1043
1018
  if (other === this) return true;
1044
- if (!(other instanceof N3Store) || this._entityIndex !== other._entityIndex) return other.every(quad => this.has(quad));
1019
+ if (!(other instanceof N3Store) || this._entityIndex !== other._entityIndex) {
1020
+ // A larger set cannot be a subset, but only compare sizes that are known without counting
1021
+ const otherSize = knownSize(other),
1022
+ thisSize = this._size;
1023
+ if (otherSize !== null && thisSize !== null && otherSize > thisSize) return false;
1024
+ return other.every(quad => this.has(quad));
1025
+ }
1026
+
1027
+ // At every level, the index of the subset cannot have more entries
1045
1028
  const g1 = this._graphs,
1046
1029
  g2 = other._graphs;
1047
- let s1, s2, p1, p2, o1;
1030
+ let s1, s2, p1, p2, o1, o2;
1048
1031
  for (const graph in g2) {
1049
1032
  if (!(s1 = g1[graph])) return false;
1050
1033
  s1 = s1.subjects;
1051
- for (const subject in s2 = g2[graph].subjects) {
1052
- if (!(p1 = s1[subject])) return false;
1053
- for (const predicate in p2 = s2[subject]) {
1054
- if (!(o1 = p1[predicate])) return false;
1055
- for (const object in p2[predicate]) if (!(object in o1)) return false;
1034
+ if ((s2 = g2[graph].subjects)[SIZE] > s1[SIZE]) return false;
1035
+ for (const subject in s2) {
1036
+ if (!(p1 = s1[subject]) || (p2 = s2[subject])[SIZE] > p1[SIZE]) return false;
1037
+ for (const predicate in p2) {
1038
+ if (!(o1 = p1[predicate]) || (o2 = p2[predicate])[SIZE] > o1[SIZE]) return false;
1039
+ for (const object in o2) if (!(object in o1)) return false;
1056
1040
  }
1057
1041
  }
1058
1042
  }
@@ -1089,11 +1073,7 @@ class N3Store {
1089
1073
  const store = new N3Store({
1090
1074
  entityIndex: this._entityIndex
1091
1075
  });
1092
- const graphs = difference(this._graphs, other._graphs);
1093
- if (graphs) {
1094
- store._graphs = graphs;
1095
- store._size = null;
1096
- }
1076
+ store._addFromIndex(this._graphs, other._graphs);
1097
1077
  return store;
1098
1078
  }
1099
1079
  return this.filter(quad => !other.has(quad));
@@ -1131,20 +1111,33 @@ class N3Store {
1131
1111
  const store = new N3Store({
1132
1112
  entityIndex: this._entityIndex
1133
1113
  });
1134
- store._graphs = merge(Object.create(null), this._graphs);
1135
- store._size = this._size;
1114
+ store._addFromIndex(this._graphs);
1136
1115
  return store;
1137
1116
  } else if (other instanceof N3Store && this._entityIndex === other._entityIndex) {
1138
1117
  const store = new N3Store({
1139
1118
  entityIndex: this._entityIndex
1140
1119
  });
1141
- const graphs = intersect(other._graphs, this._graphs);
1142
- if (graphs) {
1143
- store._graphs = graphs;
1144
- store._size = null;
1120
+ // Starting a loop over no graphs costs more than checking for an empty store
1121
+ if (this._size !== 0 && other._size !== 0) {
1122
+ if (other._graphCount < this._graphCount) store._addIntersectionFromIndex(other._graphs, this._graphs);else store._addIntersectionFromIndex(this._graphs, other._graphs);
1145
1123
  }
1146
1124
  return store;
1147
1125
  }
1126
+
1127
+ // Test the quads of the smaller dataset against the larger one
1128
+ // when both sizes are known without counting
1129
+ const otherSize = knownSize(other),
1130
+ thisSize = this._size;
1131
+ if (otherSize !== null && thisSize !== null && otherSize < thisSize && typeof other[Symbol.iterator] === 'function' &&
1132
+ // unless it is a store whose custom factories may not create RDF/JS quads,
1133
+ // either for its quads or, through its entity index, for their terms
1134
+ other._factory === _N3DataFactory.default && other._entityIndex._factory === _N3DataFactory.default) {
1135
+ const store = new N3Store({
1136
+ entityIndex: this._entityIndex
1137
+ });
1138
+ for (const quad of other) if (this.has(quad)) store.add(quad);
1139
+ return store;
1140
+ }
1148
1141
  return this.filter(quad => other.has(quad));
1149
1142
  }
1150
1143
 
@@ -1219,8 +1212,7 @@ class N3Store {
1219
1212
  const store = new N3Store({
1220
1213
  entityIndex: this._entityIndex
1221
1214
  });
1222
- store._graphs = merge(Object.create(null), this._graphs);
1223
- store._size = this._size;
1215
+ store._addFromIndex(this._graphs);
1224
1216
  store.addAll(quads);
1225
1217
  return store;
1226
1218
  }
@@ -1485,11 +1477,14 @@ class DatasetCoreAndReadableStream extends _readableStream.Readable {
1485
1477
  predicates = indexMatch(content.predicates, [predicateId, objectId, subjectId]);
1486
1478
  objects = indexMatch(content.objects, [objectId, subjectId, predicateId]);
1487
1479
  }
1488
- if (subjects) newStore._graphs[graphKey] = {
1489
- subjects,
1490
- predicates,
1491
- objects
1492
- };
1480
+ if (subjects) {
1481
+ newStore._graphs[graphKey] = {
1482
+ subjects,
1483
+ predicates,
1484
+ objects
1485
+ };
1486
+ newStore._graphCount++;
1487
+ }
1493
1488
  }
1494
1489
  }
1495
1490
  newStore._size = null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "n3",
3
- "version": "2.13.4",
3
+ "version": "2.13.6",
4
4
  "description": "Lightning fast, asynchronous, streaming Turtle / N3 / RDF library.",
5
5
  "author": "Ruben Verborgh <ruben.verborgh@gmail.com>",
6
6
  "keywords": [
@@ -131,7 +131,12 @@
131
131
  }
132
132
  ],
133
133
  "@semantic-release/npm",
134
- "@semantic-release/github"
134
+ [
135
+ "@semantic-release/github",
136
+ {
137
+ "successCommentCondition": "<% return !branch.prerelease; %>"
138
+ }
139
+ ]
135
140
  ]
136
141
  },
137
142
  "publishConfig": {