Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature: callback and event for comments #423

Merged
merged 14 commits into from
Aug 26, 2024
Merged
Show file tree
Hide file tree
Changes from 11 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
15 changes: 10 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,19 +89,22 @@ parser.parse(
c:Tom a c:Cat.
c:Jerry a c:Mouse;
c:smarterThan c:Tom.`,
(error, quad, prefixes) => {
(error, quad) => {
if (quad)
console.log(quad);
else
console.log("# That's all, folks!", prefixes);
pietercolpaert marked this conversation as resolved.
Show resolved Hide resolved
console.log("# That's all, folks!");
});
```
The callback's first argument is an optional error value, the second is a quad.
If there are no more quads,
the callback is invoked one last time with `null` for `quad`
and a hash of prefixes as third argument.
the callback is invoked one last time with `null` for `quad`.
<br>
Pass a second callback to `parse` to retrieve prefixes as they are read.
In case you would also like to process prefixes, you can instead pass an object containing multiple callbacks.
The callback to retrieve the quads is called `onQuad`.
The callback to also retrieve prefixes as they are read is called `onPrefix`.
The first argument is the prefix, the second is the IRI.
There is also a third callback called `onComment` taking only one `comment` argument.
jeswr marked this conversation as resolved.
Show resolved Hide resolved
<br>
If no callbacks are provided, parsing happens synchronously.

Expand Down Expand Up @@ -169,6 +172,8 @@ function SlowConsumer() {

A dedicated `prefix` event signals every prefix with `prefix` and `term` arguments.

Also a `comment` event can be enabled through the options object of the N3.StreamParser constructor using: `comments: true`.

## Writing

### From quads to a string
Expand Down
6 changes: 3 additions & 3 deletions src/N3Lexer.js
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ export default class N3Lexer {
this._n3Mode = options.n3 !== false;
}
// Don't output comment tokens by default
this._comments = !!options.comments;
this.comments = !!options.comments;
// Cache the last tested closing position of long literals
this._literalClosingPos = 0;
}
Expand All @@ -85,7 +85,7 @@ export default class N3Lexer {
let whiteSpaceMatch, comment;
while (whiteSpaceMatch = this._newline.exec(input)) {
// Try to find a comment
if (this._comments && (comment = this._comment.exec(whiteSpaceMatch[0])))
if (this.comments && (comment = this._comment.exec(whiteSpaceMatch[0])))
emitToken('comment', comment[1], '', this._line, whiteSpaceMatch[0].length);
// Advance the input
input = input.substr(whiteSpaceMatch[0].length, input.length);
Expand All @@ -101,7 +101,7 @@ export default class N3Lexer {
// If the input is finished, emit EOF
if (inputFinished) {
// Try to find a final comment
if (this._comments && (comment = this._comment.exec(input)))
if (this.comments && (comment = this._comment.exec(input)))
emitToken('comment', comment[1], '', this._line, input.length);
input = null;
emitToken('eof', '', '', this._line, 0);
Expand Down
45 changes: 38 additions & 7 deletions src/N3Parser.js
Original file line number Diff line number Diff line change
Expand Up @@ -1010,21 +1010,33 @@ export default class N3Parser {

// ## Public methods

// ### `parse` parses the N3 input and emits each parsed quad through the callback
// ### `parse` parses the N3 input and emits each parsed quad through the onQuad callback.
parse(input, quadCallback, prefixCallback) {
// The second parameter accepts an object { onQuad: ..., onPrefix: ..., onComment: ...}
// As a second and third parameter it still accepts a separate quadCallback and prefixCallback for backward compatibility as well
let onQuad, onPrefix, onComment;
if (quadCallback && (quadCallback.onQuad || quadCallback.onPrefix || quadCallback.onComment)) {
onQuad = quadCallback.onQuad;
onPrefix = quadCallback.onPrefix;
onComment = quadCallback.onComment;
}
else {
onQuad = quadCallback;
onPrefix = prefixCallback;
}
// The read callback is the next function to be executed when a token arrives.
// We start reading in the top context.
this._readCallback = this._readInTopContext;
this._sparqlStyle = false;
this._prefixes = Object.create(null);
this._prefixes._ = this._blankNodePrefix ? this._blankNodePrefix.substr(2)
: `b${blankNodePrefix++}_`;
this._prefixCallback = prefixCallback || noop;
this._prefixCallback = onPrefix || noop;
this._inversePredicate = false;
this._quantified = Object.create(null);

// Parse synchronously if no quad callback is given
if (!quadCallback) {
if (!onQuad) {
const quads = [];
let error;
this._callback = (e, t) => { e ? (error = e) : t && quads.push(t); };
Expand All @@ -1035,14 +1047,33 @@ export default class N3Parser {
return quads;
}

// Parse asynchronously otherwise, executing the read callback when a token arrives
this._callback = quadCallback;
this._lexer.tokenize(input, (error, token) => {
let processNextToken = (error, token) => {
if (error !== null)
this._callback(error), this._callback = noop;
else if (this._readCallback)
this._readCallback = this._readCallback(token);
});
};

// Enable checking for comments on every token when a commentCallback has been set
if (onComment) {
// Enable the lexer to return comments as tokens first (disabled by default)
this._lexer.comments = true;
// Patch the processNextToken function
processNextToken = (error, token) => {
if (error !== null)
this._callback(error), this._callback = noop;
else if (this._readCallback) {
if (token.type === 'comment')
onComment(token.value);
else
this._readCallback = this._readCallback(token);
}
};
}

// Parse asynchronously otherwise, executing the read callback when a token arrives
this._callback = onQuad;
this._lexer.tokenize(input, processNextToken);
}
}

Expand Down
18 changes: 12 additions & 6 deletions src/N3StreamParser.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,19 +11,25 @@ export default class N3StreamParser extends Transform {
// Set up parser with dummy stream to obtain `data` and `end` callbacks
const parser = new N3Parser(options);
let onData, onEnd;

const callbacks = {
// Handle quads by pushing them down the pipeline
onQuad: (error, quad) => { error && this.emit('error', error) || quad && this.push(quad); },
// Emit prefixes through the `prefix` event
onPrefix: (prefix, uri) => { this.emit('prefix', prefix, uri); },
};

if (options && options.comments)
callbacks.onComment = comment => { this.emit('comment', comment); };

parser.parse({
on: (event, callback) => {
switch (event) {
case 'data': onData = callback; break;
case 'end': onEnd = callback; break;
}
},
},
// Handle quads by pushing them down the pipeline
(error, quad) => { error && this.emit('error', error) || quad && this.push(quad); },
// Emit prefixes through the `prefix` event
(prefix, uri) => { this.emit('prefix', prefix, uri); },
);
}, callbacks);

// Implement Transform methods through parser callbacks
this._transform = (chunk, encoding, done) => { onData(chunk); done(); };
Expand Down
117 changes: 117 additions & 0 deletions test/N3Parser-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,28 @@ describe('Parser', () => {
['g', 'h', 'i']),
);

it(
'should parse three triples with comments if no comment callback is set',
shouldParse('<a> <b> #comment2\n <c> . \n<d> <e> <f>.\n<g> <h> <i>.',
['a', 'b', 'c'],
['d', 'e', 'f'],
['g', 'h', 'i']),
);

it(
'should parse three triples with comments when comment callback is set',
shouldParseWithCommentsEnabled('<a> <b> #comment2\n <c> . \n<d> <e> <f>.\n<g> <h> <i>.',
['a', 'b', 'c'],
['d', 'e', 'f'],
['g', 'h', 'i']),
);

it(
'should callback comments when a comment callback is set',
shouldCallbackComments('#comment1\n<a> <b> #comment2\n <c> . \n<d> <e> <f>.\n<g> <h> <i>.',
'comment1', 'comment2'),
);

it('should parse a triple with a literal', shouldParse('<a> <b> "string".',
['a', 'b', '"string"']));

Expand Down Expand Up @@ -203,6 +225,12 @@ describe('Parser', () => {
'Undefined prefix "d:" on line 1.'),
);

it(
'should not parse undefined prefix in datatype with comments enabled',
shouldNotParseWithComments('#comment\n<a> <b> "c"^^d:e ',
'Undefined prefix "d:" on line 2.'),
);

it(
'should parse triples with SPARQL prefixes',
shouldParse('PREFIX : <#>\n' +
Expand Down Expand Up @@ -1601,6 +1629,12 @@ describe('Parser', () => {
'Unexpected literal on line 1.'),
);

it(
'should not parse a literal as subject',
shouldNotParseWithComments(parser, '1 <a> <b>.',
'Unexpected literal on line 1.'),
);

it(
'should not parse RDF-star in the subject position',
shouldNotParse(parser, '<<<a> <b> <c>>> <a> <b> .',
Expand Down Expand Up @@ -1632,6 +1666,12 @@ describe('Parser', () => {
shouldNotParse(parser, '<<_:a <http://ex.org/b> _:b <http://ex.org/b>>> <http://ex.org/b> "c" .',
'Expected >> to follow "_:b0_b" on line 1.'),
);

it(
'should not parse nested quads with comments',
shouldNotParseWithComments(parser, '#comment1\n<<_:a <http://ex.org/b> _:b <http://ex.org/b>>> <http://ex.org/b> "c" .',
'Expected >> to follow "_:b0_b" on line 2.'),
);
});

describe('A Parser instance for the TriG format', () => {
Expand Down Expand Up @@ -3038,6 +3078,57 @@ function shouldParse(parser, input) {
};
}

function shouldParseWithCommentsEnabled(parser, input) {
const expected = Array.prototype.slice.call(arguments, 1);
// Shift parameters as necessary
if (parser.call)
expected.shift();
else
input = parser, parser = Parser;

return function (done) {
const results = [];
const items = expected.map(mapToQuad);
new parser({ baseIRI: BASE_IRI }).parse(input, {
onQuad: (error, triple) => {
expect(error).toBeFalsy();
if (triple)
results.push(triple);
else
expect(toSortedJSON(results)).toBe(toSortedJSON(items)), done();
},
onComment: comment => {
expect(comment).toBeDefined();
},
});
};
}


function shouldCallbackComments(parser, input) {
const expected = Array.prototype.slice.call(arguments, 1);
// Shift parameters as necessary
if (parser.call)
expected.shift();
else
input = parser, parser = Parser;

return function (done) {
const items = expected;
const comments = [];
new parser({ baseIRI: BASE_IRI }).parse(input, {
onQuad: (error, triple) => {
if (!triple) {
// Marks the end
expect(JSON.stringify(comments)).toBe(JSON.stringify(items));
done();
}
},
onComment: comment => { comments.push(comment); },
});
};
}

function mapToQuad(item) {
item = item.map(t => {
// don't touch if it's already an object
Expand Down Expand Up @@ -3082,6 +3173,32 @@ function shouldNotParse(parser, input, expectedError, expectedContext) {
};
}

function shouldNotParseWithComments(parser, input, expectedError, expectedContext) {
// Shift parameters if necessary
if (!parser.call)
expectedContext = expectedError, expectedError = input, input = parser, parser = Parser;

return function (done) {
new parser({ baseIRI: BASE_IRI }).parse(input, {
onQuad: (error, triple) => {
if (error) {
expect(triple).toBeFalsy();
expect(error).toBeInstanceOf(Error);
expect(error.message).toEqual(expectedError);
if (expectedContext) expect(error.context).toEqual(expectedContext);
done();
}
else if (!triple)
done(new Error(`Expected error ${expectedError}`));
},
// Enables comment mode
onComment: comment => {
expect(comment).toBeDefined();
},
});
};
}

function itShouldResolve(baseIRI, relativeIri, expected) {
let result;
describe(`resolving <${relativeIri}> against <${baseIRI}>`, () => {
Expand Down
Loading