Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 29 additions & 2 deletions src/N3Lexer.js
Original file line number Diff line number Diff line change
Expand Up @@ -326,7 +326,7 @@ export default class N3Lexer {
case 'h':
case 'o':
// Try to find an N3 verb keyword
if (this._n3Mode && (match = this._n3Verb.exec(input)))
if (this._n3Mode && (match = this._matchN3Verb(input, inputFinished)))
type = match[0];
else
inconclusive = true;
Expand All @@ -336,7 +336,7 @@ export default class N3Lexer {
// Try to find an IRI property list identifier or N3 verb keyword
if (this._n3Mode && (match = this._n3Id.exec(input)))
type = 'id';
else if (this._n3Mode && (match = this._n3Verb.exec(input)))
else if (this._n3Mode && (match = this._matchN3Verb(input, inputFinished)))
type = match[0];
else
inconclusive = true;
Expand Down Expand Up @@ -455,6 +455,33 @@ export default class N3Lexer {
function reportSyntaxError(self) { callback(self._syntaxError(/^\S*/.exec(input)[0])); }
}

// ### `_matchN3Verb` matches an N3 verb unless the input is a longer prefixed name
_matchN3Verb(input, inputFinished) {
const verb = this._n3Verb.exec(input);
if (!verb)
return null;

// Most verb boundaries cannot be part of a prefix, so keep the common path fast.
const next = input[verb[0].length];
if (next !== '-' && next !== '_' && (next < '0' || next > '9'))
return verb;

// A prefix can start with a verb and continue with characters that are also
// valid verb boundaries. Prefer the longer prefixed name when it is complete.
if (this._prefixed.exec(input) || this._prefixed.exec(`${input} `))
return null;

// If a stream chunk ends partway through such a prefix, wait for the colon
// instead of prematurely emitting the verb. Appending ": " lets the prefix
// grammar determine whether all input seen so far can be a complete prefix.
if (!inputFinished) {
const prefix = this._prefix.exec(`${input}: `);
if (prefix)
return null;
}
return verb;
}

// ### `_unescape` replaces N3 escape codes by their corresponding characters,
// allowing only the fixed escape sequences from the given replacement table
_unescape(item, replacements) {
Expand Down
57 changes: 46 additions & 11 deletions src/N3Parser.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,13 @@ export default class N3Parser {
isNTriples = /triple/.test(format), isNQuads = /quad/.test(format),
isN3 = this._n3Mode = /n3/.test(format),
isLineMode = isNTriples || isNQuads;
// Keep inverse handling off the non-N3 emission path
this._emitCurrent = this._emit;
if (isN3) {
this._createQuad = this._createQuadInDirection;
this._emit = this._emitInDirection;
this._emitCurrent = this._emitCurrentInDirection;
}
if (!(this._supportsNamedGraphs = !(isTurtle || isN3)))
this._readPredicateOrNamedGraph = this._readPredicate;
// Support triples in other graphs
Expand Down Expand Up @@ -550,7 +557,7 @@ export default class N3Parser {

// Store blank node quad
if (this._subject !== null)
this._emit(this._subject, this._predicate, this._object, this._graph);
this._emitCurrent(this._subject, this._predicate, this._object, this._graph);

// Restore the parent context containing this blank node
const empty = this._predicate === null;
Expand Down Expand Up @@ -911,7 +918,7 @@ export default class N3Parser {

// Store the last quad of the formula
if (this._subject !== null)
this._emit(this._subject, this._predicate, this._object, this._graph);
this._emitCurrent(this._subject, this._predicate, this._object, this._graph);

const formula = this._graph, empty = this._emptyFormula;
// Restore the parent context containing this formula
Expand Down Expand Up @@ -958,6 +965,7 @@ export default class N3Parser {
break;
// Semicolon means the subject is shared; predicate and object are different
case ';':
if (inversePredicate) this._inversePredicate = false;
next = this._readPredicate;
break;
// Comma means both the subject and predicate are shared; the object is different
Expand All @@ -980,6 +988,7 @@ export default class N3Parser {
if (subject !== null)
this._tripleTerm = null;
this._subject = this._readTripleTerm();
this._inversePredicate = false;
this._validAnnotation = false;
startingAnnotation = true;
next = this._readPredicate;
Expand All @@ -992,6 +1001,7 @@ export default class N3Parser {
return this._error('Annotation block can not be empty', token);
this._subject = null;
this._annotation = false;
this._inversePredicate = false;
next = this._getContextEndReader();
break;
default:
Expand All @@ -1005,10 +1015,7 @@ export default class N3Parser {
// A quad has been completed now, so return it
if (subject !== null && (!startingAnnotation || (startingAnnotation && !this._annotation))) {
const predicate = this._predicate, object = this._object;
if (!inversePredicate)
this._emit(subject, predicate, object, graph);
else
this._emit(object, predicate, subject, graph);
this._emit(subject, predicate, object, graph, inversePredicate);
}
if (startingAnnotation) {
this._annotation = true;
Expand All @@ -1018,10 +1025,11 @@ export default class N3Parser {

// ### `_readBlankNodePunctuation` reads punctuation in a blank node
_readBlankNodePunctuation(token) {
let next;
let next, resetInversePredicate = false;
switch (token.type) {
// Semicolon means the subject is shared; predicate and object are different
case ';':
resetInversePredicate = this._inversePredicate;
next = this._readPredicate;
break;
// Comma means both the subject and predicate are shared; the object is different
Expand All @@ -1044,7 +1052,9 @@ export default class N3Parser {
if (this._subject === null)
return this._error('Expected ] to follow annotation', token);
// A quad has been completed now, so return it
this._emit(this._subject, this._predicate, this._object, this._graph);
this._emitCurrent(this._subject, this._predicate, this._object, this._graph);
if (resetInversePredicate)
this._inversePredicate = false;
return next;
}

Expand Down Expand Up @@ -1269,8 +1279,8 @@ export default class N3Parser {
if (token.type !== ')>>')
return this._error(`Expected )>> but got ${token.type}`, token);
// Read the quad and restore the previous context
const quad = this._factory.quad(this._subject, this._predicate, this._object,
this._graph || this.DEFAULTGRAPH);
const quad = this._createQuad(this._subject, this._predicate, this._object,
this._graph, this._inversePredicate);
this._restoreContext('<<(', token);

// If we're in a list, continue processing that list
Expand Down Expand Up @@ -1371,6 +1381,7 @@ export default class N3Parser {
switch (token.type) {
// The subject stays shared with the next predicate-object pair
case ';':
this._inversePredicate = false;
return this._readPredicate;
// The subject and predicate stay shared with the next object
case ',':
Expand All @@ -1388,7 +1399,9 @@ export default class N3Parser {
const parentGraph = parent ? parent.graph : undefined;
const reifier = this._reifier || this._factory.blankNode();
this._reifier = null;
this._tripleTerm = this._tripleTerm || this._factory.quad(this._subject, this._predicate, this._object);
this._tripleTerm = this._tripleTerm || this._createQuad(
this._subject, this._predicate, this._object, null, this._inversePredicate,
);
this._emit(reifier, this.RDF_REIFIES, this._tripleTerm, parentGraph || this._graph || this.DEFAULTGRAPH);
return reifier;
}
Expand All @@ -1413,6 +1426,28 @@ export default class N3Parser {
}
}

// ### `_createQuad` creates a quad
_createQuad(subject, predicate, object, graph) {
return this._factory.quad(subject, predicate, object, graph || this.DEFAULTGRAPH);
}

// ### `_createQuadInDirection` creates a quad in the active predicate direction
_createQuadInDirection(subject, predicate, object, graph, inversePredicate) {
return inversePredicate ?
this._factory.quad(object, predicate, subject, graph || this.DEFAULTGRAPH) :
this._factory.quad(subject, predicate, object, graph || this.DEFAULTGRAPH);
}

// ### `_emitInDirection` sends a quad in the active predicate direction
_emitInDirection(subject, predicate, object, graph, inversePredicate) {
this._callback(null, this._createQuad(subject, predicate, object, graph, inversePredicate));
}

// ### `_emitCurrentInDirection` sends a quad in the current predicate direction
_emitCurrentInDirection(subject, predicate, object, graph) {
this._callback(null, this._createQuad(subject, predicate, object, graph, this._inversePredicate));
}

// ### `_emit` sends a quad through the callback
_emit(subject, predicate, object, graph) {
this._callback(null, this._factory.quad(subject, predicate, object, graph || this.DEFAULTGRAPH));
Expand Down
30 changes: 29 additions & 1 deletion test/N3Lexer-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -1065,6 +1065,16 @@ describe('Lexer', () => {
{ type: 'eof', line: 1 }),
);

it(
'should keep numeric characters as N3 verb boundaries when no prefix follows',
shouldTokenize(streamOf('has1', ' of-1'),
{ type: 'has', line: 1 },
{ type: 'literal', value: '1', prefix: 'http://www.w3.org/2001/XMLSchema#integer', line: 1 },
{ type: 'of', line: 1 },
{ type: 'literal', value: '-1', prefix: 'http://www.w3.org/2001/XMLSchema#integer', line: 1 },
{ type: 'eof', line: 1 }),
);

it(
'should tokenize an IRI property list identifier split across chunks',
shouldTokenize(streamOf('[ i', 'd <s> <p> <o> ]'),
Expand All @@ -1079,10 +1089,28 @@ describe('Lexer', () => {

it(
'should keep keyword-like prefixes as prefixed names',
shouldTokenize('has:p is:p of:p',
shouldTokenize('has:p is:p of:p has1:p has_:p has-foo:p is1:p is_:p is-foo:p of1:p of_:p of-foo:p',
{ type: 'prefixed', prefix: 'has', value: 'p', line: 1 },
{ type: 'prefixed', prefix: 'is', value: 'p', line: 1 },
{ type: 'prefixed', prefix: 'of', value: 'p', line: 1 },
{ type: 'prefixed', prefix: 'has1', value: 'p', line: 1 },
{ type: 'prefixed', prefix: 'has_', value: 'p', line: 1 },
{ type: 'prefixed', prefix: 'has-foo', value: 'p', line: 1 },
{ type: 'prefixed', prefix: 'is1', value: 'p', line: 1 },
{ type: 'prefixed', prefix: 'is_', value: 'p', line: 1 },
{ type: 'prefixed', prefix: 'is-foo', value: 'p', line: 1 },
{ type: 'prefixed', prefix: 'of1', value: 'p', line: 1 },
{ type: 'prefixed', prefix: 'of_', value: 'p', line: 1 },
{ type: 'prefixed', prefix: 'of-foo', value: 'p', line: 1 },
{ type: 'eof', line: 1 }),
);

it(
'should keep keyword-like prefixes split across chunks as prefixed names',
shouldTokenize(streamOf('has', '1:p is', '_:p of-', 'foo:p'),
{ type: 'prefixed', prefix: 'has1', value: 'p', line: 1 },
{ type: 'prefixed', prefix: 'is_', value: 'p', line: 1 },
{ type: 'prefixed', prefix: 'of-foo', value: 'p', line: 1 },
{ type: 'eof', line: 1 }),
);

Expand Down
56 changes: 56 additions & 0 deletions test/N3Parser-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -2899,6 +2899,62 @@ describe('Parser', () => {
shouldParse(parser, '<s> is <p> of <o>.', ['o', 'p', 's']),
);

it(
'should preserve inversion across commas and reset it after a semicolon',
shouldParse(parser, '<s> is <p> of <o1>, <o2>; <q> <r>.',
['o1', 'p', 's'], ['o2', 'p', 's'], ['s', 'q', 'r']),
);

it(
'should reset inverted predicate markers after a semicolon',
shouldParse(parser, '<s> <- <p> <o>; <q> <r>.',
['o', 'p', 's'], ['s', 'q', 'r']),
);

it(
'should apply inversion when a blank node property list closes',
shouldParse(parser, '[ is <p1> of <o1> ]. [ <- <p2> <o2> ].',
['o1', 'p1', '_:b0'], ['o2', 'p2', '_:b1']),
);

it(
'should scope inversion across blank node property-list punctuation',
shouldParse(parser, '[ is <p> of <o1>, <o2>; <q> <r> ].',
['o1', 'p', '_:b0'], ['o2', 'p', '_:b0'], ['_:b0', 'q', 'r']),
);

it(
'should apply and scope inversion inside formulas',
shouldParse(parser,
'{ <s1> is <p1> of <o1> }. { <s2> is <p2> of <o2>; <q2> <r2> }.',
['o1', 'p1', 's1', '_:b0'],
['o2', 'p2', 's2', '_:b1'], ['s2', 'q2', 'r2', '_:b1']),
);

it(
'should apply inversion inside triple terms',
shouldParse(parser, '<<( <s> is <p> of <o> )>> <q> <r>.',
[['o', 'p', 's'], 'q', 'r']),
);

it(
'should apply inversion inside reified triples',
shouldParse(parser, '<< <s> is <p> of <o> >> <q> <r>.',
['_:b0', 'q', 'r'], ['_:b0', reifies, ['o', 'p', 's']]),
);

it(
'should apply inversion to annotated triples without leaking into annotations',
shouldParse(parser, '<s> is <p> of <o> {| <q> <r> |}.',
['o', 'p', 's'], ['_:b0', 'q', 'r'], ['_:b0', reifies, ['o', 'p', 's']]),
);

it(
'should reset inversion after a reifier and semicolon',
shouldParse(parser, '<s> is <p> of <o> ~ <t>; <q> <r>.',
['o', 'p', 's'], ['t', reifies, ['o', 'p', 's']], ['s', 'q', 'r']),
);

it(
'should parse verb keywords after literal subjects',
shouldParse(parser, '"s1" has <p1> <o1>. "s2" is <p2> of <o2>.',
Expand Down