From f10c293453033a91c69127bba524f63f1aa18111 Mon Sep 17 00:00:00 2001 From: Vincent Emonet Date: Wed, 25 Sep 2024 12:08:49 +0200 Subject: [PATCH] Autocomplete is now aware of the SERVICE call the cursor is in. Bump to 0.1.8 --- README.md | 69 ++++++++++++++++++++++++-------------------- demo/index.html | 52 +++++++++++++++++---------------- package.json | 4 +-- src/sparql-editor.ts | 25 ++++++++-------- src/utils.ts | 36 +++++++++++++++++++++-- 5 files changed, 113 insertions(+), 73 deletions(-) diff --git a/README.md b/README.md index 3470007..8900f3f 100644 --- a/README.md +++ b/README.md @@ -8,42 +8,15 @@ -A standard web component to easily deploy a user-friendly SPARQL query editor for a specific SPARQL endpoint, based on the popular [YASGUI editor](https://github.com/zazuko/Yasgui) with advanced autocomplete for predicates based on classes. +A standard web component to easily deploy a user-friendly SPARQL query editor for one or more endpoints. Built on the popular [YASGUI editor](https://github.com/zazuko/Yasgui), it provides context-aware autocomplete for classes and predicates based on the content of the endpoints. -The editor retrieves metadata about the endpoint by directly querying the SPARQL endpoint, so all you need to do is to properly document your endpoint. Reducing the need for complex infrastructure, while making your SPARQL endpoints easier to query for users and machines. +The editor retrieves metadata about the endpoints by directly querying them, so all that is needed is to generate and upload some metadata to each endpoints. Reducing the need for complex infrastructure, while making your SPARQL endpoints easier to query for users and machines. -- **Prefixes** are automatically pulled from the endpoint using their definition defined with the [SHACL ontology](https://www.w3.org/TR/shacl/) (`sh:prefix`/`sh:namespace`). +- [x] **Autocomplete possibilities for properties and classes** are automatically pulled from the endpoints based on [VoID description](https://www.w3.org/TR/void/) present in the triplestore (`void:linkPredicate|void:property` and `void:class`). The suggested properties are contextually filtered based on the class of the subject at the cursor's position, and are aware of `SERVICE` clauses, ensuring relevant autocompletion even in federated queries. - The prefixes/namespaces are retrieved with this query: + Checkout the [`void-generator`](https://github.com/JervenBolleman/void-generator) project to automatically generate VoID description for your endpoint. VoID description is retrieved using this SPARQL query: ```SPARQL - PREFIX sh: - SELECT DISTINCT ?prefix ?namespace - WHERE { [] sh:namespace ?namespace ; sh:prefix ?prefix } - ORDER BY ?prefix - ``` - -- **Example SPARQL queries** defined using the SHACL ontology are automatically pulled from the endpoint (queries are defined with `sh:select|sh:ask|sh:construct|sh:describe`, and their human readable description with `rdfs:comment`). Checkout the [`sparql-examples`](https://github.com/sib-swiss/sparql-examples) project for more details. - - The example queries are retrieved with this SPARQL query: - - ```SPARQL - PREFIX sh: - PREFIX rdfs: - SELECT DISTINCT ?sq ?comment ?query - WHERE { - ?sq a sh:SPARQLExecutable ; - rdfs:comment ?comment ; - sh:select|sh:ask|sh:construct|sh:describe ?query . - } ORDER BY ?sq - ``` - -- **Autocomplete possibilities for properties and classes** are automatically pulled from the endpoint based on [VoID description](https://www.w3.org/TR/void/) present in the triplestore (`void:linkPredicate|void:property` and `void:class`). The proposed properties are filtered based on the predicates available for the class of the subject related to where your cursor is 🤯. Checkout the [`void-generator`](https://github.com/JervenBolleman/void-generator) project to automatically generate VoID description for your endpoint. - - VoID description is retrieved using this SPARQL query: - - ```SPARQL - PREFIX up: PREFIX void: PREFIX void-ext: SELECT DISTINCT ?subjectClass ?prop ?objectClass ?objectDatatype @@ -67,6 +40,32 @@ The editor retrieves metadata about the endpoint by directly querying the SPARQL } ``` +- [x] **Example SPARQL queries** defined using the SHACL ontology are automatically pulled from the endpoint (queries are defined with `sh:select|sh:ask|sh:construct|sh:describe`, and their human readable description with `rdfs:comment`). + + Checkout the [`sparql-examples`](https://github.com/sib-swiss/sparql-examples) project for more details. The example queries are retrieved with this SPARQL query: + + ```SPARQL + PREFIX sh: + PREFIX rdfs: + SELECT DISTINCT ?sq ?comment ?query + WHERE { + ?sq a sh:SPARQLExecutable ; + rdfs:comment ?comment ; + sh:select|sh:ask|sh:construct|sh:describe ?query . + } ORDER BY ?sq + ``` + +- [x] **Prefixes** are automatically pulled from the endpoint using their definition defined with the [SHACL ontology](https://www.w3.org/TR/shacl/) (`sh:prefix`/`sh:namespace`). + + The prefixes/namespaces are retrieved with this query: + + ```SPARQL + PREFIX sh: + SELECT DISTINCT ?prefix ?namespace + WHERE { [] sh:namespace ?namespace ; sh:prefix ?prefix } + ORDER BY ?prefix + ``` + 👆️ You can **try it** for a few SPARQL endpoints of the SIB, such as UniProt and Bgee, here: **[sib-swiss.github.io/sparql-editor](https://sib-swiss.github.io/sparql-editor)** ![Screenshot gene](demo/screenshot_gene.png) @@ -97,9 +96,15 @@ The editor retrieves metadata about the endpoint by directly querying the SPARQL ``` + You can also pass a list of endpoints URLs separated by commas to enable users to choose from different endpoints: + + ```html + + ``` + > [!WARNING] > -> Metadata are retrieved by a few lightweight queries sent from client-side JavaScript when the editor is initialized, so your SPARQL **endpoint should accept CORS** (either from \*, which is recommended, or just from the URL where the editor is deployed) +> Metadata are retrieved by a few lightweight queries sent from client-side JavaScript when the editor is initialized, so your SPARQL **endpoints should accept CORS** (either from \*, which is recommended, or just from the URL where the editor is deployed) ### ⚙️ Available attributes diff --git a/demo/index.html b/demo/index.html index 50b9409..32cb423 100644 --- a/demo/index.html +++ b/demo/index.html @@ -13,33 +13,35 @@
-

💫 SPARQL editors for SIB endpoints

- -

- You can check if an endpoint contains the necessary metadata here (query examples and VoID - description). -

+

💫 SPARQL editor for SIB endpoints

+ + +

Focused SPARQL editors for SIB endpoints

+ +

+ You can check if an endpoint contains the necessary metadata here (query examples and VoID + description). +

+
- + --> diff --git a/package.json b/package.json index bd6f19e..49c40dd 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@sib-swiss/sparql-editor", - "version": "0.1.7", - "description": "A standard web components to easily deploy a SPARQL query editor for a specific SPARQL endpoint using the popular YASGUI editor.", + "version": "0.1.8", + "description": "A standard web component to easily deploy a user-friendly SPARQL query editor for one or more endpoints. Built on the popular YASGUI editor, it provides context-aware autocomplete for classes and predicates based on the content of the endpoints.", "license": "MIT", "author": { "name": "Vincent Emonet", diff --git a/src/sparql-editor.ts b/src/sparql-editor.ts index 53e4e87..79a064b 100644 --- a/src/sparql-editor.ts +++ b/src/sparql-editor.ts @@ -15,6 +15,7 @@ import { getPrefixes, getVoidDescription, ExampleQuery, + getServiceUriForCursorPosition, } from "./utils"; interface EndpointsMetadata { @@ -98,7 +99,6 @@ export class SparqlEditor extends HTMLElement { ${editorCss} `; if (endpoints.length === 1) { - console.log("Only one endpoint, hiding the controlbar"); style.textContent += `.yasgui .controlbar { display: none !important; }`; @@ -189,10 +189,8 @@ export class SparqlEditor extends HTMLElement { examples: [], }; } - // console.log("get meta", this.meta[endpoint].prefixes.size); - // if (Object.keys(this.meta[endpoint].prefixes).length < 1) { if (!this.meta[endpoint].retrievedAt) { - console.log(`Getting metadata for ${endpoint}`); + // console.log(`Getting metadata for ${endpoint}`); [ this.meta[endpoint].examples, this.meta[endpoint].prefixes, @@ -231,17 +229,16 @@ export class SparqlEditor extends HTMLElement { copyEndpointOnNewTab: true, }); await this.loadCurrentEndpoint(); - console.log(this.endpointUrl()); // TODO: Not perfect, it is only triggered once for each endpoint, not everytime the endpoint changes + // It's triggered multiple times for same endpoint if you only keep 1 tab, but breaks when opening more tabs this.yasgui?.getTab()?.on("endpointChange", async () => { // console.log("Endpoint changed", endpoint, this.currentEndpoint()); await this.loadCurrentEndpoint(); }); this.yasgui?.on("tabSelect", async (yasgui: Yasgui, newTabId: string) => { - // @ts-ignore - this.loadCurrentEndpoint(yasgui.getTab(newTabId).endpointSelect.value); + this.loadCurrentEndpoint(yasgui.getTab(newTabId)?.getEndpoint()); }); // mermaid.initialize({ startOnLoad: false }); @@ -389,20 +386,24 @@ export class SparqlEditor extends HTMLElement { voidPropertyCompleter = { name: "voidProperty", bulk: false, - get: (yasqe: any, token: any) => { + get: async (yasqe: any, token: any) => { const cursor = yasqe.getCursor(); const subj = getSubjectForCursorPosition(yasqe.getValue(), cursor.line, cursor.ch); - // TODO: get the URL of the endpoint for SERVICE calls const subjTypes = extractAllSubjectsAndTypes(yasqe.getValue()); + const cursorEndpoint = + getServiceUriForCursorPosition(yasqe.getValue(), cursor.line, cursor.ch) || this.endpointUrl(); + // console.log("cursorEndpoint!", cursorEndpoint); + // Make sure the metadata is loaded for the service endpoints + await this.getMetadata(cursorEndpoint); // console.log("subj, subjTypes, unfiltered hints", subj, subjTypes, hints) - if (subj && subjTypes.has(subj) && Object.keys(this.currentEndpoint().void).length > 0) { + if (subj && subjTypes.has(subj) && Object.keys(this.meta[cursorEndpoint].void).length > 0) { const types = subjTypes.get(subj); // console.log("types", types) if (types) { const suggestPreds = new Set(); try { types.forEach(typeCurie => { - Object.keys(this.currentEndpoint().void[this.curieToUri(typeCurie)]) + Object.keys(this.meta[cursorEndpoint].void[this.curieToUri(typeCurie)]) .filter(prop => prop.indexOf(token.autocompletionString) === 0) .forEach(prop => { suggestPreds.add(prop); @@ -415,7 +416,7 @@ export class SparqlEditor extends HTMLElement { if (suggestPreds.size > 0) return Array.from(suggestPreds).sort(); } } - return this.currentEndpoint().predicates.filter(iri => iri.indexOf(token.autocompletionString) === 0); + return this.meta[cursorEndpoint].predicates.filter(iri => iri.indexOf(token.autocompletionString) === 0); }, isValidCompletionPosition: (yasqe: any) => { const token = yasqe.getCompleteToken(); diff --git a/src/utils.ts b/src/utils.ts index 9520a50..fba2abf 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -154,9 +154,8 @@ export function extractAllSubjectsAndTypes(query: string): Map]+)>\s*{/gi; + let match; + // Iterate through all SERVICE blocks in the query + while ((match = serviceRegex.exec(query)) !== null) { + const serviceUri = match[1]; + const serviceStart = match.index + match[0].length - 1; // Start of the opening brace '{' + // Find the matching closing brace accounting for nested braces + let braceDepth = 1; + let serviceEnd = serviceStart; + for (let i = serviceStart + 1; i < query.length; i++) { + if (query[i] === "{") { + braceDepth++; + } else if (query[i] === "}") { + braceDepth--; + if (braceDepth === 0) { + serviceEnd = i; + break; + } + } + } + // Check if cursor is inside this SERVICE block + const cursorPosition = partialQuery.length; + if (cursorPosition >= serviceStart && cursorPosition <= serviceEnd) { + return serviceUri; + } + } + return null; +}