diff --git a/src/N3Parser.js b/src/N3Parser.js index 1a7d7449..201ae83c 100644 --- a/src/N3Parser.js +++ b/src/N3Parser.js @@ -434,12 +434,25 @@ export default class N3Parser { this._restoreContext('list', token); // If this list is contained within a parent list, return the membership quad here. // This will be ` rdf:first .`. - if (stack.length !== 0 && stack[stack.length - 1].type === 'list') - this._emit(this._subject, this._predicate, this._object, this._graph); + if (stack.length !== 0 && stack[stack.length - 1].type === 'list') { + if (this._n3Mode) { + const { _subject, _predicate, _graph } = this; + if (this._object !== this.RDF_NIL) { + this._emit(previousList, this.RDF_REST, this.RDF_NIL, _graph); + } + return this._getPathReader(tk => { + this._emit(_subject, _predicate, this._object, _graph); + return this._readListItem(tk); + }); + } + else { + this._emit(this._subject, this._predicate, this._object, this._graph); + } + } // Was this list the parent's subject? if (this._predicate === null) { // The next token is the predicate - next = this._readPredicate; + next = this._n3Mode ? this._getPathReader(this._readPredicateOrNamedGraph) : this._readPredicate; // No list tail if this was an empty list if (this._subject === this.RDF_NIL) return next; @@ -497,7 +510,7 @@ export default class N3Parser { // If an item was read, add it to the list if (item !== null) { // In N3 mode, the item might be a path - if (this._n3Mode && (token.type === 'IRI' || token.type === 'prefixed')) { + if (this._n3Mode) { // Create a new context to add the item's path this._saveContext('item', this._graph, list, this.RDF_FIRST, item); this._subject = item, this._predicate = null; diff --git a/test/N3Parser-test.js b/test/N3Parser-test.js index 80cb2466..97f24b49 100644 --- a/test/N3Parser-test.js +++ b/test/N3Parser-test.js @@ -1733,13 +1733,103 @@ describe('Parser', () => { ['_:b3', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#rest', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#nil'], ['_:b2', 'f:son', 'ex:joe'])); + it('should parse a ! path of length 2 as subject in a list', + shouldParse(parser, '@prefix : . @prefix fam: .' + + '(:joe!fam:mother) a fam:Person.', + ['ex:joe', 'f:mother', '_:b1'], + ['_:b0', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#first', '_:b1'], + ['_:b0', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#rest', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#nil'], + ['_:b0', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#type', 'f:Person'])); + + it('should parse a !^ path of length 3 as subject in a list', + shouldParse(parser, '@prefix : . @prefix fam: .' + + '(:joe!fam:mother^fam:father) a fam:Person.', + ['ex:joe', 'f:mother', '_:b1'], + ['_:b2', 'f:father', '_:b1'], + ['_:b0', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#first', '_:b2'], + ['_:b0', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#rest', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#nil'], + ['_:b0', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#type', 'f:Person'])); + + it('should parse a ! path of length 2 starting with an empty list', + shouldParse(parser, '@prefix : . @prefix fam: .' + + '()!fam:mother a fam:Person.', + ['http://www.w3.org/1999/02/22-rdf-syntax-ns#nil', 'f:mother', '_:b0'], + ['_:b0', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#type', 'f:Person'])); + + it('should parse a ! path of length 2 starting with a non-empty list', + shouldParse(parser, '@prefix : . @prefix fam: .' + + '( :a )!fam:mother a fam:Person.', + ...list(['_:b0', 'ex:a']), + ['_:b0', 'f:mother', '_:b1'], + ['_:b1', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#type', 'f:Person'])); + + it('should parse a ! path of length 2 starting with a non-empty list in another list', + shouldParse(parser, '@prefix : . @prefix fam: .' + + '(( :a )!fam:mother 1) a fam:Person.', + ...list(['_:b0', '_:b2'], ['_:b3', '"1"^^http://www.w3.org/2001/XMLSchema#integer']), + ...list(['_:b1', 'ex:a']), + ['_:b1', 'f:mother', '_:b2'], + ['_:b0', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#type', 'f:Person'])); + + it('should parse a ! path of length 2 starting with an empty list in another list', + shouldParse(parser, '@prefix : . @prefix fam: .' + + '(()!fam:mother 1) a fam:Person.', + ['_:b0', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#type', 'f:Person'], + ...list(['_:b0', '_:b1'], ['_:b2', '"1"^^http://www.w3.org/2001/XMLSchema#integer']), + ['http://www.w3.org/1999/02/22-rdf-syntax-ns#nil', 'f:mother', '_:b1'] + )); + + it('should parse a ! path of length 2 starting with an empty list in another list as second element', + shouldParse(parser, '@prefix : . @prefix fam: .' + + '(1 ()!fam:mother) a fam:Person.', + ['_:b0', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#type', 'f:Person'], + ...list(['_:b0', '"1"^^http://www.w3.org/2001/XMLSchema#integer'], ['_:b1', '_:b2']), + ['http://www.w3.org/1999/02/22-rdf-syntax-ns#nil', 'f:mother', '_:b2'] + )); + + it('should parse a ! path of length 2 starting with an empty list in another list of one element', + shouldParse(parser, '@prefix : . @prefix fam: .' + + '(()!fam:mother) a fam:Person.', + ['_:b0', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#type', 'f:Person'], + ...list(['_:b0', '_:b1']), + ['http://www.w3.org/1999/02/22-rdf-syntax-ns#nil', 'f:mother', '_:b1'])); + + it('should parse a ! path of length 2 as nested subject in a list', + shouldParse(parser, '@prefix : . @prefix fam: .' + + '((:joe!fam:mother) 1) a fam:Person.', + ['_:b0', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#type', 'f:Person'], + ...list(['_:b0', '_:b1'], ['_:b3', '"1"^^http://www.w3.org/2001/XMLSchema#integer']), + ...list(['_:b1', '_:b2']), + ['ex:joe', 'f:mother', '_:b2'])); + + it('should parse a birthday rule', + shouldParse(parser, + '@prefix foaf: .' + + '@prefix math: .' + + '@prefix : .' + + '' + + '{' + + ' ?x :trueOnDate ?date.' + + '} <= {' + + ' ((?date ?s!foaf:birthday)!math:difference 31622400) math:integerQuotient ?age .' + + '} .', + ['?x', 'http://example.org/trueOnDate', '?date', '_:b0'], + // eslint-disable-next-line no-warning-comments + // FIXME: when merging with https://github.com/rdfjs/N3.js/pull/327 + ['_:b1', 'http://www.w3.org/2000/10/swap/log#implies', '_:b0'], + ...[ + ...list(['_:b2', '_:b6'], ['_:b7', '"31622400"^^http://www.w3.org/2001/XMLSchema#integer']), + ['_:b2', 'http://www.w3.org/2000/10/swap/math#integerQuotient', '?age'], + ...list(['_:b3', '?date'], ['_:b4', '_:b5']), + ['?s', 'http://xmlns.com/foaf/0.1/birthday', '_:b5'], + ['_:b3', 'http://www.w3.org/2000/10/swap/math#difference', '_:b6'], + ].map(elem => [...elem, '_:b1']) + )); + it('should parse a formula as list item', shouldParse(parser, ' ( { a . } ).', ['a', 'findAll', '_:b0'], - ['_:b0', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#first', 'b'], - ['_:b0', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#rest', '_:b2'], - ['_:b2', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#first', 'o'], - ['_:b2', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#rest', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#nil'], + ...list(['_:b0', 'b'], ['_:b2', 'o']), ['b', 'something', 'foo', '_:b1'], ['b', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#type', 'type', '_:b1'] )); @@ -1787,6 +1877,55 @@ describe('Parser', () => { it('should not parse nested quads', shouldNotParse(parser, '<<_:a _:b >> "c" .', 'Expected >> to follow "_:.b" on line 1.')); + + for (const [elem, value] of [ + ['()', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#nil'], + ['( )', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#nil'], + ['( )', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#nil'], + ['', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#nil'], + [':joe', 'ex:joe'], + // ['<<:joe a :Person>>', '<<:joe a :Person>>'] + ]) { + for (const pathType of ['!', '^']) { + // eslint-disable-next-line no-inner-declarations + function son(bnode) { + return pathType === '!' ? [value, 'f:son', `_:b${bnode}`] : [`_:b${bnode}`, 'f:son', value]; + } + + for (const [f, triple] of [ + [x => `(${x}) a :List .`, ['_:b0', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#type', 'ex:List']], + // [x => ` (${x}) .`, ['l', '_:b0', 'm']], + [x => ` (${x}) .`, ['l', 'is', '_:b0']], + ]) { + // eslint-disable-next-line no-inner-declarations + function check(content, ...triples) { + it(`should parse [${f(content)}]`, + shouldParse(parser, `@prefix : . @prefix fam: .${f(content)}`, + triple, ...triples)); + } + + check(`${elem}${pathType}fam:son`, ...list(['_:b0', '_:b1']), son('1')); + check(`(${elem}${pathType}fam:son)`, ...list(['_:b0', '_:b1']), ...list(['_:b1', '_:b2']), son('2')); + + check(`${elem}${pathType}fam:son `, ...list(['_:b0', '_:b1'], ['_:b2', 'x'], ['_:b3', 'y']), son('1')); + check(` ${elem}${pathType}fam:son `, ...list(['_:b0', 'x'], ['_:b1', '_:b2'], ['_:b3', 'y']), son('2')); + check(` ${elem}${pathType}fam:son`, ...list(['_:b0', 'x'], ['_:b1', 'y'], ['_:b2', '_:b3']), son('3')); + + check(`(${elem}${pathType}fam:son) `, + ...list(['_:b0', '_:b1'], ['_:b3', 'x'], ['_:b4', 'y']), + ...list(['_:b1', '_:b2']), + son('2')); + check(` (${elem}${pathType}fam:son) `, + ...list(['_:b0', 'x'], ['_:b1', '_:b2'], ['_:b4', 'y']), + ...list(['_:b2', '_:b3']), + son('3')); + check(` (${elem}${pathType}fam:son)`, + ...list(['_:b0', 'x'], ['_:b1', 'y'], ['_:b2', '_:b3']), + ...list(['_:b3', '_:b4']), + son('4')); + } + } + } }); describe('A Parser instance for the N3 format with the explicitQuantifiers option', () => { @@ -2385,3 +2524,15 @@ function itShouldResolve(baseIRI, relativeIri, expected) { }); }); } + +// creates an RDF list from the input +function list(...elems) { + const arr = []; + for (let i = 0; i < elems.length; i++) { + arr.push( + [elems[i][0], 'http://www.w3.org/1999/02/22-rdf-syntax-ns#first', elems[i][1]], + [elems[i][0], 'http://www.w3.org/1999/02/22-rdf-syntax-ns#rest', i + 1 === elems.length ? 'http://www.w3.org/1999/02/22-rdf-syntax-ns#nil' : elems[i + 1][0]] + ); + } + return arr; +}