Files
natural-language-report-ser…/node_modules/sparqljs/lib/SparqlGenerator.js
felix 4ef33c6417 WIP
2026-09-14 13:57:42 +02:00

471 lines
16 KiB
JavaScript

var XSD_INTEGER = 'http://www.w3.org/2001/XMLSchema#integer';
var XSD_STRING = 'http://www.w3.org/2001/XMLSchema#string';
function Generator(options) {
this._options = options = options || {};
var prefixes = options.prefixes || {};
this._prefixByIri = {};
var prefixIris = [];
for (var prefix in prefixes) {
var iri = prefixes[prefix];
if (isString(iri)) {
this._prefixByIri[iri] = prefix;
prefixIris.push(iri);
}
}
var iriList = prefixIris.join('|').replace(/[\]\/\(\)\*\+\?\.\\\$]/g, '\\$&');
this._prefixRegex = new RegExp('^(' + iriList + ')([a-zA-Z][\\-_a-zA-Z0-9]*)$');
this._usedPrefixes = {};
this._sparqlStar = options.sparqlStar;
this._indent = isString(options.indent) ? options.indent : ' ';
this._newline = isString(options.newline) ? options.newline : '\n';
this._explicitDatatype = Boolean(options.explicitDatatype);
}
// Converts the parsed query object into a SPARQL query
Generator.prototype.toQuery = function (q) {
var query = '';
if (q.queryType)
query += q.queryType.toUpperCase() + ' ';
if (q.reduced)
query += 'REDUCED ';
if (q.distinct)
query += 'DISTINCT ';
if (q.variables){
query += mapJoin(q.variables, undefined, function (variable) {
// String variable
if (/^[?$]/.test(variable))
return variable;
// Entity object
else if (isTerm(variable))
return this.toEntity(variable);
// Expression object
else
return '(' + this.toExpression(variable.expression) +
' AS ' + variableToString(variable.variable) + ')';
}, this) + ' ';
}
else if (q.template)
query += this.group(q.template, true) + this._newline;
if (q.from)
query += this.graphs('FROM ', q.from.default) + this.graphs('FROM NAMED ', q.from.named);
if (q.where)
query += 'WHERE ' + this.group(q.where, true) + this._newline;
if (q.updates)
query += mapJoin(q.updates, ';' + this._newline, this.toUpdate, this);
if (q.group)
query += 'GROUP BY ' + mapJoin(q.group, undefined, function (it) {
var result = isTerm(it.expression)
? this.toEntity(it.expression)
: '(' + this.toExpression(it.expression) + ')';
return it.variable ? '(' + result + ' AS ' + variableToString(it.variable) + ')' : result;
}, this) + this._newline;
if (q.having)
query += 'HAVING (' + mapJoin(q.having, undefined, this.toExpression, this) + ')' + this._newline;
if (q.order)
query += 'ORDER BY ' + mapJoin(q.order, undefined, function (it) {
var expr = '(' + this.toExpression(it.expression) + ')';
return !it.descending ? expr : 'DESC ' + expr;
}, this) + this._newline;
if (Number.isInteger(q.offset))
query += 'OFFSET ' + q.offset + this._newline;
if (Number.isInteger(q.limit))
query += 'LIMIT ' + q.limit + this._newline;
if (q.values)
query += this.values(q);
// stringify prefixes at the end to mark used ones
query = this.baseAndPrefixes(q) + query;
return query.trim();
};
Generator.prototype.baseAndPrefixes = function (q) {
var base = q.base ? ('BASE <' + q.base + '>' + this._newline) : '';
var prefixes = '';
for (var key in q.prefixes) {
if (this._options.allPrefixes || this._usedPrefixes[key])
prefixes += 'PREFIX ' + key + ': <' + q.prefixes[key] + '>' + this._newline;
}
return base + prefixes;
};
// Converts the parsed SPARQL pattern into a SPARQL pattern
Generator.prototype.toPattern = function (pattern) {
var type = pattern.type || (pattern instanceof Array) && 'array' ||
(pattern.subject && pattern.predicate && pattern.object ? 'triple' : '');
if (!(type in this))
throw new Error('Unknown entry type: ' + type);
return this[type](pattern);
};
Generator.prototype.triple = function (t) {
return this.toEntity(t.subject) + ' ' + this.toEntity(t.predicate) + ' ' + this.toEntity(t.object) + '.';
};
Generator.prototype.array = function (items) {
return mapJoin(items, this._newline, this.toPattern, this);
};
Generator.prototype.bgp = function (bgp) {
return this.encodeTriples(bgp.triples);
};
Generator.prototype.encodeTriples = function (triples) {
if (!triples.length)
return '';
var parts = [], subject = undefined, predicate = undefined;
for (var i = 0; i < triples.length; i++) {
var triple = triples[i];
// Triple with different subject
if (!equalTerms(triple.subject, subject)) {
// Terminate previous triple
if (subject)
parts.push('.' + this._newline);
subject = triple.subject;
predicate = triple.predicate;
parts.push(this.toEntity(subject), ' ', this.toEntity(predicate));
}
// Triple with same subject but different predicate
else if (!equalTerms(triple.predicate, predicate)) {
predicate = triple.predicate;
parts.push(';' + this._newline, this._indent, this.toEntity(predicate));
}
// Triple with same subject and predicate
else {
parts.push(',');
}
parts.push(' ', this.toEntity(triple.object));
}
parts.push('.');
return parts.join('');
}
Generator.prototype.graph = function (graph) {
return 'GRAPH ' + this.toEntity(graph.name) + ' ' + this.group(graph);
};
Generator.prototype.graphs = function (keyword, graphs) {
return !graphs || graphs.length === 0 ? '' :
mapJoin(graphs, '', function (g) { return keyword + this.toEntity(g) + this._newline; }, this)
}
Generator.prototype.group = function (group, inline) {
group = inline !== true ? this.array(group.patterns || group.triples)
: this.toPattern(group.type !== 'group' ? group : group.patterns);
return group.indexOf(this._newline) === -1 ? '{ ' + group + ' }' : '{' + this._newline + this.indent(group) + this._newline + '}';
};
Generator.prototype.query = function (query) {
return this.toQuery(query);
};
Generator.prototype.filter = function (filter) {
return 'FILTER(' + this.toExpression(filter.expression) + ')';
};
Generator.prototype.bind = function (bind) {
return 'BIND(' + this.toExpression(bind.expression) + ' AS ' + variableToString(bind.variable) + ')';
};
Generator.prototype.optional = function (optional) {
return 'OPTIONAL ' + this.group(optional);
};
Generator.prototype.union = function (union) {
return mapJoin(union.patterns, this._newline + 'UNION' + this._newline, function (p) { return this.group(p, true); }, this);
};
Generator.prototype.minus = function (minus) {
return 'MINUS ' + this.group(minus);
};
Generator.prototype.values = function (valuesList) {
// Gather unique keys
var keys = Object.keys(valuesList.values.reduce(function (keyHash, values) {
for (var key in values) keyHash[key] = true;
return keyHash;
}, {}));
// Check whether simple syntax can be used
var lparen, rparen;
if (keys.length === 1) {
lparen = rparen = '';
} else {
lparen = '(';
rparen = ')';
}
// Create value rows
return 'VALUES ' + lparen + keys.join(' ') + rparen + ' {' + this._newline +
mapJoin(valuesList.values, this._newline, function (values) {
return ' ' + lparen + mapJoin(keys, undefined, function (key) {
return values[key] ? this.toEntity(values[key]) : 'UNDEF';
}, this) + rparen;
}, this) + this._newline + '}';
};
Generator.prototype.service = function (service) {
return 'SERVICE ' + (service.silent ? 'SILENT ' : '') + this.toEntity(service.name) + ' ' +
this.group(service);
};
// Converts the parsed expression object into a SPARQL expression
Generator.prototype.toExpression = function (expr) {
if (isTerm(expr)) {
return this.toEntity(expr);
}
switch (expr.type.toLowerCase()) {
case 'aggregate':
return expr.aggregation.toUpperCase() +
'(' + (expr.distinct ? 'DISTINCT ' : '') + this.toExpression(expr.expression) +
(typeof expr.separator === 'string' ? '; SEPARATOR = ' + '"' + expr.separator.replace(escape, escapeReplacer) + '"' : '') + ')';
case 'functioncall':
return this.toEntity(expr.function) + '(' + mapJoin(expr.args, ', ', this.toExpression, this) + ')';
case 'operation':
var operator = expr.operator.toUpperCase(), args = expr.args || [];
switch (expr.operator.toLowerCase()) {
// Infix operators
case '<':
case '>':
case '>=':
case '<=':
case '&&':
case '||':
case '=':
case '!=':
case '+':
case '-':
case '*':
case '/':
return (isTerm(args[0]) ? this.toEntity(args[0]) : '(' + this.toExpression(args[0]) + ')') +
' ' + operator + ' ' +
(isTerm(args[1]) ? this.toEntity(args[1]) : '(' + this.toExpression(args[1]) + ')');
// Unary operators
case '!':
return '!(' + this.toExpression(args[0]) + ')';
case 'uplus':
return '+(' + this.toExpression(args[0]) + ')';
case 'uminus':
return '-(' + this.toExpression(args[0]) + ')';
// IN and NOT IN
case 'notin':
operator = 'NOT IN';
case 'in':
return this.toExpression(args[0]) + ' ' + operator +
'(' + (isString(args[1]) ? args[1] : mapJoin(args[1], ', ', this.toExpression, this)) + ')';
// EXISTS and NOT EXISTS
case 'notexists':
operator = 'NOT EXISTS';
case 'exists':
return operator + ' ' + this.group(args[0], true);
// Other expressions
default:
return operator + '(' + mapJoin(args, ', ', this.toExpression, this) + ')';
}
default:
throw new Error('Unknown expression type: ' + expr.type);
}
};
// Converts the parsed entity (or property path) into a SPARQL entity
Generator.prototype.toEntity = function (value) {
if (isTerm(value)) {
switch (value.termType) {
// variable, * selector, or blank node
case 'Wildcard':
return '*';
case 'Variable':
return variableToString(value);
case 'BlankNode':
return '_:' + value.value;
// literal
case 'Literal':
var lexical = value.value || '', language = value.language || '', datatype = value.datatype;
value = '"' + lexical.replace(escape, escapeReplacer) + '"';
if (language){
value += '@' + language;
} else if (datatype) {
// Abbreviate literals when possible
if (!this._explicitDatatype) {
switch (datatype.value) {
case XSD_STRING:
return value;
case XSD_INTEGER:
if (/^\d+$/.test(lexical))
// Add space to avoid confusion with decimals in broken parsers
return lexical + ' ';
}
}
value += '^^' + this.encodeIRI(datatype.value);
}
return value;
case 'Quad':
if (!this._sparqlStar)
throw new Error('SPARQL* support is not enabled');
if (value.graph && value.graph.termType !== "DefaultGraph") {
return '<< GRAPH ' +
this.toEntity(value.graph) +
' { ' +
this.toEntity(value.subject) + ' ' +
this.toEntity(value.predicate) + ' ' +
this.toEntity(value.object) +
' } ' +
' >>'
}
else {
return (
'<< ' +
this.toEntity(value.subject) + ' ' +
this.toEntity(value.predicate) + ' ' +
this.toEntity(value.object) +
' >>'
);
}
// IRI
default:
return this.encodeIRI(value.value);
}
}
// property path
else {
var items = value.items.map(this.toEntity, this), path = value.pathType;
switch (path) {
// prefix operator
case '^':
case '!':
return path + items[0];
// postfix operator
case '*':
case '+':
case '?':
return '(' + items[0] + path + ')';
// infix operator
default:
return '(' + items.join(path) + ')';
}
}
};
var escape = /["\\\t\n\r\b\f]/g,
escapeReplacer = function (c) { return escapeReplacements[c]; },
escapeReplacements = { '\\': '\\\\', '"': '\\"', '\t': '\\t',
'\n': '\\n', '\r': '\\r', '\b': '\\b', '\f': '\\f' };
// Represent the IRI, as a prefixed name when possible
Generator.prototype.encodeIRI = function (iri) {
var prefixMatch = this._prefixRegex.exec(iri);
if (prefixMatch) {
var prefix = this._prefixByIri[prefixMatch[1]];
this._usedPrefixes[prefix] = true;
return prefix + ':' + prefixMatch[2];
}
return '<' + iri + '>';
};
// Converts the parsed update object into a SPARQL update clause
Generator.prototype.toUpdate = function (update) {
switch (update.type || update.updateType) {
case 'load':
return 'LOAD' + (update.source ? ' ' + this.toEntity(update.source) : '') +
(update.destination ? ' INTO GRAPH ' + this.toEntity(update.destination) : '');
case 'insert':
return 'INSERT DATA ' + this.group(update.insert, true);
case 'delete':
return 'DELETE DATA ' + this.group(update.delete, true);
case 'deletewhere':
return 'DELETE WHERE ' + this.group(update.delete, true);
case 'insertdelete':
return (update.graph ? 'WITH ' + this.toEntity(update.graph) + this._newline : '') +
(update.delete.length ? 'DELETE ' + this.group(update.delete, true) + this._newline : '') +
(update.insert.length ? 'INSERT ' + this.group(update.insert, true) + this._newline : '') +
(update.using ? this.graphs('USING ', update.using.default) : '') +
(update.using ? this.graphs('USING NAMED ', update.using.named) : '') +
'WHERE ' + this.group(update.where, true);
case 'add':
case 'copy':
case 'move':
return update.type.toUpperCase()+ ' ' + (update.silent ? 'SILENT ' : '') + (update.source.default ? 'DEFAULT' : this.toEntity(update.source.name)) +
' TO ' + this.toEntity(update.destination.name);
case 'create':
case 'clear':
case 'drop':
return update.type.toUpperCase() + (update.silent ? ' SILENT ' : ' ') + (
update.graph.default ? 'DEFAULT' :
update.graph.named ? 'NAMED' :
update.graph.all ? 'ALL' :
('GRAPH ' + this.toEntity(update.graph.name))
);
default:
throw new Error('Unknown update query type: ' + update.type);
}
};
// Indents each line of the string
Generator.prototype.indent = function(text) { return text.replace(/^/gm, this._indent); }
function variableToString(variable){
return '?' + variable.value;
}
// Checks whether the object is a string
function isString(object) { return typeof object === 'string'; }
// Checks whether the object is a Term
function isTerm(object) {
return typeof object.termType === 'string';
}
// Checks whether term1 and term2 are equivalent without `.equals()` prototype method
function equalTerms(term1, term2) {
if (!term1 || !isTerm(term1)) { return false; }
if (!term2 || !isTerm(term2)) { return false; }
if (term1.termType !== term2.termType) { return false; }
switch (term1.termType) {
case 'Literal':
return term1.value === term2.value
&& term1.language === term2.language
&& equalTerms(term1.datatype, term2.datatype);
case 'Quad':
return equalTerms(term1.subject, term2.subject)
&& equalTerms(term1.predicate, term2.predicate)
&& equalTerms(term1.object, term2.object)
&& equalTerms(term1.graph, term2.graph);
default:
return term1.value === term2.value;
}
}
// Maps the array with the given function, and joins the results using the separator
function mapJoin(array, sep, func, self) {
return array.map(func, self).join(isString(sep) ? sep : ' ');
}
/**
* @param options {
* allPrefixes: boolean,
* indentation: string,
* newline: string
* }
*/
function _Generator(options = {}) {
return {
stringify: function (query) {
var currentOptions = Object.create(options);
currentOptions.prefixes = query.prefixes;
return new Generator(currentOptions).toQuery(query);
},
createGenerator: function() { return new Generator(options); }
};
};
module.exports = {
Generator: _Generator,
};