Skip to content

Commit 6d6666b

Browse files
committed
Autocomplete for properties is now based on the type of the subject attached to where the cursor is thanks to the VoID description
1 parent ea6b588 commit 6d6666b

File tree

2 files changed

+185
-2
lines changed

2 files changed

+185
-2
lines changed

README.md

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ A standard web component to easily deploy a SPARQL query editor for a specific S
1010

1111
- **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`).
1212
- **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:label|rdfs:comment`). Checkout the [`sparql-examples`](https://github.com/sib-swiss/sparql-examples) project for more details.
13-
- **Autocomplete possibilities for properties and classes** are automatically pulled from the endpoint based on VoID description present in the triplestore (`void:linkPredicate|void:property` and `void:class`). Checkout the [`void-generator`](https://github.com/JervenBolleman/void-generator) project to automatically generate VoID description for your endpoint.
13+
- **Autocomplete possibilities for properties and classes** are automatically pulled from the endpoint based on VoID description present in the triplestore (`void:linkPredicate|void:property` and `void:class`). And the proposed properties are filtered based on the properties available for the type of the subject attached to where your cursor is 🤯. Checkout the [`void-generator`](https://github.com/JervenBolleman/void-generator) project to automatically generate VoID description for your endpoint.
1414

1515
## 🚀 Use
1616

@@ -47,6 +47,50 @@ A standard web component to easily deploy a SPARQL query editor for a specific S
4747
</sparql-editor>
4848
```
4949

50+
## 📝 Basic example
51+
52+
No need for a complex project you can integrate it in any HTML page by importing from a CDN.
53+
54+
Create a `index.html` file with:
55+
56+
```html
57+
<!doctype html>
58+
<html lang="en">
59+
<head>
60+
<meta charset="utf-8" />
61+
<meta name="viewport" content="width=device-width, initial-scale=1" />
62+
<title>SPARQL editor dev</title>
63+
<meta name="description" content="SPARQL editor demo page" />
64+
<link rel="icon" type="image/png" href="https://upload.wikimedia.org/wikipedia/commons/f/f3/Rdf_logo.svg" />
65+
<!-- Import the module from a CDN -->
66+
<script type="module" src="https://unpkg.com/@sib-swiss/sparql-editor@0.1.1"></script>
67+
</head>
68+
69+
<body>
70+
<div>
71+
<sparql-editor
72+
endpoint="https://www.bgee.org/sparql/"
73+
examples-on-main-page="10"
74+
style="--btn-color: white; --btn-bg-color: #00709b;"
75+
>
76+
<h1>About</h1>
77+
<p>This SPARQL endpoint contains...</p>
78+
</sparql-editor>
79+
</div>
80+
</body>
81+
</html>
82+
```
83+
84+
Just open this HTML page in your favorite browser.
85+
86+
You can also start a basic web server with nodeJS or python:
87+
88+
```bash
89+
npx http-server
90+
# or
91+
python -m http.server
92+
```
93+
5094
## 🛠️ Development
5195

5296
> Requirement: [NodeJS](https://nodejs.org/en) installed.

src/sparql-editor.ts

Lines changed: 140 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,12 @@ type ExampleQuery = {
1111
query: string;
1212
};
1313

14+
interface VoidDict {
15+
[key: string]: {
16+
[key: string]: string[];
17+
};
18+
}
19+
1420
/**
1521
* Custom element to create a SPARQL editor for a given endpoint using YASGUI
1622
* with autocompletion for classes and properties based on VoID description stored in the endpoint
@@ -24,6 +30,7 @@ export class SparqlEditor extends HTMLElement {
2430
exampleQueries: ExampleQuery[];
2531
urlParams: any;
2632
prefixes: Map<string, string>;
33+
voidDescription: VoidDict;
2734

2835
constructor() {
2936
super();
@@ -87,6 +94,7 @@ export class SparqlEditor extends HTMLElement {
8794
["dc", "http://purl.org/dc/terms/"],
8895
["faldo", "http://biohackathon.org/resource/faldo#"],
8996
]);
97+
this.voidDescription = {};
9098

9199
Yasgui.Yasqe.forkAutocompleter("class", this.voidClassCompleter);
92100
Yasgui.Yasqe.forkAutocompleter("property", this.voidPropertyCompleter);
@@ -107,9 +115,12 @@ export class SparqlEditor extends HTMLElement {
107115
// Get prefixes and examples, and set default config for YASGUI
108116
await this.getExampleQueries();
109117
await this.getPrefixes();
118+
await this.getVoidDescription();
110119
Yasgui.Yasqe.defaults.value = this.addPrefixesToQuery(this.exampleQueries[0]?.query) || Yasgui.Yasqe.defaults.value;
111120
Yasgui.Yasr.defaults.prefixes = Object.fromEntries(this.prefixes);
112121

122+
// TODO: make exampleQueries a dict with the query IRI as key, so if the window.location matches a key, it will load the query
123+
113124
// Create YASGUI editor
114125
const editorEl = this.shadowRoot?.getElementById("yasgui") as HTMLElement;
115126
this.yasgui = new Yasgui(editorEl, {
@@ -235,6 +246,34 @@ export class SparqlEditor extends HTMLElement {
235246
return [];
236247
}
237248
},
249+
postprocessHints: (yasqe: any, hints: any) => {
250+
const cursor = yasqe.getCursor();
251+
// We retrieved the subject at the cursor position and all subjects/types using regex
252+
// Not perfect, but we can't parse the whole query with SPARQL.js since it's no fully written yet
253+
// And it would throw an error if the query is not valid
254+
const subj = getSubjectForCursorPosition(yasqe.getValue(), cursor.line, cursor.ch);
255+
const subjTypes = extractAllSubjectsAndTypes(yasqe.getValue())
256+
// console.log(hints)
257+
if (subj && subjTypes.has(subj)) {
258+
const types = subjTypes.get(subj);
259+
if (types) {
260+
// console.log("Types", types)
261+
const filteredHints = new Set()
262+
types.forEach(typeCurie => {
263+
const propSet = new Set(Object.keys(this.voidDescription[this.curieToUri(typeCurie)]));
264+
// console.log(propSet)
265+
hints.filter((obj: any) => {
266+
// console.log(this.curieToUri(obj.text))
267+
return propSet.has(this.curieToUri(obj.text).replace(/^<|>$/g, ''))
268+
}).forEach((obj: any) => {
269+
filteredHints.add(obj)
270+
})
271+
});
272+
return Array.from(filteredHints)
273+
}
274+
}
275+
return hints
276+
},
238277
};
239278

240279
addPrefixesToQuery(query: string) {
@@ -291,11 +330,56 @@ export class SparqlEditor extends HTMLElement {
291330
});
292331
}
293332

333+
async getVoidDescription() {
334+
// Get VoID description to get classes and properties for advanced autocompletion
335+
const voidQuery = `PREFIX up: <http://purl.uniprot.org/core/>
336+
PREFIX void: <http://rdfs.org/ns/void#>
337+
PREFIX void-ext: <http://ldf.fi/void-ext#>
338+
SELECT DISTINCT ?class1 ?prop ?class2 ?datatype
339+
WHERE {
340+
?cp void:class ?class1 ;
341+
void:propertyPartition ?pp .
342+
?pp void:property ?prop .
343+
OPTIONAL {
344+
{
345+
?pp void:classPartition [ void:class ?class2 ] .
346+
} UNION {
347+
?pp void-ext:datatypePartition [ void-ext:datatype ?datatype ] .
348+
}
349+
}
350+
}`;
351+
try {
352+
const response = await fetch(`${this.endpointUrl}?format=json&ac=1&query=${encodeURIComponent(voidQuery)}`);
353+
const json = await response.json();
354+
json.results.bindings.forEach((b: any) => {
355+
// clsList.push(b.class.value);
356+
if (!(b["class1"]["value"] in this.voidDescription)) {
357+
this.voidDescription[b["class1"]["value"]] = {}
358+
}
359+
if (!(b["prop"]["value"] in this.voidDescription[b["class1"]["value"]])) {
360+
this.voidDescription[b["class1"]["value"]][b["prop"]["value"]] = []
361+
}
362+
if ("class2" in b) {
363+
this.voidDescription[b["class1"]["value"]][b["prop"]["value"]].push(
364+
b["class2"]["value"]
365+
)
366+
}
367+
if ("datatype" in b) {
368+
this.voidDescription[b["class1"]["value"]][b["prop"]["value"]].push(
369+
b["datatype"]["value"]
370+
)
371+
}
372+
});
373+
} catch (error) {
374+
console.warn("Error retrieving VoID description for autocomplete:", error);
375+
}
376+
}
377+
294378
async getExampleQueries() {
295379
const exampleQueriesEl = this.shadowRoot?.getElementById("sparql-examples") as HTMLElement;
296380
const getQueryExamples = `PREFIX sh: <http://www.w3.org/ns/shacl#>
297381
PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
298-
SELECT DISTINCT ?comment ?query WHERE {
382+
SELECT DISTINCT ?sq ?comment ?query WHERE {
299383
?sq a sh:SPARQLExecutable ;
300384
rdfs:label|rdfs:comment ?comment ;
301385
sh:select|sh:ask|sh:construct|sh:describe ?query .
@@ -440,8 +524,63 @@ SELECT DISTINCT ?comment ?query WHERE {
440524
console.warn("Error fetching or processing example queries:", error);
441525
}
442526
}
527+
528+
// Function to convert CURIE to full URI using the prefix map
529+
curieToUri(curie: string) {
530+
if (/^[a-zA-Z][a-zA-Z0-9]*:[a-zA-Z][a-zA-Z0-9]*$/.test(curie)) {
531+
const [prefix, local] = curie.split(':');
532+
const namespace = this.prefixes.get(prefix);
533+
return namespace ? `${namespace}${local}` : curie; // Return as-is if prefix not found
534+
} else {
535+
// If it's already a full URI, return as-is
536+
return curie;
537+
}
538+
}
443539
}
444540

541+
function extractAllSubjectsAndTypes(query: string): Map<string, Set<string>> {
542+
const subjectTypeMap = new Map<string, Set<string>>();
543+
// Remove comments and string literals, and prefixes to avoid false matches
544+
const cleanQuery = query
545+
.replace(/^#.*$/gm, '')
546+
.replace(/'(?:[^'\\]|\\.)*'|"(?:[^"\\]|\\.)*"/g, '""')
547+
.replace(/^PREFIX\s+.*$/gmi, '') // Remove PREFIX/prefix lines;
548+
.replace(/;\s*\n/g, '; ') // Also put all triple patterns on a single line
549+
.replace(/;\s*$/g, '; ');
550+
// console.log(cleanQuery)
551+
const typePattern = /\s*(\?\w+|<[^>]+>).*?\s+(?:a|rdf:type|<http:\/\/www\.w3\.org\/1999\/02\/22-rdf-syntax-ns#type>)\s+([^\s.]+)\s*(?:;|\.)/g;
552+
553+
let match;
554+
while ((match = typePattern.exec(cleanQuery)) !== null) {
555+
const subject = match[1];
556+
const type = match[2];
557+
558+
if (!subjectTypeMap.has(subject)) {
559+
subjectTypeMap.set(subject, new Set());
560+
}
561+
subjectTypeMap.get(subject)!.add(type);
562+
}
563+
return subjectTypeMap;
564+
}
565+
566+
function getSubjectForCursorPosition(query: string, lineNumber: number, charNumber: number): string | null {
567+
const lines = query.split('\n');
568+
const currentLine = lines[lineNumber];
569+
// Extract the part of the line up to the cursor position
570+
const partOfLine = currentLine.slice(0, charNumber);
571+
const partialQuery = lines.slice(0, lineNumber).join('\n') + '\n' + partOfLine;
572+
// Put all triple patterns on a single line
573+
const cleanQuery = partialQuery.replace(/;\s*\n/g, '; ').replace(/;\s*$/g, '; ');
574+
const partialLines = cleanQuery.split('\n');
575+
const lastLine = partialLines[partialLines.length - 1];
576+
const subjectMatch = lastLine.match(/([?\w]+|[<\w]+>)\s+/);
577+
if (subjectMatch) {
578+
return subjectMatch[1];
579+
}
580+
return null;
581+
}
582+
583+
445584
customElements.define("sparql-editor", SparqlEditor);
446585

447586
// https://github.com/joachimvh/SPARQLAlgebra.js

0 commit comments

Comments
 (0)