diff --git a/README.md b/README.md index bec8efe2..8365734f 100644 --- a/README.md +++ b/README.md @@ -170,6 +170,16 @@ The parser can output a backwards chaining rule such as `_:q <= _:p.` in two way const parser = new N3.Parser({ isImpliedBy: true }); ``` +In N3 documents, the parser by default rescopes blank node labels +in lists and blank node property lists, in addition to formulas, +so `_:a` inside a list does not co-reference `_:a` outside of it. +The `formulaScopedBlankNodes` flag scopes blank node labels to formulas only, +matching N3's formula-scoped blank node semantics +(this will become the default in the next major version): +```JavaScript +const parser = new N3.Parser({ format: 'N3', formulaScopedBlankNodes: true }); +``` + ### From an RDF stream to quads `N3.Parser` can parse [Node.js streams](http://nodejs.org/api/stream.html) as they grow, diff --git a/src/N3Parser.js b/src/N3Parser.js index 878dad2c..abca9229 100644 --- a/src/N3Parser.js +++ b/src/N3Parser.js @@ -37,6 +37,8 @@ export default class N3Parser { this._lexer = options.lexer || new N3Lexer({ lineMode: isLineMode, n3: isN3, isImpliedBy: this._isImpliedBy }); // Disable explicit quantifiers by default this._explicitQuantifiers = !!options.explicitQuantifiers; + // Disable formula-only blank node scoping by default + this._formulaScopedBlankNodes = !!options.formulaScopedBlankNodes; // Disable parsing of unsupported versions by default this._parseUnsupportedVersions = !!options.parseUnsupportedVersions; this._version = options.version; @@ -87,11 +89,15 @@ export default class N3Parser { if (n3Mode) { // Every new scope resets the predicate direction this._inversePredicate = false; - // In N3, blank nodes are scoped to a formula - // (using a dot as separator, as a blank node label cannot start with it) - this._prefixes._ = (this._graph ? `${this._graph.value}.` : '.'); - // Quantifiers are scoped to a formula - this._quantified = Object.create(this._quantified); + // In N3, blank nodes and quantifiers are scoped to a formula; + // with `formulaScopedBlankNodes`, lists and blank node property lists + // share the enclosing scope instead of rescoping + if (!this._formulaScopedBlankNodes || type === 'formula') { + // Label the scope with the enclosing formula's blank node + // (using a dot as separator, as a blank node label cannot start with it) + this._prefixes._ = (this._graph ? `${this._graph.value}.` : '.'); + this._quantified = Object.create(this._quantified); + } } } diff --git a/test/N3Parser-test.js b/test/N3Parser-test.js index 963e9ee4..18ada579 100644 --- a/test/N3Parser-test.js +++ b/test/N3Parser-test.js @@ -2447,6 +2447,7 @@ describe('Parser', () => { describe('A Parser instance for the N3 format', () => { function parser() { return new Parser({ baseIRI: BASE_IRI, format: 'N3' }); } function parserIsImpliedBy() { return new Parser({ baseIRI: BASE_IRI, format: 'N3', isImpliedBy: true }); } + function parserFormulaScoped() { return new Parser({ baseIRI: BASE_IRI, format: 'N3', formulaScopedBlankNodes: true }); } it( 'should parse a single triple', @@ -2609,6 +2610,80 @@ describe('Parser', () => { ['_:b3.a', '_:b3.b', '_:b3.c', '_:b3']), ); + // The tests below pin the default behaviour of rescoping blank node + // labels in lists and blank node property lists (#332); + // the default flips to `formulaScopedBlankNodes` in a next major version (#630) + + it( + 'should rescope a blank node in a list by default', + shouldParse(parser, '

(_:a).', + ['s', 'p', '_:b0'], + ['_:b0', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#first', '_:.a'], + ['_:b0', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#rest', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#nil']), + ); + + it( + 'should not reuse identifiers of blank nodes within and outside of lists by default', + shouldParse(parser, '

(_:a). _:a .', + ['s', 'p', '_:b0'], + ['_:b0', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#first', '_:.a'], + ['_:b0', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#rest', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#nil'], + ['_:b0_a', 'b', 'c']), + ); + + it( + 'should not reuse identifiers of blank nodes within and outside of blank node property lists by default', + shouldParse(parser, '_:a

[ _:a ].', + ['_:b0_a', 'p', '_:b0'], + ['_:b0', 'q', '_:.a']), + ); + + it( + 'should scope blank nodes in a list to the enclosing formula by default', + shouldParse(parser, '_:a

. { (_:a). _:a . } .', + ['_:b0_a', 'p', 'o'], + ['s', 'q', '_:b1', '_:b0'], + ['_:b1', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#first', '_:b0.a', '_:b0'], + ['_:b1', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#rest', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#nil', '_:b0'], + ['_:b0.a', 'r', 'o2', '_:b0'], + ['_:b0', 'd', 'e']), + ); + + it( + 'should parse a blank node in a list with formulaScopedBlankNodes', + shouldParse(parserFormulaScoped, '

(_:a).', + ['s', 'p', '_:b0'], + ['_:b0', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#first', '_:b0_a'], + ['_:b0', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#rest', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#nil']), + ); + + it( + 'should reuse identifiers of blank nodes within and outside of lists with formulaScopedBlankNodes', + shouldParse(parserFormulaScoped, '

(_:a). _:a .', + ['s', 'p', '_:b0'], + ['_:b0', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#first', '_:b0_a'], + ['_:b0', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#rest', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#nil'], + ['_:b0_a', 'b', 'c']), + ); + + it( + 'should reuse identifiers of blank nodes within and outside of blank node property lists with formulaScopedBlankNodes', + shouldParse(parserFormulaScoped, '_:a

[ _:a ].', + ['_:b0_a', 'p', '_:b0'], + ['_:b0', 'q', '_:b0_a']), + ); + + it( + 'should scope blank nodes in a list to the enclosing formula with formulaScopedBlankNodes', + shouldParse(parserFormulaScoped, '_:a

. { (_:a). _:a . } .', + ['_:b0_a', 'p', 'o'], + ['s', 'q', '_:b1', '_:b0'], + ['_:b1', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#first', '_:b0.a', '_:b0'], + ['_:b1', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#rest', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#nil', '_:b0'], + ['_:b0.a', 'r', 'o2', '_:b0'], + ['_:b0', 'd', 'e']), + ); + it( 'should parse a @forSome statement', shouldParse(parser, '@forSome . .', diff --git a/test/N3Writer-test.js b/test/N3Writer-test.js index e36d57f7..c528384a 100644 --- a/test/N3Writer-test.js +++ b/test/N3Writer-test.js @@ -730,6 +730,29 @@ describe('Writer', () => { }); }); + it('should serialize a blank node in an N3 list with a valid label when formulaScopedBlankNodes is set', done => { + const quads = new Parser({ format: 'text/n3', formulaScopedBlankNodes: true }).parse(' (_:x). _:x .'); + const writer = new Writer(); + writer.addQuads(quads); + writer.end((error, output) => { + // A label such as `_:.x` would fail to reparse (#332) + expect(() => new Parser().parse(output)).not.toThrow(); + done(error); + }); + }); + + it('should serialize a blank node in an N3 list with an invalid label by default', done => { + const quads = new Parser({ format: 'text/n3' }).parse(' (_:x). _:x .'); + const writer = new Writer(); + writer.addQuads(quads); + writer.end((error, output) => { + // The default rescoping produces the label `_:.x`, + // which fails to reparse (#332; the default flips in #630) + expect(() => new Parser().parse(output)).toThrow(); + done(error); + }); + }); + it( 'should serialize subject and object triples passed by options.listHeads', done => {