Skip to content

Commit fb0b61f

Browse files
committed
feat(yasqe): always include the SPARQL Anything vocabularies
Adds the SPARQL Anything namespaces xyz (http://sparql.xyz/facade-x/data/), fx (http://sparql.xyz/facade-x/ns/) and fxe (http://sparql.xyz/facade-x/engine/) to ALWAYS_INCLUDED. SPARQL Anything publishes no RDF files, so the fx magic property, class and functions and the fxe options are listed there. - fetchPrefixData.js adds the prefixes of ALWAYS_INCLUDED to the prefix.cc data, so xyz, fx, fxe (and railml) survive the refresh at build time. - The bundled terms get an optional fifth list of functions, suggested by a new function autocompleter where an expression can start.
1 parent 8bc9572 commit fb0b61f

11 files changed

Lines changed: 351 additions & 27 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@matdata/yasqe": minor
3+
---
4+
5+
Always include the SPARQL Anything vocabularies: the `xyz:`, `fx:` and `fxe:` prefixes are always in the bundled prefix list (also after a prefix.cc refresh), the `fx:anySlot` magic property, the `fx:Root` class and the `fxe:` options (`fxe:location`, `fxe:csv.headers`...) are suggested as properties and classes, and a new `function` autocompleter suggests the `fx:` functions (`fx:entity`, `fx:String.substring`...) where an expression can start.

‎docs/user-guide.md‎

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -714,6 +714,8 @@ When typing a prefix declaration, Matgui queries [prefix.cc](https://prefix.cc)
714714
PREFIX foaf: < # Auto-suggests http://xmlns.com/foaf/0.1/
715715
```
716716

717+
MatGUI ships the prefix.cc list, so this works offline too. A few prefixes are always in it, with these namespaces, whatever prefix.cc says: `era:` (ERA ontology), `railml:` (railML 3), `rtm:` (RailTopoModel) and the three [SPARQL Anything](https://sparql-anything.cc) namespaces `xyz:` (`http://sparql.xyz/facade-x/data/`), `fx:` (`http://sparql.xyz/facade-x/ns/`) and `fxe:` (`http://sparql.xyz/facade-x/engine/`). Typing one of them in a query, such as `fxe:location`, adds its `PREFIX` declaration.
718+
717719
### Endpoint Quick Switch
718720

719721
Quick switch endpoints let you change the SPARQL endpoint of a tab with a single click. They are listed in the **Quick switch** group at the top of the endpoint dropdown (see [Endpoint Picker](#endpoint-picker)).
@@ -862,7 +864,30 @@ Ctrl+clicking on `http://dbpedia.org/resource/European_Union` automatically quer
862864
Press `Ctrl+Space` after `a` (or `rdf:type`) to get class suggestions, or in the position of a property to get property suggestions. MatGUI suggests:
863865

864866
1. **Used in this endpoint**: the classes and properties the endpoint of the tab actually uses, most used first, with the number of instances (classes) or triples (properties) next to them. This also covers vocabularies that are not registered anywhere, such as railML 3, the ERA ontology or your own ontologies.
865-
2. **Linked Open Vocabularies**: matching classes and properties of the most used vocabularies in [LOV](https://lov.linkeddata.es/dataset/lov/). MatGUI ships this list, so no request is made to LOV while you type. Terms that the endpoint already suggested are not repeated, and a term is inserted as a prefixed name with its `PREFIX` when LOV knows the vocabulary's preferred prefix. The ERA ontology (`era:`), railML 3 (`railml:`) and RailTopoModel (`rtm:`) are always included, even though they are not in LOV.
867+
2. **Linked Open Vocabularies**: matching classes and properties of the most used vocabularies in [LOV](https://lov.linkeddata.es/dataset/lov/). MatGUI ships this list, so no request is made to LOV while you type. Terms that the endpoint already suggested are not repeated, and a term is inserted as a prefixed name with its `PREFIX` when LOV knows the vocabulary's preferred prefix. The ERA ontology (`era:`), railML 3 (`railml:`), RailTopoModel (`rtm:`) and the SPARQL Anything vocabularies (`fx:`, `fxe:`, see below) are always included, even though they are not in LOV.
868+
869+
**SPARQL Anything**
870+
871+
The [SPARQL Anything](https://sparql-anything.cc) terms are always suggested, so you can write its queries without looking up the documentation:
872+
873+
- `fx:` (`http://sparql.xyz/facade-x/ns/`): the `fx:anySlot` magic property and the `fx:Root` class as properties and classes, and the functions, such as `fx:entity`, `fx:literal`, `fx:String.substring` or `fx:LevenshteinDistance`, wherever an expression can start (in `BIND`, `FILTER`, a `SELECT` expression...). A function also matches on its local name, so `substr` finds `fx:String.substring`.
874+
- `fxe:` (`http://sparql.xyz/facade-x/engine/`): the options of a SPARQL Anything `SERVICE` clause, such as `fxe:location`, `fxe:media-type`, `fxe:csv.headers` or `fxe:json.path`.
875+
- `xyz:` (`http://sparql.xyz/facade-x/data/`): the namespace of the generated data. Its terms come from your data (`xyz:name`), so only the prefix is known, unless the endpoint uses them.
876+
877+
```sparql
878+
PREFIX xyz: <http://sparql.xyz/facade-x/data/>
879+
PREFIX fx: <http://sparql.xyz/facade-x/ns/>
880+
PREFIX fxe: <http://sparql.xyz/facade-x/engine/>
881+
882+
SELECT ?name WHERE {
883+
SERVICE <x-sparql-anything:> {
884+
fxe:properties fxe:location "https://sparql-anything.cc/example1.json" .
885+
?show xyz:name ?name ; xyz:stars ?star .
886+
?star fx:anySlot "Courteney Cox" .
887+
}
888+
BIND(fx:entity("https://example.org/show/", fx:String.toLowerCase(?name)) AS ?iri)
889+
}
890+
```
866891

867892
Without typing anything, the endpoint's most used terms are listed. While you type, a suggestion matches on its full IRI, its prefixed name (`era:Op`) or, when you type a plain word (`Operat`), on any part of its local name.
868893

‎fetchLovData.js‎

Lines changed: 161 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,11 @@ const MAX_BYTES = 600_000;
2929

3030
/**
3131
* Namespaces that are always bundled, first, regardless of their popularity and of the size budget.
32-
* `prefix` overrides the prefix from LOV. `sources` are RDF files (Turtle, N-Triples...) of the vocabulary whose
32+
* `prefix` overrides the prefix from LOV; fetchPrefixData.js also adds it to the bundled prefixes, so it is always
33+
* suggested and declared when used. `sources` are RDF files (Turtle, N-Triples...) of the vocabulary whose
3334
* classes and properties in the namespace are added to those of the dump, e.g. when LOV lacks the vocabulary.
35+
* `terms` lists local names of classes, properties and functions, for vocabularies without RDF files.
36+
* A namespace without any terms is left out of the bundle, but its prefix is still bundled.
3437
*/
3538
export const ALWAYS_INCLUDED = [
3639
{
@@ -53,6 +56,150 @@ export const ALWAYS_INCLUDED = [
5356
prefix: "rtm",
5457
sources: ["https://ontology.railml.org/ontology.ttl"],
5558
},
59+
// SPARQL Anything (https://sparql-anything.cc), which publishes no RDF files for its vocabularies, so the terms are
60+
// listed here, from https://github.com/SPARQL-Anything/sparql.anything/tree/HEAD/docs and the Façade-X specs.
61+
{
62+
// Façade-X: the terms of the generated data, the magic properties and the functions
63+
namespace: "http://sparql.xyz/facade-x/ns/",
64+
prefix: "fx",
65+
terms: {
66+
classes: ["Root"],
67+
properties: ["anySlot", "slot-key", "cachedGraph", "cachedGraphCreation", "sparqlAlgebra"],
68+
functions: [
69+
"cardinal",
70+
"isContainerMembershipProperty",
71+
"before",
72+
"after",
73+
"previous",
74+
"next",
75+
"forward",
76+
"backward",
77+
"String.startsWith",
78+
"String.endsWith",
79+
"String.indexOf",
80+
"String.lastIndexOf",
81+
"String.substring",
82+
"String.toLowerCase",
83+
"String.toUpperCase",
84+
"String.trim",
85+
"String.replace",
86+
"String.stripLeading",
87+
"String.stripTrailing",
88+
"String.removeTags",
89+
"WordUtils.capitalize",
90+
"WordUtils.capitalizeFully",
91+
"WordUtils.initials",
92+
"WordUtils.swapCase",
93+
"WordUtils.uncapitalize",
94+
"LevenshteinDistance",
95+
"CosineDistance",
96+
"JaccardDistance",
97+
"JaroWinklerDistance",
98+
"LongestCommonSubsequenceDistance",
99+
"HammingDistance",
100+
"QGramDistance",
101+
"DigestUtils.md2Hex",
102+
"DigestUtils.md5Hex",
103+
"DigestUtils.sha1Hex",
104+
"DigestUtils.sha256Hex",
105+
"DigestUtils.sha384Hex",
106+
"DigestUtils.sha512Hex",
107+
"URLEncoder.encode",
108+
"URLDecoder.decode",
109+
"serial",
110+
"entity",
111+
"literal",
112+
"bnode",
113+
],
114+
},
115+
},
116+
{
117+
// Façade-X Engine: the options of a SPARQL Anything SERVICE clause (fxe:properties fxe:location "...")
118+
namespace: "http://sparql.xyz/facade-x/engine/",
119+
prefix: "fxe",
120+
terms: {
121+
classes: ["Configuration", "Option"],
122+
properties: [
123+
"properties",
124+
"location",
125+
"content",
126+
"command",
127+
"read-from-std-in",
128+
"query",
129+
"from-archive",
130+
"archive-format",
131+
"archive.matches",
132+
"root",
133+
"media-type",
134+
"namespace",
135+
"blank-nodes",
136+
"trim-strings",
137+
"null-string",
138+
"triplifier",
139+
"charset",
140+
"metadata",
141+
"audit",
142+
"ondisk",
143+
"ondisk.reuse",
144+
"strategy",
145+
"slice",
146+
"slice.size",
147+
"use-rdfs-member",
148+
"annotate-triples-with-slot-keys",
149+
"generate-predicate-labels",
150+
"http.client.useSystemProperties",
151+
"http.method",
152+
"http.payload",
153+
"http.protocol",
154+
"http.auth.user",
155+
"http.auth.password",
156+
"http.redirect",
157+
"http.sleep",
158+
"s3.endpoint",
159+
"s3.bucket-name",
160+
"s3.key",
161+
"s3.access-key",
162+
"s3.secret-key",
163+
"s3.region",
164+
"bin.encoding",
165+
"csv.delimiter",
166+
"csv.format",
167+
"csv.headers",
168+
"csv.headers-row",
169+
"csv.headers.sanitize",
170+
"csv.ignore-columns-with-no-header",
171+
"csv.null-string",
172+
"csv.quote-char",
173+
"html.browser",
174+
"html.browser.screenshot",
175+
"html.browser.timeout",
176+
"html.browser.wait",
177+
"html.metadata",
178+
"html.parser",
179+
"html.selector",
180+
"json.include-null-values",
181+
"json.literalize",
182+
"json.path",
183+
"slides.extract-sections",
184+
"spreadsheet.composite-values",
185+
"spreadsheet.evaluate-formulas",
186+
"spreadsheet.headers",
187+
"spreadsheet.headers-row",
188+
"spreadsheet.ignore-columns-with-no-header",
189+
"txt.group",
190+
"txt.regex",
191+
"txt.split",
192+
"xml.lang-tags",
193+
"xml.path",
194+
"yaml.allow-duplicate-keys",
195+
],
196+
},
197+
},
198+
{
199+
// Façade-X data: the default namespace of the generated data, whose terms come from the source (xyz:name...)
200+
namespace: "http://sparql.xyz/facade-x/data/",
201+
prefix: "xyz",
202+
},
56203
];
57204

58205
const RDF_TYPE = "http://www.w3.org/1999/02/22-rdf-syntax-ns#type";
@@ -167,26 +314,29 @@ export async function collectTerms(quads) {
167314
}
168315

169316
/**
170-
* Adds the terms of the always included namespaces to the namespaces of the dump: the configured prefix, and
171-
* the classes and properties of extraTerms (namespaces collected from their sources).
317+
* Adds the terms of the always included namespaces to the namespaces of the dump: the configured prefix, the
318+
* configured terms, and the classes and properties of extraTerms (namespaces collected from their sources).
172319
*/
173320
export function addAlwaysIncluded(namespaces, alwaysIncluded = ALWAYS_INCLUDED, extraTerms = []) {
174321
const result = namespaces.map((ns) => ({ ...ns }));
175-
for (const { namespace, prefix } of alwaysIncluded) {
322+
const merge = (current = [], added = []) => Array.from(new Set([...current, ...added])).sort();
323+
for (const { namespace, prefix, terms } of alwaysIncluded) {
176324
let ns = result.find((ns) => ns.namespace === namespace);
177325
if (!ns) result.push((ns = { namespace, classes: [], properties: [], usedBy: 0 }));
178326
if (prefix) ns.prefix = prefix;
179-
for (const extra of extraTerms.filter((extra) => extra.namespace === namespace)) {
180-
ns.classes = Array.from(new Set([...ns.classes, ...extra.classes])).sort();
181-
ns.properties = Array.from(new Set([...ns.properties, ...extra.properties])).sort();
327+
for (const extra of [...(terms ? [terms] : []), ...extraTerms.filter((extra) => extra.namespace === namespace)]) {
328+
ns.classes = merge(ns.classes, extra.classes);
329+
ns.properties = merge(ns.properties, extra.properties);
330+
if (extra.functions?.length) ns.functions = merge(ns.functions, extra.functions);
182331
}
183332
}
184-
return result.filter((ns) => ns.classes.length || ns.properties.length);
333+
return result.filter((ns) => ns.classes.length || ns.properties.length || ns.functions?.length);
185334
}
186335

187336
/**
188337
* The bundled format: the always included namespaces, then the others most used first, as
189-
* [namespace, prefix, classes, properties]. Namespaces are added until the serialized file would exceed maxBytes,
338+
* [namespace, prefix, classes, properties] or, for namespaces with functions, [namespace, prefix, classes,
339+
* properties, functions]. Namespaces are added until the serialized file would exceed maxBytes,
190340
* except the always included ones, which are added regardless.
191341
*/
192342
export function toBundle(namespaces, maxBytes = MAX_BYTES, source = DUMP_URL, alwaysIncluded = ALWAYS_INCLUDED) {
@@ -199,6 +349,7 @@ export function toBundle(namespaces, maxBytes = MAX_BYTES, source = DUMP_URL, al
199349
];
200350
for (const ns of ordered) {
201351
const entry = [ns.namespace, ns.prefix || "", ns.classes.join(" "), ns.properties.join(" ")];
352+
if (ns.functions?.length) entry.push(ns.functions.join(" "));
202353
const entrySize = JSON.stringify(entry).length + 1;
203354
if (size + entrySize > maxBytes && always.indexOf(ns.namespace) < 0) continue;
204355
bundle.namespaces.push(entry);
@@ -268,8 +419,7 @@ async function main() {
268419
if (stats.skipped) console.log(`Skipped ${stats.skipped} invalid lines`);
269420
const bundle = toBundle(addAlwaysIncluded(namespaces, ALWAYS_INCLUDED, await collectExtraTerms()));
270421
const terms = bundle.namespaces.reduce(
271-
(count, [, , classes, properties]) =>
272-
count + (classes ? classes.split(" ").length : 0) + (properties ? properties.split(" ").length : 0),
422+
(count, [, , ...lists]) => count + lists.reduce((sum, list) => sum + (list ? list.split(" ").length : 0), 0),
273423
0,
274424
);
275425
fs.writeFileSync(OUTPUT_FILE, JSON.stringify(bundle) + "\n");

‎fetchPrefixData.js‎

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,16 @@
1010
*
1111
* When a successful fetch occurs, the committed fallback file is also updated so
1212
* future offline builds stay reasonably fresh.
13+
*
14+
* The prefixes of the always included vocabularies of fetchLovData.js (ERA, railML, SPARQL Anything...) are
15+
* added to the prefix.cc data, replacing prefix.cc's namespace for the same prefix, so they are always available.
1316
*/
1417

1518
import https from "https";
1619
import http from "http";
1720
import fs from "fs";
1821
import path from "path";
22+
import { ALWAYS_INCLUDED } from "./fetchLovData.js";
1923

2024
const PREFIXES_URL_HTTPS = "https://prefix.cc/popular/all.file.json";
2125
const PREFIXES_URL_HTTP = "http://prefix.cc/popular/all.file.json";
@@ -43,6 +47,15 @@ function fetchUrl(url, options = {}) {
4347
});
4448
}
4549

50+
/** The prefix data with the prefixes of the always included vocabularies, serialized */
51+
function withAlwaysIncluded(prefixes, alwaysIncluded = ALWAYS_INCLUDED) {
52+
const result = { ...prefixes };
53+
for (const { prefix, namespace } of alwaysIncluded) {
54+
if (prefix) result[prefix] = namespace;
55+
}
56+
return JSON.stringify(result, null, 2) + "\n";
57+
}
58+
4659
async function main() {
4760
let data = null;
4861

@@ -71,7 +84,7 @@ async function main() {
7184
// Validate the fetched JSON
7285
if (data) {
7386
try {
74-
JSON.parse(data);
87+
data = withAlwaysIncluded(JSON.parse(data));
7588
} catch (e) {
7689
console.warn("Fetched data is not valid JSON, ignoring and using committed fallback.");
7790
data = null;
@@ -85,7 +98,7 @@ async function main() {
8598
} else {
8699
// Use the committed fallback
87100
if (fs.existsSync(FALLBACK_FILE)) {
88-
data = fs.readFileSync(FALLBACK_FILE, "utf-8");
101+
data = withAlwaysIncluded(JSON.parse(fs.readFileSync(FALLBACK_FILE, "utf-8")));
89102
console.log(`Using committed fallback: ${FALLBACK_FILE}`);
90103
} else {
91104
console.error("No fallback file found and all fetches failed. Cannot provide prefix data.");
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import * as Autocompleter from "./";
2+
import { bundledLovTerms, searchLov } from "./lov";
3+
import type { Token } from "../";
4+
5+
/**
6+
* The complete token can span the expression before the function name, e.g. "BIND(fx:ent": keeps the part after the
7+
* last opening parenthesis, comma or operator.
8+
*/
9+
function trimToFunctionName(token: Token): Autocompleter.AutocompletionToken {
10+
const match = /[\s(),!=&|*+]([^\s(),!=&|*+]*)$/.exec(token.string);
11+
if (!match) return token;
12+
const start = token.end - match[1].length;
13+
// `from` makes the suggestion replace the function name only
14+
return { ...token, start, string: match[1], from: { ch: start } };
15+
}
16+
17+
/**
18+
* Suggests the functions of the bundled vocabularies (see the `lovTerms` config option), such as the SPARQL Anything
19+
* functions fx:entity or fx:String.substring, where an expression can start: in FILTER, BIND, SELECT expressions...
20+
*/
21+
var conf: Autocompleter.CompleterConfig = {
22+
get: function (yasqe, token) {
23+
if (!token) return [];
24+
const lovTerms = yasqe.config.lovTerms === undefined ? bundledLovTerms : yasqe.config.lovTerms;
25+
if (!lovTerms) return [];
26+
return searchLov(lovTerms, "function", token).map((term) =>
27+
Autocompleter.endpointTermToSuggestion(yasqe, token, term, Autocompleter.FUNCTION_SECTION),
28+
);
29+
},
30+
isValidCompletionPosition: function (yasqe) {
31+
const token = trimToFunctionName(yasqe.getCompleteToken());
32+
if (token.string.length == 0) return false; //we want -something- to autocomplete
33+
if (token.string[0] === "?" || token.string[0] === "$") return false; // we are typing a var
34+
// An expression can start here: a built-in call such as STR(...) is allowed
35+
return token.state.possibleCurrent.indexOf("STR") >= 0;
36+
},
37+
preProcessToken: function (yasqe, token) {
38+
return Autocompleter.preprocessIriForCompletion(yasqe, trimToFunctionName(token));
39+
},
40+
postProcessSuggestion: function (yasqe, token, suggestedString) {
41+
return Autocompleter.postprocessIriCompletion(yasqe, token, suggestedString);
42+
},
43+
bulk: false,
44+
name: "function",
45+
};
46+
47+
export default conf;

‎packages/yasqe/src/autocompleters/index.ts‎

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -365,6 +365,7 @@ export const fetchFromLov = (
365365
const ENDPOINT_SUGGESTIONS_LIMIT = 50;
366366
export const ENDPOINT_SECTION = { name: "Used in this endpoint", rank: 0 };
367367
export const LOV_SECTION = { name: "Linked Open Vocabularies", rank: 1 };
368+
export const FUNCTION_SECTION = { name: "Functions", rank: 2 };
368369

369370
function localName(iri: string): string {
370371
return iri.substring(Math.max(iri.lastIndexOf("#"), iri.lastIndexOf("/")) + 1);
@@ -458,9 +459,16 @@ export const fetchIriSuggestions = async (
458459
};
459460

460461
import { bundledLovTerms, searchLov } from "./lov";
461-
export type { LovTerms } from "./lov";
462+
export type { LovTerms, LovTermType } from "./lov";
462463
import variableCompleter from "./variables";
463464
import prefixCompleter from "./prefixes";
464465
import propertyCompleter from "./properties";
465466
import classCompleter from "./classes";
466-
export var completers: CompleterConfig[] = [variableCompleter, prefixCompleter, propertyCompleter, classCompleter];
467+
import functionCompleter from "./functions";
468+
export var completers: CompleterConfig[] = [
469+
variableCompleter,
470+
prefixCompleter,
471+
propertyCompleter,
472+
classCompleter,
473+
functionCompleter,
474+
];

0 commit comments

Comments
 (0)