Skip to content

Commit d258f22

Browse files
committed
chore: fix round trip on deeply nested rdfstar triples
1 parent 99d6b72 commit d258f22

2 files changed

Lines changed: 86 additions & 46 deletions

File tree

src/N3DataFactory.js

Lines changed: 23 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ let DEFAULTGRAPH;
1010
let _blankNodeCounter = 0;
1111

1212
const escapedLiteral = /^"(.*".*)(?="[^"]*$)/;
13-
const quadId = /^<<("(?:""|[^"])*"[^ ]*|[^ ]+) ("(?:""|[^"])*"[^ ]*|[^ ]+) ("(?:""|[^"])*"[^ ]*|[^ ]+) ?("(?:""|[^"])*"[^ ]*|[^ ]+)?>>$/;
1413

1514
// ## DataFactory singleton
1615
const DataFactory = {
@@ -188,9 +187,8 @@ export class DefaultGraph extends Term {
188187
// ## DefaultGraph singleton
189188
DEFAULTGRAPH = new DefaultGraph();
190189

191-
192190
// ### Constructs a term from the given internal string ID
193-
export function termFromId(id, factory) {
191+
export function termFromId(id, factory, nested) {
194192
factory = factory || DataFactory;
195193

196194
// Falsy value or empty string indicate the default graph
@@ -215,21 +213,24 @@ export function termFromId(id, factory) {
215213
return factory.literal(id.substr(1, endPos - 1),
216214
id[endPos + 1] === '@' ? id.substr(endPos + 2)
217215
: factory.namedNode(id.substr(endPos + 3)));
218-
case '<':
219-
const components = quadId.exec(id);
220-
return factory.quad(
221-
termFromId(unescapeQuotes(components[1]), factory),
222-
termFromId(unescapeQuotes(components[2]), factory),
223-
termFromId(unescapeQuotes(components[3]), factory),
224-
components[4] && termFromId(unescapeQuotes(components[4]), factory)
225-
);
216+
case '[':
217+
id = JSON.parse(id);
218+
break;
226219
default:
227-
return factory.namedNode(id);
220+
if (!nested || !Array.isArray(id)) {
221+
return factory.namedNode(id);
222+
}
228223
}
224+
return factory.quad(
225+
termFromId(id[0], factory, true),
226+
termFromId(id[1], factory, true),
227+
termFromId(id[2], factory, true),
228+
id[3] && termFromId(id[3], factory, true)
229+
);
229230
}
230231

231232
// ### Constructs an internal string ID from the given term or ID string
232-
export function termToId(term) {
233+
export function termToId(term, nested) {
233234
if (typeof term === 'string')
234235
return term;
235236
if (term instanceof Term && term.termType !== 'Quad')
@@ -249,15 +250,15 @@ export function termToId(term) {
249250
case 'Quad':
250251
// To identify RDF-star quad components, we escape quotes by doubling them.
251252
// This avoids the overhead of backslash parsing of Turtle-like syntaxes.
252-
return `<<${
253-
escapeQuotes(termToId(term.subject))
254-
} ${
255-
escapeQuotes(termToId(term.predicate))
256-
} ${
257-
escapeQuotes(termToId(term.object))
258-
}${
259-
(isDefaultGraph(term.graph)) ? '' : ` ${termToId(term.graph)}`
260-
}>>`;
253+
const res = [
254+
termToId(term.subject, true),
255+
termToId(term.predicate, true),
256+
termToId(term.object, true),
257+
];
258+
if (!isDefaultGraph(term.graph)) {
259+
res.push(termToId(term.graph, true));
260+
}
261+
return nested ? res : JSON.stringify(res);
261262
default: throw new Error(`Unexpected termType: ${term.termType}`);
262263
}
263264
}

test/Term-test.js

Lines changed: 63 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -81,16 +81,17 @@ describe('Term', () => {
8181
});
8282

8383
it('should create a Quad with the default graph if the id doesnt specify the graph', () => {
84-
termFromId('<<http://ex.org/a http://ex.org/b "abc"@en-us>>').should.deep.equal(new Quad(
84+
const q = new Quad(
8585
new NamedNode('http://ex.org/a'),
8686
new NamedNode('http://ex.org/b'),
8787
new Literal('"abc"@en-us'),
8888
new DefaultGraph()
89-
));
89+
);
90+
expect(q.equals(termFromId(termToId(q)))).equals(true);
9091
});
9192

9293
it('should create a Quad with the correct graph if the id specifies a graph', () => {
93-
const id = '<<http://ex.org/a http://ex.org/b "abc"@en-us http://ex.org/d>>';
94+
const id = '["http://ex.org/a", "http://ex.org/b", "\\"abc\\"@en-us", "http://ex.org/d"]';
9495
termFromId(id).should.deep.equal(new Quad(
9596
new NamedNode('http://ex.org/a'),
9697
new NamedNode('http://ex.org/b'),
@@ -100,7 +101,7 @@ describe('Term', () => {
100101
});
101102

102103
it('should create a Quad correctly', () => {
103-
const id = '<<http://ex.org/a http://ex.org/b http://ex.org/c>>';
104+
const id = '["http://ex.org/a", "http://ex.org/b", "http://ex.org/c"]';
104105
termFromId(id).should.deep.equal(new Quad(
105106
new NamedNode('http://ex.org/a'),
106107
new NamedNode('http://ex.org/b'),
@@ -110,7 +111,7 @@ describe('Term', () => {
110111
});
111112

112113
it('should create a Quad correctly', () => {
113-
const id = '<<_:n3-123 ?var-a ?var-b _:n3-000>>';
114+
const id = '["_:n3-123", "?var-a", "?var-b", "_:n3-000"]';
114115
termFromId(id).should.deep.equal(new Quad(
115116
new BlankNode('n3-123'),
116117
new Variable('var-a'),
@@ -120,7 +121,7 @@ describe('Term', () => {
120121
});
121122

122123
it('should create a Quad correctly', () => {
123-
const id = '<<?var-a ?var-b "abc"@en-us ?var-d>>';
124+
const id = '["?var-a", "?var-b", "\\"abc\\"@en-us", "?var-d"]';
124125
termFromId(id).should.deep.equal(new Quad(
125126
new Variable('var-a'),
126127
new Variable('var-b'),
@@ -130,7 +131,7 @@ describe('Term', () => {
130131
});
131132

132133
it('should create a Quad correctly', () => {
133-
const id = '<<_:n3-000 ?var-b _:n3-123 http://ex.org/d>>';
134+
const id = '["_:n3-000", "?var-b", "_:n3-123", "http://ex.org/d"]';
134135
termFromId(id).should.deep.equal(new Quad(
135136
new BlankNode('n3-000'),
136137
new Variable('var-b'),
@@ -140,7 +141,7 @@ describe('Term', () => {
140141
});
141142

142143
it('should create a Quad correctly from literal containing escaped quotes', () => {
143-
const id = '<<_:n3-000 ?var-b "Hello ""W""orl""d!"@en-us http://ex.org/d>>';
144+
const id = '["_:n3-000", "?var-b", "\\"Hello \\"W\\"orl\\"d!\\"@en-us", "http://ex.org/d"]';
144145
termFromId(id).should.deep.equal(new Quad(
145146
new BlankNode('n3-000'),
146147
new Variable('var-b'),
@@ -150,13 +151,14 @@ describe('Term', () => {
150151
});
151152

152153
it('should create a Quad correctly from literal containing escaped quotes', () => {
153-
const id = '<<"Hello ""W""orl""d!"@en-us http://ex.org/b http://ex.org/c>>';
154-
termFromId(id).should.deep.equal(new Quad(
154+
const q = new Quad(
155155
new Literal('"Hello "W"orl"d!"@en-us'),
156156
new NamedNode('http://ex.org/b'),
157157
new NamedNode('http://ex.org/c'),
158158
new DefaultGraph()
159-
));
159+
);
160+
161+
termFromId(termToId(q)).should.deep.equal(q);
160162
});
161163

162164
describe('with a custom factory', () => {
@@ -283,7 +285,7 @@ describe('Term', () => {
283285
new NamedNode('http://ex.org/b'),
284286
new Literal('"abc"@en-us'),
285287
new DefaultGraph()
286-
)).should.equal('<<http://ex.org/a http://ex.org/b "abc"@en-us>>');
288+
)).should.equal('["http://ex.org/a","http://ex.org/b","\\"abc\\"@en-us"]');
287289
});
288290

289291
it('should create an id from a Quad', () => {
@@ -292,7 +294,7 @@ describe('Term', () => {
292294
new NamedNode('http://ex.org/b'),
293295
new Literal('"abc"@en-us'),
294296
new NamedNode('http://ex.org/d')
295-
)).should.equal('<<http://ex.org/a http://ex.org/b "abc"@en-us http://ex.org/d>>');
297+
)).should.equal('["http://ex.org/a","http://ex.org/b","\\"abc\\"@en-us","http://ex.org/d"]');
296298
});
297299

298300
it('should create an id from a manually created Quad', () => {
@@ -303,7 +305,7 @@ describe('Term', () => {
303305
graph: new NamedNode('http://ex.org/d'),
304306
termType: 'Quad',
305307
value: '',
306-
}).should.equal('<<http://ex.org/a http://ex.org/b "abc"@en-us http://ex.org/d>>');
308+
}).should.equal('["http://ex.org/a","http://ex.org/b","\\"abc\\"@en-us","http://ex.org/d"]');
307309
});
308310

309311
it('should create an id with escaped literals from a Quad', () => {
@@ -312,7 +314,7 @@ describe('Term', () => {
312314
new Variable('var-b'),
313315
new Literal('"Hello "W"orl"d!"@en-us'),
314316
new NamedNode('http://ex.org/d')
315-
)).should.equal('<<_:n3-000 ?var-b "Hello ""W""orl""d!"@en-us http://ex.org/d>>');
317+
)).should.equal('["_:n3-000","?var-b","\\"Hello \\"W\\"orl\\"d!\\"@en-us","http://ex.org/d"]');
316318
});
317319

318320
it('should create an id without graph from a Quad with default graph and Quad as subject', () => {
@@ -326,7 +328,7 @@ describe('Term', () => {
326328
new NamedNode('http://ex.org/b'),
327329
new Literal('"abc"@en-us'),
328330
new DefaultGraph()
329-
)).should.equal('<<<<_:n3-000 ?var-b "abc"@en-us http://ex.org/d>> http://ex.org/b "abc"@en-us>>');
331+
)).should.equal('[["_:n3-000","?var-b","\\"abc\\"@en-us","http://ex.org/d"],"http://ex.org/b","\\"abc\\"@en-us"]');
330332
});
331333

332334
it('should create an id without graph from a Quad with default graph and Quad as object', () => {
@@ -340,7 +342,7 @@ describe('Term', () => {
340342
new NamedNode('http://ex.org/d')
341343
),
342344
new DefaultGraph()
343-
)).should.equal('<<"abc"@en-us http://ex.org/b <<_:n3-000 ?var-b "abc"@en-us http://ex.org/d>>>>');
345+
)).should.equal('["\\"abc\\"@en-us","http://ex.org/b",["_:n3-000","?var-b","\\"abc\\"@en-us","http://ex.org/d"]]');
344346
});
345347

346348
it('should create an id without graph from a Quad with default graph and Quad as subject and object', () => {
@@ -359,7 +361,7 @@ describe('Term', () => {
359361
new NamedNode('http://ex.org/d')
360362
),
361363
new DefaultGraph()
362-
)).should.equal('<<<<_:n3-000 ?var-b "abc"@en-us http://ex.org/d>> http://ex.org/b <<_:n3-000 ?var-b "abc"@en-us http://ex.org/d>>>>');
364+
)).should.equal('[["_:n3-000","?var-b","\\"abc\\"@en-us","http://ex.org/d"],"http://ex.org/b",["_:n3-000","?var-b","\\"abc\\"@en-us","http://ex.org/d"]]');
363365
});
364366

365367
it('should create an id without graph from a Quad with Quad as subject', () => {
@@ -373,7 +375,7 @@ describe('Term', () => {
373375
new NamedNode('http://ex.org/b'),
374376
new Literal('"abc"@en-us'),
375377
new NamedNode('http://ex.org/d')
376-
)).should.equal('<<<<_:n3-000 ?var-b "abc"@en-us http://ex.org/d>> http://ex.org/b "abc"@en-us http://ex.org/d>>');
378+
)).should.equal('[["_:n3-000","?var-b","\\"abc\\"@en-us","http://ex.org/d"],"http://ex.org/b","\\"abc\\"@en-us","http://ex.org/d"]');
377379
});
378380

379381
it('should create an id without graph from a Quad with Quad as object', () => {
@@ -387,7 +389,7 @@ describe('Term', () => {
387389
new NamedNode('http://ex.org/d')
388390
),
389391
new NamedNode('http://ex.org/d')
390-
)).should.equal('<<"abc"@en-us http://ex.org/b <<_:n3-000 ?var-b "abc"@en-us http://ex.org/d>> http://ex.org/d>>');
392+
)).should.equal('["\\"abc\\"@en-us","http://ex.org/b",["_:n3-000","?var-b","\\"abc\\"@en-us","http://ex.org/d"],"http://ex.org/d"]');
391393
});
392394

393395
it('should create an id from a Quad with Quad as subject and object', () => {
@@ -406,7 +408,7 @@ describe('Term', () => {
406408
new NamedNode('http://ex.org/d')
407409
),
408410
new NamedNode('http://ex.org/d')
409-
)).should.equal('<<<<_:n3-000 ?var-b "abc"@en-us http://ex.org/d>> http://ex.org/b <<_:n3-000 ?var-b "abc"@en-us http://ex.org/d>> http://ex.org/d>>');
411+
)).should.equal('[["_:n3-000","?var-b","\\"abc\\"@en-us","http://ex.org/d"],"http://ex.org/b",["_:n3-000","?var-b","\\"abc\\"@en-us","http://ex.org/d"],"http://ex.org/d"]');
410412
});
411413

412414
it('should escape literals in nested Quads', () => {
@@ -425,11 +427,46 @@ describe('Term', () => {
425427
new NamedNode('http://ex.org/d')
426428
),
427429
new DefaultGraph()
428-
)).should.equal('<<<<_:n3-000 ?var-b "Hello ""W""orl""d!"@en-us http://ex.org/d>> http://ex.org/b <<_:n3-000 ?var-b "Hello ""W""orl""d!"@en-us http://ex.org/d>>>>');
430+
)).should.equal('[["_:n3-000","?var-b","\\"Hello \\"W\\"orl\\"d!\\"@en-us","http://ex.org/d"],"http://ex.org/b",["_:n3-000","?var-b","\\"Hello \\"W\\"orl\\"d!\\"@en-us","http://ex.org/d"]]');
431+
});
432+
433+
434+
it('should termToId <-> termFromId should roundtrip on deeply nested quad', () => {
435+
const q = new Quad(
436+
new Quad(
437+
new NamedNode('http://example.org/s1'),
438+
new NamedNode('http://example.org/p1'),
439+
new NamedNode('http://example.org/o1')
440+
),
441+
new NamedNode('http://example.org/p1'),
442+
new Quad(
443+
new Quad(
444+
new Literal('"s1"'),
445+
new NamedNode('http://example.org/p1'),
446+
new BlankNode('o1')
447+
),
448+
new NamedNode('p2'),
449+
new Quad(
450+
new Quad(
451+
new Literal('"s1"'),
452+
new NamedNode('http://example.org/p1'),
453+
new BlankNode('o1')
454+
),
455+
new NamedNode('http://example.org/p1'),
456+
new NamedNode('http://example.org/o1')
457+
)
458+
)
459+
);
460+
461+
expect(q).deep.equals(termFromId(termToId(q)));
462+
expect(termFromId(termToId(q))).deep.equals(q);
463+
expect(q.equals(termFromId(termToId(q)))).equal(true);
464+
expect(termFromId(termToId(q)).equals(q)).equal(true);
465+
expect(termFromId(termToId(q)).equals(termFromId(termToId(q)))).equal(true);
429466
});
430467

431468
it('should correctly handle deeply nested quads', () => {
432-
termToId(new Quad(
469+
const q = new Quad(
433470
new Quad(
434471
new Quad(
435472
new Quad(
@@ -474,7 +511,9 @@ describe('Term', () => {
474511
new NamedNode('http://ex.org/d')
475512
),
476513
new NamedNode('http://ex.org/d')
477-
)).should.equal('<<<<<<<<_:n3-000 ?var-b "abc"@en-us http://ex.org/d>> ?var-b <<_:n3-000 ?var-b "abc"@en-us http://ex.org/d>> http://ex.org/d>> ?var-b <<_:n3-000 ?var-b "abc"@en-us http://ex.org/d>> http://ex.org/d>> http://ex.org/b <<<<_:n3-000 ?var-b "abc"@en-us http://ex.org/d>> ?var-b <<_:n3-000 ?var-b "abc"@en-us http://ex.org/d>> http://ex.org/d>> http://ex.org/d>>');
514+
);
515+
516+
expect(q.equals(termFromId(termToId(q)))).equal(true);
478517
});
479518

480519
it('should throw on an unknown type', () => {

0 commit comments

Comments
 (0)