Skip to content

Commit 2ed11b7

Browse files
Merge pull request #210 from simonvbrae/feature/rdfstar_quad_extends_term
Added RDF* support by extending Quad from Term
2 parents 7cef7e1 + 4dba7f4 commit 2ed11b7

File tree

5 files changed

+687
-50
lines changed

5 files changed

+687
-50
lines changed

src/N3DataFactory.js

Lines changed: 47 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,15 @@
22
// See https://github.com/rdfjs/representation-task-force/blob/master/interface-spec.md
33

44
import namespaces from './IRIs';
5+
import { isDefaultGraph } from './N3Util';
56
const { rdf, xsd } = namespaces;
67

78
let DEFAULTGRAPH;
89
let _blankNodeCounter = 0;
910

11+
const escapedLiteral = /^"(.*".*)(?="[^"]*$)/;
12+
const quadId = /^<<("(?:""|[^"])*"[^ ]*|[^ ]+) ("(?:""|[^"])*"[^ ]*|[^ ]+) ("(?:""|[^"])*"[^ ]*|[^ ]+) ?("(?:""|[^"])*"[^ ]*|[^ ]+)?>>$/;
13+
1014
// ## DataFactory singleton
1115
const DataFactory = {
1216
namedNode,
@@ -186,8 +190,10 @@ export function termFromId(id, factory) {
186190

187191
// Identify the term type based on the first character
188192
switch (id[0]) {
189-
case '_': return factory.blankNode(id.substr(2));
190-
case '?': return factory.variable(id.substr(1));
193+
case '?':
194+
return factory.variable(id.substr(1));
195+
case '_':
196+
return factory.blankNode(id.substr(2));
191197
case '"':
192198
// Shortcut for internal literals
193199
if (factory === DataFactory)
@@ -200,15 +206,24 @@ export function termFromId(id, factory) {
200206
return factory.literal(id.substr(1, endPos - 1),
201207
id[endPos + 1] === '@' ? id.substr(endPos + 2)
202208
: factory.namedNode(id.substr(endPos + 3)));
203-
default: return factory.namedNode(id);
209+
case '<':
210+
const components = quadId.exec(id);
211+
return factory.quad(
212+
termFromId(unescapeQuotes(components[1]), factory),
213+
termFromId(unescapeQuotes(components[2]), factory),
214+
termFromId(unescapeQuotes(components[3]), factory),
215+
components[4] && termFromId(unescapeQuotes(components[4]), factory)
216+
);
217+
default:
218+
return factory.namedNode(id);
204219
}
205220
}
206221

207222
// ### Constructs an internal string ID from the given term or ID string
208223
export function termToId(term) {
209224
if (typeof term === 'string')
210225
return term;
211-
if (term instanceof Term)
226+
if (term instanceof Term && term.termType !== 'Quad')
212227
return term.id;
213228
if (!term)
214229
return DEFAULTGRAPH.id;
@@ -222,20 +237,38 @@ export function termToId(term) {
222237
case 'Literal': return '"' + term.value + '"' +
223238
(term.language ? '@' + term.language :
224239
(term.datatype && term.datatype.value !== xsd.string ? '^^' + term.datatype.value : ''));
240+
case 'Quad':
241+
// To identify RDF* quad components, we escape quotes by doubling them.
242+
// This avoids the overhead of backslash parsing of Turtle-like syntaxes.
243+
return `<<${
244+
escapeQuotes(termToId(term.subject))
245+
} ${
246+
escapeQuotes(termToId(term.predicate))
247+
} ${
248+
escapeQuotes(termToId(term.object))
249+
}${
250+
(isDefaultGraph(term.graph)) ? '' : ` ${termToId(term.graph)}`
251+
}>>`;
225252
default: throw new Error('Unexpected termType: ' + term.termType);
226253
}
227254
}
228255

229256

230257
// ## Quad constructor
231-
export class Quad {
258+
export class Quad extends Term {
232259
constructor(subject, predicate, object, graph) {
260+
super('');
233261
this.subject = subject;
234262
this.predicate = predicate;
235263
this.object = object;
236264
this.graph = graph || DEFAULTGRAPH;
237265
}
238266

267+
// ### The term type of this term
268+
get termType() {
269+
return 'Quad';
270+
}
271+
239272
// ### Returns a plain object representation of this quad
240273
toJSON() {
241274
return {
@@ -256,6 +289,15 @@ export class Quad {
256289
}
257290
export { Quad as Triple };
258291

292+
// ### Escapes the quotes within the given literal
293+
export function escapeQuotes(id) {
294+
return id.replace(escapedLiteral, (_, quoted) => `"${quoted.replace(/"/g, '""')}`);
295+
}
296+
297+
// ### Unescapes the quotes within the given literal
298+
export function unescapeQuotes(id) {
299+
return id.replace(escapedLiteral, (_, quoted) => `"${quoted.replace(/""/g, '"')}`);
300+
}
259301

260302
// ### Creates an IRI
261303
function namedNode(iri) {

test/N3DataFactory-test.js

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,54 @@ describe('DataFactory', function () {
130130
));
131131
});
132132

133+
it('should return a nested quad', function () {
134+
DataFactory.quad(
135+
new Quad(
136+
new NamedNode('http://ex.org/a'),
137+
new NamedNode('http://ex.org/b'),
138+
new Literal('abc'),
139+
new NamedNode('http://ex.org/d')
140+
),
141+
new NamedNode('http://ex.org/b'),
142+
new Literal('abc'),
143+
new NamedNode('http://ex.org/d')
144+
).should.deep.equal(new Quad(
145+
new Quad(
146+
new NamedNode('http://ex.org/a'),
147+
new NamedNode('http://ex.org/b'),
148+
new Literal('abc'),
149+
new NamedNode('http://ex.org/d')
150+
),
151+
new NamedNode('http://ex.org/b'),
152+
new Literal('abc'),
153+
new NamedNode('http://ex.org/d')
154+
));
155+
});
156+
157+
it('should return a nested quad', function () {
158+
DataFactory.quad(
159+
new NamedNode('http://ex.org/a'),
160+
new NamedNode('http://ex.org/b'),
161+
new Literal('abc'),
162+
new Quad(
163+
new NamedNode('http://ex.org/a'),
164+
new NamedNode('http://ex.org/b'),
165+
new Literal('abc'),
166+
new NamedNode('http://ex.org/d')
167+
)
168+
).should.deep.equal(new Quad(
169+
new NamedNode('http://ex.org/a'),
170+
new NamedNode('http://ex.org/b'),
171+
new Literal('abc'),
172+
new Quad(
173+
new NamedNode('http://ex.org/a'),
174+
new NamedNode('http://ex.org/b'),
175+
new Literal('abc'),
176+
new NamedNode('http://ex.org/d')
177+
)
178+
));
179+
});
180+
133181
it('without graph parameter returns a quad in the default graph', function () {
134182
DataFactory.quad(
135183
new NamedNode('http://ex.org/a'),

0 commit comments

Comments
 (0)