n3 2.1.2 → 2.2.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/README.md +27 -0
- package/browser/n3.esm.min.js +6 -6
- package/browser/n3.min.js +1 -1
- package/lib/N3DataFactory.js +44 -0
- package/lib/N3Lexer.js +6 -4
- package/lib/N3Parser.js +36 -6
- package/lib/N3Reasoner.js +25 -4
- package/lib/N3Store.js +15 -2
- package/lib/N3Writer.js +6 -4
- package/package.json +1 -1
- package/src/N3DataFactory.js +44 -0
- package/src/N3Lexer.js +5 -4
- package/src/N3Parser.js +45 -5
- package/src/N3Reasoner.js +28 -5
- package/src/N3Store.js +16 -2
- package/src/N3Writer.js +7 -4
package/lib/N3DataFactory.js
CHANGED
|
@@ -75,6 +75,16 @@ class Term {
|
|
|
75
75
|
// ## NamedNode constructor
|
|
76
76
|
exports.Term = Term;
|
|
77
77
|
class NamedNode extends Term {
|
|
78
|
+
// ### Creates a named node
|
|
79
|
+
/**
|
|
80
|
+
* @deprecated Create named nodes through a data factory instead
|
|
81
|
+
* (`DataFactory.namedNode(iri)`), so that term validation can be applied;
|
|
82
|
+
* the constructor assumes an already-validated IRI.
|
|
83
|
+
*/
|
|
84
|
+
constructor(iri) {
|
|
85
|
+
super(iri);
|
|
86
|
+
}
|
|
87
|
+
|
|
78
88
|
// ### The term type of this term
|
|
79
89
|
get termType() {
|
|
80
90
|
return 'NamedNode';
|
|
@@ -84,6 +94,17 @@ class NamedNode extends Term {
|
|
|
84
94
|
// ## Literal constructor
|
|
85
95
|
exports.NamedNode = NamedNode;
|
|
86
96
|
class Literal extends Term {
|
|
97
|
+
// ### Creates a literal
|
|
98
|
+
/**
|
|
99
|
+
* @deprecated Create literals through a data factory instead
|
|
100
|
+
* (`DataFactory.literal(value, languageOrDatatype)`), so that term
|
|
101
|
+
* validation can be applied; the constructor takes the internal
|
|
102
|
+
* id representation and assumes it is already valid.
|
|
103
|
+
*/
|
|
104
|
+
constructor(id) {
|
|
105
|
+
super(id);
|
|
106
|
+
}
|
|
107
|
+
|
|
87
108
|
// ### The term type of this term
|
|
88
109
|
get termType() {
|
|
89
110
|
return 'Literal';
|
|
@@ -155,6 +176,12 @@ class Literal extends Term {
|
|
|
155
176
|
// ## BlankNode constructor
|
|
156
177
|
exports.Literal = Literal;
|
|
157
178
|
class BlankNode extends Term {
|
|
179
|
+
// ### Creates a blank node
|
|
180
|
+
/**
|
|
181
|
+
* @deprecated Create blank nodes through a data factory instead
|
|
182
|
+
* (`DataFactory.blankNode(name)`), so that term validation can be applied;
|
|
183
|
+
* the constructor assumes an already-validated name.
|
|
184
|
+
*/
|
|
158
185
|
constructor(name) {
|
|
159
186
|
super(`_:${name}`);
|
|
160
187
|
}
|
|
@@ -171,6 +198,12 @@ class BlankNode extends Term {
|
|
|
171
198
|
}
|
|
172
199
|
exports.BlankNode = BlankNode;
|
|
173
200
|
class Variable extends Term {
|
|
201
|
+
// ### Creates a variable
|
|
202
|
+
/**
|
|
203
|
+
* @deprecated Create variables through a data factory instead
|
|
204
|
+
* (`DataFactory.variable(name)`), so that term validation can be applied;
|
|
205
|
+
* the constructor assumes an already-validated name.
|
|
206
|
+
*/
|
|
174
207
|
constructor(name) {
|
|
175
208
|
super(`?${name}`);
|
|
176
209
|
}
|
|
@@ -189,6 +222,11 @@ class Variable extends Term {
|
|
|
189
222
|
// ## DefaultGraph constructor
|
|
190
223
|
exports.Variable = Variable;
|
|
191
224
|
class DefaultGraph extends Term {
|
|
225
|
+
// ### Creates the default graph
|
|
226
|
+
/**
|
|
227
|
+
* @deprecated Obtain the default graph through a data factory instead
|
|
228
|
+
* (`DataFactory.defaultGraph()`).
|
|
229
|
+
*/
|
|
192
230
|
constructor() {
|
|
193
231
|
super('');
|
|
194
232
|
return DEFAULTGRAPH || this;
|
|
@@ -296,6 +334,12 @@ function termToId(term, nested) {
|
|
|
296
334
|
|
|
297
335
|
// ## Quad constructor
|
|
298
336
|
class Quad extends Term {
|
|
337
|
+
// ### Creates a quad
|
|
338
|
+
/**
|
|
339
|
+
* @deprecated Create quads through a data factory instead
|
|
340
|
+
* (`DataFactory.quad(subject, predicate, object, graph)`), so that term
|
|
341
|
+
* validation can be applied; the constructor assumes already-validated terms.
|
|
342
|
+
*/
|
|
299
343
|
constructor(subject, predicate, object, graph) {
|
|
300
344
|
super('');
|
|
301
345
|
this._subject = subject;
|
package/lib/N3Lexer.js
CHANGED
|
@@ -45,8 +45,10 @@ const escapeReplacements = {
|
|
|
45
45
|
'%': '%'
|
|
46
46
|
};
|
|
47
47
|
const illegalIriChars = /[\x00-\x20<>\\"\{\}\|\^\`]/;
|
|
48
|
-
|
|
49
|
-
|
|
48
|
+
|
|
49
|
+
// A valid code point is a Unicode scalar value: at most U+10FFFF and not a surrogate
|
|
50
|
+
function isValidCodePoint(charCode) {
|
|
51
|
+
return charCode <= 0x10FFFF && (charCode < 0xD800 || charCode > 0xDFFF);
|
|
50
52
|
}
|
|
51
53
|
const lineModeRegExps = {
|
|
52
54
|
_iri: true,
|
|
@@ -425,7 +427,7 @@ class N3Lexer {
|
|
|
425
427
|
// 4-digit unicode character
|
|
426
428
|
if (typeof unicode4 === 'string') {
|
|
427
429
|
const charCode = Number.parseInt(unicode4, 16);
|
|
428
|
-
if (
|
|
430
|
+
if (!isValidCodePoint(charCode)) {
|
|
429
431
|
invalid = true;
|
|
430
432
|
return '';
|
|
431
433
|
}
|
|
@@ -434,7 +436,7 @@ class N3Lexer {
|
|
|
434
436
|
// 8-digit unicode character
|
|
435
437
|
if (typeof unicode8 === 'string') {
|
|
436
438
|
let charCode = Number.parseInt(unicode8, 16);
|
|
437
|
-
if (
|
|
439
|
+
if (!isValidCodePoint(charCode)) {
|
|
438
440
|
invalid = true;
|
|
439
441
|
return '';
|
|
440
442
|
}
|
package/lib/N3Parser.js
CHANGED
|
@@ -292,6 +292,13 @@ class N3Parser {
|
|
|
292
292
|
case ';':
|
|
293
293
|
// Additional semicolons can be safely ignored
|
|
294
294
|
return this._predicate !== null ? this._readPredicate : this._error('Expected predicate but got ;', token);
|
|
295
|
+
case 'literal':
|
|
296
|
+
if (!this._n3Mode) return this._error('Unexpected literal', token);
|
|
297
|
+
if (token.prefix.length === 0) {
|
|
298
|
+
this._literalValue = token.value;
|
|
299
|
+
return this._completePredicateLiteral;
|
|
300
|
+
} else this._predicate = this._factory.literal(token.value, this._factory.namedNode(token.prefix));
|
|
301
|
+
break;
|
|
295
302
|
case '[':
|
|
296
303
|
if (this._n3Mode) {
|
|
297
304
|
// Start a new quad with a new blank node as subject
|
|
@@ -569,22 +576,45 @@ class N3Parser {
|
|
|
569
576
|
language: this._literalLanguage,
|
|
570
577
|
direction: token.value
|
|
571
578
|
});
|
|
572
|
-
if (component === 'subject') this._subject = term;else this._object = term;
|
|
579
|
+
if (component === 'subject') this._subject = term;else if (component === 'predicate') this._predicate = term;else this._object = term;
|
|
573
580
|
this._literalLanguage = undefined;
|
|
574
581
|
token = null;
|
|
575
582
|
}
|
|
576
583
|
if (component === 'subject') return token === null ? this._readPredicateOrNamedGraph : this._readPredicateOrNamedGraph(token);
|
|
584
|
+
if (component === 'predicate') return token === null ? this._readObject : this._readObject(token);
|
|
577
585
|
return this._completeObjectLiteralPost(token, listItem);
|
|
578
586
|
}
|
|
579
587
|
|
|
580
|
-
// Completes a literal in subject position
|
|
581
|
-
|
|
582
|
-
const completed = this._completeLiteral(token,
|
|
583
|
-
|
|
588
|
+
// Completes a literal in subject or predicate position
|
|
589
|
+
_completeTermLiteral(token, component) {
|
|
590
|
+
const completed = this._completeLiteral(token, component);
|
|
591
|
+
if (!completed) return;
|
|
592
|
+
let next;
|
|
593
|
+
if (component === 'subject') {
|
|
594
|
+
this._subject = completed.literal;
|
|
595
|
+
next = this._readPredicateOrNamedGraph;
|
|
596
|
+
} else {
|
|
597
|
+
this._predicate = completed.literal;
|
|
598
|
+
this._validAnnotation = true;
|
|
599
|
+
next = this._readObject;
|
|
600
|
+
}
|
|
584
601
|
|
|
585
602
|
// Postpone completion if the literal is only partially completed (such as lang+dir).
|
|
586
603
|
if (completed.readCb) return completed.readCb.bind(this, false);
|
|
587
|
-
|
|
604
|
+
|
|
605
|
+
// If the token was consumed as a datatype, continue with the next component;
|
|
606
|
+
// otherwise, consume the token now
|
|
607
|
+
return completed.token === null ? next : next.call(this, completed.token);
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
// Completes a literal in subject position
|
|
611
|
+
_completeSubjectLiteral(token) {
|
|
612
|
+
return this._completeTermLiteral(token, 'subject');
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
// Completes a literal in predicate position
|
|
616
|
+
_completePredicateLiteral(token) {
|
|
617
|
+
return this._completeTermLiteral(token, 'predicate');
|
|
588
618
|
}
|
|
589
619
|
|
|
590
620
|
// Completes a literal in object position
|
package/lib/N3Reasoner.js
CHANGED
|
@@ -26,14 +26,22 @@ function getRulesFromDataset(dataset) {
|
|
|
26
26
|
return rules;
|
|
27
27
|
}
|
|
28
28
|
class N3Reasoner {
|
|
29
|
-
constructor(store) {
|
|
29
|
+
constructor(store, options = {}) {
|
|
30
30
|
this._store = store;
|
|
31
|
+
// Optional safety budgets for reasoning over untrusted rules or data
|
|
32
|
+
this._maxDerivations = options.maxDerivations === undefined ? Infinity : options.maxDerivations;
|
|
33
|
+
// Caps a rule's premise count, as `_evaluatePremise` recurses once per premise
|
|
34
|
+
this._maxPremiseDepth = options.maxPremiseDepth === undefined ? Infinity : options.maxPremiseDepth;
|
|
31
35
|
}
|
|
32
36
|
_add(subject, predicate, object, graphItem, cb) {
|
|
33
37
|
// Only add to the remaining indexes if there is not already a value in the index
|
|
34
38
|
if (!this._store._addToIndex(graphItem.subjects, subject, predicate, object)) return;
|
|
35
39
|
this._store._addToIndex(graphItem.predicates, predicate, object, subject);
|
|
36
40
|
this._store._addToIndex(graphItem.objects, object, subject, predicate);
|
|
41
|
+
// Count genuinely new derivations and fail past the budget. The check comes
|
|
42
|
+
// after all three indexes are updated, so a caught error leaves the store
|
|
43
|
+
// in a consistent state (the reasoning result is merely incomplete).
|
|
44
|
+
if (++this._derivations > this._maxDerivations) throw new Error(`Reasoning exceeded the maximum of ${this._maxDerivations} derivations`);
|
|
37
45
|
cb();
|
|
38
46
|
}
|
|
39
47
|
_evaluatePremise(rule, content, cb, i = 0) {
|
|
@@ -143,10 +151,18 @@ class N3Reasoner {
|
|
|
143
151
|
};
|
|
144
152
|
}
|
|
145
153
|
reason(rules) {
|
|
154
|
+
this._derivations = 0;
|
|
146
155
|
if (!Array.isArray(rules)) {
|
|
147
156
|
rules = getRulesFromDataset(rules);
|
|
148
157
|
}
|
|
149
158
|
rules = rules.map(rule => this._createRule(rule));
|
|
159
|
+
|
|
160
|
+
// Reject rules with more body triples than the configured premise depth:
|
|
161
|
+
// `_evaluatePremise` recurses once per premise, so an over-long rule would
|
|
162
|
+
// otherwise overflow the stack with an uncatchable RangeError.
|
|
163
|
+
for (const rule of rules) {
|
|
164
|
+
if (rule.premise.length > this._maxPremiseDepth) throw new Error(`Reasoning rule exceeds the maximum premise depth of ${this._maxPremiseDepth}`);
|
|
165
|
+
}
|
|
150
166
|
for (const r1 of rules) {
|
|
151
167
|
for (const r2 of rules) {
|
|
152
168
|
for (let i = 0; i < r2.premise.length; i++) {
|
|
@@ -190,10 +206,15 @@ class N3Reasoner {
|
|
|
190
206
|
rule.premise = rule.premise.map(p => getIndex(p, set));
|
|
191
207
|
}
|
|
192
208
|
const graphs = this._store._getGraphs();
|
|
193
|
-
|
|
194
|
-
|
|
209
|
+
try {
|
|
210
|
+
for (const graphId in graphs) {
|
|
211
|
+
this._reasonGraphNaive(rules, graphs[graphId]);
|
|
212
|
+
}
|
|
213
|
+
} finally {
|
|
214
|
+
// Invalidate the cached size even if a derivation budget was exceeded,
|
|
215
|
+
// so a caught budget error leaves the store fully consistent.
|
|
216
|
+
this._store._size = null;
|
|
195
217
|
}
|
|
196
|
-
this._store._size = null;
|
|
197
218
|
}
|
|
198
219
|
}
|
|
199
220
|
exports.default = N3Reasoner;
|
package/lib/N3Store.js
CHANGED
|
@@ -275,6 +275,18 @@ class N3Store {
|
|
|
275
275
|
}
|
|
276
276
|
}
|
|
277
277
|
|
|
278
|
+
// ### `_loopByKey0Deep` executes the callback on all keys of index 2
|
|
279
|
+
// for a certain entry in index 0, possibly repeating keys
|
|
280
|
+
_loopByKey0Deep(index0, key0, callback) {
|
|
281
|
+
let index1, index2, key1, key2;
|
|
282
|
+
if (index1 = index0[key0]) {
|
|
283
|
+
for (key1 in index1) {
|
|
284
|
+
index2 = index1[key1];
|
|
285
|
+
for (key2 in index2) callback(key2);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
278
290
|
// ### `_countInIndex` counts matching quads in a three-layered index.
|
|
279
291
|
// The index base is `index0` and the keys at each level are `key0`, `key1`, and `key2`.
|
|
280
292
|
// Any of these keys can be undefined, which is interpreted as a wildcard.
|
|
@@ -685,8 +697,9 @@ class N3Store {
|
|
|
685
697
|
if (predicateId)
|
|
686
698
|
// If subject and predicate are given, the SPO index is best.
|
|
687
699
|
this._loopBy2Keys(content.subjects, subjectId, predicateId, callback);else
|
|
688
|
-
// If only subject is given, the
|
|
689
|
-
|
|
700
|
+
// If only subject is given, descending the SPO index
|
|
701
|
+
// visits only the subject's own quads.
|
|
702
|
+
this._loopByKey0Deep(content.subjects, subjectId, callback);
|
|
690
703
|
} else if (predicateId)
|
|
691
704
|
// If only predicate is given, the POS index is best.
|
|
692
705
|
this._loopByKey0(content.predicates, predicateId, callback);else
|
package/lib/N3Writer.js
CHANGED
|
@@ -46,6 +46,7 @@ class N3Writer {
|
|
|
46
46
|
constructor(outputStream, options) {
|
|
47
47
|
// ### `_prefixRegex` matches a prefixed name or IRI that begins with one of the added prefixes
|
|
48
48
|
this._prefixRegex = /$0^/;
|
|
49
|
+
this._hasPrefixes = false;
|
|
49
50
|
|
|
50
51
|
// Shift arguments if the first argument is not a stream
|
|
51
52
|
if (outputStream && typeof outputStream.write !== 'function') options = outputStream, outputStream = null;
|
|
@@ -154,7 +155,7 @@ class N3Writer {
|
|
|
154
155
|
if (entity.termType !== 'NamedNode') {
|
|
155
156
|
// If it is a list head, pretty-print it
|
|
156
157
|
if (this._lists && entity.value in this._lists) entity = this.list(this._lists[entity.value]);
|
|
157
|
-
return 'id' in entity ? entity.id : `_:${entity.value}`;
|
|
158
|
+
return entity.termType === 'Variable' ? `?${entity.value}` : 'id' in entity ? entity.id : `_:${entity.value}`;
|
|
158
159
|
}
|
|
159
160
|
let iri = entity.value;
|
|
160
161
|
// Use relative IRIs if requested and possible
|
|
@@ -163,8 +164,8 @@ class N3Writer {
|
|
|
163
164
|
}
|
|
164
165
|
// Escape special characters
|
|
165
166
|
if (escape.test(iri)) iri = iri.replace(escapeAll, characterReplacer);
|
|
166
|
-
// Try to represent the IRI as prefixed name
|
|
167
|
-
const prefixMatch = this._prefixRegex.exec(iri);
|
|
167
|
+
// Try to represent the IRI as prefixed name, unless no prefixes were added
|
|
168
|
+
const prefixMatch = this._hasPrefixes ? this._prefixRegex.exec(iri) : null;
|
|
168
169
|
return !prefixMatch ? `<${iri}>` : !prefixMatch[1] ? iri : this._prefixIRIs[prefixMatch[1]] + prefixMatch[2];
|
|
169
170
|
}
|
|
170
171
|
|
|
@@ -282,6 +283,7 @@ class N3Writer {
|
|
|
282
283
|
}
|
|
283
284
|
// Recreate the prefix matcher
|
|
284
285
|
if (hasPrefixes) {
|
|
286
|
+
this._hasPrefixes = true;
|
|
285
287
|
let IRIlist = '',
|
|
286
288
|
prefixList = '';
|
|
287
289
|
for (const prefixIRI in this._prefixIRIs) {
|
|
@@ -289,7 +291,7 @@ class N3Writer {
|
|
|
289
291
|
prefixList += (prefixList ? '|' : '') + this._prefixIRIs[prefixIRI];
|
|
290
292
|
}
|
|
291
293
|
IRIlist = (0, _Util.escapeRegex)(IRIlist, /[\]\/\(\)\*\+\?\.\\\$]/g, '\\$&');
|
|
292
|
-
this._prefixRegex = new RegExp(`^(?:${prefixList})[^\/]*$|` + `^(${IRIlist})([_a-zA-Z0-9][\\-_a-zA-Z0-9]*)$`);
|
|
294
|
+
this._prefixRegex = new RegExp(`^(?:${prefixList})[^\/]*$|` + `^(${IRIlist})([_a-zA-Z0-9](?:\\.?[\\-_a-zA-Z0-9])*)$`);
|
|
293
295
|
}
|
|
294
296
|
// End a prefix block with a newline
|
|
295
297
|
this._write(hasPrefixes ? '\n' : '', done);
|
package/package.json
CHANGED
package/src/N3DataFactory.js
CHANGED
|
@@ -65,6 +65,16 @@ export class Term {
|
|
|
65
65
|
|
|
66
66
|
// ## NamedNode constructor
|
|
67
67
|
export class NamedNode extends Term {
|
|
68
|
+
// ### Creates a named node
|
|
69
|
+
/**
|
|
70
|
+
* @deprecated Create named nodes through a data factory instead
|
|
71
|
+
* (`DataFactory.namedNode(iri)`), so that term validation can be applied;
|
|
72
|
+
* the constructor assumes an already-validated IRI.
|
|
73
|
+
*/
|
|
74
|
+
constructor(iri) {
|
|
75
|
+
super(iri);
|
|
76
|
+
}
|
|
77
|
+
|
|
68
78
|
// ### The term type of this term
|
|
69
79
|
get termType() {
|
|
70
80
|
return 'NamedNode';
|
|
@@ -73,6 +83,17 @@ export class NamedNode extends Term {
|
|
|
73
83
|
|
|
74
84
|
// ## Literal constructor
|
|
75
85
|
export class Literal extends Term {
|
|
86
|
+
// ### Creates a literal
|
|
87
|
+
/**
|
|
88
|
+
* @deprecated Create literals through a data factory instead
|
|
89
|
+
* (`DataFactory.literal(value, languageOrDatatype)`), so that term
|
|
90
|
+
* validation can be applied; the constructor takes the internal
|
|
91
|
+
* id representation and assumes it is already valid.
|
|
92
|
+
*/
|
|
93
|
+
constructor(id) {
|
|
94
|
+
super(id);
|
|
95
|
+
}
|
|
96
|
+
|
|
76
97
|
// ### The term type of this term
|
|
77
98
|
get termType() {
|
|
78
99
|
return 'Literal';
|
|
@@ -146,6 +167,12 @@ export class Literal extends Term {
|
|
|
146
167
|
|
|
147
168
|
// ## BlankNode constructor
|
|
148
169
|
export class BlankNode extends Term {
|
|
170
|
+
// ### Creates a blank node
|
|
171
|
+
/**
|
|
172
|
+
* @deprecated Create blank nodes through a data factory instead
|
|
173
|
+
* (`DataFactory.blankNode(name)`), so that term validation can be applied;
|
|
174
|
+
* the constructor assumes an already-validated name.
|
|
175
|
+
*/
|
|
149
176
|
constructor(name) {
|
|
150
177
|
super(`_:${name}`);
|
|
151
178
|
}
|
|
@@ -162,6 +189,12 @@ export class BlankNode extends Term {
|
|
|
162
189
|
}
|
|
163
190
|
|
|
164
191
|
export class Variable extends Term {
|
|
192
|
+
// ### Creates a variable
|
|
193
|
+
/**
|
|
194
|
+
* @deprecated Create variables through a data factory instead
|
|
195
|
+
* (`DataFactory.variable(name)`), so that term validation can be applied;
|
|
196
|
+
* the constructor assumes an already-validated name.
|
|
197
|
+
*/
|
|
165
198
|
constructor(name) {
|
|
166
199
|
super(`?${name}`);
|
|
167
200
|
}
|
|
@@ -179,6 +212,11 @@ export class Variable extends Term {
|
|
|
179
212
|
|
|
180
213
|
// ## DefaultGraph constructor
|
|
181
214
|
export class DefaultGraph extends Term {
|
|
215
|
+
// ### Creates the default graph
|
|
216
|
+
/**
|
|
217
|
+
* @deprecated Obtain the default graph through a data factory instead
|
|
218
|
+
* (`DataFactory.defaultGraph()`).
|
|
219
|
+
*/
|
|
182
220
|
constructor() {
|
|
183
221
|
super('');
|
|
184
222
|
return DEFAULTGRAPH || this;
|
|
@@ -299,6 +337,12 @@ export function termToId(term, nested) {
|
|
|
299
337
|
|
|
300
338
|
// ## Quad constructor
|
|
301
339
|
export class Quad extends Term {
|
|
340
|
+
// ### Creates a quad
|
|
341
|
+
/**
|
|
342
|
+
* @deprecated Create quads through a data factory instead
|
|
343
|
+
* (`DataFactory.quad(subject, predicate, object, graph)`), so that term
|
|
344
|
+
* validation can be applied; the constructor assumes already-validated terms.
|
|
345
|
+
*/
|
|
302
346
|
constructor(subject, predicate, object, graph) {
|
|
303
347
|
super('');
|
|
304
348
|
this._subject = subject;
|
package/src/N3Lexer.js
CHANGED
|
@@ -15,8 +15,9 @@ const escapeReplacements = {
|
|
|
15
15
|
};
|
|
16
16
|
const illegalIriChars = /[\x00-\x20<>\\"\{\}\|\^\`]/;
|
|
17
17
|
|
|
18
|
-
|
|
19
|
-
|
|
18
|
+
// A valid code point is a Unicode scalar value: at most U+10FFFF and not a surrogate
|
|
19
|
+
function isValidCodePoint(charCode) {
|
|
20
|
+
return charCode <= 0x10FFFF && (charCode < 0xD800 || charCode > 0xDFFF);
|
|
20
21
|
}
|
|
21
22
|
|
|
22
23
|
const lineModeRegExps = {
|
|
@@ -433,7 +434,7 @@ export default class N3Lexer {
|
|
|
433
434
|
// 4-digit unicode character
|
|
434
435
|
if (typeof unicode4 === 'string') {
|
|
435
436
|
const charCode = Number.parseInt(unicode4, 16);
|
|
436
|
-
if (
|
|
437
|
+
if (!isValidCodePoint(charCode)) {
|
|
437
438
|
invalid = true;
|
|
438
439
|
return '';
|
|
439
440
|
}
|
|
@@ -442,7 +443,7 @@ export default class N3Lexer {
|
|
|
442
443
|
// 8-digit unicode character
|
|
443
444
|
if (typeof unicode8 === 'string') {
|
|
444
445
|
let charCode = Number.parseInt(unicode8, 16);
|
|
445
|
-
if (
|
|
446
|
+
if (!isValidCodePoint(charCode)) {
|
|
446
447
|
invalid = true;
|
|
447
448
|
return '';
|
|
448
449
|
}
|
package/src/N3Parser.js
CHANGED
|
@@ -302,6 +302,18 @@ export default class N3Parser {
|
|
|
302
302
|
// Additional semicolons can be safely ignored
|
|
303
303
|
return this._predicate !== null ? this._readPredicate :
|
|
304
304
|
this._error('Expected predicate but got ;', token);
|
|
305
|
+
case 'literal':
|
|
306
|
+
if (!this._n3Mode)
|
|
307
|
+
return this._error('Unexpected literal', token);
|
|
308
|
+
|
|
309
|
+
if (token.prefix.length === 0) {
|
|
310
|
+
this._literalValue = token.value;
|
|
311
|
+
return this._completePredicateLiteral;
|
|
312
|
+
}
|
|
313
|
+
else
|
|
314
|
+
this._predicate = this._factory.literal(token.value, this._factory.namedNode(token.prefix));
|
|
315
|
+
|
|
316
|
+
break;
|
|
305
317
|
case '[':
|
|
306
318
|
if (this._n3Mode) {
|
|
307
319
|
// Start a new quad with a new blank node as subject
|
|
@@ -603,6 +615,8 @@ export default class N3Parser {
|
|
|
603
615
|
const term = this._factory.literal(this._literalValue, { language: this._literalLanguage, direction: token.value });
|
|
604
616
|
if (component === 'subject')
|
|
605
617
|
this._subject = term;
|
|
618
|
+
else if (component === 'predicate')
|
|
619
|
+
this._predicate = term;
|
|
606
620
|
else
|
|
607
621
|
this._object = term;
|
|
608
622
|
this._literalLanguage = undefined;
|
|
@@ -611,19 +625,45 @@ export default class N3Parser {
|
|
|
611
625
|
|
|
612
626
|
if (component === 'subject')
|
|
613
627
|
return token === null ? this._readPredicateOrNamedGraph : this._readPredicateOrNamedGraph(token);
|
|
628
|
+
if (component === 'predicate')
|
|
629
|
+
return token === null ? this._readObject : this._readObject(token);
|
|
614
630
|
return this._completeObjectLiteralPost(token, listItem);
|
|
615
631
|
}
|
|
616
632
|
|
|
617
|
-
// Completes a literal in subject position
|
|
618
|
-
|
|
619
|
-
const completed = this._completeLiteral(token,
|
|
620
|
-
|
|
633
|
+
// Completes a literal in subject or predicate position
|
|
634
|
+
_completeTermLiteral(token, component) {
|
|
635
|
+
const completed = this._completeLiteral(token, component);
|
|
636
|
+
if (!completed)
|
|
637
|
+
return;
|
|
638
|
+
|
|
639
|
+
let next;
|
|
640
|
+
if (component === 'subject') {
|
|
641
|
+
this._subject = completed.literal;
|
|
642
|
+
next = this._readPredicateOrNamedGraph;
|
|
643
|
+
}
|
|
644
|
+
else {
|
|
645
|
+
this._predicate = completed.literal;
|
|
646
|
+
this._validAnnotation = true;
|
|
647
|
+
next = this._readObject;
|
|
648
|
+
}
|
|
621
649
|
|
|
622
650
|
// Postpone completion if the literal is only partially completed (such as lang+dir).
|
|
623
651
|
if (completed.readCb)
|
|
624
652
|
return completed.readCb.bind(this, false);
|
|
625
653
|
|
|
626
|
-
|
|
654
|
+
// If the token was consumed as a datatype, continue with the next component;
|
|
655
|
+
// otherwise, consume the token now
|
|
656
|
+
return completed.token === null ? next : next.call(this, completed.token);
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
// Completes a literal in subject position
|
|
660
|
+
_completeSubjectLiteral(token) {
|
|
661
|
+
return this._completeTermLiteral(token, 'subject');
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
// Completes a literal in predicate position
|
|
665
|
+
_completePredicateLiteral(token) {
|
|
666
|
+
return this._completeTermLiteral(token, 'predicate');
|
|
627
667
|
}
|
|
628
668
|
|
|
629
669
|
// Completes a literal in object position
|
package/src/N3Reasoner.js
CHANGED
|
@@ -14,8 +14,12 @@ export function getRulesFromDataset(dataset) {
|
|
|
14
14
|
}
|
|
15
15
|
|
|
16
16
|
export default class N3Reasoner {
|
|
17
|
-
constructor(store) {
|
|
17
|
+
constructor(store, options = {}) {
|
|
18
18
|
this._store = store;
|
|
19
|
+
// Optional safety budgets for reasoning over untrusted rules or data
|
|
20
|
+
this._maxDerivations = options.maxDerivations === undefined ? Infinity : options.maxDerivations;
|
|
21
|
+
// Caps a rule's premise count, as `_evaluatePremise` recurses once per premise
|
|
22
|
+
this._maxPremiseDepth = options.maxPremiseDepth === undefined ? Infinity : options.maxPremiseDepth;
|
|
19
23
|
}
|
|
20
24
|
|
|
21
25
|
_add(subject, predicate, object, graphItem, cb) {
|
|
@@ -23,6 +27,11 @@ export default class N3Reasoner {
|
|
|
23
27
|
if (!this._store._addToIndex(graphItem.subjects, subject, predicate, object)) return;
|
|
24
28
|
this._store._addToIndex(graphItem.predicates, predicate, object, subject);
|
|
25
29
|
this._store._addToIndex(graphItem.objects, object, subject, predicate);
|
|
30
|
+
// Count genuinely new derivations and fail past the budget. The check comes
|
|
31
|
+
// after all three indexes are updated, so a caught error leaves the store
|
|
32
|
+
// in a consistent state (the reasoning result is merely incomplete).
|
|
33
|
+
if (++this._derivations > this._maxDerivations)
|
|
34
|
+
throw new Error(`Reasoning exceeded the maximum of ${this._maxDerivations} derivations`);
|
|
26
35
|
cb();
|
|
27
36
|
}
|
|
28
37
|
|
|
@@ -129,11 +138,20 @@ export default class N3Reasoner {
|
|
|
129
138
|
}
|
|
130
139
|
|
|
131
140
|
reason(rules) {
|
|
141
|
+
this._derivations = 0;
|
|
132
142
|
if (!Array.isArray(rules)) {
|
|
133
143
|
rules = getRulesFromDataset(rules);
|
|
134
144
|
}
|
|
135
145
|
rules = rules.map(rule => this._createRule(rule));
|
|
136
146
|
|
|
147
|
+
// Reject rules with more body triples than the configured premise depth:
|
|
148
|
+
// `_evaluatePremise` recurses once per premise, so an over-long rule would
|
|
149
|
+
// otherwise overflow the stack with an uncatchable RangeError.
|
|
150
|
+
for (const rule of rules) {
|
|
151
|
+
if (rule.premise.length > this._maxPremiseDepth)
|
|
152
|
+
throw new Error(`Reasoning rule exceeds the maximum premise depth of ${this._maxPremiseDepth}`);
|
|
153
|
+
}
|
|
154
|
+
|
|
137
155
|
for (const r1 of rules) {
|
|
138
156
|
for (const r2 of rules) {
|
|
139
157
|
for (let i = 0; i < r2.premise.length; i++) {
|
|
@@ -179,11 +197,16 @@ export default class N3Reasoner {
|
|
|
179
197
|
}
|
|
180
198
|
|
|
181
199
|
const graphs = this._store._getGraphs();
|
|
182
|
-
|
|
183
|
-
|
|
200
|
+
try {
|
|
201
|
+
for (const graphId in graphs) {
|
|
202
|
+
this._reasonGraphNaive(rules, graphs[graphId]);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
finally {
|
|
206
|
+
// Invalidate the cached size even if a derivation budget was exceeded,
|
|
207
|
+
// so a caught budget error leaves the store fully consistent.
|
|
208
|
+
this._store._size = null;
|
|
184
209
|
}
|
|
185
|
-
|
|
186
|
-
this._store._size = null;
|
|
187
210
|
}
|
|
188
211
|
}
|
|
189
212
|
|
package/src/N3Store.js
CHANGED
|
@@ -296,6 +296,19 @@ export default class N3Store {
|
|
|
296
296
|
}
|
|
297
297
|
}
|
|
298
298
|
|
|
299
|
+
// ### `_loopByKey0Deep` executes the callback on all keys of index 2
|
|
300
|
+
// for a certain entry in index 0, possibly repeating keys
|
|
301
|
+
_loopByKey0Deep(index0, key0, callback) {
|
|
302
|
+
let index1, index2, key1, key2;
|
|
303
|
+
if (index1 = index0[key0]) {
|
|
304
|
+
for (key1 in index1) {
|
|
305
|
+
index2 = index1[key1];
|
|
306
|
+
for (key2 in index2)
|
|
307
|
+
callback(key2);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
299
312
|
// ### `_countInIndex` counts matching quads in a three-layered index.
|
|
300
313
|
// The index base is `index0` and the keys at each level are `key0`, `key1`, and `key2`.
|
|
301
314
|
// Any of these keys can be undefined, which is interpreted as a wildcard.
|
|
@@ -719,8 +732,9 @@ export default class N3Store {
|
|
|
719
732
|
// If subject and predicate are given, the SPO index is best.
|
|
720
733
|
this._loopBy2Keys(content.subjects, subjectId, predicateId, callback);
|
|
721
734
|
else
|
|
722
|
-
// If only subject is given, the
|
|
723
|
-
|
|
735
|
+
// If only subject is given, descending the SPO index
|
|
736
|
+
// visits only the subject's own quads.
|
|
737
|
+
this._loopByKey0Deep(content.subjects, subjectId, callback);
|
|
724
738
|
}
|
|
725
739
|
else if (predicateId)
|
|
726
740
|
// If only predicate is given, the POS index is best.
|
package/src/N3Writer.js
CHANGED
|
@@ -31,6 +31,7 @@ export default class N3Writer {
|
|
|
31
31
|
constructor(outputStream, options) {
|
|
32
32
|
// ### `_prefixRegex` matches a prefixed name or IRI that begins with one of the added prefixes
|
|
33
33
|
this._prefixRegex = /$0^/;
|
|
34
|
+
this._hasPrefixes = false;
|
|
34
35
|
|
|
35
36
|
// Shift arguments if the first argument is not a stream
|
|
36
37
|
if (outputStream && typeof outputStream.write !== 'function')
|
|
@@ -151,7 +152,8 @@ export default class N3Writer {
|
|
|
151
152
|
// If it is a list head, pretty-print it
|
|
152
153
|
if (this._lists && (entity.value in this._lists))
|
|
153
154
|
entity = this.list(this._lists[entity.value]);
|
|
154
|
-
return '
|
|
155
|
+
return entity.termType === 'Variable' ? `?${entity.value}` :
|
|
156
|
+
'id' in entity ? entity.id : `_:${entity.value}`;
|
|
155
157
|
}
|
|
156
158
|
let iri = entity.value;
|
|
157
159
|
// Use relative IRIs if requested and possible
|
|
@@ -161,8 +163,8 @@ export default class N3Writer {
|
|
|
161
163
|
// Escape special characters
|
|
162
164
|
if (escape.test(iri))
|
|
163
165
|
iri = iri.replace(escapeAll, characterReplacer);
|
|
164
|
-
// Try to represent the IRI as prefixed name
|
|
165
|
-
const prefixMatch = this._prefixRegex.exec(iri);
|
|
166
|
+
// Try to represent the IRI as prefixed name, unless no prefixes were added
|
|
167
|
+
const prefixMatch = this._hasPrefixes ? this._prefixRegex.exec(iri) : null;
|
|
166
168
|
return !prefixMatch ? `<${iri}>` :
|
|
167
169
|
(!prefixMatch[1] ? iri : this._prefixIRIs[prefixMatch[1]] + prefixMatch[2]);
|
|
168
170
|
}
|
|
@@ -294,6 +296,7 @@ export default class N3Writer {
|
|
|
294
296
|
}
|
|
295
297
|
// Recreate the prefix matcher
|
|
296
298
|
if (hasPrefixes) {
|
|
299
|
+
this._hasPrefixes = true;
|
|
297
300
|
let IRIlist = '', prefixList = '';
|
|
298
301
|
for (const prefixIRI in this._prefixIRIs) {
|
|
299
302
|
IRIlist += IRIlist ? `|${prefixIRI}` : prefixIRI;
|
|
@@ -301,7 +304,7 @@ export default class N3Writer {
|
|
|
301
304
|
}
|
|
302
305
|
IRIlist = escapeRegex(IRIlist, /[\]\/\(\)\*\+\?\.\\\$]/g, '\\$&');
|
|
303
306
|
this._prefixRegex = new RegExp(`^(?:${prefixList})[^\/]*$|` +
|
|
304
|
-
`^(${IRIlist})([_a-zA-Z0-9][\\-_a-zA-Z0-9]*)$`);
|
|
307
|
+
`^(${IRIlist})([_a-zA-Z0-9](?:\\.?[\\-_a-zA-Z0-9])*)$`);
|
|
305
308
|
}
|
|
306
309
|
// End a prefix block with a newline
|
|
307
310
|
this._write(hasPrefixes ? '\n' : '', done);
|