@matdata/yasqe 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.
Files changed (76) hide show
  1. package/CHANGELOG.md +1 -1
  2. package/README.md +76 -59
  3. package/build/prefixes.json +2210 -2150
  4. package/build/ts/grammar/tokenizer.d.ts +9 -3
  5. package/build/ts/grammar/tokenizer.js +34 -6
  6. package/build/ts/grammar/tokenizer.js.map +1 -1
  7. package/build/ts/src/autocompleters/index.d.ts +4 -2
  8. package/build/ts/src/autocompleters/index.js +9 -33
  9. package/build/ts/src/autocompleters/index.js.map +1 -1
  10. package/build/ts/src/autocompleters/variables.js +17 -19
  11. package/build/ts/src/autocompleters/variables.js.map +1 -1
  12. package/build/ts/src/defaults.d.ts +17 -53
  13. package/build/ts/src/defaults.js +2 -5
  14. package/build/ts/src/defaults.js.map +1 -1
  15. package/build/ts/src/editor/autocompletion.d.ts +32 -0
  16. package/build/ts/src/editor/autocompletion.js +94 -0
  17. package/build/ts/src/editor/autocompletion.js.map +1 -0
  18. package/build/ts/src/editor/doc.d.ts +32 -0
  19. package/build/ts/src/editor/doc.js +121 -0
  20. package/build/ts/src/editor/doc.js.map +1 -0
  21. package/build/ts/src/editor/facade.d.ts +94 -0
  22. package/build/ts/src/editor/facade.js +392 -0
  23. package/build/ts/src/editor/facade.js.map +1 -0
  24. package/build/ts/src/editor/folding.d.ts +16 -0
  25. package/build/ts/src/editor/folding.js +190 -0
  26. package/build/ts/src/editor/folding.js.map +1 -0
  27. package/build/ts/src/editor/gutters.d.ts +8 -0
  28. package/build/ts/src/editor/gutters.js +113 -0
  29. package/build/ts/src/editor/gutters.js.map +1 -0
  30. package/build/ts/src/editor/keymap.d.ts +11 -0
  31. package/build/ts/src/editor/keymap.js +88 -0
  32. package/build/ts/src/editor/keymap.js.map +1 -0
  33. package/build/ts/src/editor/language.d.ts +11 -0
  34. package/build/ts/src/editor/language.js +19 -0
  35. package/build/ts/src/editor/language.js.map +1 -0
  36. package/build/ts/src/editor/tokenizerRunner.d.ts +37 -0
  37. package/build/ts/src/editor/tokenizerRunner.js +115 -0
  38. package/build/ts/src/editor/tokenizerRunner.js.map +1 -0
  39. package/build/ts/src/index.d.ts +49 -100
  40. package/build/ts/src/index.js +179 -100
  41. package/build/ts/src/index.js.map +1 -1
  42. package/build/ts/src/prefixes.json +2306 -2237
  43. package/build/ts/src/tooltip.js +9 -6
  44. package/build/ts/src/tooltip.js.map +1 -1
  45. package/build/yasqe.min.css +1 -1
  46. package/build/yasqe.min.css.map +3 -3
  47. package/build/yasqe.min.js +76 -91
  48. package/build/yasqe.min.js.map +4 -4
  49. package/grammar/tokenizer.ts +62 -8
  50. package/package.json +11 -6
  51. package/src/autocompleters/index.ts +24 -53
  52. package/src/autocompleters/show-hint.scss +17 -9
  53. package/src/autocompleters/variables.ts +20 -24
  54. package/src/defaults.ts +2 -6
  55. package/src/editor/autocompletion.ts +147 -0
  56. package/src/editor/doc.ts +151 -0
  57. package/src/editor/facade.ts +558 -0
  58. package/src/editor/folding.ts +225 -0
  59. package/src/editor/gutters.ts +152 -0
  60. package/src/editor/keymap.ts +133 -0
  61. package/src/editor/language.ts +47 -0
  62. package/src/editor/tokenizerRunner.ts +198 -0
  63. package/src/index.ts +193 -127
  64. package/src/prefixes.json +2210 -2150
  65. package/src/scss/buttons.scss +4 -2
  66. package/src/scss/codemirrorMods.scss +107 -11
  67. package/src/scss/yasqe.scss +4 -2
  68. package/src/tooltip.ts +16 -13
  69. package/build/ts/src/CodeMirror.d.ts +0 -30
  70. package/build/ts/src/CodeMirror.js +0 -20
  71. package/build/ts/src/CodeMirror.js.map +0 -1
  72. package/build/ts/src/prefixFold.d.ts +0 -8
  73. package/build/ts/src/prefixFold.js +0 -87
  74. package/build/ts/src/prefixFold.js.map +0 -1
  75. package/src/CodeMirror.ts +0 -54
  76. package/src/prefixFold.ts +0 -94
@@ -0,0 +1,225 @@
1
+ /**
2
+ * Code folding for SPARQL: folds bracket blocks (`{ ... }`) and the block of PREFIX declarations.
3
+ *
4
+ * Replaces the CodeMirror 5 combination of `fold.brace` and the custom `fold.prefix` helper.
5
+ * Because the SPARQL grammar is a stream parser (no syntax tree with nesting), folding is
6
+ * implemented as a `foldService` working on the tokenizer output.
7
+ */
8
+ import { EditorState, Extension, Text } from "@codemirror/state";
9
+ import { EditorView } from "@codemirror/view";
10
+ import { foldService, foldGutter, foldEffect, unfoldEffect, foldedRanges, foldable } from "@codemirror/language";
11
+ import { TokenizerRunner, Position } from "./tokenizerRunner";
12
+
13
+ export interface FoldRange {
14
+ from: number;
15
+ to: number;
16
+ }
17
+
18
+ const PREFIX_KEYWORD = "PREFIX ";
19
+
20
+ function tokenTypeAt(runner: TokenizerRunner, doc: Text, pos: Position) {
21
+ return runner.getTokenAt(doc, pos).type;
22
+ }
23
+
24
+ /**
25
+ * Whether `type` denotes a real bracket/punctuation token (i.e. not a bracket character that is
26
+ * part of a string or comment). The tokenizer styles brackets as `bracket bracket-level-N` /
27
+ * `bracket bracket-paren`, so a plain `=== "punc"` check would miss `{ } ( ) [ ]`.
28
+ */
29
+ function isBracketToken(type: string | null): boolean {
30
+ return !!type && (type === "punc" || type.split(/\s+/).some((t) => t === "punc" || t.startsWith("bracket")));
31
+ }
32
+
33
+ /**
34
+ * Finds the fold range for a block starting with a bracket on the given line. Mirrors the
35
+ * CodeMirror 5 `brace` range finder: the *last* opening bracket on the line is used.
36
+ */
37
+ export function braceFoldRange(runner: TokenizerRunner, doc: Text, line: number): FoldRange | undefined {
38
+ const lineText = doc.line(line + 1).text;
39
+ const pairs: { [open: string]: string } = { "{": "}", "[": "]", "(": ")" };
40
+ let startCh = -1;
41
+ let open = "";
42
+ for (let ch = lineText.length - 1; ch >= 0; ch--) {
43
+ const c = lineText[ch];
44
+ if (pairs[c] && isBracketToken(tokenTypeAt(runner, doc, { line, ch: ch + 1 }))) {
45
+ startCh = ch;
46
+ open = c;
47
+ break;
48
+ }
49
+ }
50
+ if (startCh < 0) return;
51
+ const close = pairs[open];
52
+
53
+ // Scan forward for the matching bracket, taking nesting into account and ignoring brackets inside strings/comments
54
+ let depth = 1;
55
+ const lastLine = doc.lines - 1;
56
+ for (let l = line; l <= lastLine; l++) {
57
+ const text = doc.line(l + 1).text;
58
+ let ch = l === line ? startCh + 1 : 0;
59
+ for (; ch < text.length; ch++) {
60
+ const c = text[ch];
61
+ if (c !== open && c !== close) continue;
62
+ if (!isBracketToken(tokenTypeAt(runner, doc, { line: l, ch: ch + 1 }))) continue;
63
+ if (c === open) depth++;
64
+ else if (--depth === 0) {
65
+ if (l === line) return; // opening and closing bracket on the same line: nothing to fold
66
+ return { from: doc.line(line + 1).from + startCh + 1, to: doc.line(l + 1).from + ch };
67
+ }
68
+ }
69
+ }
70
+ return;
71
+ }
72
+
73
+ /** Returns the char index of the first PREFIX keyword on the line (or undefined) */
74
+ export function findFirstPrefix(
75
+ runner: TokenizerRunner,
76
+ doc: Text,
77
+ line: number,
78
+ startFromCharIndex = 0,
79
+ ): number | undefined {
80
+ const lineText = doc.line(line + 1).text;
81
+ if (!lineText) return undefined;
82
+ const charIndex = lineText.toUpperCase().indexOf(PREFIX_KEYWORD, startFromCharIndex);
83
+ if (charIndex >= 0 && tokenTypeAt(runner, doc, { line, ch: charIndex + 1 }) === "keyword") {
84
+ return charIndex;
85
+ }
86
+ return undefined;
87
+ }
88
+
89
+ /** Returns the (0-based) line of the first PREFIX declaration in the document */
90
+ export function findFirstPrefixLine(runner: TokenizerRunner, doc: Text): number | undefined {
91
+ for (let i = 0; i < doc.lines; i++) {
92
+ const firstPrefix = findFirstPrefix(runner, doc, i);
93
+ if (firstPrefix !== undefined && firstPrefix >= 0) return i;
94
+ }
95
+ return undefined;
96
+ }
97
+
98
+ /**
99
+ * Fold range for the block of PREFIX declarations. Only the first PREFIX declaration of the query
100
+ * starts a foldable block. Ported from the CodeMirror 5 `fold.prefix` helper.
101
+ */
102
+ export function prefixFoldRange(runner: TokenizerRunner, doc: Text, line: number): FoldRange | undefined {
103
+ // Only the opening prefix declaration is foldable
104
+ for (let i = line - 1; i >= 0; i--) {
105
+ if (
106
+ doc
107
+ .line(i + 1)
108
+ .text.toUpperCase()
109
+ .indexOf(PREFIX_KEYWORD) >= 0
110
+ )
111
+ return;
112
+ }
113
+ const prefixStart = findFirstPrefix(runner, doc, line);
114
+ if (prefixStart === undefined) return;
115
+
116
+ const getNextNonWsToken = (l: number, ch: number) => {
117
+ let token = runner.getTokenAt(doc, { line: l, ch });
118
+ if (token.end < ch) return undefined;
119
+ while (token.type === "ws") {
120
+ const next = runner.getTokenAt(doc, { line: l, ch: token.end + 1 });
121
+ if (next.end < token.end + 1) return undefined;
122
+ token = next;
123
+ }
124
+ return token;
125
+ };
126
+ const getLastPrefixPos = (l: number, ch: number): number => {
127
+ const keyword = runner.getTokenAt(doc, { line: l, ch: ch + 1 });
128
+ if (keyword.type !== "keyword") return -1;
129
+ const shortname = getNextNonWsToken(l, keyword.end + 1);
130
+ if (!shortname || shortname.type !== "string-2") return -1;
131
+ const uri = getNextNonWsToken(l, shortname.end + 1);
132
+ if (!uri || uri.type !== "variable-3") return -1;
133
+ return uri.end;
134
+ };
135
+
136
+ let prefixEndChar = getLastPrefixPos(line, prefixStart);
137
+ let prefixEndLine = line;
138
+ let stopAtNextLine = false;
139
+ for (let i = line; i < doc.lines; i++) {
140
+ if (stopAtNextLine) break;
141
+ const text = doc.line(i + 1).text;
142
+ let pos = i === line ? prefixStart + 1 : 0;
143
+ for (;;) {
144
+ if (!stopAtNextLine && text.indexOf("{") >= 0) stopAtNextLine = true;
145
+ const nextPrefixDeclaration = text.toUpperCase().indexOf(PREFIX_KEYWORD, pos);
146
+ if (nextPrefixDeclaration < 0) break;
147
+ const endCh = getLastPrefixPos(i, nextPrefixDeclaration);
148
+ if (endCh > 0) {
149
+ prefixEndChar = endCh;
150
+ prefixEndLine = i;
151
+ pos = prefixEndChar;
152
+ }
153
+ pos++;
154
+ }
155
+ }
156
+ if (prefixEndChar < 0) return;
157
+ const from = doc.line(line + 1).from + prefixStart + PREFIX_KEYWORD.length;
158
+ const to = doc.line(prefixEndLine + 1).from + prefixEndChar;
159
+ if (to <= from) return;
160
+ return { from, to };
161
+ }
162
+
163
+ /** Combined range finder (brace first, then prefix block) for a given 0-based line */
164
+ export function foldRangeForLine(runner: TokenizerRunner, doc: Text, line: number): FoldRange | undefined {
165
+ return braceFoldRange(runner, doc, line) || prefixFoldRange(runner, doc, line);
166
+ }
167
+
168
+ /** Folding extensions: fold service + fold gutter */
169
+ export function sparqlFolding(runner: TokenizerRunner): Extension {
170
+ return [
171
+ foldService.of((state: EditorState, lineStart: number) => {
172
+ const line = state.doc.lineAt(lineStart).number - 1;
173
+ return foldRangeForLine(runner, state.doc, line) || null;
174
+ }),
175
+ foldGutter({
176
+ markerDOM(open) {
177
+ const el = document.createElement("span");
178
+ el.className = `cm-foldMarker CodeMirror-foldgutter-${open ? "open" : "folded"}`;
179
+ el.title = open ? "Fold" : "Unfold";
180
+ return el;
181
+ },
182
+ }),
183
+ ];
184
+ }
185
+
186
+ /** Fold (or unfold, when already folded) the block starting at the given position. */
187
+ export function toggleFoldAt(view: EditorView, pos: number) {
188
+ const line = view.state.doc.lineAt(pos);
189
+ // Unfold when the line is (partly) hidden by a fold
190
+ let unfolded = false;
191
+ foldedRanges(view.state).between(line.from, line.to, (from, to) => {
192
+ if (from >= line.from && from <= line.to) {
193
+ view.dispatch({ effects: unfoldEffect.of({ from, to }) });
194
+ unfolded = true;
195
+ return false;
196
+ }
197
+ return;
198
+ });
199
+ if (unfolded) return;
200
+ const range = foldable(view.state, line.from, line.to);
201
+ if (range) view.dispatch({ effects: foldEffect.of(range) });
202
+ }
203
+
204
+ /** Fold the block starting at the given position (no-op if nothing foldable or already folded) */
205
+ export function foldAt(view: EditorView, pos: number) {
206
+ const line = view.state.doc.lineAt(pos);
207
+ let alreadyFolded = false;
208
+ foldedRanges(view.state).between(line.from, line.to, () => {
209
+ alreadyFolded = true;
210
+ return false;
211
+ });
212
+ if (alreadyFolded) return;
213
+ const range = foldable(view.state, line.from, line.to);
214
+ if (range) view.dispatch({ effects: foldEffect.of(range) });
215
+ }
216
+
217
+ /** Unfold any fold that starts on the line of the given position (no-op if not folded) */
218
+ export function unfoldAt(view: EditorView, pos: number) {
219
+ const line = view.state.doc.lineAt(pos);
220
+ const effects: ReturnType<typeof unfoldEffect.of>[] = [];
221
+ foldedRanges(view.state).between(line.from, line.to, (from, to) => {
222
+ if (from >= line.from && from <= line.to) effects.push(unfoldEffect.of({ from, to }));
223
+ });
224
+ if (effects.length) view.dispatch({ effects });
225
+ }
@@ -0,0 +1,152 @@
1
+ /**
2
+ * Marker gutters replacing the CodeMirror 5 `setGutterMarker` / `clearGutter` API.
3
+ *
4
+ * Two logical marker types are supported: `gutterErrorBar` (syntax errors) and
5
+ * `gutterConstructWarning` (undefined variables in CONSTRUCT templates). Both are rendered
6
+ * in one physical gutter lane to keep gutter width minimal.
7
+ */
8
+ import { EditorState, StateEffect, StateField, RangeSet, Extension } from "@codemirror/state";
9
+ import { EditorView, gutter, GutterMarker } from "@codemirror/view";
10
+
11
+ export type GutterName = "gutterErrorBar" | "gutterConstructWarning";
12
+ export const GUTTER_NAMES: GutterName[] = ["gutterErrorBar", "gutterConstructWarning"];
13
+ const RENDER_GUTTER_NAME: GutterName = "gutterErrorBar";
14
+
15
+ class ElementMarker extends GutterMarker {
16
+ constructor(readonly element: HTMLElement) {
17
+ super();
18
+ }
19
+ eq(other: ElementMarker) {
20
+ return other.element === this.element;
21
+ }
22
+ toDOM() {
23
+ // Return a fresh clone on every render. CodeMirror 6 owns (and may move/recreate) the node
24
+ // it gets from `toDOM`; handing out the same shared element across gutter re-renders detaches
25
+ // it from a previous gutter element, which later crashes with `nextSibling` of null.
26
+ const clone = this.element.cloneNode(true) as HTMLElement;
27
+ clone.onmouseover = this.element.onmouseover;
28
+ clone.onmouseout = this.element.onmouseout;
29
+ return clone;
30
+ }
31
+ }
32
+
33
+ interface SetMarkerSpec {
34
+ gutter: GutterName;
35
+ /** 0-based line number */
36
+ line: number;
37
+ element: HTMLElement | null;
38
+ }
39
+
40
+ const setMarkerEffect = StateEffect.define<SetMarkerSpec>();
41
+ const clearGutterEffect = StateEffect.define<GutterName>();
42
+
43
+ function createMarkerField(name: GutterName): StateField<RangeSet<ElementMarker>> {
44
+ const field = StateField.define<RangeSet<ElementMarker>>({
45
+ create() {
46
+ return RangeSet.empty;
47
+ },
48
+ update(markers, tr) {
49
+ markers = markers.map(tr.changes);
50
+ for (const effect of tr.effects) {
51
+ if (effect.is(clearGutterEffect) && effect.value === name) {
52
+ markers = RangeSet.empty;
53
+ } else if (effect.is(setMarkerEffect) && effect.value.gutter === name) {
54
+ const { line, element } = effect.value;
55
+ if (line < 0 || line >= tr.state.doc.lines) continue;
56
+ const pos = tr.state.doc.line(line + 1).from;
57
+ markers = markers.update({ filter: (from) => from !== pos });
58
+ if (element) {
59
+ markers = markers.update({ add: [new ElementMarker(element).range(pos)] });
60
+ }
61
+ }
62
+ }
63
+ return markers;
64
+ },
65
+ });
66
+ return field;
67
+ }
68
+
69
+ function spacerElement() {
70
+ const el = document.createElement("span");
71
+ el.className = "cm-gutter-spacer";
72
+ el.textContent = "\u00a0";
73
+ return el;
74
+ }
75
+
76
+ function mergeMarkersWithErrorPriority(
77
+ errorMarkers: RangeSet<ElementMarker>,
78
+ warningMarkers: RangeSet<ElementMarker>,
79
+ ): RangeSet<ElementMarker> {
80
+ const byPos = new Map<number, ElementMarker>();
81
+
82
+ const warningIter = warningMarkers.iter();
83
+ while (warningIter.value) {
84
+ byPos.set(warningIter.from, warningIter.value);
85
+ warningIter.next();
86
+ }
87
+
88
+ // Errors overwrite warnings on the same line.
89
+ const errorIter = errorMarkers.iter();
90
+ while (errorIter.value) {
91
+ byPos.set(errorIter.from, errorIter.value);
92
+ errorIter.next();
93
+ }
94
+
95
+ const ranges: ReturnType<ElementMarker["range"]>[] = [];
96
+ const sortedPos = Array.from(byPos.keys()).sort((a, b) => a - b);
97
+ for (const pos of sortedPos) {
98
+ const marker = byPos.get(pos);
99
+ if (marker) ranges.push(marker.range(pos));
100
+ }
101
+ return RangeSet.of(ranges, true);
102
+ }
103
+
104
+ // Fields are defined once so that they are shared between all editor instances.
105
+ const gutterFields = new Map<GutterName, StateField<RangeSet<ElementMarker>>>();
106
+ for (const name of GUTTER_NAMES) {
107
+ gutterFields.set(name, createMarkerField(name));
108
+ }
109
+
110
+ const gutterExtensions: Extension[] = [
111
+ gutterFields.get("gutterErrorBar")!,
112
+ gutterFields.get("gutterConstructWarning")!,
113
+ gutter({
114
+ class: `cm-gutter-${RENDER_GUTTER_NAME} CodeMirror-${RENDER_GUTTER_NAME}`,
115
+ markers: (view) =>
116
+ mergeMarkersWithErrorPriority(
117
+ view.state.field(gutterFields.get("gutterErrorBar")!),
118
+ view.state.field(gutterFields.get("gutterConstructWarning")!),
119
+ ),
120
+ initialSpacer: () => new ElementMarker(spacerElement()),
121
+ }),
122
+ ];
123
+
124
+ /** Extension enabling the yasqe marker gutters */
125
+ export function markerGutters(): Extension {
126
+ return gutterExtensions;
127
+ }
128
+
129
+ /** Set (or remove when `element` is null) the marker of `line` (0-based) in the given gutter */
130
+ export function setGutterMarker(view: EditorView, line: number, name: GutterName, element: HTMLElement | null) {
131
+ view.dispatch({ effects: setMarkerEffect.of({ gutter: name, line, element }) });
132
+ }
133
+
134
+ /** Remove all markers from the given gutter */
135
+ export function clearGutter(view: EditorView, name: GutterName) {
136
+ view.dispatch({ effects: clearGutterEffect.of(name) });
137
+ }
138
+
139
+ /** Returns the marker elements of the gutter, keyed by 0-based line number */
140
+ export function getGutterMarkers(state: EditorState, name: GutterName): Map<number, HTMLElement> {
141
+ const result = new Map<number, HTMLElement>();
142
+ const field = gutterFields.get(name);
143
+ if (!field) return result;
144
+ const set = state.field(field, false);
145
+ if (!set) return result;
146
+ const iter = set.iter();
147
+ while (iter.value) {
148
+ result.set(state.doc.lineAt(iter.from).number - 1, iter.value.element);
149
+ iter.next();
150
+ }
151
+ return result;
152
+ }
@@ -0,0 +1,133 @@
1
+ /**
2
+ * Keymap helpers for the CodeMirror 6 based editor.
3
+ *
4
+ * Yasqe historically exposed key bindings using CodeMirror 5 key names
5
+ * (e.g. "Shift-Ctrl-K", "Cmd-Enter", "Esc"). To stay backwards compatible we
6
+ * accept those names in the `extraKeys` config option and convert them to the
7
+ * CodeMirror 6 keymap format.
8
+ */
9
+ import { Extension, Prec } from "@codemirror/state";
10
+ import { EditorView, KeyBinding, keymap } from "@codemirror/view";
11
+ import { defaultKeymap, historyKeymap, indentWithTab, indentLess, indentMore, insertTab } from "@codemirror/commands";
12
+ import { searchKeymap } from "@codemirror/search";
13
+ import { foldKeymap } from "@codemirror/language";
14
+ import { closeBracketsKeymap } from "@codemirror/autocomplete";
15
+
16
+ /**
17
+ * A key handler in the (CM5-compatible) `extraKeys` config. It receives the
18
+ * Yasqe instance. Returning `false` lets the event propagate to the next
19
+ * binding; anything else is considered "handled".
20
+ */
21
+ export type ExtraKeyHandler<T = any> = (yasqe: T) => any;
22
+ export type ExtraKeys<T = any> = { [key: string]: ExtraKeyHandler<T> | string };
23
+
24
+ const MODIFIERS = new Set(["shift", "ctrl", "cmd", "alt", "mod", "meta"]);
25
+
26
+ const KEY_NAME_MAP: { [cm5: string]: string } = {
27
+ esc: "Escape",
28
+ space: "Space",
29
+ enter: "Enter",
30
+ tab: "Tab",
31
+ backspace: "Backspace",
32
+ delete: "Delete",
33
+ del: "Delete",
34
+ insert: "Insert",
35
+ home: "Home",
36
+ end: "End",
37
+ pageup: "PageUp",
38
+ pagedown: "PageDown",
39
+ left: "ArrowLeft",
40
+ right: "ArrowRight",
41
+ up: "ArrowUp",
42
+ down: "ArrowDown",
43
+ };
44
+
45
+ /**
46
+ * Convert a CodeMirror 5 style key name (e.g. "Shift-Ctrl-K") to a
47
+ * CodeMirror 6 key name ("Shift-Ctrl-k").
48
+ *
49
+ * CM6 uses `event.key` values, so single characters must be lower case
50
+ * (unless shift is pressed), and special keys use their DOM names.
51
+ */
52
+ export function convertKeyName(cm5Name: string): string {
53
+ const parts = cm5Name.split(/-(?!$)/);
54
+ const modifiers: string[] = [];
55
+ let key = parts[parts.length - 1];
56
+ for (let i = 0; i < parts.length - 1; i++) {
57
+ const mod = parts[i].toLowerCase();
58
+ if (mod === "meta") modifiers.push("Cmd");
59
+ else if (MODIFIERS.has(mod)) modifiers.push(parts[i][0].toUpperCase() + mod.slice(1));
60
+ }
61
+ const lowerKey = key.toLowerCase();
62
+ if (KEY_NAME_MAP[lowerKey]) {
63
+ key = KEY_NAME_MAP[lowerKey];
64
+ } else if (/^f\d{1,2}$/i.test(key)) {
65
+ key = key.toUpperCase();
66
+ } else if (key.length === 1) {
67
+ // Letters are lower case in CM6 (they match event.key). When shift is part of
68
+ // the binding CM6 also accepts the lower case form and derives the shifted variant.
69
+ key = key.toLowerCase();
70
+ }
71
+ return [...modifiers, key].join("-");
72
+ }
73
+
74
+ /**
75
+ * Build a keymap extension from a CM5-style `extraKeys` object.
76
+ *
77
+ * @param extraKeys CM5-style key -> handler map
78
+ * @param getYasqe returns the Yasqe instance (passed to the handler)
79
+ * @param commands named commands that string handlers may refer to
80
+ */
81
+ export function extraKeysExtension<T>(
82
+ extraKeys: ExtraKeys<T> | undefined,
83
+ getYasqe: () => T,
84
+ commands: { [name: string]: (yasqe: T) => any } = {},
85
+ ): Extension {
86
+ if (!extraKeys) return [];
87
+ const bindings: KeyBinding[] = [];
88
+ for (const cm5Key of Object.keys(extraKeys)) {
89
+ const handler = extraKeys[cm5Key];
90
+ const fn: ExtraKeyHandler<T> | undefined = typeof handler === "string" ? commands[handler] : handler;
91
+ if (!fn) continue;
92
+ bindings.push({
93
+ key: convertKeyName(cm5Key),
94
+ run: () => {
95
+ const result = fn(getYasqe());
96
+ return result !== false;
97
+ },
98
+ preventDefault: true,
99
+ });
100
+ }
101
+ // Highest precedence so user defined keys win over the built-in ones.
102
+ return Prec.highest(keymap.of(bindings));
103
+ }
104
+
105
+ /**
106
+ * Tab handling that mimics CM5's `tabMode: "indent"` behaviour: Tab indents,
107
+ * Shift-Tab dedents. When `tabMode` is anything else, tab inserts a tab
108
+ * character (or spaces when `indentWithTabs` is false).
109
+ */
110
+ export function tabKeymap(tabMode: string | undefined): Extension {
111
+ if (tabMode === "indent") {
112
+ return keymap.of([
113
+ {
114
+ key: "Tab",
115
+ run: (view: EditorView) => {
116
+ // Indent selection when something is selected, otherwise behave like indentMore.
117
+ return indentMore(view);
118
+ },
119
+ shift: indentLess,
120
+ },
121
+ ]);
122
+ }
123
+ if (tabMode === "default") return keymap.of([indentWithTab]);
124
+ return keymap.of([{ key: "Tab", run: insertTab, shift: indentLess }]);
125
+ }
126
+
127
+ /**
128
+ * The default set of key bindings: standard editing, history, search, folding,
129
+ * bracket closing and autocompletion.
130
+ */
131
+ export function defaultKeymaps(): Extension {
132
+ return keymap.of([...closeBracketsKeymap, ...defaultKeymap, ...searchKeymap, ...historyKeymap, ...foldKeymap]);
133
+ }
@@ -0,0 +1,47 @@
1
+ /**
2
+ * CodeMirror 6 language definition for SPARQL 1.1, built on top of the
3
+ * legacy (stream based) LL(1) tokenizer in ../../grammar/tokenizer.ts.
4
+ *
5
+ * Every token style emitted by the tokenizer gets its own lezer Tag and a
6
+ * matching `cm-<style>` CSS class so the existing style sheets keep working.
7
+ */
8
+ import { StreamLanguage, HighlightStyle, syntaxHighlighting, LanguageSupport } from "@codemirror/language";
9
+ import { Tag } from "@lezer/highlight";
10
+ import type { Extension } from "@codemirror/state";
11
+ import sparqlTokenizer, {
12
+ TOKEN_STYLES,
13
+ TokenStyle,
14
+ State as TokenizerState,
15
+ TokenizerConfig,
16
+ } from "../../grammar/tokenizer";
17
+
18
+ export type { TokenizerState };
19
+
20
+ export const sparqlTags: Record<TokenStyle, Tag> = TOKEN_STYLES.reduce(
21
+ (acc, style) => {
22
+ acc[style] = Tag.define();
23
+ return acc;
24
+ },
25
+ {} as Record<TokenStyle, Tag>,
26
+ );
27
+
28
+ export function createSparqlParser(config: TokenizerConfig = {}) {
29
+ const parser = sparqlTokenizer(config);
30
+ parser.tokenTable = sparqlTags;
31
+ return parser;
32
+ }
33
+
34
+ export const sparqlLanguage = StreamLanguage.define<TokenizerState>(createSparqlParser());
35
+
36
+ /**
37
+ * Highlight style mapping each SPARQL token tag to the classic CodeMirror 5 class name (`cm-keyword`, `cm-atom`, ...).
38
+ */
39
+ export const sparqlHighlightStyle = HighlightStyle.define(
40
+ TOKEN_STYLES.map((style) => ({ tag: sparqlTags[style], class: `cm-${style}` })),
41
+ );
42
+
43
+ export function sparql(): LanguageSupport {
44
+ return new LanguageSupport(sparqlLanguage, [syntaxHighlighting(sparqlHighlightStyle)]);
45
+ }
46
+
47
+ export const sparqlExtensions = (): Extension => sparql();