@matdata/yasgui 5.2.0 → 5.3.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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@matdata/yasgui",
3
3
  "description": "Yet Another SPARQL GUI",
4
- "version": "5.2.0",
4
+ "version": "5.3.0",
5
5
  "main": "build/yasgui.min.js",
6
6
  "types": "build/ts/src/index.d.ts",
7
7
  "license": "MIT",
@@ -0,0 +1,391 @@
1
+ /**
2
+ * Export/Import Configuration Module
3
+ * Handles serialization and deserialization of YASGUI configuration to/from RDF Turtle format
4
+ */
5
+
6
+ import { PersistedJson } from "./PersistentConfig";
7
+
8
+ // YASGUI Configuration Ontology
9
+ export const YASGUI_NS = "https://yasgui.matdata.eu/ontology#";
10
+ export const RDF_NS = "http://www.w3.org/1999/02/22-rdf-syntax-ns#";
11
+ export const RDFS_NS = "http://www.w3.org/2000/01/rdf-schema#";
12
+ export const XSD_NS = "http://www.w3.org/2001/XMLSchema#";
13
+ export const DCTERMS_NS = "http://purl.org/dc/terms/";
14
+ export const SD_NS = "http://www.w3.org/ns/sparql-service-description#";
15
+ export const SP_NS = "http://spinrdf.org/sp#";
16
+ export const HTTP_NS = "http://www.w3.org/2011/http#";
17
+ export const SCHEMA_NS = "https://schema.org/";
18
+
19
+ /**
20
+ * Serialize configuration to Turtle format
21
+ */
22
+ export function serializeToTurtle(config: PersistedJson): string {
23
+ const lines: string[] = [];
24
+
25
+ // Prefixes
26
+ lines.push(`@prefix yasgui: <${YASGUI_NS}> .`);
27
+ lines.push(`@prefix rdf: <${RDF_NS}> .`);
28
+ lines.push(`@prefix rdfs: <${RDFS_NS}> .`);
29
+ lines.push(`@prefix xsd: <${XSD_NS}> .`);
30
+ lines.push(`@prefix dcterms: <${DCTERMS_NS}> .`);
31
+ lines.push(`@prefix sd: <${SD_NS}> .`);
32
+ lines.push(`@prefix sp: <${SP_NS}> .`);
33
+ lines.push(`@prefix http: <${HTTP_NS}> .`);
34
+ lines.push(`@prefix schema: <${SCHEMA_NS}> .`);
35
+ lines.push("");
36
+
37
+ // Main configuration node
38
+ const now = new Date().toISOString();
39
+ lines.push(`[] a yasgui:Configuration ;`);
40
+ lines.push(` dcterms:created "${now}"^^xsd:dateTime ;`);
41
+
42
+ // Theme
43
+ if (config.theme) {
44
+ lines.push(` yasgui:theme "${config.theme}" ;`);
45
+ }
46
+
47
+ // Orientation
48
+ if (config.orientation) {
49
+ lines.push(` yasgui:orientation "${config.orientation}" ;`);
50
+ }
51
+
52
+ // Endpoint history
53
+ if (config.endpointHistory && config.endpointHistory.length > 0) {
54
+ lines.push(` yasgui:endpointHistory (`);
55
+ config.endpointHistory.forEach((endpoint) => {
56
+ lines.push(` "${escapeTurtleString(endpoint)}"`);
57
+ });
58
+ lines.push(` ) ;`);
59
+ }
60
+
61
+ // Active tab
62
+ if (config.active) {
63
+ lines.push(` yasgui:activeTab "${escapeTurtleString(config.active)}" ;`);
64
+ }
65
+
66
+ // Prefixes
67
+ if (config.prefixes) {
68
+ lines.push(` yasgui:prefixesValue """${escapeTurtleString(config.prefixes)}""" ;`);
69
+ }
70
+
71
+ // Auto capture enabled
72
+ if (config.autoCaptureEnabled !== undefined) {
73
+ lines.push(` yasgui:autoCaptureEnabled "${config.autoCaptureEnabled}"^^xsd:boolean ;`);
74
+ }
75
+
76
+ // Custom endpoint buttons
77
+ if (config.customEndpointButtons && config.customEndpointButtons.length > 0) {
78
+ lines.push(` yasgui:customEndpointButton [`);
79
+ config.customEndpointButtons.forEach((button, index) => {
80
+ const isLast = index === config.customEndpointButtons!.length - 1;
81
+ lines.push(` rdfs:label "${escapeTurtleString(button.label)}" ;`);
82
+ lines.push(` sd:endpoint "${escapeTurtleString(button.endpoint)}"${isLast ? "" : " ;"}`);
83
+ if (!isLast) {
84
+ lines.push(` ] , [`);
85
+ }
86
+ });
87
+ lines.push(` ] ;`);
88
+ }
89
+
90
+ // Tabs
91
+ if (config.tabs && config.tabs.length > 0) {
92
+ lines.push(` yasgui:tab [`);
93
+ config.tabs.forEach((tabId, tabIndex) => {
94
+ const tabConfig = config.tabConfig[tabId];
95
+ const isLastTab = tabIndex === config.tabs.length - 1;
96
+
97
+ lines.push(` dcterms:identifier "${escapeTurtleString(tabId)}" ;`);
98
+ lines.push(` rdfs:label "${escapeTurtleString(tabConfig.name)}" ;`);
99
+
100
+ // Orientation (if different from default)
101
+ if (tabConfig.orientation) {
102
+ lines.push(` yasgui:orientation "${escapeTurtleString(tabConfig.orientation)}" ;`);
103
+ }
104
+
105
+ // Query
106
+ if (tabConfig.yasqe?.value) {
107
+ lines.push(` sp:text """${escapeTurtleString(tabConfig.yasqe.value)}""" ;`);
108
+ }
109
+
110
+ // Editor height
111
+ if (tabConfig.yasqe?.editorHeight) {
112
+ lines.push(` schema:height "${escapeTurtleString(tabConfig.yasqe.editorHeight)}" ;`);
113
+ }
114
+
115
+ // Request config
116
+ if (tabConfig.requestConfig) {
117
+ const reqConfig = tabConfig.requestConfig;
118
+ if (reqConfig.endpoint && typeof reqConfig.endpoint === "string") {
119
+ lines.push(` sd:endpoint "${escapeTurtleString(reqConfig.endpoint)}" ;`);
120
+ }
121
+ if (reqConfig.method && typeof reqConfig.method === "string") {
122
+ lines.push(` yasgui:requestMethod "${escapeTurtleString(reqConfig.method)}" ;`);
123
+ }
124
+ if (reqConfig.acceptHeaderSelect && typeof reqConfig.acceptHeaderSelect === "string") {
125
+ lines.push(` yasgui:acceptHeaderSelect [`);
126
+ lines.push(` http:headerName "Accept" ;`);
127
+ lines.push(` http:headerValue "${escapeTurtleString(reqConfig.acceptHeaderSelect)}"`);
128
+ lines.push(` ] ;`);
129
+ }
130
+ if (reqConfig.acceptHeaderGraph && typeof reqConfig.acceptHeaderGraph === "string") {
131
+ lines.push(` yasgui:acceptHeaderGraph [`);
132
+ lines.push(` http:headerName "Accept" ;`);
133
+ lines.push(` http:headerValue "${escapeTurtleString(reqConfig.acceptHeaderGraph)}"`);
134
+ lines.push(` ] ;`);
135
+ }
136
+ }
137
+
138
+ // YASR settings
139
+ if (tabConfig.yasr?.settings) {
140
+ const yasrSettings = tabConfig.yasr.settings;
141
+ if (yasrSettings.selectedPlugin) {
142
+ lines.push(` yasgui:selectedPlugin "${escapeTurtleString(yasrSettings.selectedPlugin)}" ;`);
143
+ }
144
+ }
145
+
146
+ // Remove trailing semicolon from last property
147
+ const lastLine = lines[lines.length - 1];
148
+ if (lastLine.endsWith(" ;")) {
149
+ lines[lines.length - 1] = lastLine.slice(0, -2);
150
+ }
151
+
152
+ if (!isLastTab) {
153
+ lines.push(` ] , [`);
154
+ }
155
+ });
156
+ lines.push(` ] .`);
157
+ } else {
158
+ // Remove trailing semicolon if no tabs
159
+ const lastLine = lines[lines.length - 1];
160
+ if (lastLine.endsWith(" ;")) {
161
+ lines[lines.length - 1] = lastLine.slice(0, -2) + " .";
162
+ }
163
+ }
164
+
165
+ return lines.join("\n");
166
+ }
167
+
168
+ /**
169
+ * Escape special characters in Turtle strings
170
+ */
171
+ function escapeTurtleString(str: string): string {
172
+ return str
173
+ .replace(/\\/g, "\\\\")
174
+ .replace(/"/g, '\\"')
175
+ .replace(/\n/g, "\\n")
176
+ .replace(/\r/g, "\\r")
177
+ .replace(/\t/g, "\\t");
178
+ }
179
+
180
+ /**
181
+ * Unescape Turtle string
182
+ */
183
+ function unescapeTurtleString(str: string): string {
184
+ return str
185
+ .replace(/\\n/g, "\n")
186
+ .replace(/\\r/g, "\r")
187
+ .replace(/\\t/g, "\t")
188
+ .replace(/\\"/g, '"')
189
+ .replace(/\\\\/g, "\\");
190
+ }
191
+
192
+ /**
193
+ * Parse Turtle format back to configuration
194
+ * This is a simplified parser focused on the structure we generate.
195
+ *
196
+ * Note: This parser is designed to handle the specific Turtle format
197
+ * produced by serializeToTurtle(). For production use with arbitrary
198
+ * Turtle input, consider using a robust RDF library like N3.js.
199
+ * The current implementation uses regex patterns that work well for
200
+ * our serialization output but may not handle all valid Turtle syntax.
201
+ */
202
+ export function parseFromTurtle(turtle: string): Partial<PersistedJson> {
203
+ const config: Partial<PersistedJson> = {
204
+ endpointHistory: [],
205
+ tabs: [],
206
+ tabConfig: {},
207
+ customEndpointButtons: [],
208
+ };
209
+
210
+ try {
211
+ // Extract endpoint history
212
+ const endpointHistoryMatch = turtle.match(/yasgui:endpointHistory\s*\(([\s\S]*?)\)/);
213
+ if (endpointHistoryMatch) {
214
+ const endpoints = endpointHistoryMatch[1].match(/"([^"]*)"/g);
215
+ if (endpoints) {
216
+ config.endpointHistory = endpoints.map((e) => unescapeTurtleString(e.slice(1, -1)));
217
+ }
218
+ }
219
+
220
+ // Extract active tab
221
+ const activeTabMatch = turtle.match(/yasgui:activeTab\s+"([^"]*)"/);
222
+ if (activeTabMatch) {
223
+ config.active = unescapeTurtleString(activeTabMatch[1]);
224
+ }
225
+
226
+ // Extract prefixes
227
+ const prefixesMatch = turtle.match(/yasgui:prefixesValue\s+"""([\s\S]*?)"""/);
228
+ if (prefixesMatch) {
229
+ config.prefixes = unescapeTurtleString(prefixesMatch[1]);
230
+ }
231
+
232
+ // Extract auto capture enabled
233
+ const autoCaptureMatch = turtle.match(/yasgui:autoCaptureEnabled\s+"([^"]*)"/);
234
+ if (autoCaptureMatch) {
235
+ config.autoCaptureEnabled = autoCaptureMatch[1] === "true";
236
+ }
237
+
238
+ // Extract theme
239
+ const themeMatch = turtle.match(/yasgui:theme\s+"([^"]*)"/);
240
+ if (themeMatch && (themeMatch[1] === "light" || themeMatch[1] === "dark")) {
241
+ config.theme = themeMatch[1] as "light" | "dark";
242
+ }
243
+
244
+ // Extract orientation
245
+ const orientationMatch = turtle.match(/yasgui:orientation\s+"([^"]*)"/);
246
+ if (orientationMatch && (orientationMatch[1] === "vertical" || orientationMatch[1] === "horizontal")) {
247
+ config.orientation = orientationMatch[1] as "vertical" | "horizontal";
248
+ }
249
+
250
+ // Extract custom endpoint buttons
251
+ const buttonPattern =
252
+ /yasgui:customEndpointButton\s+\[([\s\S]*?)rdfs:label\s+"([^"]*)"\s*;\s*sd:endpoint\s+"([^"]*)"/g;
253
+ let buttonMatch;
254
+ while ((buttonMatch = buttonPattern.exec(turtle)) !== null) {
255
+ config.customEndpointButtons!.push({
256
+ label: unescapeTurtleString(buttonMatch[2]),
257
+ endpoint: unescapeTurtleString(buttonMatch[3]),
258
+ });
259
+ }
260
+
261
+ // Extract tabs - simplified parsing
262
+ const tabPattern =
263
+ /dcterms:identifier\s+"([^"]*)"\s*;\s*rdfs:label\s+"([^"]*)"\s*;[\s\S]*?(?=dcterms:identifier|yasgui:customEndpointButton|\]\.|\]\s*,\s*\[)/g;
264
+ const tabBlocks = turtle.match(/yasgui:tab\s+\[([\s\S]*)\]\s*\./);
265
+
266
+ if (tabBlocks) {
267
+ const tabsContent = tabBlocks[1];
268
+ const tabIdMatches = [...tabsContent.matchAll(/dcterms:identifier\s+"([^"]*)"/g)];
269
+ const tabNameMatches = [...tabsContent.matchAll(/rdfs:label\s+"([^"]*)"/g)];
270
+ const queryMatches = [...tabsContent.matchAll(/sp:text\s+"""([\s\S]*?)"""/g)];
271
+ const endpointMatches = [...tabsContent.matchAll(/sd:endpoint\s+"([^"]*)"/g)];
272
+ const methodMatches = [...tabsContent.matchAll(/yasgui:requestMethod\s+"([^"]*)"/g)];
273
+
274
+ // Extract tab-level orientation
275
+ const tabOrientationMatches = [...tabsContent.matchAll(/yasgui:orientation\s+"([^"]*)"/g)];
276
+
277
+ tabIdMatches.forEach((match, index) => {
278
+ const tabId = unescapeTurtleString(match[1]);
279
+ const tabName = tabNameMatches[index] ? unescapeTurtleString(tabNameMatches[index][1]) : "Query";
280
+ const query = queryMatches[index] ? unescapeTurtleString(queryMatches[index][1]) : "";
281
+ const endpoint = endpointMatches[index] ? unescapeTurtleString(endpointMatches[index][1]) : "";
282
+ const method = methodMatches[index] ? unescapeTurtleString(methodMatches[index][1]) : "POST";
283
+ const orientation = tabOrientationMatches[index]
284
+ ? (unescapeTurtleString(tabOrientationMatches[index][1]) as "vertical" | "horizontal")
285
+ : undefined;
286
+
287
+ config.tabs!.push(tabId);
288
+ config.tabConfig![tabId] = {
289
+ id: tabId,
290
+ name: tabName,
291
+ yasqe: {
292
+ value: query,
293
+ },
294
+ requestConfig: {
295
+ queryArgument: undefined,
296
+ endpoint: endpoint,
297
+ method: method as "GET" | "POST",
298
+ acceptHeaderGraph: "text/turtle",
299
+ acceptHeaderSelect: "application/sparql-results+json",
300
+ acceptHeaderUpdate: "application/sparql-results+json",
301
+ namedGraphs: [],
302
+ defaultGraphs: [],
303
+ args: [],
304
+ headers: {},
305
+ withCredentials: false,
306
+ adjustQueryBeforeRequest: false,
307
+ },
308
+ yasr: {
309
+ settings: {},
310
+ response: undefined,
311
+ },
312
+ orientation: orientation,
313
+ };
314
+ });
315
+ }
316
+ } catch (error) {
317
+ console.error("Error parsing Turtle configuration:", error);
318
+ throw new Error("Failed to parse configuration. Please check the format.");
319
+ }
320
+
321
+ return config;
322
+ }
323
+
324
+ /**
325
+ * Download configuration as a file
326
+ */
327
+ export function downloadConfigAsFile(config: PersistedJson, filename: string = "yasgui-config.ttl") {
328
+ const turtle = serializeToTurtle(config);
329
+ const blob = new Blob([turtle], { type: "text/turtle;charset=utf-8" });
330
+ const url = URL.createObjectURL(blob);
331
+
332
+ const link = document.createElement("a");
333
+ link.href = url;
334
+ link.download = filename;
335
+ document.body.appendChild(link);
336
+ link.click();
337
+ document.body.removeChild(link);
338
+ URL.revokeObjectURL(url);
339
+ }
340
+
341
+ /**
342
+ * Copy configuration to clipboard
343
+ */
344
+ export async function copyConfigToClipboard(config: PersistedJson): Promise<void> {
345
+ const turtle = serializeToTurtle(config);
346
+ try {
347
+ await navigator.clipboard.writeText(turtle);
348
+ } catch (error) {
349
+ // Fallback for older browsers
350
+ const textArea = document.createElement("textarea");
351
+ textArea.value = turtle;
352
+ textArea.style.position = "fixed";
353
+ textArea.style.left = "-999999px";
354
+ document.body.appendChild(textArea);
355
+ textArea.select();
356
+ try {
357
+ document.execCommand("copy");
358
+ } finally {
359
+ document.body.removeChild(textArea);
360
+ }
361
+ }
362
+ }
363
+
364
+ /**
365
+ * Read configuration from file
366
+ */
367
+ export function readConfigFromFile(file: File): Promise<string> {
368
+ return new Promise((resolve, reject) => {
369
+ const reader = new FileReader();
370
+ reader.onload = (e) => {
371
+ if (e.target?.result) {
372
+ resolve(e.target.result as string);
373
+ } else {
374
+ reject(new Error("Failed to read file"));
375
+ }
376
+ };
377
+ reader.onerror = () => reject(new Error("Failed to read file"));
378
+ reader.readAsText(file);
379
+ });
380
+ }
381
+
382
+ /**
383
+ * Read configuration from clipboard
384
+ */
385
+ export async function readConfigFromClipboard(): Promise<string> {
386
+ try {
387
+ return await navigator.clipboard.readText();
388
+ } catch (error) {
389
+ throw new Error("Failed to read from clipboard. Please paste the content manually.");
390
+ }
391
+ }
@@ -11,6 +11,8 @@ export interface PersistedJson {
11
11
  prefixes?: string;
12
12
  autoCaptureEnabled?: boolean;
13
13
  customEndpointButtons?: EndpointButton[];
14
+ theme?: "light" | "dark";
15
+ orientation?: "vertical" | "horizontal";
14
16
  }
15
17
  function getDefaults(): PersistedJson {
16
18
  return {
@@ -165,4 +167,19 @@ export default class PersistentConfig {
165
167
  const storage = new YStorage(storageNamespace);
166
168
  storage.removeNamespace();
167
169
  }
170
+
171
+ /**
172
+ * Get the current persisted configuration (for export purposes)
173
+ */
174
+ public getPersistedConfig(): PersistedJson {
175
+ return this.persistedJson;
176
+ }
177
+
178
+ /**
179
+ * Update the persisted configuration (for import purposes)
180
+ */
181
+ public updatePersistedConfig(config: Partial<PersistedJson>) {
182
+ Object.assign(this.persistedJson, config);
183
+ this.toStorage();
184
+ }
168
185
  }
package/src/Tab.ts CHANGED
@@ -35,6 +35,7 @@ export interface PersistedJson {
35
35
  response: Parser.ResponseSummary | undefined;
36
36
  };
37
37
  requestConfig: YasguiRequestConfig;
38
+ orientation?: "vertical" | "horizontal";
38
39
  }
39
40
  export interface Tab {
40
41
  on(event: string | symbol, listener: (...args: any[]) => void): this;
@@ -170,8 +171,8 @@ export class Tab extends EventEmitter {
170
171
  }
171
172
  }
172
173
  }
173
- // Ctrl+Shift+F - Switch between fullscreen modes
174
- else if (event.ctrlKey && event.shiftKey && event.key === "F") {
174
+ // F9 - Switch between fullscreen modes
175
+ else if (event.key === "F9") {
175
176
  event.preventDefault();
176
177
  const yasqeFullscreen = this.yasqe?.getIsFullscreen();
177
178
  const yasrFullscreen = this.yasr?.getIsFullscreen();
@@ -358,3 +358,81 @@
358
358
  }
359
359
  }
360
360
  }
361
+
362
+ // Import/Export styles
363
+ .exportButtons,
364
+ .importButtons {
365
+ display: flex;
366
+ gap: 10px;
367
+ margin-top: 15px;
368
+ }
369
+
370
+ .dropZone {
371
+ border: 2px dashed var(--yasgui-border-color, #ccc);
372
+ border-radius: 8px;
373
+ padding: 40px 20px;
374
+ text-align: center;
375
+ background: var(--yasgui-bg-secondary, #f9f9f9);
376
+ margin-top: 15px;
377
+ transition: all 0.3s;
378
+
379
+ &.dragover {
380
+ border-color: var(--yasgui-accent-color, #337ab7);
381
+ background: var(--yasgui-accent-light, #e8f4fc);
382
+ }
383
+
384
+ .dropZoneContent {
385
+ margin-bottom: 20px;
386
+ }
387
+
388
+ .dropZoneIcon {
389
+ font-size: 48px;
390
+ margin-bottom: 10px;
391
+ }
392
+
393
+ .dropZoneText {
394
+ font-size: 16px;
395
+ font-weight: 500;
396
+ color: var(--yasgui-text-primary, #333);
397
+ margin-bottom: 8px;
398
+ }
399
+
400
+ .dropZoneOr {
401
+ font-size: 14px;
402
+ color: var(--yasgui-text-secondary, #666);
403
+ margin: 15px 0;
404
+ }
405
+ }
406
+
407
+ .importExportNotification {
408
+ position: fixed;
409
+ top: 20px;
410
+ right: 20px;
411
+ padding: 15px 20px;
412
+ border-radius: 4px;
413
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
414
+ z-index: 10001;
415
+ font-size: 14px;
416
+ animation: slideIn 0.3s ease-out;
417
+
418
+ &.success {
419
+ background: #4caf50;
420
+ color: white;
421
+ }
422
+
423
+ &.error {
424
+ background: #f44336;
425
+ color: white;
426
+ }
427
+ }
428
+
429
+ @keyframes slideIn {
430
+ from {
431
+ transform: translateX(400px);
432
+ opacity: 0;
433
+ }
434
+ to {
435
+ transform: translateX(0);
436
+ opacity: 1;
437
+ }
438
+ }