@matdata/yasqe 5.2.0 → 5.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +128 -144
- package/build/ts/grammar/tokenizer.d.ts +8 -0
- package/build/ts/grammar/tokenizer.js +21 -1
- package/build/ts/grammar/tokenizer.js.map +1 -1
- package/build/ts/src/defaults.d.ts +4 -0
- package/build/ts/src/defaults.js +32 -2
- package/build/ts/src/defaults.js.map +1 -1
- package/build/ts/src/imgs.d.ts +3 -0
- package/build/ts/src/imgs.js +3 -0
- package/build/ts/src/imgs.js.map +1 -1
- package/build/ts/src/index.d.ts +26 -0
- package/build/ts/src/index.js +261 -2
- package/build/ts/src/index.js.map +1 -1
- package/build/ts/src/prefixFold.js +2 -0
- package/build/ts/src/prefixFold.js.map +1 -1
- package/build/ts/src/prefixUtils.js.map +1 -1
- package/build/yasqe.min.css +1 -1
- package/build/yasqe.min.css.map +3 -3
- package/build/yasqe.min.js +95 -46
- package/build/yasqe.min.js.map +4 -4
- package/grammar/_tokenizer-table.d.ts +9 -0
- package/grammar/tokenizer.ts +27 -1
- package/package.json +3 -2
- package/src/autocompleters/show-hint.scss +1 -2
- package/src/defaults.ts +32 -2
- package/src/imgs.ts +6 -0
- package/src/index.ts +353 -3
- package/src/prefixFold.ts +1 -0
- package/src/prefixUtils.ts +5 -2
- package/src/scss/buttons.scss +126 -0
- package/src/scss/yasqe.scss +12 -0
- package/src/sparql-formatter.d.ts +10 -0
package/grammar/tokenizer.ts
CHANGED
|
@@ -36,6 +36,10 @@ export interface State {
|
|
|
36
36
|
lastPredicateOffset: number;
|
|
37
37
|
currentPnameNs: string | undefined;
|
|
38
38
|
possibleFullIri: boolean;
|
|
39
|
+
inConstructTemplate: boolean;
|
|
40
|
+
seenWhereClause: boolean;
|
|
41
|
+
constructVariables: { [varName: string]: string };
|
|
42
|
+
whereVariables: { [varName: string]: string };
|
|
39
43
|
}
|
|
40
44
|
export interface Token {
|
|
41
45
|
quotePos: "end" | "start" | "content" | undefined;
|
|
@@ -467,6 +471,13 @@ export default function (config: CodeMirror.EditorConfiguration): CodeMirror.Mod
|
|
|
467
471
|
case "storeProperty":
|
|
468
472
|
state.storeProperty = true;
|
|
469
473
|
break;
|
|
474
|
+
case "constructTemplate":
|
|
475
|
+
state.inConstructTemplate = true;
|
|
476
|
+
break;
|
|
477
|
+
case "whereClause":
|
|
478
|
+
state.inConstructTemplate = false;
|
|
479
|
+
state.seenWhereClause = true;
|
|
480
|
+
break;
|
|
470
481
|
}
|
|
471
482
|
}
|
|
472
483
|
|
|
@@ -517,7 +528,18 @@ export default function (config: CodeMirror.EditorConfiguration): CodeMirror.Mod
|
|
|
517
528
|
// Incremental LL1 parse
|
|
518
529
|
while (state.stack.length > 0 && tokenCat && state.OK && !finished) {
|
|
519
530
|
topSymbol = state.stack.pop();
|
|
520
|
-
if (topSymbol === "var" && tokenOb.string)
|
|
531
|
+
if (topSymbol === "var" && tokenOb.string) {
|
|
532
|
+
state.variables[tokenOb.string] = tokenOb.string;
|
|
533
|
+
// Track variables separately for CONSTRUCT template validation
|
|
534
|
+
if (state.queryType === "CONSTRUCT") {
|
|
535
|
+
if (state.inConstructTemplate) {
|
|
536
|
+
state.constructVariables[tokenOb.string] = tokenOb.string;
|
|
537
|
+
} else if (state.seenWhereClause) {
|
|
538
|
+
// Only track as WHERE variable if we've actually entered the WHERE clause
|
|
539
|
+
state.whereVariables[tokenOb.string] = tokenOb.string;
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
}
|
|
521
543
|
if (!ll1_table[topSymbol]) {
|
|
522
544
|
// Top symbol is a terminal
|
|
523
545
|
if (topSymbol == tokenCat) {
|
|
@@ -721,6 +743,10 @@ export default function (config: CodeMirror.EditorConfiguration): CodeMirror.Mod
|
|
|
721
743
|
errorMsg: undefined,
|
|
722
744
|
inPrefixDecl: false,
|
|
723
745
|
possibleFullIri: false,
|
|
746
|
+
inConstructTemplate: false,
|
|
747
|
+
seenWhereClause: false,
|
|
748
|
+
constructVariables: {},
|
|
749
|
+
whereVariables: {},
|
|
724
750
|
};
|
|
725
751
|
},
|
|
726
752
|
indent: indent,
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@matdata/yasqe",
|
|
3
3
|
"description": "Yet Another SPARQL Query Editor",
|
|
4
|
-
"version": "5.
|
|
4
|
+
"version": "5.4.0",
|
|
5
5
|
"main": "build/yasqe.min.js",
|
|
6
6
|
"types": "build/ts/src/index.d.ts",
|
|
7
7
|
"license": "MIT",
|
|
@@ -27,7 +27,8 @@
|
|
|
27
27
|
"@matdata/yasgui-utils": "^4.6.1",
|
|
28
28
|
"codemirror": "^5.51.0",
|
|
29
29
|
"lodash-es": "^4.17.15",
|
|
30
|
-
"query-string": "^6.10.1"
|
|
30
|
+
"query-string": "^6.10.1",
|
|
31
|
+
"sparql-formatter": "^1.0.2"
|
|
31
32
|
},
|
|
32
33
|
"devDependencies": {
|
|
33
34
|
"@types/codemirror": "0.0.100",
|
package/src/defaults.ts
CHANGED
|
@@ -27,7 +27,7 @@ SELECT * WHERE {
|
|
|
27
27
|
rangeFinder: new (<any>CodeMirror).fold.combine((<any>CodeMirror).fold.brace, (<any>CodeMirror).fold.prefix),
|
|
28
28
|
},
|
|
29
29
|
collapsePrefixesOnLoad: false,
|
|
30
|
-
gutters: ["gutterErrorBar", "CodeMirror-linenumbers", "CodeMirror-foldgutter"],
|
|
30
|
+
gutters: ["gutterErrorBar", "gutterConstructWarning", "CodeMirror-linenumbers", "CodeMirror-foldgutter"],
|
|
31
31
|
matchBrackets: true,
|
|
32
32
|
fixedGutter: true,
|
|
33
33
|
syntaxErrorCheck: true,
|
|
@@ -68,7 +68,7 @@ SELECT * WHERE {
|
|
|
68
68
|
},
|
|
69
69
|
"Shift-Ctrl-F": function (_yasqe: any) {
|
|
70
70
|
const yasqe: Yasqe = _yasqe;
|
|
71
|
-
yasqe.
|
|
71
|
+
yasqe.format();
|
|
72
72
|
},
|
|
73
73
|
"Ctrl-S": function (_yasqe: any) {
|
|
74
74
|
const yasqe: Yasqe = _yasqe;
|
|
@@ -130,6 +130,36 @@ SELECT * WHERE {
|
|
|
130
130
|
editorHeight: "300px",
|
|
131
131
|
queryingDisabled: undefined,
|
|
132
132
|
prefixCcApi: prefixCcApi,
|
|
133
|
+
showFormatButton: true,
|
|
134
|
+
checkConstructVariables: true,
|
|
135
|
+
snippets: [
|
|
136
|
+
{
|
|
137
|
+
label: "SELECT",
|
|
138
|
+
code: "SELECT * WHERE {\n ?s ?p ?o .\n} LIMIT 10",
|
|
139
|
+
group: "Query Types",
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
label: "CONSTRUCT",
|
|
143
|
+
code: "CONSTRUCT {\n ?s ?p ?o .\n} WHERE {\n ?s ?p ?o .\n} LIMIT 10",
|
|
144
|
+
group: "Query Types",
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
label: "ASK",
|
|
148
|
+
code: "ASK {\n ?s ?p ?o .\n}",
|
|
149
|
+
group: "Query Types",
|
|
150
|
+
},
|
|
151
|
+
{
|
|
152
|
+
label: "FILTER",
|
|
153
|
+
code: "FILTER (?var > 100)",
|
|
154
|
+
group: "Patterns",
|
|
155
|
+
},
|
|
156
|
+
{
|
|
157
|
+
label: "OPTIONAL",
|
|
158
|
+
code: "OPTIONAL {\n ?s ?p ?o .\n}",
|
|
159
|
+
group: "Patterns",
|
|
160
|
+
},
|
|
161
|
+
],
|
|
162
|
+
showSnippetsBar: true,
|
|
133
163
|
};
|
|
134
164
|
const requestConfig: PlainRequestConfig = {
|
|
135
165
|
queryArgument: undefined, //undefined means: get query argument based on query mode
|
package/src/imgs.ts
CHANGED
|
@@ -12,3 +12,9 @@ export var fullscreen =
|
|
|
12
12
|
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M7 14H5v5h5v-2H7v-3zm-2-4h2V7h3V5H5v5zm12 7h-3v2h5v-5h-2v3zM14 5v2h3v3h2V5h-5z"/></svg>';
|
|
13
13
|
export var fullscreenExit =
|
|
14
14
|
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M5 16h3v3h2v-5H5v2zm3-8H5v2h5V5H8v3zm6 11h2v-3h3v-2h-5v5zm2-11V5h-2v5h5V8h-3z"/></svg>';
|
|
15
|
+
export var format =
|
|
16
|
+
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M3 3v18h18v-2H5V3H3zm14 4V5l-4 4 4 4V9h4V7h-4zM7 9v2h10V9H7zm0 4v2h10v-2H7zm0 4v2h10v-2H7z"/></svg>';
|
|
17
|
+
export var snippet =
|
|
18
|
+
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M14 2H6c-1.1 0-1.99.9-1.99 2L4 20c0 1.1.89 2 1.99 2H18c1.1 0 2-.9 2-2V8l-6-6zm2 16H8v-2h8v2zm0-4H8v-2h8v2zm-3-5V3.5L18.5 9H13z"/></svg>';
|
|
19
|
+
export var chevronDown =
|
|
20
|
+
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M7.41 8.59L12 13.17l4.59-4.58L18 10l-6 6-6-6 1.41-1.41z"/></svg>';
|
package/src/index.ts
CHANGED
|
@@ -11,11 +11,12 @@ import { drawSvgStringAsElement, addClass, removeClass } from "@matdata/yasgui-u
|
|
|
11
11
|
import * as Sparql from "./sparql";
|
|
12
12
|
import * as imgs from "./imgs";
|
|
13
13
|
import * as Autocompleter from "./autocompleters";
|
|
14
|
-
import { merge, escape } from "lodash-es";
|
|
14
|
+
import { merge, mergeWith, escape } from "lodash-es";
|
|
15
15
|
|
|
16
16
|
import getDefaults from "./defaults";
|
|
17
17
|
import CodeMirror from "./CodeMirror";
|
|
18
18
|
import { YasqeAjaxConfig } from "./sparql";
|
|
19
|
+
import { spfmt } from "sparql-formatter";
|
|
19
20
|
|
|
20
21
|
export interface Yasqe {
|
|
21
22
|
on(eventName: "query", handler: (instance: Yasqe, req: Request, abortController?: AbortController) => void): void;
|
|
@@ -55,6 +56,8 @@ export class Yasqe extends CodeMirror {
|
|
|
55
56
|
private fullscreenBtn: HTMLButtonElement | undefined;
|
|
56
57
|
private isFullscreen: boolean = false;
|
|
57
58
|
private resizeWrapper?: HTMLDivElement;
|
|
59
|
+
private snippetsBar?: HTMLDivElement;
|
|
60
|
+
private snippetsClickHandler?: (e: MouseEvent) => void;
|
|
58
61
|
public rootEl: HTMLDivElement;
|
|
59
62
|
public storage: YStorage;
|
|
60
63
|
public config: Config;
|
|
@@ -65,7 +68,13 @@ export class Yasqe extends CodeMirror {
|
|
|
65
68
|
this.rootEl = document.createElement("div");
|
|
66
69
|
this.rootEl.className = "yasqe";
|
|
67
70
|
parent.appendChild(this.rootEl);
|
|
68
|
-
|
|
71
|
+
// Use mergeWith to replace arrays instead of merging them by index
|
|
72
|
+
// This ensures that snippets: [] properly overrides default snippets
|
|
73
|
+
this.config = mergeWith({}, Yasqe.defaults, conf, (objValue: any, srcValue: any) => {
|
|
74
|
+
if (Array.isArray(srcValue)) {
|
|
75
|
+
return srcValue;
|
|
76
|
+
}
|
|
77
|
+
});
|
|
69
78
|
//inherit codemirror props
|
|
70
79
|
const cm = (CodeMirror as any)(this.rootEl, this.config);
|
|
71
80
|
//Assign our functions to the cm object. This is needed, as some functions (like the ctrl-enter callback)
|
|
@@ -79,18 +88,36 @@ export class Yasqe extends CodeMirror {
|
|
|
79
88
|
//Do some post processing
|
|
80
89
|
this.storage = new YStorage(Yasqe.storageNamespace);
|
|
81
90
|
this.drawButtons();
|
|
91
|
+
this.drawSnippetsBar();
|
|
82
92
|
const storageId = this.getStorageId();
|
|
83
93
|
// this.getWrapperElement
|
|
84
94
|
if (storageId) {
|
|
85
95
|
const persConf = this.storage.get<any>(storageId);
|
|
86
96
|
if (persConf && typeof persConf === "string") {
|
|
87
|
-
this.persistentConfig = { query: persConf, editorHeight: this.config.editorHeight };
|
|
97
|
+
this.persistentConfig = { query: persConf, editorHeight: this.config.editorHeight };
|
|
88
98
|
} else {
|
|
89
99
|
this.persistentConfig = persConf;
|
|
90
100
|
}
|
|
91
101
|
if (!this.persistentConfig)
|
|
92
102
|
this.persistentConfig = { query: this.getValue(), editorHeight: this.config.editorHeight };
|
|
103
|
+
|
|
104
|
+
// Ensure autoformatOnQuery is true by default
|
|
105
|
+
if (this.persistentConfig && typeof this.persistentConfig.autoformatOnQuery === "undefined") {
|
|
106
|
+
this.persistentConfig.autoformatOnQuery = true;
|
|
107
|
+
}
|
|
108
|
+
|
|
93
109
|
if (this.persistentConfig && this.persistentConfig.query) this.setValue(this.persistentConfig.query);
|
|
110
|
+
} else {
|
|
111
|
+
// If no storage, ensure persistentConfig exists and autoformatOnQuery is true
|
|
112
|
+
if (!this.persistentConfig) {
|
|
113
|
+
this.persistentConfig = {
|
|
114
|
+
query: this.getValue(),
|
|
115
|
+
editorHeight: this.config.editorHeight,
|
|
116
|
+
autoformatOnQuery: true,
|
|
117
|
+
};
|
|
118
|
+
} else if (typeof this.persistentConfig.autoformatOnQuery === "undefined") {
|
|
119
|
+
this.persistentConfig.autoformatOnQuery = true;
|
|
120
|
+
}
|
|
94
121
|
}
|
|
95
122
|
this.config.autocompleters.forEach((c) => this.enableCompleter(c).then(() => {}, console.warn));
|
|
96
123
|
if (this.config.consumeShareLink) {
|
|
@@ -99,6 +126,7 @@ export class Yasqe extends CodeMirror {
|
|
|
99
126
|
window.addEventListener("hashchange", this.handleHashChange);
|
|
100
127
|
}
|
|
101
128
|
this.checkSyntax();
|
|
129
|
+
this.checkConstructVariables();
|
|
102
130
|
// Size codemirror to the
|
|
103
131
|
if (this.persistentConfig && this.persistentConfig.editorHeight) {
|
|
104
132
|
this.getWrapperElement().style.height = this.persistentConfig.editorHeight;
|
|
@@ -115,6 +143,7 @@ export class Yasqe extends CodeMirror {
|
|
|
115
143
|
};
|
|
116
144
|
private handleChange() {
|
|
117
145
|
this.checkSyntax();
|
|
146
|
+
this.checkConstructVariables();
|
|
118
147
|
this.updateQueryButton();
|
|
119
148
|
}
|
|
120
149
|
private handleBlur() {
|
|
@@ -123,6 +152,7 @@ export class Yasqe extends CodeMirror {
|
|
|
123
152
|
private handleChanges() {
|
|
124
153
|
// e.g. handle blur
|
|
125
154
|
this.checkSyntax();
|
|
155
|
+
this.checkConstructVariables();
|
|
126
156
|
this.updateQueryButton();
|
|
127
157
|
}
|
|
128
158
|
private handleCursorActivity() {
|
|
@@ -347,6 +377,23 @@ export class Yasqe extends CodeMirror {
|
|
|
347
377
|
this.updateQueryButton();
|
|
348
378
|
}
|
|
349
379
|
|
|
380
|
+
/**
|
|
381
|
+
* Draw format btn
|
|
382
|
+
*/
|
|
383
|
+
if (this.config.showFormatButton) {
|
|
384
|
+
const formatBtn = document.createElement("button");
|
|
385
|
+
addClass(formatBtn, "yasqe_formatButton");
|
|
386
|
+
const formatIcon = drawSvgStringAsElement(imgs.format);
|
|
387
|
+
addClass(formatIcon, "formatIcon");
|
|
388
|
+
formatBtn.appendChild(formatIcon);
|
|
389
|
+
formatBtn.onclick = () => {
|
|
390
|
+
this.format();
|
|
391
|
+
};
|
|
392
|
+
formatBtn.title = "Format query (Shift+Ctrl+F)";
|
|
393
|
+
formatBtn.setAttribute("aria-label", "Format query");
|
|
394
|
+
buttons.appendChild(formatBtn);
|
|
395
|
+
}
|
|
396
|
+
|
|
350
397
|
/**
|
|
351
398
|
* Draw fullscreen btn
|
|
352
399
|
*/
|
|
@@ -379,6 +426,165 @@ export class Yasqe extends CodeMirror {
|
|
|
379
426
|
public getIsFullscreen() {
|
|
380
427
|
return this.isFullscreen;
|
|
381
428
|
}
|
|
429
|
+
|
|
430
|
+
private insertSnippet(code: string) {
|
|
431
|
+
const doc = this.getDoc();
|
|
432
|
+
const cursor = doc.getCursor();
|
|
433
|
+
doc.replaceRange(code, cursor);
|
|
434
|
+
// Move cursor to end of inserted code
|
|
435
|
+
const lines = code.split("\n");
|
|
436
|
+
const lastLine = lines[lines.length - 1];
|
|
437
|
+
doc.setCursor({
|
|
438
|
+
line: cursor.line + lines.length - 1,
|
|
439
|
+
ch: lines.length === 1 ? cursor.ch + lastLine.length : lastLine.length,
|
|
440
|
+
});
|
|
441
|
+
this.focus();
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
private drawSnippetsBar() {
|
|
445
|
+
// Check if snippets bar should be shown
|
|
446
|
+
const shouldShow =
|
|
447
|
+
this.config.showSnippetsBar &&
|
|
448
|
+
(this.persistentConfig?.showSnippetsBar === undefined || this.persistentConfig.showSnippetsBar) &&
|
|
449
|
+
this.config.snippets.length > 0;
|
|
450
|
+
|
|
451
|
+
if (!shouldShow) {
|
|
452
|
+
// Remove existing bar if present
|
|
453
|
+
if (this.snippetsBar) {
|
|
454
|
+
this.snippetsBar.remove();
|
|
455
|
+
this.snippetsBar = undefined;
|
|
456
|
+
}
|
|
457
|
+
return;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
// Create snippets bar if it doesn't exist
|
|
461
|
+
if (!this.snippetsBar) {
|
|
462
|
+
this.snippetsBar = document.createElement("div");
|
|
463
|
+
addClass(this.snippetsBar, "yasqe_snippetsBar");
|
|
464
|
+
// Insert before the CodeMirror wrapper element
|
|
465
|
+
const cmWrapper = this.getWrapperElement();
|
|
466
|
+
cmWrapper.parentElement?.insertBefore(this.snippetsBar, cmWrapper);
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
// Clear existing content
|
|
470
|
+
this.snippetsBar.innerHTML = "";
|
|
471
|
+
|
|
472
|
+
const snippets = this.config.snippets;
|
|
473
|
+
|
|
474
|
+
// If 10 or fewer snippets, show all as buttons
|
|
475
|
+
if (snippets.length <= 10) {
|
|
476
|
+
snippets.forEach((snippet) => {
|
|
477
|
+
const btn = document.createElement("button");
|
|
478
|
+
addClass(btn, "yasqe_snippetButton");
|
|
479
|
+
btn.textContent = snippet.label;
|
|
480
|
+
btn.title = snippet.code;
|
|
481
|
+
btn.setAttribute("aria-label", `Insert ${snippet.label} snippet`);
|
|
482
|
+
btn.onclick = () => this.insertSnippet(snippet.code);
|
|
483
|
+
this.snippetsBar!.appendChild(btn);
|
|
484
|
+
});
|
|
485
|
+
} else {
|
|
486
|
+
// Group snippets by group property
|
|
487
|
+
const grouped: { [group: string]: Snippet[] } = {};
|
|
488
|
+
const ungrouped: Snippet[] = [];
|
|
489
|
+
|
|
490
|
+
snippets.forEach((snippet) => {
|
|
491
|
+
if (snippet.group) {
|
|
492
|
+
if (!grouped[snippet.group]) grouped[snippet.group] = [];
|
|
493
|
+
grouped[snippet.group].push(snippet);
|
|
494
|
+
} else {
|
|
495
|
+
ungrouped.push(snippet);
|
|
496
|
+
}
|
|
497
|
+
});
|
|
498
|
+
|
|
499
|
+
// Create dropdown for each group
|
|
500
|
+
Object.keys(grouped).forEach((groupName) => {
|
|
501
|
+
const dropdown = document.createElement("div");
|
|
502
|
+
addClass(dropdown, "yasqe_snippetDropdown");
|
|
503
|
+
|
|
504
|
+
const dropdownBtn = document.createElement("button");
|
|
505
|
+
addClass(dropdownBtn, "yasqe_snippetDropdownButton");
|
|
506
|
+
dropdownBtn.textContent = groupName + " ";
|
|
507
|
+
const chevron = drawSvgStringAsElement(imgs.chevronDown);
|
|
508
|
+
addClass(chevron, "chevronIcon");
|
|
509
|
+
dropdownBtn.appendChild(chevron);
|
|
510
|
+
dropdownBtn.setAttribute("aria-label", `${groupName} snippets`);
|
|
511
|
+
dropdownBtn.setAttribute("aria-expanded", "false");
|
|
512
|
+
|
|
513
|
+
const dropdownContent = document.createElement("div");
|
|
514
|
+
addClass(dropdownContent, "yasqe_snippetDropdownContent");
|
|
515
|
+
dropdownContent.setAttribute("role", "menu");
|
|
516
|
+
|
|
517
|
+
grouped[groupName].forEach((snippet) => {
|
|
518
|
+
const item = document.createElement("button");
|
|
519
|
+
addClass(item, "yasqe_snippetDropdownItem");
|
|
520
|
+
item.textContent = snippet.label;
|
|
521
|
+
item.title = snippet.code;
|
|
522
|
+
item.setAttribute("role", "menuitem");
|
|
523
|
+
item.onclick = () => {
|
|
524
|
+
this.insertSnippet(snippet.code);
|
|
525
|
+
dropdownContent.style.display = "none";
|
|
526
|
+
dropdownBtn.setAttribute("aria-expanded", "false");
|
|
527
|
+
};
|
|
528
|
+
dropdownContent.appendChild(item);
|
|
529
|
+
});
|
|
530
|
+
|
|
531
|
+
dropdownBtn.onclick = (e) => {
|
|
532
|
+
e.stopPropagation();
|
|
533
|
+
const isOpen = dropdownContent.style.display === "block";
|
|
534
|
+
// Close all other dropdowns
|
|
535
|
+
const allDropdowns = this.snippetsBar!.querySelectorAll(".yasqe_snippetDropdownContent");
|
|
536
|
+
allDropdowns.forEach((dd) => {
|
|
537
|
+
(dd as HTMLElement).style.display = "none";
|
|
538
|
+
});
|
|
539
|
+
const allButtons = this.snippetsBar!.querySelectorAll(".yasqe_snippetDropdownButton");
|
|
540
|
+
allButtons.forEach((btn) => btn.setAttribute("aria-expanded", "false"));
|
|
541
|
+
|
|
542
|
+
// Toggle this dropdown
|
|
543
|
+
if (!isOpen) {
|
|
544
|
+
dropdownContent.style.display = "block";
|
|
545
|
+
dropdownBtn.setAttribute("aria-expanded", "true");
|
|
546
|
+
}
|
|
547
|
+
};
|
|
548
|
+
|
|
549
|
+
dropdown.appendChild(dropdownBtn);
|
|
550
|
+
dropdown.appendChild(dropdownContent);
|
|
551
|
+
this.snippetsBar!.appendChild(dropdown);
|
|
552
|
+
});
|
|
553
|
+
|
|
554
|
+
// Add ungrouped snippets as individual buttons
|
|
555
|
+
ungrouped.forEach((snippet) => {
|
|
556
|
+
const btn = document.createElement("button");
|
|
557
|
+
addClass(btn, "yasqe_snippetButton");
|
|
558
|
+
btn.textContent = snippet.label;
|
|
559
|
+
btn.title = snippet.code;
|
|
560
|
+
btn.setAttribute("aria-label", `Insert ${snippet.label} snippet`);
|
|
561
|
+
btn.onclick = () => this.insertSnippet(snippet.code);
|
|
562
|
+
this.snippetsBar!.appendChild(btn);
|
|
563
|
+
});
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
// Set up click handler for closing dropdowns when clicking outside
|
|
567
|
+
// Remove any existing handler first
|
|
568
|
+
if (this.snippetsClickHandler) {
|
|
569
|
+
document.removeEventListener("click", this.snippetsClickHandler);
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
// Create and store the handler
|
|
573
|
+
this.snippetsClickHandler = (e: MouseEvent) => {
|
|
574
|
+
if (this.snippetsBar && !this.snippetsBar.contains(e.target as Node)) {
|
|
575
|
+
const allDropdowns = this.snippetsBar.querySelectorAll(".yasqe_snippetDropdownContent");
|
|
576
|
+
allDropdowns.forEach((dd) => {
|
|
577
|
+
(dd as HTMLElement).style.display = "none";
|
|
578
|
+
});
|
|
579
|
+
const allButtons = this.snippetsBar.querySelectorAll(".yasqe_snippetDropdownButton");
|
|
580
|
+
allButtons.forEach((btn) => btn.setAttribute("aria-expanded", "false"));
|
|
581
|
+
}
|
|
582
|
+
};
|
|
583
|
+
|
|
584
|
+
// Add the handler
|
|
585
|
+
document.addEventListener("click", this.snippetsClickHandler);
|
|
586
|
+
}
|
|
587
|
+
|
|
382
588
|
private drawResizer() {
|
|
383
589
|
if (this.resizeWrapper) return;
|
|
384
590
|
this.resizeWrapper = document.createElement("div");
|
|
@@ -643,6 +849,32 @@ export class Yasqe extends CodeMirror {
|
|
|
643
849
|
}
|
|
644
850
|
});
|
|
645
851
|
}
|
|
852
|
+
|
|
853
|
+
public formatQuery() {
|
|
854
|
+
try {
|
|
855
|
+
const currentQuery = this.getValue();
|
|
856
|
+
const formatted = spfmt.format(currentQuery);
|
|
857
|
+
this.setValue(formatted);
|
|
858
|
+
// Collapse prefixes after formatting
|
|
859
|
+
this.collapsePrefixes(true);
|
|
860
|
+
} catch (error) {
|
|
861
|
+
console.warn(
|
|
862
|
+
"Failed to format SPARQL query using sparql-formatter. This may be due to syntax errors in the query. Falling back to legacy formatter.",
|
|
863
|
+
error,
|
|
864
|
+
);
|
|
865
|
+
// If formatting fails, fall back to the built-in autoformat
|
|
866
|
+
this.autoformat();
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
public format() {
|
|
871
|
+
const formatterType = this.persistentConfig?.formatterType || "sparql-formatter";
|
|
872
|
+
if (formatterType === "legacy") {
|
|
873
|
+
this.autoformat();
|
|
874
|
+
} else {
|
|
875
|
+
this.formatQuery();
|
|
876
|
+
}
|
|
877
|
+
}
|
|
646
878
|
//values in the form of {?var: 'value'}, or [{?var: 'value'}]
|
|
647
879
|
public getQueryWithValues(values: string | { [varName: string]: string } | Array<{ [varName: string]: string }>) {
|
|
648
880
|
if (!values) return this.getValue();
|
|
@@ -709,6 +941,7 @@ export class Yasqe extends CodeMirror {
|
|
|
709
941
|
public setCheckSyntaxErrors(isEnabled: boolean) {
|
|
710
942
|
this.config.syntaxErrorCheck = isEnabled;
|
|
711
943
|
this.checkSyntax();
|
|
944
|
+
this.checkConstructVariables();
|
|
712
945
|
}
|
|
713
946
|
public checkSyntax() {
|
|
714
947
|
this.queryValid = true;
|
|
@@ -765,6 +998,77 @@ export class Yasqe extends CodeMirror {
|
|
|
765
998
|
}
|
|
766
999
|
}
|
|
767
1000
|
}
|
|
1001
|
+
|
|
1002
|
+
public setCheckConstructVariables(isEnabled: boolean) {
|
|
1003
|
+
this.config.checkConstructVariables = isEnabled;
|
|
1004
|
+
if (!isEnabled) {
|
|
1005
|
+
// Clear any existing warnings when disabled
|
|
1006
|
+
this.clearGutter("gutterConstructWarning");
|
|
1007
|
+
} else {
|
|
1008
|
+
this.checkConstructVariables();
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
public checkConstructVariables() {
|
|
1013
|
+
// Clear any existing warnings first
|
|
1014
|
+
this.clearGutter("gutterConstructWarning");
|
|
1015
|
+
|
|
1016
|
+
// Only check if enabled, query is valid, and it's a CONSTRUCT query
|
|
1017
|
+
if (!this.config.checkConstructVariables || !this.queryValid || this.getQueryType() !== "CONSTRUCT") {
|
|
1018
|
+
return;
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
// Get the final state after parsing the entire query
|
|
1022
|
+
const lastLine = this.getDoc().lastLine();
|
|
1023
|
+
const token: Token = this.getTokenAt({ line: lastLine, ch: this.getDoc().getLine(lastLine).length }, true);
|
|
1024
|
+
|
|
1025
|
+
const state = token.state as TokenizerState;
|
|
1026
|
+
|
|
1027
|
+
// Check for undefined variables in CONSTRUCT template
|
|
1028
|
+
const undefinedVars: string[] = [];
|
|
1029
|
+
for (const varName in state.constructVariables) {
|
|
1030
|
+
if (!state.whereVariables[varName]) {
|
|
1031
|
+
undefinedVars.push(varName);
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
if (undefinedVars.length === 0) {
|
|
1036
|
+
return;
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
// Find lines where undefined variables are used in CONSTRUCT template
|
|
1040
|
+
// Note: This iterates through all lines but filters by inConstructTemplate flag
|
|
1041
|
+
// For large queries, this could be optimized by tracking line ranges during tokenization
|
|
1042
|
+
for (let l = 0; l < this.getDoc().lineCount(); ++l) {
|
|
1043
|
+
const lineToken: Token = this.getTokenAt({ line: l, ch: this.getDoc().getLine(l).length }, true);
|
|
1044
|
+
const lineState = lineToken.state as TokenizerState;
|
|
1045
|
+
|
|
1046
|
+
// Only mark variables in the CONSTRUCT template
|
|
1047
|
+
if (lineState.queryType === "CONSTRUCT" && lineState.inConstructTemplate) {
|
|
1048
|
+
const line = this.getDoc().getLine(l);
|
|
1049
|
+
// Check if this line contains any undefined variable (use word boundary to avoid partial matches)
|
|
1050
|
+
for (const undefinedVar of undefinedVars) {
|
|
1051
|
+
// Escape special regex characters in variable name
|
|
1052
|
+
// Use negative lookbehind/lookahead to ensure we match the full variable name
|
|
1053
|
+
// Variables can be followed by whitespace, punctuation, or end of line
|
|
1054
|
+
const escapedVar = undefinedVar.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1055
|
+
const varRegex = new RegExp(`${escapedVar}(?![a-zA-Z0-9_])`);
|
|
1056
|
+
if (varRegex.test(line)) {
|
|
1057
|
+
const warningEl = drawSvgStringAsElement(imgs.warning);
|
|
1058
|
+
warningEl.className = "constructVariableWarning";
|
|
1059
|
+
tooltip(
|
|
1060
|
+
this,
|
|
1061
|
+
warningEl,
|
|
1062
|
+
escape(`Variable ${undefinedVar} is used in CONSTRUCT but not defined in WHERE clause`),
|
|
1063
|
+
);
|
|
1064
|
+
this.setGutterMarker(l, "gutterConstructWarning", warningEl);
|
|
1065
|
+
break; // Only one marker per line
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
1068
|
+
}
|
|
1069
|
+
}
|
|
1070
|
+
}
|
|
1071
|
+
|
|
768
1072
|
/**
|
|
769
1073
|
* Token management
|
|
770
1074
|
*/
|
|
@@ -818,6 +1122,31 @@ export class Yasqe extends CodeMirror {
|
|
|
818
1122
|
}
|
|
819
1123
|
}
|
|
820
1124
|
|
|
1125
|
+
/**
|
|
1126
|
+
* Snippets management
|
|
1127
|
+
*/
|
|
1128
|
+
public setSnippetsBarVisible(visible: boolean) {
|
|
1129
|
+
if (!this.persistentConfig) {
|
|
1130
|
+
this.persistentConfig = {
|
|
1131
|
+
query: this.getValue(),
|
|
1132
|
+
editorHeight: this.config.editorHeight,
|
|
1133
|
+
showSnippetsBar: visible,
|
|
1134
|
+
};
|
|
1135
|
+
} else {
|
|
1136
|
+
this.persistentConfig.showSnippetsBar = visible;
|
|
1137
|
+
}
|
|
1138
|
+
this.saveQuery();
|
|
1139
|
+
this.drawSnippetsBar();
|
|
1140
|
+
}
|
|
1141
|
+
|
|
1142
|
+
public getSnippetsBarVisible(): boolean {
|
|
1143
|
+
return (
|
|
1144
|
+
this.config.showSnippetsBar &&
|
|
1145
|
+
(this.persistentConfig?.showSnippetsBar === undefined || this.persistentConfig.showSnippetsBar) &&
|
|
1146
|
+
this.config.snippets.length > 0
|
|
1147
|
+
);
|
|
1148
|
+
}
|
|
1149
|
+
|
|
821
1150
|
/**
|
|
822
1151
|
* Autocompleter management
|
|
823
1152
|
*/
|
|
@@ -875,6 +1204,10 @@ export class Yasqe extends CodeMirror {
|
|
|
875
1204
|
*/
|
|
876
1205
|
public query(config?: Sparql.YasqeAjaxConfig) {
|
|
877
1206
|
if (this.config.queryingDisabled) return Promise.reject("Querying is disabled.");
|
|
1207
|
+
// Auto-format query before execution if enabled in persistent config
|
|
1208
|
+
if (this.persistentConfig?.autoformatOnQuery) {
|
|
1209
|
+
this.format();
|
|
1210
|
+
}
|
|
878
1211
|
// Abort previous request
|
|
879
1212
|
this.abortQuery();
|
|
880
1213
|
return Sparql.executeQuery(this, config);
|
|
@@ -929,6 +1262,10 @@ export class Yasqe extends CodeMirror {
|
|
|
929
1262
|
this.unregisterEventListeners();
|
|
930
1263
|
this.resizeWrapper?.removeEventListener("mousedown", this.initDrag, false);
|
|
931
1264
|
this.resizeWrapper?.removeEventListener("dblclick", this.expandEditor);
|
|
1265
|
+
if (this.snippetsClickHandler) {
|
|
1266
|
+
document.removeEventListener("click", this.snippetsClickHandler);
|
|
1267
|
+
this.snippetsClickHandler = undefined;
|
|
1268
|
+
}
|
|
932
1269
|
for (const autocompleter in this.autocompleters) {
|
|
933
1270
|
this.disableCompleter(autocompleter);
|
|
934
1271
|
}
|
|
@@ -1044,6 +1381,12 @@ export type PlainRequestConfig = {
|
|
|
1044
1381
|
export type PartialConfig = {
|
|
1045
1382
|
[P in keyof Config]?: Config[P] extends object ? Partial<Config[P]> : Config[P];
|
|
1046
1383
|
};
|
|
1384
|
+
|
|
1385
|
+
export interface Snippet {
|
|
1386
|
+
label: string;
|
|
1387
|
+
code: string;
|
|
1388
|
+
group?: string;
|
|
1389
|
+
}
|
|
1047
1390
|
export interface Config extends Partial<CodeMirror.EditorConfiguration> {
|
|
1048
1391
|
mode: string;
|
|
1049
1392
|
collapsePrefixesOnLoad: boolean;
|
|
@@ -1081,10 +1424,17 @@ export interface Config extends Partial<CodeMirror.EditorConfiguration> {
|
|
|
1081
1424
|
editorHeight: string;
|
|
1082
1425
|
queryingDisabled: string | undefined; // The string will be the message displayed when hovered
|
|
1083
1426
|
prefixCcApi: string; // the suggested default prefixes URL API getter
|
|
1427
|
+
showFormatButton: boolean; // Show a button to format the query
|
|
1428
|
+
checkConstructVariables: boolean; // Check for undefined variables in CONSTRUCT queries
|
|
1429
|
+
snippets: Snippet[]; // Code snippets to show in the snippets bar
|
|
1430
|
+
showSnippetsBar: boolean; // Show the snippets bar
|
|
1084
1431
|
}
|
|
1085
1432
|
export interface PersistentConfig {
|
|
1086
1433
|
query: string;
|
|
1087
1434
|
editorHeight: string;
|
|
1435
|
+
formatterType?: "sparql-formatter" | "legacy"; // Which formatter to use
|
|
1436
|
+
autoformatOnQuery?: boolean; // Auto-format query on execution
|
|
1437
|
+
showSnippetsBar?: boolean; // User preference for snippets bar visibility
|
|
1088
1438
|
}
|
|
1089
1439
|
// export var _Yasqe = _Yasqe;
|
|
1090
1440
|
|
package/src/prefixFold.ts
CHANGED
|
@@ -15,6 +15,7 @@ export function findFirstPrefixLine(yasqe: Yasqe) {
|
|
|
15
15
|
|
|
16
16
|
export function findFirstPrefix(yasqe: Yasqe, line: number, startFromCharIndex = 0, lineText?: string) {
|
|
17
17
|
if (!lineText) lineText = yasqe.getDoc().getLine(line);
|
|
18
|
+
if (!lineText) return undefined;
|
|
18
19
|
lineText = lineText.toUpperCase();
|
|
19
20
|
const charIndex = lineText.indexOf(PREFIX_KEYWORD, startFromCharIndex);
|
|
20
21
|
if (charIndex >= 0) {
|
package/src/prefixUtils.ts
CHANGED
|
@@ -30,7 +30,10 @@ export function removePrefixes(yasqe: Yasqe, prefixes: Prefixes) {
|
|
|
30
30
|
yasqe.setValue(
|
|
31
31
|
yasqe
|
|
32
32
|
.getValue()
|
|
33
|
-
.replace(
|
|
33
|
+
.replace(
|
|
34
|
+
new RegExp("PREFIX\\s*" + pref + ":\\s*" + escapeRegex("<" + prefixes[pref] + ">") + "\\s*", "ig"),
|
|
35
|
+
"",
|
|
36
|
+
),
|
|
34
37
|
);
|
|
35
38
|
}
|
|
36
39
|
yasqe.collapsePrefixes(false);
|
|
@@ -48,7 +51,7 @@ export function getPrefixesFromQuery(yasqe: Yasqe): Token["state"]["prefixes"] {
|
|
|
48
51
|
//as https://github.com/TriplyDB/YASGUI/issues/84)
|
|
49
52
|
return yasqe.getTokenAt(
|
|
50
53
|
{ line: yasqe.getDoc().lastLine(), ch: yasqe.getDoc().getLine(yasqe.getDoc().lastLine()).length },
|
|
51
|
-
true
|
|
54
|
+
true,
|
|
52
55
|
).state.prefixes;
|
|
53
56
|
}
|
|
54
57
|
|