n3 3.0.0-alpha.4 → 3.0.0-alpha.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/README.md +56 -0
- package/browser/n3.esm.min.js +15 -16
- package/browser/n3.min.js +15 -16
- package/lib/N3Lexer.js +255 -160
- package/lib/N3Parser.js +70 -32
- package/lib/N3Reasoner.js +1 -1
- package/lib/N3Store.js +50 -6
- package/lib/N3StreamParser.js +4 -0
- package/lib/N3Writer.js +65 -19
- package/lib/Util.js +1 -1
- package/package.json +2 -29
- package/src/N3Lexer.js +269 -171
- package/src/N3Parser.js +81 -32
- package/src/N3Reasoner.js +1 -1
- package/src/N3Store.js +61 -9
- package/src/N3StreamParser.js +2 -0
- package/src/N3Writer.js +70 -23
- package/src/Util.js +1 -1
package/src/N3Parser.js
CHANGED
|
@@ -4,6 +4,10 @@ import N3DataFactory from './N3DataFactory';
|
|
|
4
4
|
import namespaces from './IRIs';
|
|
5
5
|
|
|
6
6
|
let blankNodePrefix = 0;
|
|
7
|
+
// Detects `.` and `..` path segments in an IRI
|
|
8
|
+
const dotSegments = /(^|\/)\.\.?($|[/#?])/;
|
|
9
|
+
// Detects `.` and `..` segments in the path of an IRI, ignoring its query and fragment
|
|
10
|
+
const pathDotSegments = /^[^?#]*(?:^|\/)\.\.?(?:$|[/#?])/;
|
|
7
11
|
|
|
8
12
|
// ## Constructor
|
|
9
13
|
export default class N3Parser {
|
|
@@ -47,7 +51,16 @@ export default class N3Parser {
|
|
|
47
51
|
this._resolveRelativeIRI = iri => { return null; };
|
|
48
52
|
this._blankNodePrefix = typeof options.blankNodePrefix !== 'string' ? '' :
|
|
49
53
|
options.blankNodePrefix.replace(/^(?!_:)/, '_:');
|
|
50
|
-
|
|
54
|
+
// Map the token types of additional directives (@name and NAME) to their names
|
|
55
|
+
this._directives = null;
|
|
56
|
+
if (options.directives) {
|
|
57
|
+
this._directives = Object.create(null);
|
|
58
|
+
for (const name of options.directives)
|
|
59
|
+
this._directives[`@${name.toLowerCase()}`] = this._directives[name.toUpperCase()] = name;
|
|
60
|
+
}
|
|
61
|
+
this._lexer = options.lexer || new N3Lexer({
|
|
62
|
+
lineMode: isLineMode, n3: isN3, isImpliedBy: this._isImpliedBy, directives: options.directives,
|
|
63
|
+
});
|
|
51
64
|
// Disable explicit quantifiers by default
|
|
52
65
|
this._explicitQuantifiers = !!options.explicitQuantifiers;
|
|
53
66
|
// Scope blank node labels to formulas only, unless explicitly disabled
|
|
@@ -55,6 +68,10 @@ export default class N3Parser {
|
|
|
55
68
|
// Disable parsing of unsupported versions by default
|
|
56
69
|
this._parseUnsupportedVersions = !!options.parseUnsupportedVersions;
|
|
57
70
|
this._version = options.version;
|
|
71
|
+
// Maximum nesting depth of triple terms (default 1024; Infinity disables the limit)
|
|
72
|
+
const maxTripleTermDepth = options.maxTripleTermDepth;
|
|
73
|
+
this._maxTripleTermDepth = maxTripleTermDepth === Infinity ||
|
|
74
|
+
Number.isInteger(maxTripleTermDepth) && maxTripleTermDepth >= 0 ? maxTripleTermDepth : 1024;
|
|
58
75
|
}
|
|
59
76
|
|
|
60
77
|
// ## Static class methods
|
|
@@ -71,6 +88,7 @@ export default class N3Parser {
|
|
|
71
88
|
if (!baseIRI) {
|
|
72
89
|
this._base = '';
|
|
73
90
|
this._basePath = '';
|
|
91
|
+
this._basePathHasDotSegments = false;
|
|
74
92
|
}
|
|
75
93
|
else {
|
|
76
94
|
// Remove fragment if present
|
|
@@ -83,12 +101,32 @@ export default class N3Parser {
|
|
|
83
101
|
const path = queryPos < 0 ? baseIRI : baseIRI.substr(0, queryPos);
|
|
84
102
|
this._basePath = baseIRI.indexOf('/') < 0 ? baseIRI :
|
|
85
103
|
path.substr(0, path.lastIndexOf('/') + 1);
|
|
86
|
-
|
|
104
|
+
const base = baseIRI;
|
|
105
|
+
baseIRI = baseIRI.match(/^(?:([a-z][a-z0-9+.-]*:))?(\/\/[^\/?#]*)?/i);
|
|
87
106
|
this._baseRoot = baseIRI[0];
|
|
88
107
|
this._baseScheme = baseIRI[1];
|
|
108
|
+
// If the base has an authority but an empty path,
|
|
109
|
+
// relative IRIs merge under the path '/' (RFC 3986 §5.2.3)
|
|
110
|
+
if (baseIRI[2] !== undefined && (base.length === this._baseRoot.length || base[this._baseRoot.length] === '?'))
|
|
111
|
+
this._basePath = `${this._baseRoot}/`;
|
|
112
|
+
// Check once whether resolving against the base path needs to remove dot segments
|
|
113
|
+
this._basePathHasDotSegments = dotSegments.test(this._basePath);
|
|
89
114
|
}
|
|
90
115
|
}
|
|
91
116
|
|
|
117
|
+
// ### `_enterTripleTerm` stores the current parsing context when entering a triple term,
|
|
118
|
+
// failing if triple terms nest deeper than the maximum depth
|
|
119
|
+
_enterTripleTerm(type, subject, predicate, token) {
|
|
120
|
+
if (this._tripleTermDepth >= this._maxTripleTermDepth) {
|
|
121
|
+
this._error(`Triple terms nested deeper than ${this._maxTripleTermDepth} level${this._maxTripleTermDepth === 1 ? '' : 's'}`, token);
|
|
122
|
+
return false;
|
|
123
|
+
}
|
|
124
|
+
this._tripleTermDepth++;
|
|
125
|
+
this._saveContext(type, this._graph, subject, predicate, null);
|
|
126
|
+
this._graph = null;
|
|
127
|
+
return true;
|
|
128
|
+
}
|
|
129
|
+
|
|
92
130
|
// ### `_saveContext` stores the current parsing context
|
|
93
131
|
// when entering a new scope (list, blank node, formula)
|
|
94
132
|
_saveContext(type, graph, subject, predicate, object) {
|
|
@@ -110,7 +148,8 @@ export default class N3Parser {
|
|
|
110
148
|
// Prefix and base declarations are scoped to their formula,
|
|
111
149
|
// so record prefix changes to undo them when the formula ends
|
|
112
150
|
if (type === 'formula') {
|
|
113
|
-
context.base = [this._base, this._basePath, this._baseRoot, this._baseScheme
|
|
151
|
+
context.base = [this._base, this._basePath, this._baseRoot, this._baseScheme,
|
|
152
|
+
this._basePathHasDotSegments];
|
|
114
153
|
this._prefixChanges = [];
|
|
115
154
|
}
|
|
116
155
|
this._contextStack.push(context);
|
|
@@ -152,7 +191,8 @@ export default class N3Parser {
|
|
|
152
191
|
this._inversePredicate = context.inverse;
|
|
153
192
|
this._expectOf = context.expectOf;
|
|
154
193
|
if (type === 'formula')
|
|
155
|
-
[this._base, this._basePath, this._baseRoot, this._baseScheme
|
|
194
|
+
[this._base, this._basePath, this._baseRoot, this._baseScheme,
|
|
195
|
+
this._basePathHasDotSegments] = context.base;
|
|
156
196
|
if (this._prefixChanges !== context.prefixChanges) {
|
|
157
197
|
undoChanges(this._prefixes, this._prefixChanges);
|
|
158
198
|
this._prefixChanges = context.prefixChanges;
|
|
@@ -207,8 +247,13 @@ export default class N3Parser {
|
|
|
207
247
|
case 'GRAPH':
|
|
208
248
|
if (this._supportsNamedGraphs)
|
|
209
249
|
return this._readNamedGraphLabel;
|
|
210
|
-
// Otherwise, the next token must be a subject
|
|
250
|
+
// Otherwise, the next token must be a subject, unless it is an additional directive
|
|
211
251
|
default:
|
|
252
|
+
if (this._directives !== null && token.type in this._directives && this._graph === null) {
|
|
253
|
+
this._sparqlStyle = token.type[0] !== '@';
|
|
254
|
+
this._directiveCallback(this._directives[token.type]);
|
|
255
|
+
return this._readDeclarationPunctuation;
|
|
256
|
+
}
|
|
212
257
|
return this._readSubject(token);
|
|
213
258
|
}
|
|
214
259
|
}
|
|
@@ -339,13 +384,9 @@ export default class N3Parser {
|
|
|
339
384
|
case '<<(':
|
|
340
385
|
if (!this._n3Mode)
|
|
341
386
|
return this._error('Disallowed triple term as subject', token);
|
|
342
|
-
this.
|
|
343
|
-
this._graph = null;
|
|
344
|
-
return this._readSubject;
|
|
387
|
+
return this._enterTripleTerm('<<(', null, null, token) && this._readSubject;
|
|
345
388
|
case '<<':
|
|
346
|
-
this.
|
|
347
|
-
this._graph = null;
|
|
348
|
-
return this._readSubject;
|
|
389
|
+
return this._enterTripleTerm('<<', null, null, token) && this._readSubject;
|
|
349
390
|
default:
|
|
350
391
|
// Read the subject entity
|
|
351
392
|
if ((this._subject = this._readEntity(token)) === undefined)
|
|
@@ -490,13 +531,9 @@ export default class N3Parser {
|
|
|
490
531
|
this._graph = this._factory.blankNode());
|
|
491
532
|
return this._readInFormulaContext;
|
|
492
533
|
case '<<(':
|
|
493
|
-
this.
|
|
494
|
-
this._graph = null;
|
|
495
|
-
return this._readSubject;
|
|
534
|
+
return this._enterTripleTerm('<<(', this._subject, this._predicate, token) && this._readSubject;
|
|
496
535
|
case '<<':
|
|
497
|
-
this.
|
|
498
|
-
this._graph = null;
|
|
499
|
-
return this._readSubject;
|
|
536
|
+
return this._enterTripleTerm('<<', this._subject, this._predicate, token) && this._readSubject;
|
|
500
537
|
default:
|
|
501
538
|
// Read the object entity
|
|
502
539
|
if ((this._object = this._readEntity(token)) === undefined)
|
|
@@ -716,13 +753,9 @@ export default class N3Parser {
|
|
|
716
753
|
this._subject = null;
|
|
717
754
|
return this._readInFormulaContext;
|
|
718
755
|
case '<<(':
|
|
719
|
-
this._saveContext('<<(', this._graph, null, null, null);
|
|
720
|
-
this._graph = null;
|
|
721
|
-
next = this._readSubject;
|
|
722
|
-
break;
|
|
723
756
|
case '<<':
|
|
724
|
-
this.
|
|
725
|
-
|
|
757
|
+
if (!this._enterTripleTerm(token.type, null, null, token))
|
|
758
|
+
return;
|
|
726
759
|
next = this._readSubject;
|
|
727
760
|
break;
|
|
728
761
|
default:
|
|
@@ -1088,7 +1121,8 @@ export default class N3Parser {
|
|
|
1088
1121
|
_readVersion(token) {
|
|
1089
1122
|
if (token.type !== 'literal')
|
|
1090
1123
|
return this._error('Expected literal to follow version declaration', token);
|
|
1091
|
-
|
|
1124
|
+
// Only short strings are allowed, so no numbers or booleans (which have a datatype prefix)
|
|
1125
|
+
if (token.prefix !== '' || token.tripleQuoted)
|
|
1092
1126
|
return this._error('Version declarations must use single quotes', token);
|
|
1093
1127
|
this._versionCallback(token.value);
|
|
1094
1128
|
if (!this._isValidVersion(token.value))
|
|
@@ -1272,6 +1306,7 @@ export default class N3Parser {
|
|
|
1272
1306
|
const quad = this._createQuad(this._subject, this._predicate, this._object,
|
|
1273
1307
|
this._graph, this._inversePredicate);
|
|
1274
1308
|
this._restoreContext('<<(', token);
|
|
1309
|
+
this._tripleTermDepth--;
|
|
1275
1310
|
|
|
1276
1311
|
// If we're in a list, continue processing that list
|
|
1277
1312
|
const stack = this._contextStack, parent = stack.length && stack[stack.length - 1];
|
|
@@ -1307,6 +1342,7 @@ export default class N3Parser {
|
|
|
1307
1342
|
this._tripleTerm = null;
|
|
1308
1343
|
const reifier = this._readTripleTerm();
|
|
1309
1344
|
this._restoreContext('<<', token);
|
|
1345
|
+
this._tripleTermDepth--;
|
|
1310
1346
|
|
|
1311
1347
|
// // If we're in a list, continue processing that list
|
|
1312
1348
|
const stack = this._contextStack, parent = stack.length && stack[stack.length - 1];
|
|
@@ -1540,19 +1576,29 @@ export default class N3Parser {
|
|
|
1540
1576
|
}
|
|
1541
1577
|
// Resolve root-relative IRIs at the root of the base IRI
|
|
1542
1578
|
case '/':
|
|
1543
|
-
// Resolve scheme-relative IRIs to the scheme
|
|
1544
|
-
|
|
1579
|
+
// Resolve scheme-relative IRIs to the scheme,
|
|
1580
|
+
// keeping the reference's authority intact (RFC 3986 §5.2.4)
|
|
1581
|
+
if (iri[1] !== '/')
|
|
1582
|
+
return this._baseRoot + this._removeDotSegments(iri);
|
|
1583
|
+
// Without a base scheme, a leading ':' marks where the authority starts
|
|
1584
|
+
return this._baseScheme ? this._removeDotSegments(this._baseScheme + iri) :
|
|
1585
|
+
this._removeDotSegments(`:${iri}`).substr(1);
|
|
1545
1586
|
// Resolve all other IRIs at the base IRI's path
|
|
1546
1587
|
default:
|
|
1547
1588
|
// Relative IRIs cannot contain a colon in the first path segment
|
|
1548
|
-
|
|
1589
|
+
if (/^[^/:]*:/.test(iri))
|
|
1590
|
+
return null;
|
|
1591
|
+
// Only scan the joined IRI for dot segments if either part can contain them,
|
|
1592
|
+
// as the base path can be long and the joined IRI would need to be copied
|
|
1593
|
+
return this._basePathHasDotSegments || pathDotSegments.test(iri) ?
|
|
1594
|
+
this._removeDotSegments(this._basePath + iri) : this._basePath + iri;
|
|
1549
1595
|
}
|
|
1550
1596
|
}
|
|
1551
1597
|
|
|
1552
1598
|
// ### `_removeDotSegments` resolves './' and '../' path segments in an IRI as per RFC3986
|
|
1553
1599
|
_removeDotSegments(iri) {
|
|
1554
1600
|
// Don't modify the IRI if it does not contain any dot segments
|
|
1555
|
-
if (
|
|
1601
|
+
if (!dotSegments.test(iri))
|
|
1556
1602
|
return iri;
|
|
1557
1603
|
|
|
1558
1604
|
// Start with an imaginary slash before the IRI in order to resolve trailing './' and '../'
|
|
@@ -1566,8 +1612,8 @@ export default class N3Parser {
|
|
|
1566
1612
|
if (pathStart < 0) {
|
|
1567
1613
|
// Skip two slashes before the authority
|
|
1568
1614
|
if (iri[++i] === '/' && iri[++i] === '/')
|
|
1569
|
-
// Skip to
|
|
1570
|
-
while ((pathStart = i + 1) < length && iri[pathStart] !== '/')
|
|
1615
|
+
// Skip to the end of the authority
|
|
1616
|
+
while ((pathStart = i + 1) < length && iri[pathStart] !== '/' && iri[pathStart] !== '?' && iri[pathStart] !== '#')
|
|
1571
1617
|
i = pathStart;
|
|
1572
1618
|
}
|
|
1573
1619
|
break;
|
|
@@ -1618,13 +1664,14 @@ export default class N3Parser {
|
|
|
1618
1664
|
parse(input, quadCallback, prefixCallback, versionCallback) {
|
|
1619
1665
|
// The second parameter also accepts an object of named callbacks.
|
|
1620
1666
|
// As a second and third parameter it still accepts a separate quadCallback and prefixCallback for backward compatibility as well
|
|
1621
|
-
let onQuad, onPrefix, onComment, onVersion, onToken, onTokenEnd;
|
|
1667
|
+
let onQuad, onPrefix, onComment, onVersion, onDirective, onToken, onTokenEnd;
|
|
1622
1668
|
if (quadCallback && (quadCallback.onQuad || quadCallback.onPrefix || quadCallback.onComment || quadCallback.onVersion ||
|
|
1623
|
-
quadCallback.onToken || quadCallback.onTokenEnd)) {
|
|
1669
|
+
quadCallback.onDirective || quadCallback.onToken || quadCallback.onTokenEnd)) {
|
|
1624
1670
|
onQuad = quadCallback.onQuad;
|
|
1625
1671
|
onPrefix = quadCallback.onPrefix;
|
|
1626
1672
|
onComment = quadCallback.onComment;
|
|
1627
1673
|
onVersion = quadCallback.onVersion;
|
|
1674
|
+
onDirective = quadCallback.onDirective;
|
|
1628
1675
|
onToken = quadCallback.onToken;
|
|
1629
1676
|
onTokenEnd = quadCallback.onTokenEnd;
|
|
1630
1677
|
}
|
|
@@ -1645,6 +1692,7 @@ export default class N3Parser {
|
|
|
1645
1692
|
this._prefixes[''] = this._resolveIRI('#');
|
|
1646
1693
|
this._prefixCallback = onPrefix || noop;
|
|
1647
1694
|
this._versionCallback = onVersion || noop;
|
|
1695
|
+
this._directiveCallback = onDirective || noop;
|
|
1648
1696
|
this._inversePredicate = false;
|
|
1649
1697
|
this._expectOf = false;
|
|
1650
1698
|
this._quantified = Object.create(null);
|
|
@@ -1652,6 +1700,7 @@ export default class N3Parser {
|
|
|
1652
1700
|
this._prefixChanges = null;
|
|
1653
1701
|
this._quantifiedChanges = null;
|
|
1654
1702
|
this._emptyFormula = false;
|
|
1703
|
+
this._tripleTermDepth = 0;
|
|
1655
1704
|
|
|
1656
1705
|
let readToken = token => {
|
|
1657
1706
|
return this._readCallback = this._readCallback(token);
|
package/src/N3Reasoner.js
CHANGED
|
@@ -156,7 +156,7 @@ export default class N3Reasoner {
|
|
|
156
156
|
}
|
|
157
157
|
|
|
158
158
|
_createRule({ premise, conclusion }) {
|
|
159
|
-
const varMapping =
|
|
159
|
+
const varMapping = Object.create(null);
|
|
160
160
|
|
|
161
161
|
const toId = value => value.termType === 'Variable' ?
|
|
162
162
|
// If the term is a variable, then create an empty object that values can be placed into
|
package/src/N3Store.js
CHANGED
|
@@ -118,6 +118,35 @@ function difference(s1, s2, depth = 4) {
|
|
|
118
118
|
return target;
|
|
119
119
|
}
|
|
120
120
|
|
|
121
|
+
// Returns the key of a term in the entity index.
|
|
122
|
+
// Keys mark the term type by their first character, so the IRI of a named node
|
|
123
|
+
// that starts with such a marker (as relative IRIs can) is wrapped in < and >.
|
|
124
|
+
const markedIRI = /^[?_"[.<]/;
|
|
125
|
+
function entityKey(term) {
|
|
126
|
+
// Strings are term ids, which only need wrapping when they are IRIs starting with <
|
|
127
|
+
if (typeof term === 'string')
|
|
128
|
+
return term.charCodeAt(0) !== 0x3C || term === '<>' ? term : `<${term}>`;
|
|
129
|
+
// IDs of IRIs usually start with a lowercase scheme letter, which never marks a term type
|
|
130
|
+
const id = termToId(term);
|
|
131
|
+
if (id.charCodeAt(0) >= 0x61 || !term)
|
|
132
|
+
return id;
|
|
133
|
+
// Keys of quoted triples are built from the keys of their components
|
|
134
|
+
if (term.termType === 'Quad')
|
|
135
|
+
return JSON.stringify(quadKeyParts(term));
|
|
136
|
+
return term.termType !== 'NamedNode' || !markedIRI.test(term.value) ? id : `<${term.value}>`;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Returns the keys of the components of a quad, nested like the parts of its internal id
|
|
140
|
+
function quadKeyParts(quad) {
|
|
141
|
+
const parts = [nestedKey(quad.subject), nestedKey(quad.predicate), nestedKey(quad.object)];
|
|
142
|
+
if (quad.graph && !isDefaultGraph(quad.graph))
|
|
143
|
+
parts.push(nestedKey(quad.graph));
|
|
144
|
+
return parts;
|
|
145
|
+
}
|
|
146
|
+
function nestedKey(term) {
|
|
147
|
+
return term.termType === 'Quad' ? quadKeyParts(term) : entityKey(term);
|
|
148
|
+
}
|
|
149
|
+
|
|
121
150
|
// ## Constructor
|
|
122
151
|
export class N3EntityIndex {
|
|
123
152
|
constructor(options = {}) {
|
|
@@ -135,6 +164,10 @@ export class N3EntityIndex {
|
|
|
135
164
|
}
|
|
136
165
|
|
|
137
166
|
_termFromId(id) {
|
|
167
|
+
// A key in < and > is a named node
|
|
168
|
+
if (id[0] === '<')
|
|
169
|
+
return this._factory.namedNode(id.substring(1, id.length - 1));
|
|
170
|
+
// A key starting with . is a quoted triple
|
|
138
171
|
if (id[0] === '.') {
|
|
139
172
|
const entities = this._entities;
|
|
140
173
|
const terms = id.split('.');
|
|
@@ -159,18 +192,34 @@ export class N3EntityIndex {
|
|
|
159
192
|
return s && p && o && (isDefaultGraph(term.graph) || (g = this._termToNumericId(term.graph))) &&
|
|
160
193
|
this._ids[g ? `.${s}.${p}.${o}.${g}` : `.${s}.${p}.${o}`];
|
|
161
194
|
}
|
|
162
|
-
return this._ids[
|
|
195
|
+
return typeof term === 'string' ? this._stringToNumericId(term) : this._ids[entityKey(term)];
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Returns the numeric id of a term given as a string id
|
|
199
|
+
_stringToNumericId(term) {
|
|
200
|
+
// The string is the key unless it is an IRI starting with <. Read the first character
|
|
201
|
+
// from the stored key after a match, which is cheaper than from a concatenated string.
|
|
202
|
+
const id = this._ids[term];
|
|
203
|
+
if (id ? this._entities[id].charCodeAt(0) !== 0x3C : term.charCodeAt(0) !== 0x3C)
|
|
204
|
+
return id;
|
|
205
|
+
return term === '<>' ? id : this._ids[`<${term}>`];
|
|
163
206
|
}
|
|
164
207
|
|
|
165
208
|
_termToNewNumericId(term) {
|
|
209
|
+
if (typeof term === 'string')
|
|
210
|
+
return this._stringToNumericId(term) || this._addEntity(entityKey(term));
|
|
166
211
|
// This assumes that no graph term is present - we may wish to error if there is one
|
|
167
212
|
const str = term && term.termType === 'Quad' ?
|
|
168
213
|
`.${this._termToNewNumericId(term.subject)}.${this._termToNewNumericId(term.predicate)}.${this._termToNewNumericId(term.object)}${
|
|
169
214
|
isDefaultGraph(term.graph) ? '' : `.${this._termToNewNumericId(term.graph)}`
|
|
170
215
|
}`
|
|
171
|
-
:
|
|
216
|
+
: entityKey(term);
|
|
217
|
+
|
|
218
|
+
return this._ids[str] || this._addEntity(str);
|
|
219
|
+
}
|
|
172
220
|
|
|
173
|
-
|
|
221
|
+
_addEntity(key) {
|
|
222
|
+
return this._ids[this._entities[++this._id] = key] = this._id;
|
|
174
223
|
}
|
|
175
224
|
|
|
176
225
|
createBlankNode(suggestedName) {
|
|
@@ -600,9 +649,12 @@ export default class N3Store {
|
|
|
600
649
|
}
|
|
601
650
|
|
|
602
651
|
// ### `removeQuads` removes multiple quads from the store
|
|
652
|
+
// returns `true` if all quads were removed, `false` if some were not found
|
|
603
653
|
removeQuads(quads) {
|
|
654
|
+
let removed = true;
|
|
604
655
|
for (let i = 0; i < quads.length; i++)
|
|
605
|
-
this.removeQuad(quads[i]);
|
|
656
|
+
removed = this.removeQuad(quads[i]) && removed;
|
|
657
|
+
return removed;
|
|
606
658
|
}
|
|
607
659
|
|
|
608
660
|
// ### `remove` removes a stream of quads from the store
|
|
@@ -1382,7 +1434,7 @@ function intersectMatchPatterns(left, right) {
|
|
|
1382
1434
|
const leftTerm = left[i], rightTerm = right[i];
|
|
1383
1435
|
if (leftTerm === null || leftTerm === undefined)
|
|
1384
1436
|
result[i] = rightTerm;
|
|
1385
|
-
else if (rightTerm === null || rightTerm === undefined ||
|
|
1437
|
+
else if (rightTerm === null || rightTerm === undefined || entityKey(leftTerm) === entityKey(rightTerm))
|
|
1386
1438
|
result[i] = leftTerm;
|
|
1387
1439
|
else
|
|
1388
1440
|
return false;
|
|
@@ -1433,10 +1485,10 @@ class DatasetCoreAndReadableStream extends Readable {
|
|
|
1433
1485
|
_matchesQuad(quad) {
|
|
1434
1486
|
const { subject, predicate, object, graph } = this;
|
|
1435
1487
|
return !this._matchesNothing &&
|
|
1436
|
-
(subject === null || subject === undefined ||
|
|
1437
|
-
(predicate === null || predicate === undefined ||
|
|
1438
|
-
(object === null || object === undefined ||
|
|
1439
|
-
(graph === null || graph === undefined ||
|
|
1488
|
+
(subject === null || subject === undefined || entityKey(subject) === entityKey(quad.subject)) &&
|
|
1489
|
+
(predicate === null || predicate === undefined || entityKey(predicate) === entityKey(quad.predicate)) &&
|
|
1490
|
+
(object === null || object === undefined || entityKey(object) === entityKey(quad.object)) &&
|
|
1491
|
+
(graph === null || graph === undefined || entityKey(graph) === entityKey(quad.graph));
|
|
1440
1492
|
}
|
|
1441
1493
|
|
|
1442
1494
|
// ### `_assertMatchesPattern` rejects a Quad outside this view.
|
package/src/N3StreamParser.js
CHANGED
|
@@ -16,6 +16,8 @@ export default class N3StreamParser extends Transform {
|
|
|
16
16
|
onQuad: (error, quad) => { error && this.emit('error', error) || quad && this.push(quad); },
|
|
17
17
|
// Emit prefixes through the `prefix` event
|
|
18
18
|
onPrefix: (prefix, uri) => { this.emit('prefix', prefix, uri); },
|
|
19
|
+
// Emit additional directives through the `directive` event
|
|
20
|
+
onDirective: name => { this.emit('directive', name); },
|
|
19
21
|
};
|
|
20
22
|
|
|
21
23
|
if (options && options.comments)
|
package/src/N3Writer.js
CHANGED
|
@@ -8,15 +8,25 @@ import { escapeRegex } from './Util';
|
|
|
8
8
|
const DEFAULTGRAPH = N3DataFactory.defaultGraph();
|
|
9
9
|
|
|
10
10
|
const { rdf, xsd } = namespaces;
|
|
11
|
+
const { hasOwnProperty } = Object.prototype;
|
|
11
12
|
|
|
12
|
-
// Characters in
|
|
13
|
+
// Characters that require escaping in Turtle, TriG, and N3,
|
|
14
|
+
// where characters outside the Basic Multilingual Plane are escaped as well
|
|
13
15
|
const escape = /["\\\t\n\r\b\f\u0000-\u0019\ud800-\udbff]/,
|
|
14
16
|
escapeAll = /["\\\t\n\r\b\f\u0000-\u0019]|[\ud800-\udbff][\udc00-\udfff]/g,
|
|
17
|
+
// Characters that require escaping in canonical N-Triples and N-Quads:
|
|
18
|
+
// U+0000–U+001F, `"`, `\`, U+007F, U+FFFE, and U+FFFF.
|
|
19
|
+
// A negated class is as fast to test as a shorter class listing characters.
|
|
20
|
+
canonicalEscape = /[^ !#-\[\]-~\u0080-\uFFFD]/,
|
|
21
|
+
canonicalEscapeAll = /[^ !#-\[\]-~\u0080-\uFFFD]/g,
|
|
15
22
|
escapedCharacters = {
|
|
16
23
|
'\\': '\\\\', '"': '\\"', '\t': '\\t',
|
|
17
24
|
'\n': '\\n', '\r': '\\r', '\b': '\\b', '\f': '\\f',
|
|
18
25
|
};
|
|
19
26
|
|
|
27
|
+
// Characters that a version label cannot contain
|
|
28
|
+
const invalidVersionLabel = /["\\\u0000-\u001f\u007f]|[\ud800-\udbff](?![\udc00-\udfff])|(?:^|[^\ud800-\udbff])[\udc00-\udfff]/;
|
|
29
|
+
|
|
20
30
|
// ## Placeholder class to represent already pretty-printed terms
|
|
21
31
|
class SerializedTerm extends Term {
|
|
22
32
|
// Pretty-printed nodes are not equal to any other node
|
|
@@ -53,12 +63,22 @@ export default class N3Writer {
|
|
|
53
63
|
this._endStream = options.end === undefined ? true : !!options.end;
|
|
54
64
|
}
|
|
55
65
|
|
|
66
|
+
// A version label is written as-is, so it cannot contain characters that need escaping,
|
|
67
|
+
// nor unpaired surrogates (which cannot be encoded)
|
|
68
|
+
if (options.version && invalidVersionLabel.test(options.version))
|
|
69
|
+
throw new Error(`Invalid version label: ${JSON.stringify(options.version)}`);
|
|
70
|
+
|
|
56
71
|
// Initialize writer, depending on the format
|
|
57
72
|
this._subject = null;
|
|
58
73
|
if (!(/triple|quad/i).test(options.format)) {
|
|
59
74
|
this._lineMode = false;
|
|
75
|
+
this._escape = escape, this._escapeAll = escapeAll, this._characterReplacer = characterReplacer;
|
|
60
76
|
this._graph = DEFAULTGRAPH;
|
|
77
|
+
if (options.version)
|
|
78
|
+
this._write(`@version "${options.version}".\n`);
|
|
61
79
|
this._prefixIRIs = Object.create(null);
|
|
80
|
+
// Escaped prefix IRIs and names for the prefix matcher, computed once per prefix
|
|
81
|
+
this._prefixPatterns = Object.create(null);
|
|
62
82
|
if (options.baseIRI) {
|
|
63
83
|
this._baseIri = new BaseIRI(options.baseIRI);
|
|
64
84
|
if (options.writeBase !== false)
|
|
@@ -69,6 +89,11 @@ export default class N3Writer {
|
|
|
69
89
|
else {
|
|
70
90
|
this._lineMode = true;
|
|
71
91
|
this._writeQuad = this._writeQuadLine;
|
|
92
|
+
// N-Triples and N-Quads are written in their canonical form
|
|
93
|
+
this._escape = canonicalEscape, this._escapeAll = canonicalEscapeAll;
|
|
94
|
+
this._characterReplacer = canonicalCharacterReplacer;
|
|
95
|
+
if (options.version)
|
|
96
|
+
this._write(`VERSION "${options.version}"\n`);
|
|
72
97
|
}
|
|
73
98
|
}
|
|
74
99
|
|
|
@@ -84,6 +109,15 @@ export default class N3Writer {
|
|
|
84
109
|
this._outputStream.write(string, 'utf8', callback);
|
|
85
110
|
}
|
|
86
111
|
|
|
112
|
+
// ### `_endStatement` finishes a pending statement and closes an open graph block
|
|
113
|
+
_endStatement() {
|
|
114
|
+
if (this._subject !== null) {
|
|
115
|
+
this._write(this._inDefaultGraph ? '.\n' : '\n}\n');
|
|
116
|
+
this._subject = null;
|
|
117
|
+
this._graph = DEFAULTGRAPH;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
87
121
|
// ### `_writeQuad` writes the quad to the output stream
|
|
88
122
|
_writeQuad(subject, predicate, object, graph, done) {
|
|
89
123
|
try {
|
|
@@ -161,7 +195,7 @@ export default class N3Writer {
|
|
|
161
195
|
// A blank node or list is represented as-is
|
|
162
196
|
if (entity.termType !== 'NamedNode') {
|
|
163
197
|
// If it is a list head, pretty-print it
|
|
164
|
-
if (this._lists && (entity.value
|
|
198
|
+
if (this._lists && hasOwnProperty.call(this._lists, entity.value))
|
|
165
199
|
entity = this.list(this._lists[entity.value]);
|
|
166
200
|
// Terms from this library already hold their serialization as id
|
|
167
201
|
if (entity instanceof Term)
|
|
@@ -174,10 +208,10 @@ export default class N3Writer {
|
|
|
174
208
|
iri = this._baseIri.toRelative(iri);
|
|
175
209
|
}
|
|
176
210
|
// Escape special characters
|
|
177
|
-
if (
|
|
178
|
-
iri = iri.replace(
|
|
211
|
+
if (this._escape.test(iri))
|
|
212
|
+
iri = iri.replace(this._escapeAll, this._characterReplacer);
|
|
179
213
|
// Try to represent the IRI as prefixed name, unless no prefixes were added
|
|
180
|
-
const prefixMatch = this._hasPrefixes ? this._prefixRegex.exec(iri) : null;
|
|
214
|
+
const prefixMatch = this._hasPrefixes ? (this._prefixRegex || this._createPrefixRegex()).exec(iri) : null;
|
|
181
215
|
return !prefixMatch ? `<${iri}>` : this._prefixIRIs[prefixMatch[1]] + prefixMatch[2];
|
|
182
216
|
}
|
|
183
217
|
|
|
@@ -185,8 +219,8 @@ export default class N3Writer {
|
|
|
185
219
|
_encodeLiteral(literal) {
|
|
186
220
|
// Escape special characters
|
|
187
221
|
let value = literal.value;
|
|
188
|
-
if (
|
|
189
|
-
value = value.replace(
|
|
222
|
+
if (this._escape.test(value))
|
|
223
|
+
value = value.replace(this._escapeAll, this._characterReplacer);
|
|
190
224
|
|
|
191
225
|
// Write a language-tagged literal
|
|
192
226
|
const language = literal.language;
|
|
@@ -257,11 +291,13 @@ export default class N3Writer {
|
|
|
257
291
|
|
|
258
292
|
// ### `_encodeQuad` encodes an RDF-star quad
|
|
259
293
|
_encodeQuad({ subject, predicate, object, graph }) {
|
|
260
|
-
|
|
294
|
+
// Canonical N-Triples and N-Quads put spaces inside the delimiters
|
|
295
|
+
const space = this._lineMode ? ' ' : '';
|
|
296
|
+
return `<<(${space}${
|
|
261
297
|
this._encodeSubject(subject)} ${
|
|
262
298
|
this._encodePredicate(predicate)} ${
|
|
263
299
|
this._encodeObject(object)}${
|
|
264
|
-
isDefaultGraph(graph) ? '' : ` ${this._encodeIriOrBlank(graph)}`})>>`;
|
|
300
|
+
isDefaultGraph(graph) ? '' : ` ${this._encodeIriOrBlank(graph)}`}${space})>>`;
|
|
265
301
|
}
|
|
266
302
|
|
|
267
303
|
// ### `_blockedWrite` replaces `_write` after the writer has been closed
|
|
@@ -309,27 +345,30 @@ export default class N3Writer {
|
|
|
309
345
|
iri = iri.value;
|
|
310
346
|
hasPrefixes = true;
|
|
311
347
|
// Finish a possible pending quad
|
|
312
|
-
|
|
313
|
-
this._write(this._inDefaultGraph ? '.\n' : '\n}\n');
|
|
314
|
-
this._subject = null, this._graph = '';
|
|
315
|
-
}
|
|
348
|
+
this._endStatement();
|
|
316
349
|
// Store and write the prefix
|
|
317
350
|
this._prefixIRIs[iri] = (prefix += ':');
|
|
351
|
+
this._prefixPatterns[iri] = escapeRegex(iri);
|
|
318
352
|
this._write(`@prefix ${prefix} <${iri}>.\n`);
|
|
319
353
|
}
|
|
320
|
-
// Recreate the prefix matcher
|
|
354
|
+
// Recreate the prefix matcher when it is next needed, so that adding
|
|
355
|
+
// prefixes one by one does not rebuild it for every prefix
|
|
321
356
|
if (hasPrefixes) {
|
|
322
357
|
this._hasPrefixes = true;
|
|
323
|
-
|
|
324
|
-
for (const prefixIRI in this._prefixIRIs)
|
|
325
|
-
IRIlist += IRIlist ? `|${prefixIRI}` : prefixIRI;
|
|
326
|
-
IRIlist = escapeRegex(IRIlist, /[\]\/\(\)\*\+\?\.\\\$]/g, '\\$&');
|
|
327
|
-
this._prefixRegex = new RegExp(`^(${IRIlist})([_a-zA-Z0-9](?:\\.?[\\-_a-zA-Z0-9])*)$`);
|
|
358
|
+
this._prefixRegex = null;
|
|
328
359
|
}
|
|
329
360
|
// End a prefix block with a newline
|
|
330
361
|
this._write(hasPrefixes ? '\n' : '', done);
|
|
331
362
|
}
|
|
332
363
|
|
|
364
|
+
// ### `_createPrefixRegex` creates the matcher for the current prefixes
|
|
365
|
+
_createPrefixRegex() {
|
|
366
|
+
let IRIlist = '';
|
|
367
|
+
for (const prefixIRI in this._prefixPatterns)
|
|
368
|
+
IRIlist += IRIlist ? `|${this._prefixPatterns[prefixIRI]}` : this._prefixPatterns[prefixIRI];
|
|
369
|
+
return this._prefixRegex = new RegExp(`^(${IRIlist})([_a-zA-Z0-9](?:\\.?[\\-_a-zA-Z0-9])*)$`);
|
|
370
|
+
}
|
|
371
|
+
|
|
333
372
|
// ### `blank` creates a blank node with the given content
|
|
334
373
|
blank(predicate, object) {
|
|
335
374
|
let children = predicate, child, length;
|
|
@@ -385,10 +424,7 @@ export default class N3Writer {
|
|
|
385
424
|
// ### `end` signals the end of the output stream
|
|
386
425
|
end(done) {
|
|
387
426
|
// Finish a possible pending quad
|
|
388
|
-
|
|
389
|
-
this._write(this._inDefaultGraph ? '.\n' : '\n}\n');
|
|
390
|
-
this._subject = null;
|
|
391
|
-
}
|
|
427
|
+
this._endStatement();
|
|
392
428
|
// Disallow further writing
|
|
393
429
|
this._write = this._blockedWrite;
|
|
394
430
|
|
|
@@ -421,3 +457,14 @@ function characterReplacer(character) {
|
|
|
421
457
|
}
|
|
422
458
|
return result;
|
|
423
459
|
}
|
|
460
|
+
|
|
461
|
+
// Replaces a character by its canonical N-Triples escape:
|
|
462
|
+
// a short escape where one exists, and an upper-case `\uXXXX` escape otherwise
|
|
463
|
+
function canonicalCharacterReplacer(character) {
|
|
464
|
+
let result = escapedCharacters[character];
|
|
465
|
+
if (result === undefined) {
|
|
466
|
+
result = character.charCodeAt(0).toString(16).toUpperCase();
|
|
467
|
+
result = '\\u0000'.substr(0, 6 - result.length) + result;
|
|
468
|
+
}
|
|
469
|
+
return result;
|
|
470
|
+
}
|
package/src/Util.js
CHANGED