// N3.js implementations of the RDF/JS core data types // See https://github.com/rdfjs/representation-task-force/blob/master/interface-spec.md import namespaces from './IRIs'; const { rdf, xsd } = namespaces; // eslint-disable-next-line prefer-const let DEFAULTGRAPH; let _blankNodeCounter = 0; const escapedLiteral = /^"(.*".*)(?="[^"]*$)/; // ## DataFactory singleton const DataFactory = { namedNode, blankNode, variable, literal, defaultGraph, quad, triple: quad, }; export default DataFactory; // ## Term constructor export class Term { constructor(id) { this.id = id; } // ### The value of this term get value() { return this.id; } // ### Returns whether this object represents the same term as the other equals(other) { // If both terms were created by this library, // equality can be computed through ids if (other instanceof Term) return this.id === other.id; // Otherwise, compare term type and value return !!other && this.termType === other.termType && this.value === other.value; } // ### Implement hashCode for Immutable.js, since we implement `equals` // https://immutable-js.com/docs/v4.0.0/ValueObject/#hashCode() hashCode() { return 0; } // ### Returns a plain object representation of this term toJSON() { return { termType: this.termType, value: this.value, }; } } // ## NamedNode constructor export class NamedNode extends Term { // ### The term type of this term get termType() { return 'NamedNode'; } } // ## Literal constructor export class Literal extends Term { // ### The term type of this term get termType() { return 'Literal'; } // ### The text value of this literal get value() { return this.id.substring(1, this.id.lastIndexOf('"')); } // ### The language of this literal get language() { // Find the last quotation mark (e.g., '"abc"@en-us') const id = this.id; let atPos = id.lastIndexOf('"') + 1; // If "@" it follows, return the remaining substring; empty otherwise return atPos < id.length && id[atPos++] === '@' ? id.substr(atPos).toLowerCase() : ''; } // ### The datatype IRI of this literal get datatype() { return new NamedNode(this.datatypeString); } // ### The datatype string of this literal get datatypeString() { // Find the last quotation mark (e.g., '"abc"^^http://ex.org/types#t') const id = this.id, dtPos = id.lastIndexOf('"') + 1; const char = dtPos < id.length ? id[dtPos] : ''; // If "^" it follows, return the remaining substring return char === '^' ? id.substr(dtPos + 2) : // If "@" follows, return rdf:langString; xsd:string otherwise (char !== '@' ? xsd.string : rdf.langString); } // ### Returns whether this object represents the same term as the other equals(other) { // If both literals were created by this library, // equality can be computed through ids if (other instanceof Literal) return this.id === other.id; // Otherwise, compare term type, value, language, and datatype return !!other && !!other.datatype && this.termType === other.termType && this.value === other.value && this.language === other.language && this.datatype.value === other.datatype.value; } toJSON() { return { termType: this.termType, value: this.value, language: this.language, datatype: { termType: 'NamedNode', value: this.datatypeString }, }; } } // ## BlankNode constructor export class BlankNode extends Term { constructor(name) { super(`_:${name}`); } // ### The term type of this term get termType() { return 'BlankNode'; } // ### The name of this blank node get value() { return this.id.substr(2); } } export class Variable extends Term { constructor(name) { super(`?${name}`); } // ### The term type of this term get termType() { return 'Variable'; } // ### The name of this variable get value() { return this.id.substr(1); } } // ## DefaultGraph constructor export class DefaultGraph extends Term { constructor() { super(''); return DEFAULTGRAPH || this; } // ### The term type of this term get termType() { return 'DefaultGraph'; } // ### Returns whether this object represents the same term as the other equals(other) { // If both terms were created by this library, // equality can be computed through strict equality; // otherwise, compare term types. return (this === other) || (!!other && (this.termType === other.termType)); } } // ## DefaultGraph singleton DEFAULTGRAPH = new DefaultGraph(); // ### Constructs a term from the given internal string ID // The third 'nested' parameter of this function is to aid // 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) { factory = factory || DataFactory; // Falsy value or empty string indicate the default graph if (!id) return factory.defaultGraph(); // Identify the term type based on the first character switch (id[0]) { case '?': return factory.variable(id.substr(1)); case '_': return factory.blankNode(id.substr(2)); case '"': // Shortcut for internal literals if (factory === DataFactory) return new Literal(id); // Literal without datatype or language if (id[id.length - 1] === '"') return factory.literal(id.substr(1, id.length - 2)); // Literal with datatype or language const endPos = id.lastIndexOf('"', id.length - 1); return factory.literal(id.substr(1, endPos - 1), id[endPos + 1] === '@' ? id.substr(endPos + 2) : factory.namedNode(id.substr(endPos + 3))); case '[': id = JSON.parse(id); break; default: if (!nested || !Array.isArray(id)) { return factory.namedNode(id); } } return factory.quad( termFromId(id[0], factory, true), termFromId(id[1], factory, true), termFromId(id[2], factory, true), id[3] && termFromId(id[3], factory, true) ); } // ### Constructs an internal string ID from the given term or ID string // The third 'nested' parameter of this function is to aid // 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 termToId(term, nested) { if (typeof term === 'string') return term; if (term instanceof Term && term.termType !== 'Quad') return term.id; if (!term) return DEFAULTGRAPH.id; // Term instantiated with another library switch (term.termType) { case 'NamedNode': return term.value; case 'BlankNode': return `_:${term.value}`; case 'Variable': return `?${term.value}`; case 'DefaultGraph': return ''; case 'Literal': return `"${term.value}"${ term.language ? `@${term.language}` : (term.datatype && term.datatype.value !== xsd.string ? `^^${term.datatype.value}` : '')}`; case 'Quad': const res = [ termToId(term.subject, true), termToId(term.predicate, true), termToId(term.object, true), ]; if (term.graph && term.graph.termType !== 'DefaultGraph') { res.push(termToId(term.graph, true)); } return nested ? res : JSON.stringify(res); default: throw new Error(`Unexpected termType: ${term.termType}`); } } // ## Quad constructor export class Quad extends Term { constructor(subject, predicate, object, graph) { super(''); this._subject = subject; this._predicate = predicate; this._object = object; this._graph = graph || DEFAULTGRAPH; } // ### The term type of this term get termType() { return 'Quad'; } get subject() { return this._subject; } get predicate() { return this._predicate; } get object() { return this._object; } get graph() { return this._graph; } // ### Returns a plain object representation of this quad toJSON() { return { termType: this.termType, subject: this._subject.toJSON(), predicate: this._predicate.toJSON(), object: this._object.toJSON(), graph: this._graph.toJSON(), }; } // ### Returns whether this object represents the same quad as the other equals(other) { return !!other && this._subject.equals(other.subject) && this._predicate.equals(other.predicate) && this._object.equals(other.object) && this._graph.equals(other.graph); } } export { Quad as Triple }; // ### Escapes the quotes within the given literal export function escapeQuotes(id) { return id.replace(escapedLiteral, (_, quoted) => `"${quoted.replace(/"/g, '""')}`); } // ### Unescapes the quotes within the given literal export function unescapeQuotes(id) { return id.replace(escapedLiteral, (_, quoted) => `"${quoted.replace(/""/g, '"')}`); } // ### Creates an IRI function namedNode(iri) { return new NamedNode(iri); } // ### Creates a blank node function blankNode(name) { return new BlankNode(name || `n3-${_blankNodeCounter++}`); } // ### Creates a literal function literal(value, languageOrDataType) { // Create a language-tagged string if (typeof languageOrDataType === 'string') return new Literal(`"${value}"@${languageOrDataType.toLowerCase()}`); // Automatically determine datatype for booleans and numbers let datatype = languageOrDataType ? languageOrDataType.value : ''; if (datatype === '') { // Convert a boolean if (typeof value === 'boolean') datatype = xsd.boolean; // Convert an integer or double else if (typeof value === 'number') { if (Number.isFinite(value)) datatype = Number.isInteger(value) ? xsd.integer : xsd.double; else { datatype = xsd.double; if (!Number.isNaN(value)) value = value > 0 ? 'INF' : '-INF'; } } } // Create a datatyped literal return (datatype === '' || datatype === xsd.string) ? new Literal(`"${value}"`) : new Literal(`"${value}"^^${datatype}`); } // ### Creates a variable function variable(name) { return new Variable(name); } // ### Returns the default graph function defaultGraph() { return DEFAULTGRAPH; } // ### Creates a quad function quad(subject, predicate, object, graph) { return new Quad(subject, predicate, object, graph); }