Skip to content

Commit da945e9

Browse files
committed
feat: support annotated triples
1 parent f837b8d commit da945e9

4 files changed

Lines changed: 146 additions & 2 deletions

File tree

src/N3Lexer.js

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -302,12 +302,20 @@ export default class N3Lexer {
302302
case ')':
303303
case '{':
304304
case '}':
305+
if (input.length > 1 && input[1] === '|') {
306+
type = '{|', matchLength = 2;
307+
break;
308+
}
305309
if (!this._lineMode) {
306310
matchLength = 1;
307311
type = firstChar;
308312
}
309313
break;
310-
314+
case '|':
315+
if (input.length > 1 && input[1] === '}') {
316+
type = '|}', matchLength = 2;
317+
break;
318+
}
311319
default:
312320
inconclusive = true;
313321
}

src/N3Parser.js

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -624,6 +624,19 @@ export default class N3Parser {
624624
case ',':
625625
next = this._readObject;
626626
break;
627+
case '{|':
628+
if (!this._supportsRDFStar)
629+
return this._error('Unexpected RDF* syntax', token);
630+
631+
// TODO: Have error handling behavior here
632+
// TODO: See if we can just emit and then save null context
633+
this._saveContext('{|', this._graph, this._subject, this._predicate, this._object);
634+
635+
// Note - we always use the default graph for the quoted triple component
636+
this._subject = this._quad(this._subject, this._predicate, this._object, this.DEFAULTGRAPH);
637+
this._predicate = null;
638+
this._object = null;
639+
return this._readPredicate;
627640
default:
628641
// An entity means this is a quad (only allowed if not already inside a graph)
629642
if (this._supportsQuads && this._graph === null && (graph = this._readEntity(token)) !== undefined) {
@@ -872,6 +885,18 @@ export default class N3Parser {
872885
}
873886
}
874887

888+
// ### `_readRDFStarTail` reads the end of a nested RDF* triple
889+
_readAnnotatedTail(token) {
890+
this._emit(this._subject, this._predicate, this._object, this._graph);
891+
// if (this._subject && this._predicate && this._object) {
892+
// this._emit(this._subject, this._predicate, this._object, this._graph);
893+
// }
894+
if (token.type !== '|}')
895+
return this._readPredicate;
896+
this._restoreContext('{|', token);
897+
return this._getContextEndReader();
898+
}
899+
875900
// ### `_getContextEndReader` gets the next reader function at the end of a context
876901
_getContextEndReader() {
877902
const contextStack = this._contextStack;
@@ -887,6 +912,8 @@ export default class N3Parser {
887912
return this._readFormulaTail;
888913
case '<<':
889914
return this._readRDFStarTail;
915+
case '{|':
916+
return this._readAnnotatedTail;
890917
}
891918
}
892919

test/N3Lexer-test.js

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -869,6 +869,52 @@ describe('Lexer', () => {
869869
shouldNotTokenize('<<<http://ex.org/?bla#foo> \n\t<http://ex.org/?bla#bar> \n\t<http://ex.org/?bla#boo>> .',
870870
'Unexpected ">" on line 3.'));
871871

872+
it('should tokenize an RDF-star annotated statement',
873+
shouldTokenize('<a> <b> <c> {| <d> <e> |}',
874+
{ type: 'IRI', value: 'a', line: 1 },
875+
{ type: 'IRI', value: 'b', line: 1 },
876+
{ type: 'IRI', value: 'c', line: 1 },
877+
{ type: '{|', line: 1 },
878+
{ type: 'IRI', value: 'd', line: 1 },
879+
{ type: 'IRI', value: 'e', line: 1 },
880+
{ type: '|}', line: 1 },
881+
{ type: 'eof', line: 1 }));
882+
883+
it('should tokenize an RDF-star annotated statement with multiple annotations',
884+
shouldTokenize('<a> <b> <c> {| <d> <e>; <f> <g> |}',
885+
{ type: 'IRI', value: 'a', line: 1 },
886+
{ type: 'IRI', value: 'b', line: 1 },
887+
{ type: 'IRI', value: 'c', line: 1 },
888+
{ type: '{|', line: 1 },
889+
{ type: 'IRI', value: 'd', line: 1 },
890+
{ type: 'IRI', value: 'e', line: 1 },
891+
{ type: ';', line: 1 },
892+
{ type: 'IRI', value: 'f', line: 1 },
893+
{ type: 'IRI', value: 'g', line: 1 },
894+
{ type: '|}', line: 1 },
895+
{ type: 'eof', line: 1 }));
896+
897+
it('should tokenize an RDF-star annotated statement with multiple annotations, one containing a blank node',
898+
shouldTokenize('<a> <b> <c> {| <d> [ <e> "f" ]; <f> <g> |}',
899+
{ type: 'IRI', value: 'a', line: 1 },
900+
{ type: 'IRI', value: 'b', line: 1 },
901+
{ type: 'IRI', value: 'c', line: 1 },
902+
{ type: '{|', line: 1 },
903+
{ type: 'IRI', value: 'd', line: 1 },
904+
{ type: '[', value: '', line: 1 },
905+
{ type: 'IRI', value: 'e', line: 1 },
906+
{ type: 'literal', value: 'f', line: 1 },
907+
{ type: ']', value: '', line: 1 },
908+
{ type: ';', line: 1 },
909+
{ type: 'IRI', value: 'f', line: 1 },
910+
{ type: 'IRI', value: 'g', line: 1 },
911+
{ type: '|}', line: 1 },
912+
{ type: 'eof', line: 1 }));
913+
914+
it('should not tokenize an annotated statement that is not closed',
915+
shouldNotTokenize('<a> <b> <c> {| <d> [ <e> "f" ]; <f> <g> |',
916+
'Unexpected "|" on line 1.'));
917+
872918
it('should tokenize a split RDF* statement with IRIs',
873919
shouldTokenize(streamOf('<', '<<http://ex.org/?bla#foo> \n\t<http://ex.org/?bla#bar> \n\t<http://ex.org/?bla#boo>>> .'),
874920
{ type: '<<', line: 1 },

test/N3Parser-test.js

Lines changed: 64 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1189,6 +1189,65 @@ describe('Parser', () => {
11891189
shouldParse('<a> <b> <c> <g>.\n<<<a> <b> <c>>> <d> <e>.',
11901190
['a', 'b', 'c', 'g'],
11911191
[['a', 'b', 'c'], 'd', 'e']));
1192+
1193+
it('should parse an explicit triple with reified annotation',
1194+
shouldParse('<a> <b> <c> {| <d> <e> |} .',
1195+
['a', 'b', 'c'],
1196+
[['a', 'b', 'c'], 'd', 'e']));
1197+
1198+
const q = ['http://example.com/ns#s', 'http://example.com/ns#p',
1199+
['http://example.com/ns#a', 'http://example.com/ns#b', 'http://example.com/ns#c']];
1200+
1201+
it('should parse an explicit triple with reified annotation containing prefixed iris',
1202+
shouldParse('PREFIX : <http://example.com/ns#> \n :s :p <<:a :b :c>> {| :q :z |} .',
1203+
q, [q, 'http://example.com/ns#q', 'http://example.com/ns#z']));
1204+
1205+
it('should parse an explicit triple with 2 reified annotations',
1206+
shouldParse('<a> <b> <c> {| <d> <e>; <f> <g> |} .',
1207+
['a', 'b', 'c'],
1208+
[['a', 'b', 'c'], 'd', 'e'],
1209+
[['a', 'b', 'c'], 'f', 'g']));
1210+
1211+
// TODO: See if this is required by the spec tests
1212+
// it('should parse an explicit triple with reified annotation containing punctuation',
1213+
// shouldParse('<a> <b> <c> {| <d> <e> . |} .',
1214+
// ['a', 'b', 'c'],
1215+
// [['a', 'b', 'c'], 'd', 'e']));
1216+
1217+
it('should parse an explicit triple with reified annotation in a named graph',
1218+
shouldParse('<G> { <a> <b> <c> {| <d> <e> |} . }',
1219+
['a', 'b', 'c', 'G'],
1220+
[['a', 'b', 'c'], 'd', 'e', 'G']));
1221+
1222+
it('should parse an explicit triple with 2 reified annotations in a named graph',
1223+
shouldParse('<G> { <a> <b> <c> {| <d> <e>; <f> <g> |} . }',
1224+
['a', 'b', 'c', 'G'],
1225+
[['a', 'b', 'c'], 'd', 'e', 'G'],
1226+
[['a', 'b', 'c'], 'f', 'g', 'G']));
1227+
1228+
it('should not parse an annotated object in list',
1229+
shouldNotParse('<a> <b> ( <c> {| <d> <e> |} )',
1230+
'Expected entity but got {| on line 1.'));
1231+
1232+
it('should not parse an annotated statement in list',
1233+
shouldNotParse('<a> <b> ( <c> <d> <e> {| <d> <e> |} )',
1234+
'Expected entity but got {| on line 1.'));
1235+
1236+
it('should not parse fourth term in quoted triple',
1237+
shouldNotParse('<< <a> <b> <c> <g> >> <p> <q>',
1238+
'Expected >> to follow "http://example.org/c" but got IRI on line 1.'));
1239+
1240+
it('should not parse fourth term in quoted triple object',
1241+
shouldNotParse('<p> <q> << <a> <b> <c> <g> >>',
1242+
'Expected >> to follow "http://example.org/c" but got IRI on line 1.'));
1243+
1244+
it('should not parse quoted triple as predicate',
1245+
shouldNotParse('<p> << <a> <b> <c> >> <q>',
1246+
'Expected entity but got << on line 1.'));
1247+
1248+
it('should not parse quoted quad as predicate',
1249+
shouldNotParse('<p> << <a> <b> <c> <d> >> <q>',
1250+
'Expected entity but got << on line 1.'));
11921251
});
11931252

11941253
describe('An Parser instance without document IRI', () => {
@@ -1369,6 +1428,10 @@ describe('Parser', () => {
13691428
shouldNotParse(parser, '<<<a> <b> <c>>> <a> <b> .',
13701429
'Unexpected RDF* syntax on line 1.'));
13711430

1431+
it('should not parse annotated statement',
1432+
shouldNotParse(parser, '<a> <b> <c> {| <a> <b> |} .',
1433+
'Unexpected RDF* syntax on line 1.'));
1434+
13721435
it('should not parse RDF* in the object position',
13731436
shouldNotParse(parser, '<a> <b> <<a> <b> <c>>>.',
13741437
'Unexpected RDF* syntax on line 1.'));
@@ -2494,7 +2557,7 @@ function mapToQuad(item) {
24942557
function toSortedJSON(triples) {
24952558
triples = triples.map(t => {
24962559
return JSON.stringify([
2497-
t.subject.toJSON(), t.predicate.toJSON(), t.object.toJSON(), t.graph.toJSON(),
2560+
t.subject && t.subject.toJSON(), t.predicate && t.predicate.toJSON(), t.object && t.object.toJSON(), t.graph && t.graph.toJSON(),
24982561
]);
24992562
});
25002563
triples.sort();

0 commit comments

Comments
 (0)