diff --git a/src/N3DataFactory.js b/src/N3DataFactory.js index cb300263..1ba33d93 100644 --- a/src/N3DataFactory.js +++ b/src/N3DataFactory.js @@ -5,6 +5,8 @@ import namespaces from './IRIs'; const { rdf, xsd } = namespaces; +const DEFAULT_CONTEXT = { token: null }; + // eslint-disable-next-line prefer-const let DEFAULTGRAPH; let _blankNodeCounter = 0; @@ -12,11 +14,12 @@ let _blankNodeCounter = 0; const escapedLiteral = /^"(.*".*)(?="[^"]*$)/; // ## DataFactory singleton +// Note: The default data factory does not set the token field of terms. const DataFactory = { - namedNode, - blankNode, - variable, - literal, + namedNode: iri => namedNode(iri), + blankNode: name => blankNode(name), + variable: name => variable(name), + literal: (name, datatype) => literal(name, datatype), defaultGraph, quad, triple: quad, @@ -25,8 +28,9 @@ export default DataFactory; // ## Term constructor export class Term { - constructor(id) { + constructor(id, context = DEFAULT_CONTEXT) { this.id = id; + this.context = context; } // ### The value of this term @@ -132,8 +136,8 @@ export class Literal extends Term { // ## BlankNode constructor export class BlankNode extends Term { - constructor(name) { - super(`_:${name}`); + constructor(name, context = DEFAULT_CONTEXT) { + super(`_:${name}`, context); } // ### The term type of this term @@ -148,8 +152,8 @@ export class BlankNode extends Term { } export class Variable extends Term { - constructor(name) { - super(`?${name}`); + constructor(name, context = DEFAULT_CONTEXT) { + super(`?${name}`, context); } // ### The term type of this term @@ -166,7 +170,8 @@ export class Variable extends Term { // ## DefaultGraph constructor export class DefaultGraph extends Term { constructor() { - super(''); + super('', DEFAULT_CONTEXT); + return DEFAULTGRAPH || this; } @@ -192,7 +197,7 @@ DEFAULTGRAPH = new DefaultGraph(); // with recursion over nested terms. It should not be used // by consumers of this library. // See https://github.com/rdfjs/N3.js/pull/311#discussion_r1061042725 -export function termFromId(id, factory, nested) { +export function termFromId(id, factory, nested, token) { factory = factory || DataFactory; // Falsy value or empty string indicate the default graph @@ -208,7 +213,7 @@ export function termFromId(id, factory, nested) { case '"': // Shortcut for internal literals if (factory === DataFactory) - return new Literal(id); + return new Literal(id, token); // Literal without datatype or language if (id[id.length - 1] === '"') return factory.literal(id.substr(1, id.length - 2)); @@ -273,7 +278,8 @@ export function termToId(term, nested) { // ## Quad constructor export class Quad extends Term { constructor(subject, predicate, object, graph) { - super(''); + super('', DEFAULT_CONTEXT); + this._subject = subject; this._predicate = predicate; this._object = object; @@ -333,20 +339,20 @@ export function unescapeQuotes(id) { } // ### Creates an IRI -function namedNode(iri) { - return new NamedNode(iri); +export function namedNode(iri, context = DEFAULT_CONTEXT) { + return new NamedNode(iri, context); } // ### Creates a blank node -function blankNode(name) { - return new BlankNode(name || `n3-${_blankNodeCounter++}`); +export function blankNode(name, context = DEFAULT_CONTEXT) { + return new BlankNode(name || `n3-${_blankNodeCounter++}`, context); } // ### Creates a literal -function literal(value, languageOrDataType) { +export function literal(value, languageOrDataType, context = DEFAULT_CONTEXT) { // Create a language-tagged string if (typeof languageOrDataType === 'string') - return new Literal(`"${value}"@${languageOrDataType.toLowerCase()}`); + return new Literal(`"${value}"@${languageOrDataType.toLowerCase()}`, context); // Automatically determine datatype for booleans and numbers let datatype = languageOrDataType ? languageOrDataType.value : ''; @@ -368,13 +374,13 @@ function literal(value, languageOrDataType) { // Create a datatyped literal return (datatype === '' || datatype === xsd.string) ? - new Literal(`"${value}"`) : - new Literal(`"${value}"^^${datatype}`); + new Literal(`"${value}"`, context) : + new Literal(`"${value}"^^${datatype}`, context); } // ### Creates a variable -function variable(name) { - return new Variable(name); +export function variable(name, context = DEFAULT_CONTEXT) { + return new Variable(name, context); } // ### Returns the default graph diff --git a/src/N3Parser.js b/src/N3Parser.js index fa2fc8f7..fbd56924 100644 --- a/src/N3Parser.js +++ b/src/N3Parser.js @@ -159,7 +159,7 @@ export default class N3Parser { const iri = this._resolveIRI(token.value); if (iri === null) return this._error('Invalid IRI', token); - value = this._namedNode(iri); + value = this._namedNode(iri, { token }); break; // Read a prefixed name case 'type': @@ -167,15 +167,15 @@ export default class N3Parser { const prefix = this._prefixes[token.prefix]; if (prefix === undefined) return this._error(`Undefined prefix "${token.prefix}:"`, token); - value = this._namedNode(prefix + token.value); + value = this._namedNode(prefix + token.value, { token }); break; // Read a blank node case 'blank': - value = this._blankNode(this._prefixes[token.prefix] + token.value); + value = this._blankNode(this._prefixes[token.prefix] + token.value, { token }); break; // Read a variable case 'var': - value = this._variable(token.value.substr(1)); + value = this._variable(token.value.substr(1), { token }); break; // Everything else is not an entity default: @@ -194,7 +194,7 @@ export default class N3Parser { case '[': // Start a new quad with a new blank node as subject this._saveContext('blank', this._graph, - this._subject = this._blankNode(), null, null); + this._subject = this._blankNode(undefined, { token }), null, null); return this._readBlankNodeHead; case '(': // Start a new list @@ -206,7 +206,7 @@ export default class N3Parser { if (!this._n3Mode) return this._error('Unexpected graph', token); this._saveContext('formula', this._graph, - this._graph = this._blankNode(), null, null); + this._graph = this._blankNode(undefined, { token }), null, null); return this._readSubject; case '}': // No subject; the graph in which we are reading is closed instead @@ -234,7 +234,7 @@ export default class N3Parser { return this._completeSubjectLiteral; } else - this._subject = this._literal(token.value, this._namedNode(token.prefix)); + this._subject = this._literal(token.value, this._namedNode(token.prefix, { token }), { token }); break; case '<<': @@ -282,7 +282,7 @@ export default class N3Parser { if (this._n3Mode) { // Start a new quad with a new blank node as subject this._saveContext('blank', this._graph, this._subject, - this._subject = this._blankNode(), null); + this._subject = this._blankNode(undefined, { token }), null); return this._readBlankNodeHead; } case 'blank': @@ -307,12 +307,12 @@ export default class N3Parser { } // Pre-datatyped string literal (prefix stores the datatype) else - this._object = this._literal(token.value, this._namedNode(token.prefix)); + this._object = this._literal(token.value, this._namedNode(token.prefix, { token }), { token }); 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._blankNode()); + this._subject = this._blankNode(undefined, { token })); return this._readBlankNodeHead; case '(': // Start a new list @@ -324,7 +324,7 @@ export default class N3Parser { if (!this._n3Mode) return this._error('Unexpected graph', token); this._saveContext('formula', this._graph, this._subject, this._predicate, - this._graph = this._blankNode()); + this._graph = this._blankNode(undefined, { token })); return this._readSubject; case '<<': if (!this._supportsRDFStar) @@ -419,14 +419,14 @@ export default class N3Parser { case '[': // Stack the current list quad and start a new quad with a blank node as subject this._saveContext('blank', this._graph, - list = this._blankNode(), this.RDF_FIRST, - this._subject = item = this._blankNode()); + list = this._blankNode(undefined, token), this.RDF_FIRST, + this._subject = item = this._blankNode(undefined, { token })); next = this._readBlankNodeHead; break; case '(': // Stack the current list quad and start a new list this._saveContext('list', this._graph, - list = this._blankNode(), this.RDF_FIRST, this.RDF_NIL); + list = this._blankNode(undefined, { token }), this.RDF_FIRST, this.RDF_NIL); this._subject = null; break; case ')': @@ -462,7 +462,7 @@ export default class N3Parser { } // Pre-datatyped string literal (prefix stores the datatype) else { - item = this._literal(token.value, this._namedNode(token.prefix)); + item = this._literal(token.value, this._namedNode(token.prefix, { token }), { token }); next = this._getContextEndReader(); } break; @@ -471,7 +471,7 @@ export default class N3Parser { if (!this._n3Mode) return this._error('Unexpected graph', token); this._saveContext('formula', this._graph, this._subject, this._predicate, - this._graph = this._blankNode()); + this._graph = this._blankNode(undefined, { token })); return this._readSubject; default: if ((item = this._readEntity(token)) === undefined) @@ -480,7 +480,7 @@ export default class N3Parser { // Create a new blank node if no item head was assigned yet if (list === null) - this._subject = list = this._blankNode(); + this._subject = list = this._blankNode(undefined, { token }); // Is this the first element of the list? if (previousList === null) { @@ -524,7 +524,7 @@ export default class N3Parser { // ### `_completeLiteral` completes a literal with an optional datatype or language _completeLiteral(token) { // Create a simple string literal by default - let literal = this._literal(this._literalValue); + let literal = this._literal(this._literalValue, undefined, { token }); switch (token.type) { // Create a datatyped literal @@ -532,12 +532,12 @@ export default class N3Parser { case 'typeIRI': const datatype = this._readEntity(token); if (datatype === undefined) return; // No datatype means an error occurred - literal = this._literal(this._literalValue, datatype); + literal = this._literal(this._literalValue, datatype, { token }); token = null; break; // Create a language-tagged string case 'langcode': - literal = this._literal(this._literalValue, token.value); + literal = this._literal(this._literalValue, token.value, { token }); token = null; break; } @@ -721,7 +721,7 @@ export default class N3Parser { _readNamedGraphBlankLabel(token) { if (token.type !== ']') return this._error('Invalid graph label', token); - this._subject = this._blankNode(); + this._subject = this._blankNode(undefined, { token }); return this._readGraph; } @@ -751,17 +751,17 @@ export default class N3Parser { } // Without explicit quantifiers, map entities to a quantified entity if (!this._explicitQuantifiers) - this._quantified[entity.id] = this._quantifier(this._blankNode().value); + this._quantified[entity.id] = this._quantifier(this._blankNode(undefined, { token }).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._blankNode(), this.QUANTIFIERS_GRAPH); + this._subject = this._blankNode(undefined, { token }), this.QUANTIFIERS_GRAPH); // Otherwise, continue the previous list else this._emit(this._subject, this.RDF_REST, - this._subject = this._blankNode(), this.QUANTIFIERS_GRAPH); + this._subject = this._blankNode(undefined, { token }), this.QUANTIFIERS_GRAPH); // Output the list item this._emit(this._subject, this.RDF_FIRST, entity, this.QUANTIFIERS_GRAPH); } @@ -818,7 +818,7 @@ export default class N3Parser { // ### `_readForwardPath` reads a '!' path _readForwardPath(token) { let subject, predicate; - const object = this._blankNode(); + const object = this._blankNode(undefined, { token }); // The next token is the predicate if ((predicate = this._readEntity(token)) === undefined) return; @@ -835,7 +835,7 @@ export default class N3Parser { // ### `_readBackwardPath` reads a '^' path _readBackwardPath(token) { - const subject = this._blankNode(); + const subject = this._blankNode(undefined, { token }); let predicate, object; // The next token is the predicate if ((predicate = this._readEntity(token)) === undefined) diff --git a/src/index.js b/src/index.js index 8307958e..15c22247 100644 --- a/src/index.js +++ b/src/index.js @@ -20,6 +20,10 @@ import { termFromId, termToId, + namedNode, + blankNode, + literal, + variable, } from './N3DataFactory'; // Named exports @@ -31,9 +35,7 @@ export { StreamParser, StreamWriter, Util, - DataFactory, - Term, NamedNode, Literal, @@ -45,6 +47,10 @@ export { termFromId, termToId, + namedNode, + blankNode, + literal, + variable, }; // Export all named exports as a default object for backward compatibility @@ -56,9 +62,7 @@ export default { StreamParser, StreamWriter, Util, - DataFactory, - Term, NamedNode, Literal, @@ -70,4 +74,8 @@ export default { termFromId, termToId, + namedNode, + blankNode, + literal, + variable, }; diff --git a/test/N3Parser-test.js b/test/N3Parser-test.js index 66d8ecf6..e57b54e8 100644 --- a/test/N3Parser-test.js +++ b/test/N3Parser-test.js @@ -1,4 +1,4 @@ -import { Parser, NamedNode, BlankNode, Quad, termFromId, DataFactory as DF } from '../src'; +import { Parser, NamedNode, BlankNode, Quad, termFromId, DataFactory as DF, literal, variable, namedNode, blankNode, DataFactory } from '../src'; import rdfDataModel from '@rdfjs/data-model'; import { isomorphic } from 'rdf-isomorphic'; @@ -2509,6 +2509,27 @@ describe('Parser', () => { { s: 'n-http://example.org/a', p: 'v-b', o: 'b-b0_d', g: 'defaultGraph' }, ]); }); + + it('should provide tokens using a custom factory', () => { + parser = new Parser({ + factory: { + ...DataFactory, + namedNode: namedNode, + blankNode: blankNode, + literal: literal, + variable: variable, + }, + }); + + for (const quad of parser.parse(' 1, _:d.')) { + expect(quad.subject.context).toBeDefined(); + expect(quad.subject.context.token).toBeDefined(); + expect(quad.predicate.context).toBeDefined(); + expect(quad.predicate.context.token).toBeDefined(); + expect(quad.object.context).toBeDefined(); + expect(quad.object.context.token).toBeDefined(); + } + }); }); describe('A parser instance with external data factory', () => {