diff --git a/backend/static/js/codemirror/modes/sparql/sparql-hint.js b/backend/static/js/codemirror/modes/sparql/sparql-hint.js index 69afefe2..6755c17f 100755 --- a/backend/static/js/codemirror/modes/sparql/sparql-hint.js +++ b/backend/static/js/codemirror/modes/sparql/sparql-hint.js @@ -5,14 +5,13 @@ var size = 40; // size for next auto completion call var resultSize = 0; // result size for counter badge var lastWidget = undefined; // last auto completion widget instance var activeLine; // the current active line that holds loader / counter badge -var activeLineBadgeLine; // the bade holder in the current active line +var activeLineBadgeLine; // the badge holder in the current active line var activeLineNumber; // the line number of the active line (replaced by loader) var sparqlCallback; var sparqlFrom; var sparqlTo; var sparqlTimeout; -var sparqlRequest; var suggestions; (function (mod) { @@ -148,30 +147,35 @@ var suggestions; } + // This function defines the CORE LOGIC OF THE QLEVER UI SUGGESTIONS + // + // TODO: explain how it works in this comment. CodeMirror.registerHelper("hint", "sparql", function (editor, callback, options) { - - // ************************************************************************************ - // - // - // CORE LOGIC OF QLEVER UI SUGGESTIONS + // If a previous AC query is still running, cancel it. // - // - // ************************************************************************************ - - // skip everything that is running by now + // NOTE: `sparqlTimeout` and `activeLine` are both set in the function + // `getQleverSuggestions` (which sends the AC query to the backend and turns + // the result into nice-looking suggestions). window.clearTimeout(sparqlTimeout); - if (sparqlRequest) { sparqlRequest.abort(); } - - // reset the previous loader if (activeLine) { activeLine.html(activeLineNumber); } - var cur = editor.getCursor(); // current cursor position - var absolutePosition = editor.indexFromPos((cur)); // absolute cursor position in text - var context = getCurrentContext(absolutePosition); // get current context - suggestions = []; + // Get the current cursor position and its index in the whole query. + var cur = editor.getCursor(); + var absolutePosition = editor.indexFromPos((cur)); + + // Get the element of the query tree at that position. + // + // TODO: This is currently a major bug, since for a typical SPARQL query, + // there are different tree elements for ordinary triples, the FILTER + // clauses (one element each), or the UNION or OPTIONAL clauses (again, one + // element each). That way, the sensitive AC queries do not work correctly + // when one of FILTER, UNION, or OPTIONAL is present, or after these + // clauses. + var context = getCurrentContext(absolutePosition); + // console.log("context", context); log('Position: ' + absolutePosition, 'suggestions'); if (context) { @@ -180,7 +184,7 @@ var suggestions; log('Context: None', 'suggestions'); } - // get current token + // Get the token at the current cursor position. var line = editor.getLine(cur.line).slice(0, cur.ch); var token = getLastLineToken(line); var start, end; @@ -189,15 +193,19 @@ var suggestions; } else { start = token.start; end = token.end; - } + // Determine all types that potentially could (not necessarily actually do) + // occur in this context. These are all `TYPES` which contain the name of + // the current context in their `availableInContext`. types = getAvailableTypes(context); + // console.log("Types: ", types); sparqlCallback = callback; sparqlFrom = Pos(cur.line, start); sparqlTo = Pos(cur.line, end); + suggestions = []; // TODO: Why is this a global variable? var allTypeSuggestions = []; for (var i = 0; i < types.length; i++) { for (var suggestion of getTypeSuggestions(types[i], context)) { @@ -236,10 +244,12 @@ function getAvailableTypes(context) { if (context) { contextName = context.w3name; } + // console.log(`getAvailableTypes -> contextName: ${contextName}`); // check for complex types that are valid in this context for (var i = 0; i < COMPLEXTYPES.length; i++) { if (COMPLEXTYPES[i].availableInContext.indexOf(contextName) != -1) { + // console.log(`getAvailableTypes -> type: ${COMPLEXTYPES[i].name}`); types.push(COMPLEXTYPES[i]); } } @@ -268,6 +278,10 @@ function detectPropertyPath(predicate) { return propertyPath; } +// Get suggestions for the given "context" (element of the query tree). +// +// NOTE: This function is only called via the `TRIPLE` type in `COMPLEXTYPES` +// from `sparql.js`. It eventually calls `getQleverSuggestions`. function getDynamicSuggestions(context) { var cur = editor.getCursor(); var line = editor.getLine(cur.line); @@ -276,10 +290,10 @@ function getDynamicSuggestions(context) { word = getLastLineToken(line.slice(0, cur.ch)); if (word.endsInWhitespace) { word = ""; } else { word = word.string; } - // get current line + // Get current line and split into `words` by whitespace. var words = line.slice(0, cur.ch).trimLeft().replace(' ', ' ').split(" "); - // Find words that are separated by whitespace but seem to be belong together + // Find words that are separated by whitespace but seem to belong together. var whiteSpaceWord = ""; for (var i = words.length - 1; i >= 0; i--) { var prevWord = words[i] @@ -317,6 +331,7 @@ function getDynamicSuggestions(context) { // Get editor lines and remove current line. var lines = context['content'].split('\n'); + // console.log("lines", lines); for (var i = 0; i < lines.length; i++) { if (lines[i] == line) { lines.splice(i, 1); @@ -358,7 +373,11 @@ function getDynamicSuggestions(context) { } else { - // find connected lines in given select clause + // Find part of query to plug in for %CONNECTED_TRIPLES% in AC query + // templates. + // + // TODO: Lines with FILTER on connected variables are missing, + // for example https://qlever.cs.uni-freiburg.de/wikidata/Z0FvRA const variableRegex = /\?\w+\b/g; let seenVariables = line.match(variableRegex); if (seenVariables) { @@ -428,15 +447,23 @@ function getDynamicSuggestions(context) { } mode1Query = SUGGESTPREDICATES_CONTEXT_INSENSITIVE; mode2Query = SUGGESTPREDICATES; - } else if (words.length == 3) { predicateForObject = words[1]; suggestVariables = "normal"; appendToSuggestions = ' .'; nameList = objectNames; - mode1Query = SUGGESTOBJECTS_CONTEXT_INSENSITIVE; + // TRYING STH OUT: Make object AC queries "half-sensitive" (ignore the + // context triples, but not the predicate, see head.html). If you want + // to restrict this to certain instances, use a condition like SLUG == + // "pubchem". + if (SLUG != "uniprot") { + mode1Query = SUGGESTOBJECTS_CONTEXT_HALFSENSITIVE; + } else { + mode1Query = SUGGESTOBJECTS_CONTEXT_INSENSITIVE; + } mode2Query = SUGGESTOBJECTS; - // replace the prefixes + + // Replace the prefixes. var propertyPath = detectPropertyPath(words[1]); for (var i in propertyPath) { @@ -486,7 +513,10 @@ function getDynamicSuggestions(context) { var subjectVarName = subject.split(/[.\/\#:]/g).slice(-1)[0].replace(/@\w*$/, '').replace(/\s/g, '_').replace(/[^a-zA-Z0-9_]/g, '').toLowerCase(); var objectVarName = lastWord.split(/[.\/\#:]/g).slice(-1)[0] .replace(/@\w*$/, "").replace(/\s/g, "_") - .replace(/^has([A-Z_-])/, "$1$").replace(/[^a-zA-Z0-9_]/g, "").toLowerCase(); + .replace(/^has([A-Z_-])/, "$1$") + .replace(/^([a-z]+)edBy/, "$1$") + .replace(/^(year)[A-Z_]\w*/, "$1$") + .replace(/[^a-zA-Z0-9_]/g, "").toLowerCase(); response.push('?' + objectVarName + ' .'); response.push('?' + subjectVarName + '_' + objectVarName + ' .'); @@ -684,22 +714,33 @@ function parseAndEvaluateCondition(condition, word, lines, words) { return conditionSatisfied; } +// Get result of SPARQL query with timeout. +// +// NOTE: The function returns immediately with a `fetch` promise. To wait for +// the result of the `fetch` (or its failure), call `await fetchTimeout(...)`. +// +// TODO: Better use QLever's timeout, so that the query no longer burdens the +// backend after the timeout. This would have the drawback that the code would +// then only work for SPARQL enpoints supporting a "timeout" argument. Note that +// Virtuoso does have such an argument, too, but the unit is milliseconds and not +// seconds like for QLever. const fetchTimeout = (sparqlQuery, timeoutSeconds, { ...options } = {}) => { - const ms = timeoutSeconds * 1000; + const timeoutMilliseconds = timeoutSeconds * 1000; const controller = new AbortController(); + // `BASEURL` is defined in backend/templates/partials/head.html . It's the + // base URL from the backend configuration. const promise = fetch(BASEURL, { - // BASEURL + "?query=" + encodeURIComponent(sparqlQuery), { method: "POST", body: sparqlQuery, signal: controller.signal, - headers: { + headers: { "Content-type": "application/sparql-query", "Accept": "application/qlever-results+json" }, ...options }); - if (ms > 0) { - const timeout = setTimeout(() => controller.abort(), ms); + if (timeoutMilliseconds > 0) { + const timeout = setTimeout(() => controller.abort(), timeoutMilliseconds); return promise.finally(() => clearTimeout(timeout)); } else { return promise; @@ -728,16 +769,21 @@ function getSuggestionsSparqlQuery(sparqlQuery) { + sparqlQuery.replace(/^PREFIX.*/mg, ""), "requests"); return sparqlQuery; - // let url = BASEURL + "?query=" + encodeURIComponent(sparqlQuery); - // return url; } -function getQleverSuggestions(sparqlQuery, prefixesRelation, appendix, nameList, predicateForObject, word, mixedModeQuery) { - /* mixedModeQuery is the case-insensitive query that is sent additionally to the case-sensitive query when mixed mode is enabled. */ +// Get suggestions from QLever backend and show them to the user. +function getQleverSuggestions( + sparqlQuery, // the main AC query + prefixes, // prefixes used for abbreviating IRIs + appendix, + nameList, + predicateForObject, + word, + mixedModeQuery // `mixedModeQuery` : the "backup" query when in mixed mode +) { - - // show the loading indicator and badge + // Show the loading indicator and badge activeLineBadgeLine = $('.CodeMirror-activeline-background'); activeLine = $('.CodeMirror-activeline-gutter .CodeMirror-gutter-elt'); activeLineNumber = activeLine.html(); @@ -749,33 +795,42 @@ function getQleverSuggestions(sparqlQuery, prefixesRelation, appendix, nameList, const mixedModeSparqlQuery = getSuggestionsSparqlQuery(mixedModeQuery); var dynamicSuggestions = []; + // The actual code for getting the suggestions and showing them. + // + // TODO: Why is this wrapped in a `setTimeout` with a timeout of only 500 + // milliseconds? + const sparqlTimeoutDuration = 500; sparqlTimeout = window.setTimeout(async function () { + // Issue AC query (or two when in mixed mode) and get `response`. try { + // When in mixed mode, first issue the alternative query (but don't wait + // for it to return, `fetchTimeout` returns a promise). let mixedModeQuery; if (mixedModeSparqlQuery) { - mixedModeQuery = fetchTimeout(mixedModeSparqlQuery, DEFAULT_TIMEOUT); // start the mixed mode query, but async + mixedModeQuery = fetchTimeout(mixedModeSparqlQuery, DEFAULT_TIMEOUT); } + // Issue the main autocompletion query (and wait for it to return). const mainQueryTimeout = mixedModeSparqlQuery ? MIXED_MODE_TIMEOUT : DEFAULT_TIMEOUT; let response; let mainQueryHasTimedOut = false; - let showTimeoutError = false; + let allQueriesHaveTimedOut = false; try { - response = await fetchTimeout(lastSparqlQuery, mainQueryTimeout); // start the main query and wait for it to return + response = await fetchTimeout(lastSparqlQuery, mainQueryTimeout); } catch (error) { if (error.name === "AbortError") { mainQueryHasTimedOut = true; - showTimeoutError = true; + allQueriesHaveTimedOut = true; } else { throw error; } } + // If the main query timed out (or failed for other reasons), and we are + // in mixed mode, take the result of the alternative query. if (mainQueryHasTimedOut && mixedModeSparqlQuery) { - // the main query timed out. - // get the mixedModeQuery's response and continue with that log("The main query timed out. Using the context-insensitive suggestions.", 'requests') try { response = await mixedModeQuery; - showTimeoutError = false; + allQueriesHaveTimedOut = false; } catch (error) { if (error.name !== "AbortError") { throw error; @@ -783,14 +838,20 @@ function getQleverSuggestions(sparqlQuery, prefixesRelation, appendix, nameList, } } + // Get the actual query result as `data`. let data; - if (showTimeoutError) { + if (allQueriesHaveTimedOut) { data = {exception: "The request was cancelled due to timeout"}; } else { data = await response.json(); } - + // Show the suggestions to the user. + // + // NOTE: This involves some post-processing (for example, showing the + // suggestions with prefixes like `wdt:` and not with their full IRIs as + // returned by the backend) and several hacks (for example, when wdt:P31 + // is a suggestion, also add wdt:P31/wdt:P279*). if (data.res) { log("Got suggestions from QLever.", 'other'); log("Query took " + data.time.total + " and found " + data.resultsize + " lines", 'requests'); @@ -818,15 +879,18 @@ function getQleverSuggestions(sparqlQuery, prefixesRelation, appendix, nameList, } } - // add back the prefixes + // Abbreviate IRIs using the prefixes of the SPARQL query. var replacePrefix = ""; var prefixName = ""; - for (var prefix in prefixesRelation) { - if (entity.indexOf(prefixesRelation[prefix]) > 0 && prefixesRelation[prefix].length > replacePrefix.length) { - replacePrefix = prefixesRelation[prefix]; + for (var prefix in prefixes) { + if (entity.indexOf(prefixes[prefix]) > 0 && prefixes[prefix].length > replacePrefix.length) { + replacePrefix = prefixes[prefix]; prefixName = prefix; } } + + // Repeat, using ALL prefixes from the backend configuraion + // (`FILLPREFIXES` is `fillPrefixes` from the backend configuration). if (FILLPREFIXES) { for (var prefix in COLLECTEDPREFIXES) { if (entity.indexOf(COLLECTEDPREFIXES[prefix]) > 0 && COLLECTEDPREFIXES[prefix].length > replacePrefix.length) { @@ -847,7 +911,6 @@ function getQleverSuggestions(sparqlQuery, prefixesRelation, appendix, nameList, } } - // console.log("URL: " + window.location); var nameIndex = data.selected.indexOf(SUGGESTIONNAMEVARIABLE); var altNameIndex = data.selected.indexOf(SUGGESTIONALTNAMEVARIABLE); var entityName = (nameIndex != -1) ? result[nameIndex] : ""; @@ -919,11 +982,11 @@ function getQleverSuggestions(sparqlQuery, prefixesRelation, appendix, nameList, $('#suggestionErrorBlock').html('Error while collecting suggestions:
' + data.exception + '
') console.error(data.exception); } - // reset loading indicator - + + // Reset loading indicator. $('#aBadge').remove(); - // add badge + // Add badge that shows the total numbers of suggestions. if (data.resultsize != undefined && data.resultsize != null) { resultSize = data.resultsize; activeLineBadgeLine.prepend('' + data.resultsize + ''); @@ -945,17 +1008,26 @@ function getQleverSuggestions(sparqlQuery, prefixesRelation, appendix, nameList, return []; } - }, 500); + }, sparqlTimeoutDuration); } -/** - -Returns the suggestions defined for a given complex type - -**/ +// Get all suggestions for the given `type`by computing the cross-product of the +// elements in the array `type.suggestions`, see `COMPLEXTYPES` in `sparql.js`. +// +// For example, for the type `LIMIT`, the suggestion array is ['LIMIT ', [1, 10, +// 100, 1000], '\n'], and the list of suggestions computed is hence +// +// LIMIT 1 +// LIMIT 10 +// LIMIT 100 +// LIMIT 1000 +// +// An element of `type.suggestions` can also be a function. For example, +// `TRIPLE` contains `function (c) { return getDynamicSuggestions(c); }`. These +// functions are called with the given `context`. function getTypeSuggestions(type, context) { typeSuggestions = [] @@ -1043,11 +1115,16 @@ function getTypeSuggestions(type, context) { return typeSuggestions; } -/** - -Build a query tree - -**/ +// Compute tree representation of the given (partial) SPARQL query. +// +// NOTE: The result is returned as an array of elements with fields `w3name`, +// `content`, `start`, `end`, and optionally `children` (the latter giving this +// a tree-like structure)giving this a tree-like structure). The values for +// the fields `start` and `end` are absolute and not relative to the second +// argument `start` of the function. +// +// NOTE: To get the tree for the whole query from the editor window, +// call `buildQueryTree(editor.getValue(), 0)`. function buildQueryTree(content, start) { var tree = []; @@ -1056,134 +1133,125 @@ function buildQueryTree(content, start) { var tempString = ""; var tempElement = { w3name: 'PrefixDecl', start: start } + // Iteratively go through the query character by character. while (i < content.length) { tempString += content[i]; + // Determine whether we should start a new tree element (`tempElement`) and + // finish the previous one. The following sequences (minus the quotes) + // start a new element: + // + // "SELECT ", "WHERE {", "OPTIONAL {", + // + // TODO: A lot of code duplication here and some mistakes. Also, why are the + // regexes hard-coded here and not part of the definitions of `CONTEXTS` in + // `sparql.js`? if (/SELECT $/i.test(tempString)) { - - // shorten the end of prefix decl by what we needed to add to match SELECT tempElement['content'] = tempString.slice(0, tempString.length - 7); tempElement['end'] = i + start - 6; tree.push(tempElement); tempString = ""; - tempElement = { w3name: 'SelectClause', suggestInSameLine: true, start: i + start } - } else if (/WHERE \{$/i.test(tempString)) { - - // shorten the end of the select clause by what we needed to add to match WHERE { tempElement['content'] = tempString.slice(0, tempString.length - 7); tempElement['end'] = i + start - 6; tree.push(tempElement); tempString = ""; - tempElement = { w3name: 'WhereClause', suggestInSameLine: true, start: i + start } - } else if (/OPTIONAL \{$/i.test(tempString)) { - - // shorten the end of the previous clause by what we needed to add to match OPTIONAL { - tempElement['content'] = tempString.slice(0, tempString.length - 7); - tempElement['end'] = i + start - 3; + tempElement['content'] = tempString.slice(0, tempString.length - 10); + tempElement['end'] = i + start - 9; tree.push(tempElement); tempString = ""; - tempElement = { w3name: 'WhereClause', suggestInSameLine: true, start: i + start } - } else if (/MINUS \{$/i.test(tempString)) { - - // shorten the end of the previous clause by what we needed to add to match OPTIONAL { - tempElement['content'] = tempString.slice(0, tempString.length - 4); - tempElement['end'] = i + start - 3; + tempElement['content'] = tempString.slice(0, tempString.length - 7); + tempElement['end'] = i + start - 6; tree.push(tempElement); tempString = ""; - tempElement = { w3name: 'WhereClause', suggestInSameLine: true, start: i + start } - - } else if (/VALUES \(?$/i.test(tempString)) { - - // shorten the end of the previous clause by what we needed to add to match VALUES { - tempElement['content'] = tempString.slice(0, tempString.length - 7); - tempElement['end'] = i + start - 6; + } else if (/VALUES $/i.test(tempString)) { + tempElement['content'] = tempString.slice(0, tempString.length - 8); + tempElement['end'] = i + start - 7; tree.push(tempElement); tempString = ""; - tempElement = { w3name: 'ValuesClause', suggestInSameLine: true, start: i + start } - - } else if (/FILTER \(?$/i.test(tempString)) { - - // shorten the end of the previous clause by what we needed to add to match VALUES { + } else if (/FILTER $/i.test(tempString)) { + // } else if (/FILTER \(?$/i.test(tempString)) { tempElement['content'] = tempString.slice(0, tempString.length - 7); tempElement['end'] = i + start - 6; tree.push(tempElement); tempString = ""; - tempElement = { w3name: 'Filter', suggestInSameLine: true, start: i + start } - } else if (tempElement.w3name == 'ValuesClause' && /{$/i.test(tempString)) { - - // shorten the end of the previous clause by what we needed to add to match VALUES { + // TODO: How does this go together with the regex for VALUES above? tempElement['content'] = tempString.slice(0, tempString.length - 7); tempElement['end'] = i + start; tree.push(tempElement); tempString = ""; - tempElement = { w3name: 'DataBlock', suggestInSameLine: true, start: i + start } - } else if (/UNION \{$/i.test(tempString)) { - - // shorten the end of the previous clause by what we needed to add to match UNION { tempElement['content'] = tempString.slice(0, tempString.length - 7); tempElement['end'] = i + start - 6; tempElement.w3name = 'WhereClause'; tree.push(tempElement); tempString = ""; - tempElement = { w3name: 'WhereClause', suggestInSameLine: true, start: i + start } - } else if (tempString.endsWith('{')) { - // fast forward recursion + // Iterate to find the closing `}` or the end of `content`. Extend + // `tempString` until before the closing `}`, while `subString` is the + // part after the opening `{`. var depth = 1; var subStart = i; var subString = ""; - while (depth != 0 && i <= content.length) { - i++; + i++; + while (i < content.length) { // NOTE: was <= content.length and i++ at beginning. if (content[i] == '}') { depth -= 1; + if (depth == 0) break; } else if (content[i] == '{') { depth += 1; } - if (depth != 0) { - subString += content[i]; - tempString += content[i]; - } + subString += content[i]; + tempString += content[i]; + i++; } + // Recursively build tree for the part in "{...}". if (tempElement['children']) { tempElement['children'] = tempElement['children'].concat(buildQueryTree(subString, subStart)); } else { tempElement['children'] = buildQueryTree(subString, subStart); } - - } else if (tempString.endsWith('}') || ((tempElement.w3name == "OrderCondition" || tempElement.w3name == "GroupCondition") && tempString.endsWith('\n'))) { - - // shorten the whereclause by what we needed to add to match the } + } + // Solution modifiers at the end of the query. + else if (tempString.endsWith('}') || ((tempElement.w3name == "OrderCondition" || tempElement.w3name == "GroupCondition") && tempString.endsWith('\n'))) { tempElement['content'] = tempString.slice(0, tempString.length - 1); tempElement['end'] = i + start - 1; tree.push(tempElement); tempString = ""; - tempElement = { w3name: 'SolutionModifier', suggestInSameLine: true, start: i + start } - } + // Go to the next character. i++; } + // Add the last element. tempElement['content'] = tempString; tempElement['end'] = content.length + start; tree.push(tempElement); + // Some post-processing for `OptionalClause`, `UnionClause`, and + // `SolutionModifier`. + // + // TODO: Why do we need to explicitly set the name of the first child of + // `OptionalClause` and `UnionClause` to `WhereClause`? + // + // TODO: Why the additional parsing of the `SolutionModifier` here? Is it + // because we have additional structure within a line here, whereas the code + // above assumes that new elements only start at new lines? for (var element of tree) { if (element.w3name == "OptionalClause" || element.w3name == "UnionClause") { if (element['children']) { @@ -1252,33 +1320,35 @@ function printQueryTree(tree, absPosition, prefix) { return logString; } -/** - -Returns the current context - -@params absPosition - absolute position in text - -**/ +// Get the context (see `CONTEXTS` from sparql.js) of the given +// position. function getCurrentContext(absPosition) { var tree = buildQueryTree(editor.getValue(), 0); log("\n" + printQueryTree(tree, absPosition, ""), 'parsing'); return searchTree(tree, absPosition); } +// TODO: This function appears to be unused. function getNextContext(absPosition) { var tree = buildQueryTree(editor.getValue(), 0); var current = searchTree(tree, absPosition); for (var i = absPosition; i < editor.getValue().length + 1; i++) { var found = searchTree(tree, i); if (current != found && found != undefined) { - console.log('Found ' + found.w3name); + // console.log('Found ' + found.w3name); return found; } } return false; } +// For the given query tree, find the element (each element is a "context" from +// sparql.js:CONTEXTS) that contains the given position and return it. function searchTree(tree, absPosition) { + // Iterate over all elements in the tree and return the first one that + // contains the given `absPosition`. If that position is contained within the + // children of an element, recurse (so that, effectively, we only return + // leaf elements). for (var element of tree) { if (absPosition >= element.start && absPosition <= element.end) { if (element.children && absPosition >= element.children[0].start && absPosition <= element.children[element.children.length - 1].end) { @@ -1304,20 +1374,20 @@ function searchTree(tree, absPosition) { } } } + log("Could not find context for position " + absPosition, 'parsing'); return undefined; } -/** - -Returns the context by its name - -@params absPosition - absolute position in text - -**/ +// Find the *first* occurrence of the given context (for example, `WhereClause`, +// see `CONTEXTS` in sparql.js) in the content of the editor window. +// +// TODO: Why is it OK to always only find the first match? This is used a lot in +// sparql.js:`COMPLEXTYPES` and seems to be central for the query parsing. function getContextByName(name) { var editorContent = editor.getValue() var foundContext = undefined; + // TODO: Should not `CONTEXTS` be an associative array if we use it like this? $(CONTEXTS).each(function (index, context) { if (context.w3name == name) { @@ -1363,13 +1433,13 @@ function getPrefixLines() { } /** - + Returns the value of the given context - + @params context - the current context - + - Excludes duplicate definitions if told to do so - + **/ function getValueOfContext(context) { var editorContent = editor.getValue(); @@ -1384,17 +1454,17 @@ function getValueOfContext(context) { } /** - + Returns prefixes to suggest - + @params context - the current context @params excludeAggregationVariables - excludes variables that are the result of an aggregation (SUM(?x) as ?aggregate_variable) @params variableType - can be "text" for text variables, "normal" for normal variables or "both" for both @params predicateResultType - only return variables of given type or higher - + - Excludes duplicate definitions if told to do so - Add list with all unused variables as one suggestion - + **/ function getVariables(context, excludeAggregationVariables, variableType, predicateResultType) { var variables = []; @@ -1454,13 +1524,13 @@ function getVariables(context, excludeAggregationVariables, variableType, predic } /** - + Returns prefixes to suggest - + @params context - the prefix context - -- Excludes duplicate definitions - + +- Excludes duplicate definitions + **/ function getPrefixSuggestions(context) { var prefixes = [] diff --git a/backend/static/js/codemirror/modes/sparql/sparql.js b/backend/static/js/codemirror/modes/sparql/sparql.js index 7e866bfb..778558fc 100644 --- a/backend/static/js/codemirror/modes/sparql/sparql.js +++ b/backend/static/js/codemirror/modes/sparql/sparql.js @@ -41,7 +41,7 @@ const CONTEXTS = [ }, { w3name: 'Filter', - definition: /Filter \(([\s\S]*)\)/gi, + definition: /FILTER \(([\s\S]*)\)/gi, }, { w3name: 'ValuesClause', @@ -69,6 +69,8 @@ const CONTEXTS = [ }, ]; +// TODO: Understand how the unnamed function in `suggestions`is called and with +// which `c`. const COMPLEXTYPES = [ { name: 'PREFIX', diff --git a/backend/static/js/helper.js b/backend/static/js/helper.js index f5a34fc2..b402aba0 100644 --- a/backend/static/js/helper.js +++ b/backend/static/js/helper.js @@ -444,19 +444,22 @@ async function rewriteQuery(query, kwargs = {}) { function rewriteQueryNoAsyncPart(query) { var query_rewritten = query; - // HACK 1: Rewrite FILTER CONTAINS(?title, "info* retr*") using + // HACK 1: Rewrite FILTER KEYWORDS(?title, "info* retr*") using // ql:contains-entity and ql:contains-word. - var num_rewrites_filter_contains = 0; - var m_var = "?qlm_"; - var filter_contains_re = /FILTER\s+CONTAINS\((\?[\w_]+),\s*(\"[^\"]+\")\)\s*\.?\s*/i; - while (query_rewritten.match(filter_contains_re)) { - query_rewritten = query_rewritten.replace(filter_contains_re, - m_var + ' ql:contains-entity $1 . ' + m_var + ' ql:contains-word $2 . '); - m_var = m_var + "i"; - num_rewrites_filter_contains += 1; - } - if (num_rewrites_filter_contains > 0) { - console.log("Rewrote query with \"FILTER CONTAINS\""); + const rewriteFilterKeywords = true; + if (rewriteFilterKeywords) { + var num_rewrites_filter_contains = 0; + var m_var = "?qlm_"; + var filter_contains_re = /FILTER\s+KEYWORDS\((\?[\w_]+),\s*(\"[^\"]+\")\)\s*\.?\s*/i; + while (query_rewritten.match(filter_contains_re)) { + query_rewritten = query_rewritten.replace(filter_contains_re, + m_var + ' ql:contains-entity $1 . ' + m_var + ' ql:contains-word $2 . '); + m_var = m_var + "i"; + num_rewrites_filter_contains += 1; + } + if (num_rewrites_filter_contains > 0) { + console.log("Rewrote query with \"FILTER KEYWORDS\""); + } } // HACK 2: Rewrite query to use ql:has-predicate if it fits a certain pattern @@ -854,6 +857,38 @@ function htmlEscape(str) { // table. function getFormattedResultEntry(str, maxLength, column = undefined) { + // Get the variable name from the table header. TODO: it is inefficient to do + // this for every table entry. + var var_name = $($("#resTable").find("th")[column + 1]).html(); + + // If the entry is or contains a link, make it clickable (see where these + // variables are set in the following code). + let isLink = false; + let linkStart = ""; + let linkEnd = ""; + + // HACK: If the variable ends in "_sparql" or "_mapview", consider the value + // as a SPARQL query, and show it in the QLever UI or on a map, respectively. + if (var_name.endsWith("_sparql") || var_name.endsWith("_mapview")) { + isLink = true; + if (var_name.endsWith("_sparql")) { + mapview_url = `https://qlever.cs.uni-freiburg.de/${SLUG}/` + + `?query=${encodeURIComponent(str)}`; + icon_class = "glyphicon glyphicon-search"; + str = "Query view"; + } else { + mapview_url = `https://qlever.cs.uni-freiburg.de/mapui-petri/` + + `?query=${encodeURIComponent(str)}` + + `&mode=objects&backend=${BASEURL}`; + icon_class = "glyphicon glyphicon-globe"; + str = "Map view"; + } + linkStart = `` + + ` ` + + ``; + linkEnd = ''; + } + // TODO: Do we really want to replace each _ by a space right in the // beginning? str = str.replace(/_/g, ' '); @@ -913,7 +948,6 @@ function getFormattedResultEntry(str, maxLength, column = undefined) { // HACK Hannah 16.09.2021: Custom formatting depending on the variable name in // the column header. - var var_name = $($("#resTable").find("th")[column + 1]).html(); // console.log("Check if \"" + str + "\" in column \"" + var_name + "\" is a float ..."); if (var_name.endsWith("?note") || var_name.endsWith("_note")) str = parseFloat(str).toFixed(2).toString(); if (var_name.endsWith("_per_paper")) str = parseFloat(str).toFixed(2).toString(); @@ -924,9 +958,6 @@ function getFormattedResultEntry(str, maxLength, column = undefined) { pos = cpy.lastIndexOf("^^") pos_http = cpy.indexOf("http"); - let isLink = false; - let linkStart = ""; - let linkEnd = ""; // For typed literals (with a ^^ part), prepend icon to header that links to // that type. @@ -956,7 +987,7 @@ function getFormattedResultEntry(str, maxLength, column = undefined) { // For IRIs that start with http display the item depending on the link type. // For images, a thumbnail of the image is shown. For other links, prepend a - // symbol that depends on the link tpye and links to the respective URL. + // symbol that depends on the link type and links to the respective URL. // // TODO: What if http occur somewhere inside a literal or a link? } else if (pos_http > 0) { diff --git a/backend/static/js/qleverUI.js b/backend/static/js/qleverUI.js index 737f866f..fab512fe 100755 --- a/backend/static/js/qleverUI.js +++ b/backend/static/js/qleverUI.js @@ -33,7 +33,7 @@ $(window).resize(function (e) { }); $(document).ready(function () { - + // Initialize the editor. editor = CodeMirror.fromTextArea(document.getElementById("query"), { mode: "application/sparql-query", indentWithTabs: true, smartIndent: false, @@ -53,10 +53,10 @@ $(document).ready(function () { "Ctrl-R": "replace" }, }); - + // Set the width of the editor window. editor.setSize($('#queryBlock').width(), 350); - + // Make the editor resizable. $('.CodeMirror').resizable({ resize: function () { @@ -64,44 +64,44 @@ $(document).ready(function () { editor.setSize($(this).width(), $(this).height()); } }); - + // Initialize the tooltips. $('[data-toggle="tooltip"]').tooltip(); - + // If there is a theme cookie: use it! if (getCookie("theme") != "") { changeTheme(getCookie("theme")); } - + // Load the backends statistics. handleStatsDisplay(); - + // Initialize the name hover. if (SUBJECTNAME || PREDICATENAME || OBJECTNAME) { $('.cm-entity').hover(showRealName); } - + // Initialization done. log('Editor initialized', 'other'); - + // Do some custom activities on cursor activity editor.on("cursorActivity", function (instance) { $('[data-tooltip=tooltip]').tooltip('hide'); cleanLines(instance); }); - + editor.on("update", function (instance, event) { $('[data-tooltip=tooltip]').tooltip('hide'); - + // (re)initialize the name hover if (SUBJECTNAME || PREDICATENAME || OBJECTNAME) { $('.cm-entity').hover(showRealName); } }); - + // Do some custom activities (overwrite codemirror behaviour) editor.on("keyup", function (instance, event) { - + // For each prefix in COLLECTEDPREFIXES, check whether it occurs somewhere // in the query and if so, add it before the first SELECT (and move // the cursor accordingly). @@ -128,7 +128,7 @@ $(document).ready(function () { var line = instance.getLine(cur.line); var token = instance.getTokenAt(cur); var string = ''; - + // do not overwrite ENTER inside an completion window // esc - 27 // arrow left - 37 @@ -136,7 +136,7 @@ $(document).ready(function () { // arrow right - 39 // arrow down - 40 if (instance.state.completionActive || event.keyCode == 27 || (event.keyCode >= 37 && event.keyCode <= 40)) { - + // for for autocompletions opened unintented if (line[cur.ch] == "}" || line[cur.ch + 1] == "}" || line[cur.ch - 1] == "}") { if(instance && instance.state && instance.state.completionActive){ @@ -145,12 +145,12 @@ $(document).ready(function () { } return; } - + if (token.string.match(new RegExp('^[.`\w?<@]\w*$'))) { string = token.string; } // do not suggest anything inside a word - + if ((line[cur.ch] == " " || line[cur.ch + 1] == " " || line[cur.ch + 1] == undefined) && line[cur.ch] != "}" && line[cur.ch + 1] != "}" && line[cur.ch - 1] != "}") { // invoke autocompletion after a very short delay window.setTimeout(function () { @@ -162,17 +162,17 @@ $(document).ready(function () { console.warn('Skipped completion due to cursor position'); } }); - + // when completion is chosen - remove the counter badge editor.on("endCompletion", function () { $('#aBadge').remove(); }); - + function showRealName(element) { - + // collect prefixes (as string and dict) // TODO: move this to a function. Also use this new function in sparql-hint.js var prefixes = ""; var lines = getPrefixLines(); - + for (var line of lines) { if (line.trim().startsWith("PREFIX")) { var match = /PREFIX (.*): ?<(.*)>/g.exec(line.trim()); @@ -181,22 +181,22 @@ $(document).ready(function () { } } } - + // TODO: move this "get current element with its prefix" to a function. Also use this new function in sparql-hint.js values = $(this).parent().text().trim().split(' '); element = $(this).text().trim(); domElement = this; - + if ($(this).prev().hasClass('cm-prefix-name') || $(this).prev().hasClass('cm-string-language')) { element = $(this).prev().text() + element; } - + if ($(this).next().hasClass('cm-entity-name')) { element = element + $(this).next().text(); } - + index = values.indexOf(element.replace(/^\^/, "")); - + if (index == 0) { if (SUBJECTNAME != "") { addNameHover(element, domElement, subjectNames, SUBJECTNAME, prefixes); @@ -210,7 +210,7 @@ $(document).ready(function () { addNameHover(element, domElement, objectNames, OBJECTNAME, prefixes); } } - + return true; } @@ -258,7 +258,7 @@ $(document).ready(function () { // }); // window.location.href = await getQueryString(query) + "&action=csv_export"; }); - + // TSV report: like for CSV report above. $("#tsvbtn").click(async function () { log('Download TSV', 'other'); @@ -270,7 +270,7 @@ $(document).ready(function () { download_link.click(); // window.location.href = await getQueryString(query) + "&action=tsv_export"; }); - + // Generating the various links for sharing. $("#sharebtn").click(async function () { // Rewrite the query, normalize it, and escape quotes. @@ -309,10 +309,10 @@ $(document).ready(function () { + " --data \"" + queryRewrittenAndNormalizedAndWithEscapedQuotes + "\""); $("#queryStringUnescaped").val(queryRewrittenAndNormalizedAndWithEscapedQuotes); }, "json"); - + if (editor.state.completionActive) { editor.state.completionActive.close(); } }); - + $(".copy-clipboard-button").click(function () { var link = $(this).parent().parent().find("input")[0]; link.select(); @@ -320,12 +320,12 @@ $(document).ready(function () { document.execCommand("copy"); $(this).parent().parent().parent().find(".ok-text").collapse("show"); }); - + }); function addNameHover(element, domElement, list, namepredicate, prefixes) { element = element.replace(/^(@[a-zA-Z-]+@|\^)/, ""); - + if ($(domElement).data('tooltip') == 'tooltip') { return; } @@ -365,7 +365,7 @@ function getResultTime(resultTimes) { parseFloat(resultTimes.computeResult.replace(/[^\d\.]/g, "")) ]; timeList.push(timeList[0] - timeList[1]); // time for resolving and sending - + for (const i in timeList) { const time = timeList[i]; let timeAmount = Math.round(time); @@ -444,7 +444,7 @@ async function processQuery(sendLimit=0, element=$("#exebtn")) { log('Preparing query...', 'other'); log('Element: ' + element, 'other'); if (sendLimit >= 0) { displayStatus("Waiting for response..."); } - + $(element).find('.glyphicon').addClass('glyphicon-spin glyphicon-refresh'); $(element).find('.glyphicon').removeClass('glyphicon-remove'); $(element).find('.glyphicon').css('color', $(element).css('color')); @@ -489,7 +489,7 @@ async function processQuery(sendLimit=0, element=$("#exebtn")) { headers: headers, success: function (result) { log('Evaluating and displaying results...', 'other'); - + $(element).find('.glyphicon').removeClass('glyphicon-spin'); // For non-query commands like "cmd=clear-cache" just remove the "Waiting @@ -524,7 +524,7 @@ async function processQuery(sendLimit=0, element=$("#exebtn")) { const columns = result.selected; // If more than predefined number of results, create "Show all" button - // (onclick action defined further down). + // (onclick action defined further down). let showAllButton = ""; if (nofRows < parseInt(resultSize)) { showAllButton = "" @@ -555,7 +555,7 @@ async function processQuery(sendLimit=0, element=$("#exebtn")) { // the Django configuration of the respective backend). var res = "
"; if (showAllButton || (mapViewButtonVanilla && mapViewButtonPetri)) { - if (BASEURL.match("wikidata|osm-")) { + if (BASEURL.match("wikidata|osm-|dblp")) { res += `
${showAllButton} ${mapViewButtonPetri}
`; // res += `
${showAllButton} ${mapViewButtonVanilla} ${mapViewButtonPetri}
`; } else { @@ -641,7 +641,7 @@ async function processQuery(sendLimit=0, element=$("#exebtn")) { $("html, body").animate({ scrollTop: $("#resTable").scrollTop() + 500 }, 500); - + // MAX_VALUE ensures this always has priority over the websocket updates appendRuntimeInformation(result.runtimeInformation, result.query, result.time, { queryId, updateTimeStamp: Number.MAX_VALUE }); renderRuntimeInformationToDom(); @@ -674,12 +674,12 @@ async function processQuery(sendLimit=0, element=$("#exebtn")) { } }); } - + function handleStatsDisplay() { log('Loading backend statistics...', 'other'); $('#statsButton span').html('Loading information...'); $('#statsButton').attr('disabled', 'disabled'); - + $.getJSON(BASEURL + "?cmd=stats", function (result) { log('Evaluating and displaying stats...', 'other'); $("#kbname").html(result.kbindex ?? result["name-index"]); @@ -730,7 +730,7 @@ function renderRuntimeInformationToDom(entry = undefined) { runtime_info, query } = entry || Array.from(request_log.values()).pop(); - + if (runtime_info["query_execution_tree"] === null) { $("#result-query").text(""); $("#meta-info").text(""); @@ -801,7 +801,7 @@ function renderRuntimeInformationToDom(entry = undefined) { $("p.node-status").filter(function() { return $(this).text() === "failed because child failed"}).addClass("child-failed"); $("p.node-status").filter(function() { return $(this).text() === "not yet started"}).parent().addClass("not-started"); $("p.node-status").filter(function() { return $(this).text() === "optimized out"}).addClass("optimized-out"); - + if ($('#logRequests').is(':checked')) { const queryHistoryList = $("