1030 lines
40 KiB
JavaScript
1030 lines
40 KiB
JavaScript
"use strict";
|
|
|
|
Object.defineProperty(exports, "__esModule", {
|
|
value: true
|
|
});
|
|
exports.default = void 0;
|
|
var _N3Lexer = _interopRequireDefault(require("./N3Lexer"));
|
|
var _N3DataFactory = _interopRequireDefault(require("./N3DataFactory"));
|
|
var _IRIs = _interopRequireDefault(require("./IRIs"));
|
|
function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
|
|
// **N3Parser** parses N3 documents.
|
|
|
|
let blankNodePrefix = 0;
|
|
|
|
// ## Constructor
|
|
class N3Parser {
|
|
constructor(options) {
|
|
this._contextStack = [];
|
|
this._graph = null;
|
|
|
|
// Set the document IRI
|
|
options = options || {};
|
|
this._setBase(options.baseIRI);
|
|
options.factory && initDataFactory(this, options.factory);
|
|
|
|
// Set supported features depending on the format
|
|
const format = typeof options.format === 'string' ? options.format.match(/\w*$/)[0].toLowerCase() : '',
|
|
isTurtle = /turtle/.test(format),
|
|
isTriG = /trig/.test(format),
|
|
isNTriples = /triple/.test(format),
|
|
isNQuads = /quad/.test(format),
|
|
isN3 = this._n3Mode = /n3/.test(format),
|
|
isLineMode = isNTriples || isNQuads;
|
|
if (!(this._supportsNamedGraphs = !(isTurtle || isN3))) this._readPredicateOrNamedGraph = this._readPredicate;
|
|
// Support triples in other graphs
|
|
this._supportsQuads = !(isTurtle || isTriG || isNTriples || isN3);
|
|
// Whether the log:isImpliedBy predicate is supported
|
|
this._isImpliedBy = options.isImpliedBy;
|
|
// Support nesting of triples
|
|
this._supportsRDFStar = format === '' || /star|\*$/.test(format);
|
|
// Disable relative IRIs in N-Triples or N-Quads mode
|
|
if (isLineMode) this._resolveRelativeIRI = iri => {
|
|
return null;
|
|
};
|
|
this._blankNodePrefix = typeof options.blankNodePrefix !== 'string' ? '' : options.blankNodePrefix.replace(/^(?!_:)/, '_:');
|
|
this._lexer = options.lexer || new _N3Lexer.default({
|
|
lineMode: isLineMode,
|
|
n3: isN3,
|
|
isImpliedBy: this._isImpliedBy
|
|
});
|
|
// Disable explicit quantifiers by default
|
|
this._explicitQuantifiers = !!options.explicitQuantifiers;
|
|
}
|
|
|
|
// ## Static class methods
|
|
|
|
// ### `_resetBlankNodePrefix` restarts blank node prefix identification
|
|
static _resetBlankNodePrefix() {
|
|
blankNodePrefix = 0;
|
|
}
|
|
|
|
// ## Private methods
|
|
|
|
// ### `_setBase` sets the base IRI to resolve relative IRIs
|
|
_setBase(baseIRI) {
|
|
if (!baseIRI) {
|
|
this._base = '';
|
|
this._basePath = '';
|
|
} else {
|
|
// Remove fragment if present
|
|
const fragmentPos = baseIRI.indexOf('#');
|
|
if (fragmentPos >= 0) baseIRI = baseIRI.substr(0, fragmentPos);
|
|
// Set base IRI and its components
|
|
this._base = baseIRI;
|
|
this._basePath = baseIRI.indexOf('/') < 0 ? baseIRI : baseIRI.replace(/[^\/?]*(?:\?.*)?$/, '');
|
|
baseIRI = baseIRI.match(/^(?:([a-z][a-z0-9+.-]*:))?(?:\/\/[^\/]*)?/i);
|
|
this._baseRoot = baseIRI[0];
|
|
this._baseScheme = baseIRI[1];
|
|
}
|
|
}
|
|
|
|
// ### `_saveContext` stores the current parsing context
|
|
// when entering a new scope (list, blank node, formula)
|
|
_saveContext(type, graph, subject, predicate, object) {
|
|
const n3Mode = this._n3Mode;
|
|
this._contextStack.push({
|
|
type,
|
|
subject,
|
|
predicate,
|
|
object,
|
|
graph,
|
|
inverse: n3Mode ? this._inversePredicate : false,
|
|
blankPrefix: n3Mode ? this._prefixes._ : '',
|
|
quantified: n3Mode ? this._quantified : null
|
|
});
|
|
// The settings below only apply to N3 streams
|
|
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);
|
|
}
|
|
}
|
|
|
|
// ### `_restoreContext` restores the parent context
|
|
// when leaving a scope (list, blank node, formula)
|
|
_restoreContext(type, token) {
|
|
// Obtain the previous context
|
|
const context = this._contextStack.pop();
|
|
if (!context || context.type !== type) return this._error(`Unexpected ${token.type}`, token);
|
|
|
|
// Restore the quad of the previous context
|
|
this._subject = context.subject;
|
|
this._predicate = context.predicate;
|
|
this._object = context.object;
|
|
this._graph = context.graph;
|
|
|
|
// Restore N3 context settings
|
|
if (this._n3Mode) {
|
|
this._inversePredicate = context.inverse;
|
|
this._prefixes._ = context.blankPrefix;
|
|
this._quantified = context.quantified;
|
|
}
|
|
}
|
|
|
|
// ### `_readInTopContext` reads a token when in the top context
|
|
_readInTopContext(token) {
|
|
switch (token.type) {
|
|
// If an EOF token arrives in the top context, signal that we're done
|
|
case 'eof':
|
|
if (this._graph !== null) return this._error('Unclosed graph', token);
|
|
delete this._prefixes._;
|
|
return this._callback(null, null, this._prefixes);
|
|
// It could be a prefix declaration
|
|
case 'PREFIX':
|
|
this._sparqlStyle = true;
|
|
case '@prefix':
|
|
return this._readPrefix;
|
|
// It could be a base declaration
|
|
case 'BASE':
|
|
this._sparqlStyle = true;
|
|
case '@base':
|
|
return this._readBaseIRI;
|
|
// It could be a graph
|
|
case '{':
|
|
if (this._supportsNamedGraphs) {
|
|
this._graph = '';
|
|
this._subject = null;
|
|
return this._readSubject;
|
|
}
|
|
case 'GRAPH':
|
|
if (this._supportsNamedGraphs) return this._readNamedGraphLabel;
|
|
// Otherwise, the next token must be a subject
|
|
default:
|
|
return this._readSubject(token);
|
|
}
|
|
}
|
|
|
|
// ### `_readEntity` reads an IRI, prefixed name, blank node, or variable
|
|
_readEntity(token, quantifier) {
|
|
let value;
|
|
switch (token.type) {
|
|
// Read a relative or absolute IRI
|
|
case 'IRI':
|
|
case 'typeIRI':
|
|
const iri = this._resolveIRI(token.value);
|
|
if (iri === null) return this._error('Invalid IRI', token);
|
|
value = this._factory.namedNode(iri);
|
|
break;
|
|
// Read a prefixed name
|
|
case 'type':
|
|
case 'prefixed':
|
|
const prefix = this._prefixes[token.prefix];
|
|
if (prefix === undefined) return this._error(`Undefined prefix "${token.prefix}:"`, token);
|
|
value = this._factory.namedNode(prefix + token.value);
|
|
break;
|
|
// Read a blank node
|
|
case 'blank':
|
|
value = this._factory.blankNode(this._prefixes[token.prefix] + token.value);
|
|
break;
|
|
// Read a variable
|
|
case 'var':
|
|
value = this._factory.variable(token.value.substr(1));
|
|
break;
|
|
// Everything else is not an entity
|
|
default:
|
|
return this._error(`Expected entity but got ${token.type}`, token);
|
|
}
|
|
// In N3 mode, replace the entity if it is quantified
|
|
if (!quantifier && this._n3Mode && value.id in this._quantified) value = this._quantified[value.id];
|
|
return value;
|
|
}
|
|
|
|
// ### `_readSubject` reads a quad's subject
|
|
_readSubject(token) {
|
|
this._predicate = null;
|
|
switch (token.type) {
|
|
case '[':
|
|
// Start a new quad with a new blank node as subject
|
|
this._saveContext('blank', this._graph, this._subject = this._factory.blankNode(), null, null);
|
|
return this._readBlankNodeHead;
|
|
case '(':
|
|
// Start a new list
|
|
this._saveContext('list', this._graph, this.RDF_NIL, null, null);
|
|
this._subject = null;
|
|
return this._readListItem;
|
|
case '{':
|
|
// Start a new formula
|
|
if (!this._n3Mode) return this._error('Unexpected graph', token);
|
|
this._saveContext('formula', this._graph, this._graph = this._factory.blankNode(), null, null);
|
|
return this._readSubject;
|
|
case '}':
|
|
// No subject; the graph in which we are reading is closed instead
|
|
return this._readPunctuation(token);
|
|
case '@forSome':
|
|
if (!this._n3Mode) return this._error('Unexpected "@forSome"', token);
|
|
this._subject = null;
|
|
this._predicate = this.N3_FORSOME;
|
|
this._quantifier = 'blankNode';
|
|
return this._readQuantifierList;
|
|
case '@forAll':
|
|
if (!this._n3Mode) return this._error('Unexpected "@forAll"', token);
|
|
this._subject = null;
|
|
this._predicate = this.N3_FORALL;
|
|
this._quantifier = 'variable';
|
|
return this._readQuantifierList;
|
|
case 'literal':
|
|
if (!this._n3Mode) return this._error('Unexpected literal', token);
|
|
if (token.prefix.length === 0) {
|
|
this._literalValue = token.value;
|
|
return this._completeSubjectLiteral;
|
|
} else this._subject = this._factory.literal(token.value, this._factory.namedNode(token.prefix));
|
|
break;
|
|
case '<<':
|
|
if (!this._supportsRDFStar) return this._error('Unexpected RDF-star syntax', token);
|
|
this._saveContext('<<', this._graph, null, null, null);
|
|
this._graph = null;
|
|
return this._readSubject;
|
|
default:
|
|
// Read the subject entity
|
|
if ((this._subject = this._readEntity(token)) === undefined) return;
|
|
// In N3 mode, the subject might be a path
|
|
if (this._n3Mode) return this._getPathReader(this._readPredicateOrNamedGraph);
|
|
}
|
|
|
|
// The next token must be a predicate,
|
|
// or, if the subject was actually a graph IRI, a named graph
|
|
return this._readPredicateOrNamedGraph;
|
|
}
|
|
|
|
// ### `_readPredicate` reads a quad's predicate
|
|
_readPredicate(token) {
|
|
const type = token.type;
|
|
switch (type) {
|
|
case 'inverse':
|
|
this._inversePredicate = true;
|
|
case 'abbreviation':
|
|
this._predicate = this.ABBREVIATIONS[token.value];
|
|
break;
|
|
case '.':
|
|
case ']':
|
|
case '}':
|
|
// Expected predicate didn't come, must have been trailing semicolon
|
|
if (this._predicate === null) return this._error(`Unexpected ${type}`, token);
|
|
this._subject = null;
|
|
return type === ']' ? this._readBlankNodeTail(token) : this._readPunctuation(token);
|
|
case ';':
|
|
// Additional semicolons can be safely ignored
|
|
return this._predicate !== null ? this._readPredicate : this._error('Expected predicate but got ;', token);
|
|
case '[':
|
|
if (this._n3Mode) {
|
|
// Start a new quad with a new blank node as subject
|
|
this._saveContext('blank', this._graph, this._subject, this._subject = this._factory.blankNode(), null);
|
|
return this._readBlankNodeHead;
|
|
}
|
|
case 'blank':
|
|
if (!this._n3Mode) return this._error('Disallowed blank node as predicate', token);
|
|
default:
|
|
if ((this._predicate = this._readEntity(token)) === undefined) return;
|
|
}
|
|
// The next token must be an object
|
|
return this._readObject;
|
|
}
|
|
|
|
// ### `_readObject` reads a quad's object
|
|
_readObject(token) {
|
|
switch (token.type) {
|
|
case 'literal':
|
|
// Regular literal, can still get a datatype or language
|
|
if (token.prefix.length === 0) {
|
|
this._literalValue = token.value;
|
|
return this._readDataTypeOrLang;
|
|
}
|
|
// Pre-datatyped string literal (prefix stores the datatype)
|
|
else this._object = this._factory.literal(token.value, this._factory.namedNode(token.prefix));
|
|
break;
|
|
case '[':
|
|
// Start a new quad with a new blank node as subject
|
|
this._saveContext('blank', this._graph, this._subject, this._predicate, this._subject = this._factory.blankNode());
|
|
return this._readBlankNodeHead;
|
|
case '(':
|
|
// Start a new list
|
|
this._saveContext('list', this._graph, this._subject, this._predicate, this.RDF_NIL);
|
|
this._subject = null;
|
|
return this._readListItem;
|
|
case '{':
|
|
// Start a new formula
|
|
if (!this._n3Mode) return this._error('Unexpected graph', token);
|
|
this._saveContext('formula', this._graph, this._subject, this._predicate, this._graph = this._factory.blankNode());
|
|
return this._readSubject;
|
|
case '<<':
|
|
if (!this._supportsRDFStar) return this._error('Unexpected RDF-star syntax', token);
|
|
this._saveContext('<<', this._graph, this._subject, this._predicate, null);
|
|
this._graph = null;
|
|
return this._readSubject;
|
|
default:
|
|
// Read the object entity
|
|
if ((this._object = this._readEntity(token)) === undefined) return;
|
|
// In N3 mode, the object might be a path
|
|
if (this._n3Mode) return this._getPathReader(this._getContextEndReader());
|
|
}
|
|
return this._getContextEndReader();
|
|
}
|
|
|
|
// ### `_readPredicateOrNamedGraph` reads a quad's predicate, or a named graph
|
|
_readPredicateOrNamedGraph(token) {
|
|
return token.type === '{' ? this._readGraph(token) : this._readPredicate(token);
|
|
}
|
|
|
|
// ### `_readGraph` reads a graph
|
|
_readGraph(token) {
|
|
if (token.type !== '{') return this._error(`Expected graph but got ${token.type}`, token);
|
|
// The "subject" we read is actually the GRAPH's label
|
|
this._graph = this._subject, this._subject = null;
|
|
return this._readSubject;
|
|
}
|
|
|
|
// ### `_readBlankNodeHead` reads the head of a blank node
|
|
_readBlankNodeHead(token) {
|
|
if (token.type === ']') {
|
|
this._subject = null;
|
|
return this._readBlankNodeTail(token);
|
|
} else {
|
|
this._predicate = null;
|
|
return this._readPredicate(token);
|
|
}
|
|
}
|
|
|
|
// ### `_readBlankNodeTail` reads the end of a blank node
|
|
_readBlankNodeTail(token) {
|
|
if (token.type !== ']') return this._readBlankNodePunctuation(token);
|
|
|
|
// Store blank node quad
|
|
if (this._subject !== null) this._emit(this._subject, this._predicate, this._object, this._graph);
|
|
|
|
// Restore the parent context containing this blank node
|
|
const empty = this._predicate === null;
|
|
this._restoreContext('blank', token);
|
|
// If the blank node was the object, restore previous context and read punctuation
|
|
if (this._object !== null) return this._getContextEndReader();
|
|
// If the blank node was the predicate, continue reading the object
|
|
else if (this._predicate !== null) return this._readObject;
|
|
// If the blank node was the subject, continue reading the predicate
|
|
else
|
|
// If the blank node was empty, it could be a named graph label
|
|
return empty ? this._readPredicateOrNamedGraph : this._readPredicateAfterBlank;
|
|
}
|
|
|
|
// ### `_readPredicateAfterBlank` reads a predicate after an anonymous blank node
|
|
_readPredicateAfterBlank(token) {
|
|
switch (token.type) {
|
|
case '.':
|
|
case '}':
|
|
// No predicate is coming if the triple is terminated here
|
|
this._subject = null;
|
|
return this._readPunctuation(token);
|
|
default:
|
|
return this._readPredicate(token);
|
|
}
|
|
}
|
|
|
|
// ### `_readListItem` reads items from a list
|
|
_readListItem(token) {
|
|
let item = null,
|
|
// The item of the list
|
|
list = null,
|
|
// The list itself
|
|
next = this._readListItem; // The next function to execute
|
|
const previousList = this._subject,
|
|
// The previous list that contains this list
|
|
stack = this._contextStack,
|
|
// The stack of parent contexts
|
|
parent = stack[stack.length - 1]; // The parent containing the current list
|
|
|
|
switch (token.type) {
|
|
case '[':
|
|
// Stack the current list quad and start a new quad with a blank node as subject
|
|
this._saveContext('blank', this._graph, list = this._factory.blankNode(), this.RDF_FIRST, this._subject = item = this._factory.blankNode());
|
|
next = this._readBlankNodeHead;
|
|
break;
|
|
case '(':
|
|
// Stack the current list quad and start a new list
|
|
this._saveContext('list', this._graph, list = this._factory.blankNode(), this.RDF_FIRST, this.RDF_NIL);
|
|
this._subject = null;
|
|
break;
|
|
case ')':
|
|
// Closing the list; restore the parent context
|
|
this._restoreContext('list', token);
|
|
// If this list is contained within a parent list, return the membership quad here.
|
|
// This will be `<parent list element> rdf:first <this list>.`.
|
|
if (stack.length !== 0 && stack[stack.length - 1].type === 'list') 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;
|
|
// No list tail if this was an empty list
|
|
if (this._subject === this.RDF_NIL) return next;
|
|
}
|
|
// The list was in the parent context's object
|
|
else {
|
|
next = this._getContextEndReader();
|
|
// No list tail if this was an empty list
|
|
if (this._object === this.RDF_NIL) return next;
|
|
}
|
|
// Close the list by making the head nil
|
|
list = this.RDF_NIL;
|
|
break;
|
|
case 'literal':
|
|
// Regular literal, can still get a datatype or language
|
|
if (token.prefix.length === 0) {
|
|
this._literalValue = token.value;
|
|
next = this._readListItemDataTypeOrLang;
|
|
}
|
|
// Pre-datatyped string literal (prefix stores the datatype)
|
|
else {
|
|
item = this._factory.literal(token.value, this._factory.namedNode(token.prefix));
|
|
next = this._getContextEndReader();
|
|
}
|
|
break;
|
|
case '{':
|
|
// Start a new formula
|
|
if (!this._n3Mode) return this._error('Unexpected graph', token);
|
|
this._saveContext('formula', this._graph, this._subject, this._predicate, this._graph = this._factory.blankNode());
|
|
return this._readSubject;
|
|
default:
|
|
if ((item = this._readEntity(token)) === undefined) return;
|
|
}
|
|
|
|
// Create a new blank node if no item head was assigned yet
|
|
if (list === null) this._subject = list = this._factory.blankNode();
|
|
|
|
// Is this the first element of the list?
|
|
if (previousList === null) {
|
|
// This list is either the subject or the object of its parent
|
|
if (parent.predicate === null) parent.subject = list;else parent.object = list;
|
|
} else {
|
|
// Continue the previous list with the current list
|
|
this._emit(previousList, this.RDF_REST, list, this._graph);
|
|
}
|
|
// 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')) {
|
|
// 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;
|
|
// _readPath will restore the context and output the item
|
|
return this._getPathReader(this._readListItem);
|
|
}
|
|
// Output the item
|
|
this._emit(list, this.RDF_FIRST, item, this._graph);
|
|
}
|
|
return next;
|
|
}
|
|
|
|
// ### `_readDataTypeOrLang` reads an _optional_ datatype or language
|
|
_readDataTypeOrLang(token) {
|
|
return this._completeObjectLiteral(token, false);
|
|
}
|
|
|
|
// ### `_readListItemDataTypeOrLang` reads an _optional_ datatype or language in a list
|
|
_readListItemDataTypeOrLang(token) {
|
|
return this._completeObjectLiteral(token, true);
|
|
}
|
|
|
|
// ### `_completeLiteral` completes a literal with an optional datatype or language
|
|
_completeLiteral(token) {
|
|
// Create a simple string literal by default
|
|
let literal = this._factory.literal(this._literalValue);
|
|
switch (token.type) {
|
|
// Create a datatyped literal
|
|
case 'type':
|
|
case 'typeIRI':
|
|
const datatype = this._readEntity(token);
|
|
if (datatype === undefined) return; // No datatype means an error occurred
|
|
literal = this._factory.literal(this._literalValue, datatype);
|
|
token = null;
|
|
break;
|
|
// Create a language-tagged string
|
|
case 'langcode':
|
|
literal = this._factory.literal(this._literalValue, token.value);
|
|
token = null;
|
|
break;
|
|
}
|
|
return {
|
|
token,
|
|
literal
|
|
};
|
|
}
|
|
|
|
// Completes a literal in subject position
|
|
_completeSubjectLiteral(token) {
|
|
this._subject = this._completeLiteral(token).literal;
|
|
return this._readPredicateOrNamedGraph;
|
|
}
|
|
|
|
// Completes a literal in object position
|
|
_completeObjectLiteral(token, listItem) {
|
|
const completed = this._completeLiteral(token);
|
|
if (!completed) return;
|
|
this._object = completed.literal;
|
|
|
|
// If this literal was part of a list, write the item
|
|
// (we could also check the context stack, but passing in a flag is faster)
|
|
if (listItem) this._emit(this._subject, this.RDF_FIRST, this._object, this._graph);
|
|
// If the token was consumed, continue with the rest of the input
|
|
if (completed.token === null) return this._getContextEndReader();
|
|
// Otherwise, consume the token now
|
|
else {
|
|
this._readCallback = this._getContextEndReader();
|
|
return this._readCallback(completed.token);
|
|
}
|
|
}
|
|
|
|
// ### `_readFormulaTail` reads the end of a formula
|
|
_readFormulaTail(token) {
|
|
if (token.type !== '}') return this._readPunctuation(token);
|
|
|
|
// Store the last quad of the formula
|
|
if (this._subject !== null) this._emit(this._subject, this._predicate, this._object, this._graph);
|
|
|
|
// Restore the parent context containing this formula
|
|
this._restoreContext('formula', token);
|
|
// If the formula was the subject, continue reading the predicate.
|
|
// If the formula was the object, read punctuation.
|
|
return this._object === null ? this._readPredicate : this._getContextEndReader();
|
|
}
|
|
|
|
// ### `_readPunctuation` reads punctuation between quads or quad parts
|
|
_readPunctuation(token) {
|
|
let next,
|
|
graph = this._graph;
|
|
const subject = this._subject,
|
|
inversePredicate = this._inversePredicate;
|
|
switch (token.type) {
|
|
// A closing brace ends a graph
|
|
case '}':
|
|
if (this._graph === null) return this._error('Unexpected graph closing', token);
|
|
if (this._n3Mode) return this._readFormulaTail(token);
|
|
this._graph = null;
|
|
// A dot just ends the statement, without sharing anything with the next
|
|
case '.':
|
|
this._subject = null;
|
|
next = this._contextStack.length ? this._readSubject : this._readInTopContext;
|
|
if (inversePredicate) this._inversePredicate = false;
|
|
break;
|
|
// Semicolon means the subject is shared; predicate and object are different
|
|
case ';':
|
|
next = this._readPredicate;
|
|
break;
|
|
// Comma means both the subject and predicate are shared; the object is different
|
|
case ',':
|
|
next = this._readObject;
|
|
break;
|
|
// {| means that the current triple is annotated with predicate-object pairs.
|
|
case '{|':
|
|
if (!this._supportsRDFStar) return this._error('Unexpected RDF-star syntax', token);
|
|
// Continue using the last triple as quoted triple subject for the predicate-object pairs.
|
|
const predicate = this._predicate,
|
|
object = this._object;
|
|
this._subject = this._factory.quad(subject, predicate, object, this.DEFAULTGRAPH);
|
|
next = this._readPredicate;
|
|
break;
|
|
// |} means that the current quoted triple in annotation syntax is finalized.
|
|
case '|}':
|
|
if (this._subject.termType !== 'Quad') return this._error('Unexpected asserted triple closing', token);
|
|
this._subject = null;
|
|
next = this._readPunctuation;
|
|
break;
|
|
default:
|
|
// An entity means this is a quad (only allowed if not already inside a graph)
|
|
if (this._supportsQuads && this._graph === null && (graph = this._readEntity(token)) !== undefined) {
|
|
next = this._readQuadPunctuation;
|
|
break;
|
|
}
|
|
return this._error(`Expected punctuation to follow "${this._object.id}"`, token);
|
|
}
|
|
// A quad has been completed now, so return it
|
|
if (subject !== null) {
|
|
const predicate = this._predicate,
|
|
object = this._object;
|
|
if (!inversePredicate) this._emit(subject, predicate, object, graph);else this._emit(object, predicate, subject, graph);
|
|
}
|
|
return next;
|
|
}
|
|
|
|
// ### `_readBlankNodePunctuation` reads punctuation in a blank node
|
|
_readBlankNodePunctuation(token) {
|
|
let next;
|
|
switch (token.type) {
|
|
// Semicolon means the subject is shared; predicate and object are different
|
|
case ';':
|
|
next = this._readPredicate;
|
|
break;
|
|
// Comma means both the subject and predicate are shared; the object is different
|
|
case ',':
|
|
next = this._readObject;
|
|
break;
|
|
default:
|
|
return this._error(`Expected punctuation to follow "${this._object.id}"`, token);
|
|
}
|
|
// A quad has been completed now, so return it
|
|
this._emit(this._subject, this._predicate, this._object, this._graph);
|
|
return next;
|
|
}
|
|
|
|
// ### `_readQuadPunctuation` reads punctuation after a quad
|
|
_readQuadPunctuation(token) {
|
|
if (token.type !== '.') return this._error('Expected dot to follow quad', token);
|
|
return this._readInTopContext;
|
|
}
|
|
|
|
// ### `_readPrefix` reads the prefix of a prefix declaration
|
|
_readPrefix(token) {
|
|
if (token.type !== 'prefix') return this._error('Expected prefix to follow @prefix', token);
|
|
this._prefix = token.value;
|
|
return this._readPrefixIRI;
|
|
}
|
|
|
|
// ### `_readPrefixIRI` reads the IRI of a prefix declaration
|
|
_readPrefixIRI(token) {
|
|
if (token.type !== 'IRI') return this._error(`Expected IRI to follow prefix "${this._prefix}:"`, token);
|
|
const prefixNode = this._readEntity(token);
|
|
this._prefixes[this._prefix] = prefixNode.value;
|
|
this._prefixCallback(this._prefix, prefixNode);
|
|
return this._readDeclarationPunctuation;
|
|
}
|
|
|
|
// ### `_readBaseIRI` reads the IRI of a base declaration
|
|
_readBaseIRI(token) {
|
|
const iri = token.type === 'IRI' && this._resolveIRI(token.value);
|
|
if (!iri) return this._error('Expected valid IRI to follow base declaration', token);
|
|
this._setBase(iri);
|
|
return this._readDeclarationPunctuation;
|
|
}
|
|
|
|
// ### `_readNamedGraphLabel` reads the label of a named graph
|
|
_readNamedGraphLabel(token) {
|
|
switch (token.type) {
|
|
case 'IRI':
|
|
case 'blank':
|
|
case 'prefixed':
|
|
return this._readSubject(token), this._readGraph;
|
|
case '[':
|
|
return this._readNamedGraphBlankLabel;
|
|
default:
|
|
return this._error('Invalid graph label', token);
|
|
}
|
|
}
|
|
|
|
// ### `_readNamedGraphLabel` reads a blank node label of a named graph
|
|
_readNamedGraphBlankLabel(token) {
|
|
if (token.type !== ']') return this._error('Invalid graph label', token);
|
|
this._subject = this._factory.blankNode();
|
|
return this._readGraph;
|
|
}
|
|
|
|
// ### `_readDeclarationPunctuation` reads the punctuation of a declaration
|
|
_readDeclarationPunctuation(token) {
|
|
// SPARQL-style declarations don't have punctuation
|
|
if (this._sparqlStyle) {
|
|
this._sparqlStyle = false;
|
|
return this._readInTopContext(token);
|
|
}
|
|
if (token.type !== '.') return this._error('Expected declaration to end with a dot', token);
|
|
return this._readInTopContext;
|
|
}
|
|
|
|
// Reads a list of quantified symbols from a @forSome or @forAll statement
|
|
_readQuantifierList(token) {
|
|
let entity;
|
|
switch (token.type) {
|
|
case 'IRI':
|
|
case 'prefixed':
|
|
if ((entity = this._readEntity(token, true)) !== undefined) break;
|
|
default:
|
|
return this._error(`Unexpected ${token.type}`, token);
|
|
}
|
|
// Without explicit quantifiers, map entities to a quantified entity
|
|
if (!this._explicitQuantifiers) this._quantified[entity.id] = this._factory[this._quantifier](this._factory.blankNode().value);
|
|
// With explicit quantifiers, output the reified quantifier
|
|
else {
|
|
// If this is the first item, start a new quantifier list
|
|
if (this._subject === null) this._emit(this._graph || this.DEFAULTGRAPH, this._predicate, this._subject = this._factory.blankNode(), this.QUANTIFIERS_GRAPH);
|
|
// Otherwise, continue the previous list
|
|
else this._emit(this._subject, this.RDF_REST, this._subject = this._factory.blankNode(), this.QUANTIFIERS_GRAPH);
|
|
// Output the list item
|
|
this._emit(this._subject, this.RDF_FIRST, entity, this.QUANTIFIERS_GRAPH);
|
|
}
|
|
return this._readQuantifierPunctuation;
|
|
}
|
|
|
|
// Reads punctuation from a @forSome or @forAll statement
|
|
_readQuantifierPunctuation(token) {
|
|
// Read more quantifiers
|
|
if (token.type === ',') return this._readQuantifierList;
|
|
// End of the quantifier list
|
|
else {
|
|
// With explicit quantifiers, close the quantifier list
|
|
if (this._explicitQuantifiers) {
|
|
this._emit(this._subject, this.RDF_REST, this.RDF_NIL, this.QUANTIFIERS_GRAPH);
|
|
this._subject = null;
|
|
}
|
|
// Read a dot
|
|
this._readCallback = this._getContextEndReader();
|
|
return this._readCallback(token);
|
|
}
|
|
}
|
|
|
|
// ### `_getPathReader` reads a potential path and then resumes with the given function
|
|
_getPathReader(afterPath) {
|
|
this._afterPath = afterPath;
|
|
return this._readPath;
|
|
}
|
|
|
|
// ### `_readPath` reads a potential path
|
|
_readPath(token) {
|
|
switch (token.type) {
|
|
// Forward path
|
|
case '!':
|
|
return this._readForwardPath;
|
|
// Backward path
|
|
case '^':
|
|
return this._readBackwardPath;
|
|
// Not a path; resume reading where we left off
|
|
default:
|
|
const stack = this._contextStack,
|
|
parent = stack.length && stack[stack.length - 1];
|
|
// If we were reading a list item, we still need to output it
|
|
if (parent && parent.type === 'item') {
|
|
// The list item is the remaining subejct after reading the path
|
|
const item = this._subject;
|
|
// Switch back to the context of the list
|
|
this._restoreContext('item', token);
|
|
// Output the list item
|
|
this._emit(this._subject, this.RDF_FIRST, item, this._graph);
|
|
}
|
|
return this._afterPath(token);
|
|
}
|
|
}
|
|
|
|
// ### `_readForwardPath` reads a '!' path
|
|
_readForwardPath(token) {
|
|
let subject, predicate;
|
|
const object = this._factory.blankNode();
|
|
// The next token is the predicate
|
|
if ((predicate = this._readEntity(token)) === undefined) return;
|
|
// If we were reading a subject, replace the subject by the path's object
|
|
if (this._predicate === null) subject = this._subject, this._subject = object;
|
|
// If we were reading an object, replace the subject by the path's object
|
|
else subject = this._object, this._object = object;
|
|
// Emit the path's current quad and read its next section
|
|
this._emit(subject, predicate, object, this._graph);
|
|
return this._readPath;
|
|
}
|
|
|
|
// ### `_readBackwardPath` reads a '^' path
|
|
_readBackwardPath(token) {
|
|
const subject = this._factory.blankNode();
|
|
let predicate, object;
|
|
// The next token is the predicate
|
|
if ((predicate = this._readEntity(token)) === undefined) return;
|
|
// If we were reading a subject, replace the subject by the path's subject
|
|
if (this._predicate === null) object = this._subject, this._subject = subject;
|
|
// If we were reading an object, replace the subject by the path's subject
|
|
else object = this._object, this._object = subject;
|
|
// Emit the path's current quad and read its next section
|
|
this._emit(subject, predicate, object, this._graph);
|
|
return this._readPath;
|
|
}
|
|
|
|
// ### `_readRDFStarTailOrGraph` reads the graph of a nested RDF-star quad or the end of a nested RDF-star triple
|
|
_readRDFStarTailOrGraph(token) {
|
|
if (token.type !== '>>') {
|
|
// An entity means this is a quad (only allowed if not already inside a graph)
|
|
if (this._supportsQuads && this._graph === null && (this._graph = this._readEntity(token)) !== undefined) return this._readRDFStarTail;
|
|
return this._error(`Expected >> to follow "${this._object.id}"`, token);
|
|
}
|
|
return this._readRDFStarTail(token);
|
|
}
|
|
|
|
// ### `_readRDFStarTail` reads the end of a nested RDF-star triple
|
|
_readRDFStarTail(token) {
|
|
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);
|
|
this._restoreContext('<<', token);
|
|
// If the triple was the subject, continue by reading the predicate.
|
|
if (this._subject === null) {
|
|
this._subject = quad;
|
|
return this._readPredicate;
|
|
}
|
|
// If the triple was the object, read context end.
|
|
else {
|
|
this._object = quad;
|
|
return this._getContextEndReader();
|
|
}
|
|
}
|
|
|
|
// ### `_getContextEndReader` gets the next reader function at the end of a context
|
|
_getContextEndReader() {
|
|
const contextStack = this._contextStack;
|
|
if (!contextStack.length) return this._readPunctuation;
|
|
switch (contextStack[contextStack.length - 1].type) {
|
|
case 'blank':
|
|
return this._readBlankNodeTail;
|
|
case 'list':
|
|
return this._readListItem;
|
|
case 'formula':
|
|
return this._readFormulaTail;
|
|
case '<<':
|
|
return this._readRDFStarTailOrGraph;
|
|
}
|
|
}
|
|
|
|
// ### `_emit` sends a quad through the callback
|
|
_emit(subject, predicate, object, graph) {
|
|
this._callback(null, this._factory.quad(subject, predicate, object, graph || this.DEFAULTGRAPH));
|
|
}
|
|
|
|
// ### `_error` emits an error message through the callback
|
|
_error(message, token) {
|
|
const err = new Error(`${message} on line ${token.line}.`);
|
|
err.context = {
|
|
token: token,
|
|
line: token.line,
|
|
previousToken: this._lexer.previousToken
|
|
};
|
|
this._callback(err);
|
|
this._callback = noop;
|
|
}
|
|
|
|
// ### `_resolveIRI` resolves an IRI against the base path
|
|
_resolveIRI(iri) {
|
|
return /^[a-z][a-z0-9+.-]*:/i.test(iri) ? iri : this._resolveRelativeIRI(iri);
|
|
}
|
|
|
|
// ### `_resolveRelativeIRI` resolves an IRI against the base path,
|
|
// assuming that a base path has been set and that the IRI is indeed relative
|
|
_resolveRelativeIRI(iri) {
|
|
// An empty relative IRI indicates the base IRI
|
|
if (!iri.length) return this._base;
|
|
// Decide resolving strategy based in the first character
|
|
switch (iri[0]) {
|
|
// Resolve relative fragment IRIs against the base IRI
|
|
case '#':
|
|
return this._base + iri;
|
|
// Resolve relative query string IRIs by replacing the query string
|
|
case '?':
|
|
return this._base.replace(/(?:\?.*)?$/, iri);
|
|
// Resolve root-relative IRIs at the root of the base IRI
|
|
case '/':
|
|
// Resolve scheme-relative IRIs to the scheme
|
|
return (iri[1] === '/' ? this._baseScheme : this._baseRoot) + this._removeDotSegments(iri);
|
|
// Resolve all other IRIs at the base IRI's path
|
|
default:
|
|
// Relative IRIs cannot contain a colon in the first path segment
|
|
return /^[^/:]*:/.test(iri) ? null : this._removeDotSegments(this._basePath + iri);
|
|
}
|
|
}
|
|
|
|
// ### `_removeDotSegments` resolves './' and '../' path segments in an IRI as per RFC3986
|
|
_removeDotSegments(iri) {
|
|
// Don't modify the IRI if it does not contain any dot segments
|
|
if (!/(^|\/)\.\.?($|[/#?])/.test(iri)) return iri;
|
|
|
|
// Start with an imaginary slash before the IRI in order to resolve trailing './' and '../'
|
|
const length = iri.length;
|
|
let result = '',
|
|
i = -1,
|
|
pathStart = -1,
|
|
segmentStart = 0,
|
|
next = '/';
|
|
while (i < length) {
|
|
switch (next) {
|
|
// The path starts with the first slash after the authority
|
|
case ':':
|
|
if (pathStart < 0) {
|
|
// Skip two slashes before the authority
|
|
if (iri[++i] === '/' && iri[++i] === '/')
|
|
// Skip to slash after the authority
|
|
while ((pathStart = i + 1) < length && iri[pathStart] !== '/') i = pathStart;
|
|
}
|
|
break;
|
|
// Don't modify a query string or fragment
|
|
case '?':
|
|
case '#':
|
|
i = length;
|
|
break;
|
|
// Handle '/.' or '/..' path segments
|
|
case '/':
|
|
if (iri[i + 1] === '.') {
|
|
next = iri[++i + 1];
|
|
switch (next) {
|
|
// Remove a '/.' segment
|
|
case '/':
|
|
result += iri.substring(segmentStart, i - 1);
|
|
segmentStart = i + 1;
|
|
break;
|
|
// Remove a trailing '/.' segment
|
|
case undefined:
|
|
case '?':
|
|
case '#':
|
|
return result + iri.substring(segmentStart, i) + iri.substr(i + 1);
|
|
// Remove a '/..' segment
|
|
case '.':
|
|
next = iri[++i + 1];
|
|
if (next === undefined || next === '/' || next === '?' || next === '#') {
|
|
result += iri.substring(segmentStart, i - 2);
|
|
// Try to remove the parent path from result
|
|
if ((segmentStart = result.lastIndexOf('/')) >= pathStart) result = result.substr(0, segmentStart);
|
|
// Remove a trailing '/..' segment
|
|
if (next !== '/') return `${result}/${iri.substr(i + 1)}`;
|
|
segmentStart = i + 1;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
next = iri[++i];
|
|
}
|
|
return result + iri.substring(segmentStart);
|
|
}
|
|
|
|
// ## Public methods
|
|
|
|
// ### `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 = onPrefix || noop;
|
|
this._inversePredicate = false;
|
|
this._quantified = Object.create(null);
|
|
|
|
// Parse synchronously if no quad callback is given
|
|
if (!onQuad) {
|
|
const quads = [];
|
|
let error;
|
|
this._callback = (e, t) => {
|
|
e ? error = e : t && quads.push(t);
|
|
};
|
|
this._lexer.tokenize(input).every(token => {
|
|
return this._readCallback = this._readCallback(token);
|
|
});
|
|
if (error) throw error;
|
|
return quads;
|
|
}
|
|
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);
|
|
}
|
|
}
|
|
|
|
// The empty function
|
|
exports.default = N3Parser;
|
|
function noop() {}
|
|
|
|
// Initializes the parser with the given data factory
|
|
function initDataFactory(parser, factory) {
|
|
parser._factory = factory;
|
|
parser.DEFAULTGRAPH = factory.defaultGraph();
|
|
|
|
// Set common named nodes
|
|
parser.RDF_FIRST = factory.namedNode(_IRIs.default.rdf.first);
|
|
parser.RDF_REST = factory.namedNode(_IRIs.default.rdf.rest);
|
|
parser.RDF_NIL = factory.namedNode(_IRIs.default.rdf.nil);
|
|
parser.N3_FORALL = factory.namedNode(_IRIs.default.r.forAll);
|
|
parser.N3_FORSOME = factory.namedNode(_IRIs.default.r.forSome);
|
|
parser.ABBREVIATIONS = {
|
|
'a': factory.namedNode(_IRIs.default.rdf.type),
|
|
'=': factory.namedNode(_IRIs.default.owl.sameAs),
|
|
'>': factory.namedNode(_IRIs.default.log.implies),
|
|
'<': factory.namedNode(_IRIs.default.log.isImpliedBy)
|
|
};
|
|
parser.QUANTIFIERS_GRAPH = factory.namedNode('urn:n3:quantifiers');
|
|
}
|
|
initDataFactory(N3Parser.prototype, _N3DataFactory.default); |