@matdata/yasqe 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.
@@ -0,0 +1,9 @@
1
+ export interface TokenizerTable {
2
+ [key: string]: any;
3
+ }
4
+
5
+ export const table: TokenizerTable;
6
+ export const keywords: any;
7
+ export const punct: any;
8
+ export const acceptEmpty: any;
9
+ export const startSymbol: string;
@@ -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) state.variables[tokenOb.string] = 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.2.0",
4
+ "version": "5.3.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.autoformat();
71
+ yasqe.format();
72
72
  },
73
73
  "Ctrl-S": function (_yasqe: any) {
74
74
  const yasqe: Yasqe = _yasqe;
@@ -130,6 +130,8 @@ SELECT * WHERE {
130
130
  editorHeight: "300px",
131
131
  queryingDisabled: undefined,
132
132
  prefixCcApi: prefixCcApi,
133
+ showFormatButton: true,
134
+ checkConstructVariables: true,
133
135
  };
134
136
  const requestConfig: PlainRequestConfig = {
135
137
  queryArgument: undefined, //undefined means: get query argument based on query mode
package/src/imgs.ts CHANGED
@@ -12,3 +12,5 @@ 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>';
package/src/index.ts CHANGED
@@ -16,6 +16,7 @@ import { merge, escape } from "lodash-es";
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;
@@ -84,13 +85,30 @@ export class Yasqe extends CodeMirror {
84
85
  if (storageId) {
85
86
  const persConf = this.storage.get<any>(storageId);
86
87
  if (persConf && typeof persConf === "string") {
87
- this.persistentConfig = { query: persConf, editorHeight: this.config.editorHeight }; // Migrate to object based localstorage
88
+ this.persistentConfig = { query: persConf, editorHeight: this.config.editorHeight };
88
89
  } else {
89
90
  this.persistentConfig = persConf;
90
91
  }
91
92
  if (!this.persistentConfig)
92
93
  this.persistentConfig = { query: this.getValue(), editorHeight: this.config.editorHeight };
94
+
95
+ // Ensure autoformatOnQuery is true by default
96
+ if (this.persistentConfig && typeof this.persistentConfig.autoformatOnQuery === "undefined") {
97
+ this.persistentConfig.autoformatOnQuery = true;
98
+ }
99
+
93
100
  if (this.persistentConfig && this.persistentConfig.query) this.setValue(this.persistentConfig.query);
101
+ } else {
102
+ // If no storage, ensure persistentConfig exists and autoformatOnQuery is true
103
+ if (!this.persistentConfig) {
104
+ this.persistentConfig = {
105
+ query: this.getValue(),
106
+ editorHeight: this.config.editorHeight,
107
+ autoformatOnQuery: true,
108
+ };
109
+ } else if (typeof this.persistentConfig.autoformatOnQuery === "undefined") {
110
+ this.persistentConfig.autoformatOnQuery = true;
111
+ }
94
112
  }
95
113
  this.config.autocompleters.forEach((c) => this.enableCompleter(c).then(() => {}, console.warn));
96
114
  if (this.config.consumeShareLink) {
@@ -99,6 +117,7 @@ export class Yasqe extends CodeMirror {
99
117
  window.addEventListener("hashchange", this.handleHashChange);
100
118
  }
101
119
  this.checkSyntax();
120
+ this.checkConstructVariables();
102
121
  // Size codemirror to the
103
122
  if (this.persistentConfig && this.persistentConfig.editorHeight) {
104
123
  this.getWrapperElement().style.height = this.persistentConfig.editorHeight;
@@ -115,6 +134,7 @@ export class Yasqe extends CodeMirror {
115
134
  };
116
135
  private handleChange() {
117
136
  this.checkSyntax();
137
+ this.checkConstructVariables();
118
138
  this.updateQueryButton();
119
139
  }
120
140
  private handleBlur() {
@@ -123,6 +143,7 @@ export class Yasqe extends CodeMirror {
123
143
  private handleChanges() {
124
144
  // e.g. handle blur
125
145
  this.checkSyntax();
146
+ this.checkConstructVariables();
126
147
  this.updateQueryButton();
127
148
  }
128
149
  private handleCursorActivity() {
@@ -347,6 +368,23 @@ export class Yasqe extends CodeMirror {
347
368
  this.updateQueryButton();
348
369
  }
349
370
 
371
+ /**
372
+ * Draw format btn
373
+ */
374
+ if (this.config.showFormatButton) {
375
+ const formatBtn = document.createElement("button");
376
+ addClass(formatBtn, "yasqe_formatButton");
377
+ const formatIcon = drawSvgStringAsElement(imgs.format);
378
+ addClass(formatIcon, "formatIcon");
379
+ formatBtn.appendChild(formatIcon);
380
+ formatBtn.onclick = () => {
381
+ this.format();
382
+ };
383
+ formatBtn.title = "Format query (Shift+Ctrl+F)";
384
+ formatBtn.setAttribute("aria-label", "Format query");
385
+ buttons.appendChild(formatBtn);
386
+ }
387
+
350
388
  /**
351
389
  * Draw fullscreen btn
352
390
  */
@@ -643,6 +681,32 @@ export class Yasqe extends CodeMirror {
643
681
  }
644
682
  });
645
683
  }
684
+
685
+ public formatQuery() {
686
+ try {
687
+ const currentQuery = this.getValue();
688
+ const formatted = spfmt.format(currentQuery);
689
+ this.setValue(formatted);
690
+ // Collapse prefixes after formatting
691
+ this.collapsePrefixes(true);
692
+ } catch (error) {
693
+ console.warn(
694
+ "Failed to format SPARQL query using sparql-formatter. This may be due to syntax errors in the query. Falling back to legacy formatter.",
695
+ error,
696
+ );
697
+ // If formatting fails, fall back to the built-in autoformat
698
+ this.autoformat();
699
+ }
700
+ }
701
+
702
+ public format() {
703
+ const formatterType = this.persistentConfig?.formatterType || "sparql-formatter";
704
+ if (formatterType === "legacy") {
705
+ this.autoformat();
706
+ } else {
707
+ this.formatQuery();
708
+ }
709
+ }
646
710
  //values in the form of {?var: 'value'}, or [{?var: 'value'}]
647
711
  public getQueryWithValues(values: string | { [varName: string]: string } | Array<{ [varName: string]: string }>) {
648
712
  if (!values) return this.getValue();
@@ -709,6 +773,7 @@ export class Yasqe extends CodeMirror {
709
773
  public setCheckSyntaxErrors(isEnabled: boolean) {
710
774
  this.config.syntaxErrorCheck = isEnabled;
711
775
  this.checkSyntax();
776
+ this.checkConstructVariables();
712
777
  }
713
778
  public checkSyntax() {
714
779
  this.queryValid = true;
@@ -765,6 +830,77 @@ export class Yasqe extends CodeMirror {
765
830
  }
766
831
  }
767
832
  }
833
+
834
+ public setCheckConstructVariables(isEnabled: boolean) {
835
+ this.config.checkConstructVariables = isEnabled;
836
+ if (!isEnabled) {
837
+ // Clear any existing warnings when disabled
838
+ this.clearGutter("gutterConstructWarning");
839
+ } else {
840
+ this.checkConstructVariables();
841
+ }
842
+ }
843
+
844
+ public checkConstructVariables() {
845
+ // Clear any existing warnings first
846
+ this.clearGutter("gutterConstructWarning");
847
+
848
+ // Only check if enabled, query is valid, and it's a CONSTRUCT query
849
+ if (!this.config.checkConstructVariables || !this.queryValid || this.getQueryType() !== "CONSTRUCT") {
850
+ return;
851
+ }
852
+
853
+ // Get the final state after parsing the entire query
854
+ const lastLine = this.getDoc().lastLine();
855
+ const token: Token = this.getTokenAt({ line: lastLine, ch: this.getDoc().getLine(lastLine).length }, true);
856
+
857
+ const state = token.state as TokenizerState;
858
+
859
+ // Check for undefined variables in CONSTRUCT template
860
+ const undefinedVars: string[] = [];
861
+ for (const varName in state.constructVariables) {
862
+ if (!state.whereVariables[varName]) {
863
+ undefinedVars.push(varName);
864
+ }
865
+ }
866
+
867
+ if (undefinedVars.length === 0) {
868
+ return;
869
+ }
870
+
871
+ // Find lines where undefined variables are used in CONSTRUCT template
872
+ // Note: This iterates through all lines but filters by inConstructTemplate flag
873
+ // For large queries, this could be optimized by tracking line ranges during tokenization
874
+ for (let l = 0; l < this.getDoc().lineCount(); ++l) {
875
+ const lineToken: Token = this.getTokenAt({ line: l, ch: this.getDoc().getLine(l).length }, true);
876
+ const lineState = lineToken.state as TokenizerState;
877
+
878
+ // Only mark variables in the CONSTRUCT template
879
+ if (lineState.queryType === "CONSTRUCT" && lineState.inConstructTemplate) {
880
+ const line = this.getDoc().getLine(l);
881
+ // Check if this line contains any undefined variable (use word boundary to avoid partial matches)
882
+ for (const undefinedVar of undefinedVars) {
883
+ // Escape special regex characters in variable name
884
+ // Use negative lookbehind/lookahead to ensure we match the full variable name
885
+ // Variables can be followed by whitespace, punctuation, or end of line
886
+ const escapedVar = undefinedVar.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
887
+ const varRegex = new RegExp(`${escapedVar}(?![a-zA-Z0-9_])`);
888
+ if (varRegex.test(line)) {
889
+ const warningEl = drawSvgStringAsElement(imgs.warning);
890
+ warningEl.className = "constructVariableWarning";
891
+ tooltip(
892
+ this,
893
+ warningEl,
894
+ escape(`Variable ${undefinedVar} is used in CONSTRUCT but not defined in WHERE clause`),
895
+ );
896
+ this.setGutterMarker(l, "gutterConstructWarning", warningEl);
897
+ break; // Only one marker per line
898
+ }
899
+ }
900
+ }
901
+ }
902
+ }
903
+
768
904
  /**
769
905
  * Token management
770
906
  */
@@ -875,6 +1011,10 @@ export class Yasqe extends CodeMirror {
875
1011
  */
876
1012
  public query(config?: Sparql.YasqeAjaxConfig) {
877
1013
  if (this.config.queryingDisabled) return Promise.reject("Querying is disabled.");
1014
+ // Auto-format query before execution if enabled in persistent config
1015
+ if (this.persistentConfig?.autoformatOnQuery) {
1016
+ this.format();
1017
+ }
878
1018
  // Abort previous request
879
1019
  this.abortQuery();
880
1020
  return Sparql.executeQuery(this, config);
@@ -1081,10 +1221,14 @@ export interface Config extends Partial<CodeMirror.EditorConfiguration> {
1081
1221
  editorHeight: string;
1082
1222
  queryingDisabled: string | undefined; // The string will be the message displayed when hovered
1083
1223
  prefixCcApi: string; // the suggested default prefixes URL API getter
1224
+ showFormatButton: boolean; // Show a button to format the query
1225
+ checkConstructVariables: boolean; // Check for undefined variables in CONSTRUCT queries
1084
1226
  }
1085
1227
  export interface PersistentConfig {
1086
1228
  query: string;
1087
1229
  editorHeight: string;
1230
+ formatterType?: "sparql-formatter" | "legacy"; // Which formatter to use
1231
+ autoformatOnQuery?: boolean; // Auto-format query on execution
1088
1232
  }
1089
1233
  // export var _Yasqe = _Yasqe;
1090
1234
 
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) {
@@ -30,7 +30,10 @@ export function removePrefixes(yasqe: Yasqe, prefixes: Prefixes) {
30
30
  yasqe.setValue(
31
31
  yasqe
32
32
  .getValue()
33
- .replace(new RegExp("PREFIX\\s*" + pref + ":\\s*" + escapeRegex("<" + prefixes[pref] + ">") + "\\s*", "ig"), "")
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
 
@@ -212,6 +212,26 @@
212
212
  }
213
213
  }
214
214
 
215
+ .yasqe_formatButton {
216
+ display: inline-block;
217
+ border: none;
218
+ background: none;
219
+ cursor: pointer;
220
+ padding: 0;
221
+ margin-left: 5px;
222
+
223
+ svg {
224
+ height: 25px;
225
+ width: 25px;
226
+ }
227
+
228
+ &:hover {
229
+ svg {
230
+ fill: #337ab7;
231
+ }
232
+ }
233
+ }
234
+
215
235
  .yasqe_fullscreenButton {
216
236
  display: inline-block;
217
237
  border: none;
@@ -49,6 +49,18 @@
49
49
  }
50
50
  }
51
51
 
52
+ .constructVariableWarning {
53
+ width: 13px;
54
+ height: 13px;
55
+ margin-top: 2px;
56
+ margin-left: 2px;
57
+ svg {
58
+ g {
59
+ fill: orange;
60
+ }
61
+ }
62
+ }
63
+
52
64
  .yasqe_tooltip {
53
65
  background: #333;
54
66
  background: rgba(0, 0, 0, 0.8);
@@ -0,0 +1,10 @@
1
+ declare module "sparql-formatter" {
2
+ export interface SpfmtFormatter {
3
+ format: (sparql: string, formattingMode?: string, indentDepth?: number) => string;
4
+ parseSparql: (sparql: string) => unknown;
5
+ parseSparqlAsCompact: (sparql: string) => unknown;
6
+ formatAst: (ast: unknown, indentDepth?: number) => string;
7
+ }
8
+
9
+ export const spfmt: SpfmtFormatter;
10
+ }