@matdata/yasr 5.20.2 → 6.0.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 +76 -59
- package/build/ts/src/index.d.ts +9 -0
- package/build/ts/src/index.js +61 -1
- package/build/ts/src/index.js.map +1 -1
- package/build/ts/src/parsers/index.js +16 -2
- package/build/ts/src/parsers/index.js.map +1 -1
- package/build/ts/src/plugins/error/index.js +1 -1
- package/build/ts/src/plugins/response/index.d.ts +11 -9
- package/build/ts/src/plugins/response/index.js +212 -34
- package/build/ts/src/plugins/response/index.js.map +1 -1
- package/build/ts/src/plugins/response/uriUtils.d.ts +4 -0
- package/build/ts/src/plugins/response/uriUtils.js +65 -0
- package/build/ts/src/plugins/response/uriUtils.js.map +1 -0
- package/build/yasr.min.css +1 -1
- package/build/yasr.min.css.map +3 -3
- package/build/yasr.min.js +56 -44
- package/build/yasr.min.js.map +4 -4
- package/package.json +13 -6
- package/src/index.ts +74 -3
- package/src/main.scss +12 -0
- package/src/parsers/index.ts +15 -3
- package/src/plugins/error/index.ts +1 -1
- package/src/plugins/response/index.scss +78 -2
- package/src/plugins/response/index.ts +202 -38
- package/src/plugins/response/uriUtils.ts +117 -0
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@matdata/yasr",
|
|
3
|
-
"description": "
|
|
4
|
-
"version": "
|
|
3
|
+
"description": "MatGUI results viewer: a modern SPARQL result set viewer (formerly Yasgui/YASR).",
|
|
4
|
+
"version": "6.0.0",
|
|
5
5
|
"main": "build/yasr.min.js",
|
|
6
6
|
"types": "build/ts/src/index.d.ts",
|
|
7
7
|
"license": "MIT",
|
|
@@ -17,18 +17,26 @@
|
|
|
17
17
|
"Semantic Web",
|
|
18
18
|
"Linked Data"
|
|
19
19
|
],
|
|
20
|
-
"bugs": "https://github.com/Matdata-eu/
|
|
20
|
+
"bugs": "https://github.com/Matdata-eu/Matgui/issues/",
|
|
21
21
|
"repository": {
|
|
22
22
|
"type": "git",
|
|
23
|
-
"url": "https://github.com/Matdata-eu/
|
|
23
|
+
"url": "https://github.com/Matdata-eu/Matgui.git",
|
|
24
24
|
"directory": "packages/yasr"
|
|
25
25
|
},
|
|
26
26
|
"dependencies": {
|
|
27
|
+
"@codemirror/commands": "^6.11.0",
|
|
28
|
+
"@codemirror/lang-json": "^6.0.2",
|
|
29
|
+
"@codemirror/lang-xml": "^6.1.0",
|
|
30
|
+
"@codemirror/language": "^6.12.4",
|
|
31
|
+
"@codemirror/legacy-modes": "^6.5.3",
|
|
32
|
+
"@codemirror/state": "^6.7.2",
|
|
33
|
+
"@codemirror/view": "^6.43.10",
|
|
27
34
|
"@fortawesome/free-solid-svg-icons": "^7.1.0",
|
|
28
35
|
"@json2csv/plainjs": "^7.0.4",
|
|
36
|
+
"@lezer/highlight": "^1.2.3",
|
|
29
37
|
"@matdata/yasgui-utils": "^4.6.1",
|
|
30
38
|
"@matdata/yasqe": "^4.6.1",
|
|
31
|
-
"codemirror": "^
|
|
39
|
+
"codemirror-lang-turtle": "^0.0.2",
|
|
32
40
|
"colors": "^1.4.0",
|
|
33
41
|
"column-resizer": "^1.4.0",
|
|
34
42
|
"datatables.net": "^2.0.5",
|
|
@@ -40,7 +48,6 @@
|
|
|
40
48
|
"papaparse": "^5.3.1"
|
|
41
49
|
},
|
|
42
50
|
"devDependencies": {
|
|
43
|
-
"@types/codemirror": "0.0.100",
|
|
44
51
|
"@types/jquery": "^3.5.32",
|
|
45
52
|
"@types/lodash-es": "^4.17.3",
|
|
46
53
|
"@types/n3": "^1.1.5",
|
package/src/index.ts
CHANGED
|
@@ -43,6 +43,13 @@ export class Yasr extends EventEmitter {
|
|
|
43
43
|
private loadingEl: HTMLDivElement | undefined;
|
|
44
44
|
private isLoading: boolean = false;
|
|
45
45
|
|
|
46
|
+
// Result history
|
|
47
|
+
private static readonly MAX_HISTORY_SIZE = 20;
|
|
48
|
+
private resultHistory: { data: any; duration?: number }[] = [];
|
|
49
|
+
private historyIndex: number = -1;
|
|
50
|
+
private historyBackBtn: HTMLButtonElement | undefined;
|
|
51
|
+
private historyForwardBtn: HTMLButtonElement | undefined;
|
|
52
|
+
|
|
46
53
|
// Utils
|
|
47
54
|
public utils = { addScript: addScript, addCSS: addCss, sanitize: sanitize };
|
|
48
55
|
|
|
@@ -350,6 +357,7 @@ export class Yasr extends EventEmitter {
|
|
|
350
357
|
this.drawPluginSelectors();
|
|
351
358
|
this.drawResponseInfo();
|
|
352
359
|
this.drawPluginElement();
|
|
360
|
+
this.drawHistoryButtons();
|
|
353
361
|
this.drawDownloadIcon();
|
|
354
362
|
this.drawFullscreenButton();
|
|
355
363
|
if (this.loadingEl) {
|
|
@@ -357,6 +365,49 @@ export class Yasr extends EventEmitter {
|
|
|
357
365
|
}
|
|
358
366
|
this.drawDocumentationButton();
|
|
359
367
|
}
|
|
368
|
+
|
|
369
|
+
private drawHistoryButtons() {
|
|
370
|
+
// Guard: only create buttons once
|
|
371
|
+
if (this.historyBackBtn && this.historyForwardBtn) return;
|
|
372
|
+
|
|
373
|
+
const createBtn = (ariaLabel: string, iconClass: string, title: string): HTMLButtonElement => {
|
|
374
|
+
const btn = document.createElement("button");
|
|
375
|
+
addClass(btn, "yasr_btn", "yasr_btn_history", "btn_icon");
|
|
376
|
+
btn.setAttribute("aria-label", ariaLabel);
|
|
377
|
+
btn.title = title;
|
|
378
|
+
btn.disabled = true;
|
|
379
|
+
const icon = document.createElement("i");
|
|
380
|
+
addClass(icon, "fas", iconClass);
|
|
381
|
+
icon.setAttribute("aria-hidden", "true");
|
|
382
|
+
btn.appendChild(icon);
|
|
383
|
+
return btn;
|
|
384
|
+
};
|
|
385
|
+
|
|
386
|
+
this.historyBackBtn = createBtn("Previous result", "fa-chevron-left", "Previous result");
|
|
387
|
+
this.historyBackBtn.addEventListener("click", () => this.navigateHistory(-1));
|
|
388
|
+
|
|
389
|
+
this.historyForwardBtn = createBtn("Next result", "fa-chevron-right", "Next result");
|
|
390
|
+
this.historyForwardBtn.addEventListener("click", () => this.navigateHistory(1));
|
|
391
|
+
|
|
392
|
+
this.headerEl.appendChild(this.historyBackBtn);
|
|
393
|
+
this.headerEl.appendChild(this.historyForwardBtn);
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
private updateHistoryButtons() {
|
|
397
|
+
if (!this.historyBackBtn || !this.historyForwardBtn) return;
|
|
398
|
+
this.historyBackBtn.disabled = this.historyIndex <= 0;
|
|
399
|
+
this.historyForwardBtn.disabled = this.historyIndex >= this.resultHistory.length - 1;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
private navigateHistory(direction: -1 | 1) {
|
|
403
|
+
const newIndex = this.historyIndex + direction;
|
|
404
|
+
if (newIndex < 0 || newIndex >= this.resultHistory.length) return;
|
|
405
|
+
this.historyIndex = newIndex;
|
|
406
|
+
const entry = this.resultHistory[this.historyIndex];
|
|
407
|
+
this.results = new Parser(entry.data, entry.duration);
|
|
408
|
+
this.draw();
|
|
409
|
+
this.updateHistoryButtons();
|
|
410
|
+
}
|
|
360
411
|
private downloadBtn: HTMLAnchorElement | undefined;
|
|
361
412
|
private drawDownloadIcon() {
|
|
362
413
|
this.downloadBtn = document.createElement("a");
|
|
@@ -519,7 +570,7 @@ export class Yasr extends EventEmitter {
|
|
|
519
570
|
const icon = document.createElement("i");
|
|
520
571
|
addClass(icon, "fas", "fa-circle-question");
|
|
521
572
|
this.documentationLink.appendChild(icon);
|
|
522
|
-
this.documentationLink.href = "https://
|
|
573
|
+
this.documentationLink.href = "https://matgui-doc.matdata.eu/docs/user-guide";
|
|
523
574
|
this.documentationLink.target = "_blank";
|
|
524
575
|
this.documentationLink.rel = "noopener noreferrer";
|
|
525
576
|
this.headerEl.appendChild(this.documentationLink); // We can do this as long as the help-element is the last item in the row
|
|
@@ -551,6 +602,11 @@ export class Yasr extends EventEmitter {
|
|
|
551
602
|
}
|
|
552
603
|
|
|
553
604
|
public getResponseFromStorage() {
|
|
605
|
+
if (this.config.maxPersistentResponseSize <= 0) {
|
|
606
|
+
const storageId = this.getStorageId(this.config.persistenceLabelResponse);
|
|
607
|
+
if (storageId) this.storage.remove(storageId);
|
|
608
|
+
return;
|
|
609
|
+
}
|
|
554
610
|
const storageId = this.getStorageId(this.config.persistenceLabelResponse);
|
|
555
611
|
if (storageId) {
|
|
556
612
|
return this.storage.get(storageId);
|
|
@@ -581,6 +637,10 @@ export class Yasr extends EventEmitter {
|
|
|
581
637
|
}
|
|
582
638
|
private storeResponse() {
|
|
583
639
|
const storageId = this.getStorageId(this.config.persistenceLabelResponse);
|
|
640
|
+
if (storageId && this.config.maxPersistentResponseSize <= 0) {
|
|
641
|
+
this.storage.remove(storageId);
|
|
642
|
+
return;
|
|
643
|
+
}
|
|
584
644
|
if (storageId && this.results) {
|
|
585
645
|
const storeObj = this.results.getAsStoreObject(this.config.maxPersistentResponseSize);
|
|
586
646
|
if (storeObj && !storeObj.error) {
|
|
@@ -596,7 +656,17 @@ export class Yasr extends EventEmitter {
|
|
|
596
656
|
this.hideLoading();
|
|
597
657
|
this.results = new Parser(data, duration);
|
|
598
658
|
|
|
659
|
+
// Truncate any forward history and push new entry
|
|
660
|
+
this.resultHistory = this.resultHistory.slice(0, this.historyIndex + 1);
|
|
661
|
+
this.resultHistory.push({ data, duration });
|
|
662
|
+
// Cap history to avoid unbounded memory growth
|
|
663
|
+
if (this.resultHistory.length > Yasr.MAX_HISTORY_SIZE) {
|
|
664
|
+
this.resultHistory = this.resultHistory.slice(this.resultHistory.length - Yasr.MAX_HISTORY_SIZE);
|
|
665
|
+
}
|
|
666
|
+
this.historyIndex = this.resultHistory.length - 1;
|
|
667
|
+
|
|
599
668
|
this.draw();
|
|
669
|
+
this.updateHistoryButtons();
|
|
600
670
|
|
|
601
671
|
this.storeResponse();
|
|
602
672
|
}
|
|
@@ -605,7 +675,7 @@ export class Yasr extends EventEmitter {
|
|
|
605
675
|
* Execute a background SPARQL query from within a plugin.
|
|
606
676
|
*
|
|
607
677
|
* This delegates to the `executeQuery` callback supplied in the Yasr
|
|
608
|
-
* configuration (typically wired up by the
|
|
678
|
+
* configuration (typically wired up by the Matgui `Tab`). The raw
|
|
609
679
|
* response is returned so the calling plugin can process it without
|
|
610
680
|
* replacing the current result display.
|
|
611
681
|
*
|
|
@@ -693,7 +763,7 @@ export interface Config {
|
|
|
693
763
|
|
|
694
764
|
/**
|
|
695
765
|
* Optional callback that allows plugins to execute a background SPARQL query.
|
|
696
|
-
* When set (e.g. by the
|
|
766
|
+
* When set (e.g. by the Matgui Tab), plugins can call `yasr.executeQuery(queryString)`
|
|
697
767
|
* to run an arbitrary query and receive the raw response, independently of the
|
|
698
768
|
* main query editor.
|
|
699
769
|
*
|
|
@@ -722,5 +792,6 @@ Yasr.registerPlugin("response", YasrPluginResponse.default as any);
|
|
|
722
792
|
Yasr.registerPlugin("error", YasrPluginError.default as any);
|
|
723
793
|
|
|
724
794
|
export type { Plugin, DownloadInfo } from "./plugins";
|
|
795
|
+
export { buildSubjectOfQuery, buildObjectOfQuery } from "./plugins/response/uriUtils";
|
|
725
796
|
|
|
726
797
|
export default Yasr;
|
package/src/main.scss
CHANGED
|
@@ -11,6 +11,8 @@
|
|
|
11
11
|
|
|
12
12
|
.yasr_results {
|
|
13
13
|
flex: 1;
|
|
14
|
+
min-height: 0;
|
|
15
|
+
overflow: auto;
|
|
14
16
|
}
|
|
15
17
|
|
|
16
18
|
.yasr_btn {
|
|
@@ -164,6 +166,16 @@
|
|
|
164
166
|
}
|
|
165
167
|
}
|
|
166
168
|
|
|
169
|
+
.yasr_btn_history {
|
|
170
|
+
border: none;
|
|
171
|
+
background: none;
|
|
172
|
+
color: var(--yasgui-button-text, #505050);
|
|
173
|
+
|
|
174
|
+
&:not([disabled]):hover {
|
|
175
|
+
color: var(--yasgui-button-hover, #337ab7);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
167
179
|
.yasr_fullscreenButton {
|
|
168
180
|
border: none;
|
|
169
181
|
background: none;
|
package/src/parsers/index.ts
CHANGED
|
@@ -292,9 +292,21 @@ class Parser {
|
|
|
292
292
|
};
|
|
293
293
|
}
|
|
294
294
|
if (summary) {
|
|
295
|
-
//
|
|
296
|
-
if (summary.data
|
|
297
|
-
|
|
295
|
+
// If data is set, it should be less than max limit. For non-strings, compare serialized size.
|
|
296
|
+
if (summary.data !== undefined) {
|
|
297
|
+
let dataSize = 0;
|
|
298
|
+
if (typeof summary.data === "string") {
|
|
299
|
+
dataSize = summary.data.length;
|
|
300
|
+
} else {
|
|
301
|
+
try {
|
|
302
|
+
dataSize = JSON.stringify(summary.data).length;
|
|
303
|
+
} catch (_e) {
|
|
304
|
+
return undefined;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
if (dataSize > maxResponseSize) {
|
|
308
|
+
return undefined;
|
|
309
|
+
}
|
|
298
310
|
}
|
|
299
311
|
return summary;
|
|
300
312
|
}
|
|
@@ -45,7 +45,7 @@ export default class Error implements Plugin<never> {
|
|
|
45
45
|
|
|
46
46
|
const cors = document.createElement("li");
|
|
47
47
|
const firstPart = document.createElement("span");
|
|
48
|
-
firstPart.textContent = "Endpoint is not accessible from the
|
|
48
|
+
firstPart.textContent = "Endpoint is not accessible from the Matgui server and website, and the endpoint is not ";
|
|
49
49
|
cors.appendChild(firstPart);
|
|
50
50
|
const secondPart = document.createElement("a");
|
|
51
51
|
secondPart.textContent = "CORS-enabled";
|
|
@@ -1,10 +1,31 @@
|
|
|
1
1
|
.yasr {
|
|
2
2
|
.yasr_results {
|
|
3
3
|
position: relative;
|
|
4
|
-
.
|
|
5
|
-
border: 1px solid #d1d1d1;
|
|
4
|
+
.cm-editor {
|
|
5
|
+
border: 1px solid var(--yasgui-border-color, #d1d1d1);
|
|
6
6
|
margin-top: 5px;
|
|
7
7
|
height: 100%;
|
|
8
|
+
position: relative;
|
|
9
|
+
background: var(--yasgui-bg-primary, #fff);
|
|
10
|
+
color: var(--yasgui-text-primary, #000);
|
|
11
|
+
font-size: 13px;
|
|
12
|
+
&.cm-focused {
|
|
13
|
+
outline: none;
|
|
14
|
+
}
|
|
15
|
+
.cm-scroller {
|
|
16
|
+
font-family: monospace;
|
|
17
|
+
line-height: 1.4;
|
|
18
|
+
}
|
|
19
|
+
.cm-gutters {
|
|
20
|
+
background: var(--yasgui-bg-secondary, #f7f7f7);
|
|
21
|
+
border-right: 1px solid var(--yasgui-border-color, #ddd);
|
|
22
|
+
color: var(--yasgui-text-secondary, #999);
|
|
23
|
+
}
|
|
24
|
+
.cm-foldPlaceholder {
|
|
25
|
+
background: var(--yasgui-bg-secondary, #eee);
|
|
26
|
+
border: 1px solid var(--yasgui-border-color, #ddd);
|
|
27
|
+
color: var(--yasgui-text-secondary, #888);
|
|
28
|
+
}
|
|
8
29
|
&.overflow::before {
|
|
9
30
|
background: linear-gradient(transparent 0%, #ffffffaa 75%, white 100%);
|
|
10
31
|
content: "";
|
|
@@ -14,6 +35,61 @@
|
|
|
14
35
|
position: absolute;
|
|
15
36
|
pointer-events: none;
|
|
16
37
|
}
|
|
38
|
+
// Token colors (classHighlighter emits `tok-*` classes). Light theme:
|
|
39
|
+
.tok-string,
|
|
40
|
+
.tok-string2 {
|
|
41
|
+
color: #a11;
|
|
42
|
+
}
|
|
43
|
+
.tok-number,
|
|
44
|
+
.tok-atom,
|
|
45
|
+
.tok-bool {
|
|
46
|
+
color: #164;
|
|
47
|
+
}
|
|
48
|
+
.tok-propertyName,
|
|
49
|
+
.tok-attributeName {
|
|
50
|
+
color: #00c;
|
|
51
|
+
}
|
|
52
|
+
.tok-tagName,
|
|
53
|
+
.tok-keyword {
|
|
54
|
+
color: #170;
|
|
55
|
+
}
|
|
56
|
+
.tok-comment {
|
|
57
|
+
color: #a50;
|
|
58
|
+
}
|
|
59
|
+
.tok-punctuation,
|
|
60
|
+
.tok-angleBracket {
|
|
61
|
+
color: #555;
|
|
62
|
+
}
|
|
63
|
+
// Dark theme
|
|
64
|
+
&.cm-s-yasgui-dark {
|
|
65
|
+
&.overflow::before {
|
|
66
|
+
background: linear-gradient(transparent 0%, #1e1e1eaa 75%, var(--yasgui-bg-primary, #1e1e1e) 100%);
|
|
67
|
+
}
|
|
68
|
+
.tok-string,
|
|
69
|
+
.tok-string2 {
|
|
70
|
+
color: #ce9178;
|
|
71
|
+
}
|
|
72
|
+
.tok-number,
|
|
73
|
+
.tok-atom,
|
|
74
|
+
.tok-bool {
|
|
75
|
+
color: #b5cea8;
|
|
76
|
+
}
|
|
77
|
+
.tok-propertyName,
|
|
78
|
+
.tok-attributeName {
|
|
79
|
+
color: #9cdcfe;
|
|
80
|
+
}
|
|
81
|
+
.tok-tagName,
|
|
82
|
+
.tok-keyword {
|
|
83
|
+
color: #569cd6;
|
|
84
|
+
}
|
|
85
|
+
.tok-comment {
|
|
86
|
+
color: #6a9955;
|
|
87
|
+
}
|
|
88
|
+
.tok-punctuation,
|
|
89
|
+
.tok-angleBracket {
|
|
90
|
+
color: #d4d4d4;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
17
93
|
}
|
|
18
94
|
.overlay {
|
|
19
95
|
width: 100%;
|
|
@@ -4,20 +4,22 @@
|
|
|
4
4
|
import { Plugin } from "../";
|
|
5
5
|
import Yasr from "../../";
|
|
6
6
|
import "./index.scss";
|
|
7
|
-
import
|
|
8
|
-
import "codemirror/
|
|
9
|
-
import "codemirror/
|
|
10
|
-
import "
|
|
11
|
-
import "codemirror/
|
|
12
|
-
|
|
13
|
-
import "codemirror
|
|
14
|
-
import "codemirror/mode/
|
|
15
|
-
|
|
16
|
-
import "codemirror/mode/javascript/javascript.js";
|
|
17
|
-
import "codemirror/lib/codemirror.css";
|
|
18
|
-
import "codemirror/theme/material-palenight.css";
|
|
7
|
+
import { EditorState, Extension } from "@codemirror/state";
|
|
8
|
+
import { EditorView, lineNumbers, highlightSpecialChars } from "@codemirror/view";
|
|
9
|
+
import { foldGutter, bracketMatching, syntaxHighlighting, StreamLanguage } from "@codemirror/language";
|
|
10
|
+
import { classHighlighter } from "@lezer/highlight";
|
|
11
|
+
import { json } from "@codemirror/lang-json";
|
|
12
|
+
import { xml } from "@codemirror/lang-xml";
|
|
13
|
+
import { turtle } from "codemirror-lang-turtle";
|
|
14
|
+
import { javascript } from "@codemirror/legacy-modes/mode/javascript";
|
|
19
15
|
import { addClass, removeClass } from "@matdata/yasgui-utils";
|
|
20
16
|
import { DeepReadonly } from "ts-essentials";
|
|
17
|
+
import {
|
|
18
|
+
extractUriAtOffset,
|
|
19
|
+
buildSubjectOfQuery,
|
|
20
|
+
buildObjectOfQuery,
|
|
21
|
+
stripDuplicatePrefixDeclarations,
|
|
22
|
+
} from "./uriUtils";
|
|
21
23
|
|
|
22
24
|
export interface PluginConfig {
|
|
23
25
|
maxLines: number;
|
|
@@ -26,10 +28,12 @@ export default class Response implements Plugin<PluginConfig> {
|
|
|
26
28
|
private yasr: Yasr;
|
|
27
29
|
label = "Response";
|
|
28
30
|
priority = 2;
|
|
29
|
-
helpReference = "https://
|
|
31
|
+
helpReference = "https://matgui-doc.matdata.eu/docs/user-guide#response-plugin";
|
|
30
32
|
private config: DeepReadonly<PluginConfig>;
|
|
31
33
|
private overLay: HTMLDivElement | undefined;
|
|
32
|
-
private cm:
|
|
34
|
+
private cm: EditorView | undefined;
|
|
35
|
+
/** Turtle appended to the response view through Ctrl+Click URI exploration actions. */
|
|
36
|
+
private appendedContent = "";
|
|
33
37
|
constructor(yasr: Yasr) {
|
|
34
38
|
this.yasr = yasr;
|
|
35
39
|
this.config = Response.defaults;
|
|
@@ -81,27 +85,185 @@ export default class Response implements Plugin<PluginConfig> {
|
|
|
81
85
|
|
|
82
86
|
// Detect current theme from document
|
|
83
87
|
const isDarkTheme = document.documentElement.getAttribute("data-theme") === "dark";
|
|
84
|
-
const cmTheme = isDarkTheme ? "material-palenight" : "default";
|
|
85
|
-
|
|
86
|
-
const codemirrorOpts: Partial<CodeMirror.EditorConfiguration> = {
|
|
87
|
-
readOnly: true,
|
|
88
|
-
lineNumbers: true,
|
|
89
|
-
lineWrapping: true,
|
|
90
|
-
foldGutter: true,
|
|
91
|
-
gutters: ["CodeMirror-linenumbers", "CodeMirror-foldgutter"],
|
|
92
|
-
value: value,
|
|
93
|
-
extraKeys: { Tab: false },
|
|
94
|
-
theme: cmTheme,
|
|
95
|
-
};
|
|
96
|
-
const mode = this.yasr.results?.getType();
|
|
97
|
-
if (mode === "json") {
|
|
98
|
-
codemirrorOpts.mode = { name: "javascript", json: true };
|
|
99
|
-
}
|
|
100
88
|
|
|
101
|
-
|
|
102
|
-
|
|
89
|
+
const extensions: Extension[] = [
|
|
90
|
+
EditorState.readOnly.of(true),
|
|
91
|
+
EditorView.editable.of(false),
|
|
92
|
+
lineNumbers(),
|
|
93
|
+
foldGutter(),
|
|
94
|
+
highlightSpecialChars(),
|
|
95
|
+
bracketMatching(),
|
|
96
|
+
EditorView.lineWrapping,
|
|
97
|
+
// Emit stable `tok-*` classes so the theme SCSS controls colors (light & dark)
|
|
98
|
+
syntaxHighlighting(classHighlighter),
|
|
99
|
+
EditorView.editorAttributes.of({ class: `CodeMirror cm-s-${isDarkTheme ? "yasgui-dark" : "default"}` }),
|
|
100
|
+
];
|
|
101
|
+
const language = this.getLanguageExtension();
|
|
102
|
+
if (language) extensions.push(language);
|
|
103
|
+
|
|
104
|
+
this.destroyEditor();
|
|
105
|
+
// A fresh response replaces any DESCRIBE results appended to the previous one.
|
|
106
|
+
this.appendedContent = "";
|
|
107
|
+
this.cm = new EditorView({
|
|
108
|
+
state: EditorState.create({ doc: value, extensions }),
|
|
109
|
+
parent: this.yasr.resultsEl,
|
|
110
|
+
});
|
|
111
|
+
this.cm.dom.addEventListener("mousedown", this.handleMouseDown);
|
|
112
|
+
// Don't show less originally we've already set the value in the editor state
|
|
103
113
|
if (lines.length > config.maxLines) this.showLess(false);
|
|
104
114
|
}
|
|
115
|
+
private getLanguageExtension(): Extension | undefined {
|
|
116
|
+
const type = this.yasr.results?.getType();
|
|
117
|
+
if (type === "json") return json();
|
|
118
|
+
if (type === "xml") return xml();
|
|
119
|
+
if (type === "ttl") return turtle();
|
|
120
|
+
if (type === "n-triples") return turtle();
|
|
121
|
+
const contentType = this.yasr.results?.getContentType() || "";
|
|
122
|
+
if (contentType.indexOf("json") >= 0) return json();
|
|
123
|
+
if (contentType.indexOf("xml") >= 0 || contentType.indexOf("html") >= 0) return xml();
|
|
124
|
+
if (contentType.indexOf("javascript") >= 0) return StreamLanguage.define(javascript);
|
|
125
|
+
return undefined;
|
|
126
|
+
}
|
|
127
|
+
private destroyEditor() {
|
|
128
|
+
if (this.cm) {
|
|
129
|
+
this.cm.dom.removeEventListener("mousedown", this.handleMouseDown);
|
|
130
|
+
this.cm.destroy();
|
|
131
|
+
this.cm = undefined;
|
|
132
|
+
}
|
|
133
|
+
this.overLay?.remove();
|
|
134
|
+
this.overLay = undefined;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Ctrl/Cmd+Click on a URI runs a CONSTRUCT query for triples where the URI is the subject.
|
|
139
|
+
* Ctrl/Cmd+Shift+Click runs a CONSTRUCT query for triples where the URI is the object.
|
|
140
|
+
*/
|
|
141
|
+
private handleMouseDown = (event: MouseEvent) => {
|
|
142
|
+
if (!event.ctrlKey && !event.metaKey) return;
|
|
143
|
+
if (event.button !== 0) return;
|
|
144
|
+
if (!this.cm) return;
|
|
145
|
+
// Only act when the host application wired up a query executor.
|
|
146
|
+
if (!this.yasr.config.executeQuery) return;
|
|
147
|
+
|
|
148
|
+
const pos = this.cm.posAtCoords({ x: event.clientX, y: event.clientY });
|
|
149
|
+
if (pos === null) return;
|
|
150
|
+
|
|
151
|
+
const uri = extractUriAtOffset(this.cm.state.doc.toString(), pos);
|
|
152
|
+
if (!uri) return;
|
|
153
|
+
|
|
154
|
+
event.preventDefault();
|
|
155
|
+
event.stopPropagation();
|
|
156
|
+
|
|
157
|
+
if (event.shiftKey) {
|
|
158
|
+
void this.runObjectOfQuery(uri);
|
|
159
|
+
} else {
|
|
160
|
+
void this.runSubjectOfQuery(uri);
|
|
161
|
+
}
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
private async runSubjectOfQuery(uri: string) {
|
|
165
|
+
if (!this.yasr.config.executeQuery) return;
|
|
166
|
+
const cmAtStart = this.cm;
|
|
167
|
+
if (!cmAtStart) return;
|
|
168
|
+
|
|
169
|
+
const query = buildSubjectOfQuery(uri);
|
|
170
|
+
try {
|
|
171
|
+
this.yasr.showLoading();
|
|
172
|
+
const response = await this.yasr.executeQuery(query, { acceptHeader: this.getConstructAcceptHeader() });
|
|
173
|
+
if (this.cm !== cmAtStart) return;
|
|
174
|
+
|
|
175
|
+
const content = this.getResponseContent(response);
|
|
176
|
+
if (content) {
|
|
177
|
+
this.appendToView(`\n\n# Triples where <${uri}> is subject\n${content.trim()}\n`);
|
|
178
|
+
} else {
|
|
179
|
+
this.appendToView(`\n\n# Triples where <${uri}> is subject: no data\n`);
|
|
180
|
+
}
|
|
181
|
+
} catch (error) {
|
|
182
|
+
if (this.cm !== cmAtStart) return;
|
|
183
|
+
|
|
184
|
+
console.error("Subject-of query failed:", error);
|
|
185
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
186
|
+
this.appendToView(`\n\n# Triples where <${uri}> is subject failed: ${message}\n`);
|
|
187
|
+
} finally {
|
|
188
|
+
if (this.cm === cmAtStart) this.yasr.hideLoading();
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
private async runObjectOfQuery(uri: string) {
|
|
193
|
+
if (!this.yasr.config.executeQuery) return;
|
|
194
|
+
const cmAtStart = this.cm;
|
|
195
|
+
if (!cmAtStart) return;
|
|
196
|
+
|
|
197
|
+
const query = buildObjectOfQuery(uri);
|
|
198
|
+
try {
|
|
199
|
+
this.yasr.showLoading();
|
|
200
|
+
const response = await this.yasr.executeQuery(query, { acceptHeader: this.getConstructAcceptHeader() });
|
|
201
|
+
if (this.cm !== cmAtStart) return;
|
|
202
|
+
|
|
203
|
+
const content = this.getResponseContent(response);
|
|
204
|
+
if (content) {
|
|
205
|
+
this.appendToView(`\n\n# Triples where <${uri}> is object\n${content.trim()}\n`);
|
|
206
|
+
} else {
|
|
207
|
+
this.appendToView(`\n\n# Triples where <${uri}> is object: no data\n`);
|
|
208
|
+
}
|
|
209
|
+
} catch (error) {
|
|
210
|
+
if (this.cm !== cmAtStart) return;
|
|
211
|
+
|
|
212
|
+
console.error("Object-of query failed:", error);
|
|
213
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
214
|
+
this.appendToView(`\n\n# Triples where <${uri}> is object failed: ${message}\n`);
|
|
215
|
+
} finally {
|
|
216
|
+
if (this.cm === cmAtStart) this.yasr.hideLoading();
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Pick an Accept header for follow-up CONSTRUCT requests. When the current response
|
|
222
|
+
* is an RDF graph format, reuse it so the appended triples match what is shown.
|
|
223
|
+
*/
|
|
224
|
+
private getConstructAcceptHeader(): string {
|
|
225
|
+
const contentType = this.yasr.results?.getContentType();
|
|
226
|
+
if (contentType) {
|
|
227
|
+
const lower = contentType.toLowerCase();
|
|
228
|
+
if (
|
|
229
|
+
lower.includes("turtle") ||
|
|
230
|
+
lower.includes("trig") ||
|
|
231
|
+
lower.includes("triple") ||
|
|
232
|
+
lower.includes("quad") ||
|
|
233
|
+
lower.includes("rdf")
|
|
234
|
+
) {
|
|
235
|
+
return contentType.split(";")[0].trim();
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
return "text/turtle";
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
private getResponseContent(response: unknown): string {
|
|
242
|
+
if (typeof response === "string") return response;
|
|
243
|
+
if (response && typeof response === "object") {
|
|
244
|
+
const record = response as Record<string, unknown>;
|
|
245
|
+
if (typeof record.content === "string") return record.content;
|
|
246
|
+
if (typeof record.data === "string") return record.data;
|
|
247
|
+
}
|
|
248
|
+
return "";
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Append text (query result or status message) to the response view and make sure
|
|
253
|
+
* the full content is revealed.
|
|
254
|
+
*/
|
|
255
|
+
private appendToView(text: string) {
|
|
256
|
+
if (!this.cm || !text) return;
|
|
257
|
+
const existingVisibleContent = (this.yasr.results?.getOriginalResponseAsString() || "") + this.appendedContent;
|
|
258
|
+
this.appendedContent += stripDuplicatePrefixDeclarations(text, existingVisibleContent);
|
|
259
|
+
// Reveal the full response together with the appended DESCRIBE results.
|
|
260
|
+
this.showMore();
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
private setValue(value: string) {
|
|
264
|
+
if (!this.cm) return;
|
|
265
|
+
this.cm.dispatch({ changes: { from: 0, to: this.cm.state.doc.length, insert: value } });
|
|
266
|
+
}
|
|
105
267
|
private limitData(value: string) {
|
|
106
268
|
const lines = value.split("\n");
|
|
107
269
|
if (lines.length > this.config.maxLines) {
|
|
@@ -116,7 +278,7 @@ export default class Response implements Plugin<PluginConfig> {
|
|
|
116
278
|
showLess(setValue = true) {
|
|
117
279
|
if (!this.cm) return;
|
|
118
280
|
// Add overflow
|
|
119
|
-
addClass(this.cm.
|
|
281
|
+
addClass(this.cm.dom, "overflow");
|
|
120
282
|
|
|
121
283
|
// Remove old instance
|
|
122
284
|
if (this.overLay) {
|
|
@@ -157,9 +319,9 @@ export default class Response implements Plugin<PluginConfig> {
|
|
|
157
319
|
|
|
158
320
|
overlayContent.appendChild(downloadButton);
|
|
159
321
|
this.overLay.appendChild(overlayContent);
|
|
160
|
-
this.cm.
|
|
322
|
+
this.cm.dom.appendChild(this.overLay);
|
|
161
323
|
if (setValue) {
|
|
162
|
-
this.
|
|
324
|
+
this.setValue(this.limitData(this.yasr.results?.getOriginalResponseAsString() || ""));
|
|
163
325
|
}
|
|
164
326
|
}
|
|
165
327
|
/**
|
|
@@ -167,11 +329,13 @@ export default class Response implements Plugin<PluginConfig> {
|
|
|
167
329
|
*/
|
|
168
330
|
showMore() {
|
|
169
331
|
if (!this.cm) return;
|
|
170
|
-
removeClass(this.cm.
|
|
332
|
+
removeClass(this.cm.dom, "overflow");
|
|
171
333
|
this.overLay?.remove();
|
|
172
334
|
this.overLay = undefined;
|
|
173
|
-
this.
|
|
174
|
-
|
|
335
|
+
this.setValue((this.yasr.results?.getOriginalResponseAsString() || "") + this.appendedContent);
|
|
336
|
+
}
|
|
337
|
+
destroy() {
|
|
338
|
+
this.destroyEditor();
|
|
175
339
|
}
|
|
176
340
|
public static defaults: PluginConfig = {
|
|
177
341
|
maxLines: 30,
|