skir-codemirror-plugin 0.9.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 (72) hide show
  1. package/README.md +126 -0
  2. package/dist/codemirror/create_editor_state.d.ts +20 -0
  3. package/dist/codemirror/create_editor_state.d.ts.map +1 -0
  4. package/dist/codemirror/create_editor_state.js +252 -0
  5. package/dist/codemirror/create_editor_state.js.map +1 -0
  6. package/dist/codemirror/enter_key_handler.d.ts +8 -0
  7. package/dist/codemirror/enter_key_handler.d.ts.map +1 -0
  8. package/dist/codemirror/enter_key_handler.js +181 -0
  9. package/dist/codemirror/enter_key_handler.js.map +1 -0
  10. package/dist/codemirror/json_completion.d.ts +4 -0
  11. package/dist/codemirror/json_completion.d.ts.map +1 -0
  12. package/dist/codemirror/json_completion.js +150 -0
  13. package/dist/codemirror/json_completion.js.map +1 -0
  14. package/dist/codemirror/json_linter.d.ts +4 -0
  15. package/dist/codemirror/json_linter.d.ts.map +1 -0
  16. package/dist/codemirror/json_linter.js +277 -0
  17. package/dist/codemirror/json_linter.js.map +1 -0
  18. package/dist/codemirror/json_state.d.ts +16 -0
  19. package/dist/codemirror/json_state.d.ts.map +1 -0
  20. package/dist/codemirror/json_state.js +124 -0
  21. package/dist/codemirror/json_state.js.map +1 -0
  22. package/dist/codemirror/status_bar.d.ts +3 -0
  23. package/dist/codemirror/status_bar.d.ts.map +1 -0
  24. package/dist/codemirror/status_bar.js +123 -0
  25. package/dist/codemirror/status_bar.js.map +1 -0
  26. package/dist/index.d.ts +4 -0
  27. package/dist/index.d.ts.map +1 -0
  28. package/dist/index.js +2 -0
  29. package/dist/index.js.map +1 -0
  30. package/dist/json/json_parser.d.ts +3 -0
  31. package/dist/json/json_parser.d.ts.map +1 -0
  32. package/dist/json/json_parser.js +414 -0
  33. package/dist/json/json_parser.js.map +1 -0
  34. package/dist/json/json_parser.test.d.ts +2 -0
  35. package/dist/json/json_parser.test.d.ts.map +1 -0
  36. package/dist/json/json_parser.test.js +337 -0
  37. package/dist/json/json_parser.test.js.map +1 -0
  38. package/dist/json/schema_validator.d.ts +3 -0
  39. package/dist/json/schema_validator.d.ts.map +1 -0
  40. package/dist/json/schema_validator.js +525 -0
  41. package/dist/json/schema_validator.js.map +1 -0
  42. package/dist/json/schema_validator.test.d.ts +2 -0
  43. package/dist/json/schema_validator.test.d.ts.map +1 -0
  44. package/dist/json/schema_validator.test.js +212 -0
  45. package/dist/json/schema_validator.test.js.map +1 -0
  46. package/dist/json/to_json.d.ts +6 -0
  47. package/dist/json/to_json.d.ts.map +1 -0
  48. package/dist/json/to_json.js +61 -0
  49. package/dist/json/to_json.js.map +1 -0
  50. package/dist/json/to_json.test.d.ts +2 -0
  51. package/dist/json/to_json.test.d.ts.map +1 -0
  52. package/dist/json/to_json.test.js +128 -0
  53. package/dist/json/to_json.test.js.map +1 -0
  54. package/dist/json/types.d.ts +170 -0
  55. package/dist/json/types.d.ts.map +1 -0
  56. package/dist/json/types.js +2 -0
  57. package/dist/json/types.js.map +1 -0
  58. package/package.json +85 -0
  59. package/src/codemirror/create_editor_state.ts +278 -0
  60. package/src/codemirror/enter_key_handler.ts +232 -0
  61. package/src/codemirror/json_completion.ts +182 -0
  62. package/src/codemirror/json_linter.ts +358 -0
  63. package/src/codemirror/json_state.ts +170 -0
  64. package/src/codemirror/status_bar.ts +137 -0
  65. package/src/index.ts +6 -0
  66. package/src/json/json_parser.test.ts +360 -0
  67. package/src/json/json_parser.ts +461 -0
  68. package/src/json/schema_validator.test.ts +230 -0
  69. package/src/json/schema_validator.ts +558 -0
  70. package/src/json/to_json.test.ts +150 -0
  71. package/src/json/to_json.ts +70 -0
  72. package/src/json/types.ts +254 -0
@@ -0,0 +1,137 @@
1
+ import { EditorView, Panel, showPanel } from "@codemirror/view";
2
+ import { Path, TypeHint } from "../json/types";
3
+ import { jsonStateField } from "./json_state";
4
+
5
+ function createStatusBarPanel(view: EditorView): Panel {
6
+ const dom = document.createElement("div");
7
+ dom.className = "cm-status-bar";
8
+
9
+ function updateCursor(): void {
10
+ const pos = view.state.selection.main.head;
11
+
12
+ const jsonState = view.state.field(jsonStateField, false);
13
+ const validationResult = jsonState?.validationResult;
14
+ const rootTypeHint = validationResult?.rootTypeHint;
15
+ const nodes: Node[] = [];
16
+ if (rootTypeHint) {
17
+ const typeHint = findTypeHint(pos, rootTypeHint);
18
+ if (typeHint) {
19
+ const { pathToTypeHint } = validationResult;
20
+ const builder = new NavigatorNodesBuilder(pathToTypeHint, view);
21
+ builder.appendNodesForPath(typeHint.valueContext.path);
22
+ nodes.push(...builder.nodes);
23
+ nodes.reverse();
24
+ }
25
+ }
26
+ dom.replaceChildren(...nodes);
27
+ }
28
+
29
+ updateCursor();
30
+
31
+ return {
32
+ dom,
33
+ update(update): void {
34
+ if (update.selectionSet) {
35
+ updateCursor();
36
+ }
37
+ },
38
+ top: false,
39
+ };
40
+ }
41
+
42
+ export function statusBar(): ReturnType<typeof showPanel.of> {
43
+ return showPanel.of(createStatusBarPanel);
44
+ }
45
+
46
+ function findTypeHint(pos: number, root: TypeHint): TypeHint | undefined {
47
+ // Check if pos is within the root's segment
48
+ const segment = root.valueContext.value.segment;
49
+ if (pos < segment.start || pos > segment.end) {
50
+ return undefined;
51
+ }
52
+
53
+ // Binary search through childHints to find a child containing pos
54
+ let left = 0;
55
+ let right = root.childHints.length - 1;
56
+ let foundChild: TypeHint | undefined;
57
+
58
+ while (left <= right) {
59
+ const mid = Math.floor((left + right) / 2);
60
+ const child = root.childHints[mid];
61
+ const childSegment = child.valueContext.value.segment;
62
+
63
+ if (pos < childSegment.start) {
64
+ right = mid - 1;
65
+ } else if (pos > childSegment.end) {
66
+ left = mid + 1;
67
+ } else {
68
+ // pos is within this child's segment
69
+ foundChild = child;
70
+ break;
71
+ }
72
+ }
73
+
74
+ // If we found a child containing pos, recursively search deeper
75
+ if (foundChild) {
76
+ const deeperHint = findTypeHint(pos, foundChild);
77
+ return deeperHint !== undefined ? deeperHint : foundChild;
78
+ }
79
+
80
+ // No child contains pos, so root is the deepest match
81
+ return root;
82
+ }
83
+
84
+ class NavigatorNodesBuilder {
85
+ readonly nodes: Node[] = [];
86
+
87
+ constructor(
88
+ private readonly pathToTypeHint: ReadonlyMap<Path, TypeHint>,
89
+ private readonly view: EditorView,
90
+ ) {}
91
+
92
+ appendNodesForPath(path: Path): void {
93
+ const typeHint = this.pathToTypeHint.get(path)!;
94
+ const pos = typeHint.valueContext.value.segment.start;
95
+ const link = document.createElement("a");
96
+ link.className = "cm-status-bar-link";
97
+
98
+ link.addEventListener("click", (e) => {
99
+ e.preventDefault();
100
+ this.view.dispatch({
101
+ selection: { anchor: pos, head: pos },
102
+ scrollIntoView: true,
103
+ });
104
+ this.view.focus();
105
+ });
106
+
107
+ switch (path.kind) {
108
+ case "root": {
109
+ link.append("root");
110
+ this.nodes.push(link);
111
+ break;
112
+ }
113
+ case "array-item": {
114
+ this.appendNodesForPath(path.arrayPath);
115
+ if (path.key != null) {
116
+ link.append(`[${path.index}|${path.key}]`);
117
+ } else {
118
+ link.append(`[${path.index}]`);
119
+ }
120
+ this.nodes.push(link);
121
+ break;
122
+ }
123
+ case "field-value": {
124
+ this.appendNodesForPath(path.structPath);
125
+ link.append(`.${path.fieldName}`);
126
+ this.nodes.push(link);
127
+ break;
128
+ }
129
+ case "variant-value": {
130
+ this.appendNodesForPath(path.enumPath);
131
+ link.append(`.value("${path.variantName}")`);
132
+ this.nodes.push(link);
133
+ break;
134
+ }
135
+ }
136
+ }
137
+ }
package/src/index.ts ADDED
@@ -0,0 +1,6 @@
1
+ export { createEditorState } from "./codemirror/create_editor_state";
2
+ export type {
3
+ CreateEditorStateParams,
4
+ CustomTheme,
5
+ } from "./codemirror/create_editor_state";
6
+ export type { Json } from "./json/types";
@@ -0,0 +1,360 @@
1
+ import { expect } from "buckwheat";
2
+ import { describe, it } from "mocha";
3
+ import { parseJsonValue } from "./json_parser.js";
4
+
5
+ describe("json_parser", () => {
6
+ it("parses true", () => {
7
+ expect(parseJsonValue("true")).toMatch({
8
+ value: {
9
+ kind: "literal",
10
+ jsonCode: "true",
11
+ type: "boolean",
12
+ },
13
+ errors: [],
14
+ edits: [],
15
+ });
16
+ });
17
+
18
+ it("parses false", () => {
19
+ expect(parseJsonValue("false")).toMatch({
20
+ value: {
21
+ kind: "literal",
22
+ jsonCode: "false",
23
+ type: "boolean",
24
+ },
25
+ errors: [],
26
+ edits: [],
27
+ });
28
+ });
29
+
30
+ it("parses null", () => {
31
+ expect(parseJsonValue("null")).toMatch({
32
+ value: {
33
+ kind: "literal",
34
+ jsonCode: "null",
35
+ type: "null",
36
+ },
37
+ errors: [],
38
+ edits: [],
39
+ });
40
+ });
41
+
42
+ it("parses number", () => {
43
+ expect(parseJsonValue("123.45")).toMatch({
44
+ value: {
45
+ kind: "literal",
46
+ jsonCode: "123.45",
47
+ type: "number",
48
+ },
49
+ errors: [],
50
+ edits: [],
51
+ });
52
+ });
53
+
54
+ it("parses string", () => {
55
+ expect(parseJsonValue('"hello"')).toMatch({
56
+ value: {
57
+ kind: "literal",
58
+ jsonCode: '"hello"',
59
+ type: "string",
60
+ },
61
+ errors: [],
62
+ edits: [],
63
+ });
64
+ });
65
+
66
+ it("parses empty array", () => {
67
+ expect(parseJsonValue("[]")).toMatch({
68
+ value: { kind: "array", values: [] },
69
+ errors: [],
70
+ edits: [],
71
+ });
72
+ });
73
+
74
+ it("parses array with values", () => {
75
+ expect(parseJsonValue("[1, true]")).toMatch({
76
+ value: {
77
+ kind: "array",
78
+ values: [
79
+ { kind: "literal", jsonCode: "1", type: "number" },
80
+ { kind: "literal", jsonCode: "true", type: "boolean" },
81
+ ],
82
+ },
83
+ errors: [],
84
+ edits: [
85
+ { segment: { start: 1, end: 1 }, replacement: "\n " },
86
+ { segment: { start: 3, end: 4 }, replacement: "\n " },
87
+ { segment: { start: 8, end: 8 }, replacement: "\n" },
88
+ ],
89
+ });
90
+ });
91
+
92
+ it("parses empty object", () => {
93
+ expect(parseJsonValue("{}")).toMatch({
94
+ value: {
95
+ kind: "object",
96
+ keyValues: {},
97
+ },
98
+ errors: [],
99
+ edits: [],
100
+ });
101
+ });
102
+
103
+ it("parses object with values", () => {
104
+ expect(parseJsonValue('{"a": 1}')).toMatch({
105
+ value: {
106
+ kind: "object",
107
+ keyValues: {
108
+ a: {
109
+ key: "a",
110
+ value: { kind: "literal", jsonCode: "1", type: "number" },
111
+ },
112
+ },
113
+ },
114
+ errors: [],
115
+ edits: [
116
+ { segment: { start: 1, end: 1 }, replacement: "\n " },
117
+ { segment: { start: 7, end: 7 }, replacement: "\n" },
118
+ ],
119
+ });
120
+ });
121
+
122
+ it("parses arrays with whitespace", () => {
123
+ expect(parseJsonValue(" [ 1 ] ")).toMatch({
124
+ value: {
125
+ kind: "array",
126
+ values: [{ kind: "literal", jsonCode: "1" }],
127
+ },
128
+ errors: [],
129
+ edits: [
130
+ { segment: { start: 0, end: 1 }, replacement: "" },
131
+ { segment: { start: 2, end: 3 }, replacement: "\n " },
132
+ { segment: { start: 4, end: 5 }, replacement: "\n" },
133
+ { segment: { start: 6, end: 7 }, replacement: "" },
134
+ ],
135
+ });
136
+ });
137
+
138
+ it("reports error for invalid token", () => {
139
+ expect(parseJsonValue("@")).toMatch({
140
+ value: undefined,
141
+ errors: [
142
+ {
143
+ kind: "error",
144
+ message: "not a token",
145
+ segment: { start: 0, end: 1 },
146
+ },
147
+ ],
148
+ edits: [],
149
+ });
150
+ });
151
+
152
+ it("reports error for unclosed array", () => {
153
+ expect(parseJsonValue("[")).toMatch({
154
+ value: { kind: "array", values: [] },
155
+ errors: [
156
+ {
157
+ kind: "error",
158
+ message: "expected: ']'",
159
+ segment: { start: 1, end: 1 },
160
+ },
161
+ ],
162
+ edits: [{ segment: { start: 1, end: 1 }, replacement: "\n " }],
163
+ });
164
+ });
165
+
166
+ it("parses object with missing value (lenient)", () => {
167
+ expect(
168
+ parseJsonValue(`{
169
+ "foo": true,
170
+ "bar"
171
+ }`),
172
+ ).toMatch({
173
+ value: {
174
+ kind: "object",
175
+ keyValues: {
176
+ foo: {
177
+ key: "foo",
178
+ value: { kind: "literal", jsonCode: "true", type: "boolean" },
179
+ },
180
+ },
181
+ },
182
+ errors: [
183
+ {
184
+ kind: "error",
185
+ message: "expected: ':'",
186
+ segment: { start: 37, end: 38 },
187
+ },
188
+ ],
189
+ });
190
+ });
191
+
192
+ it("parses object with trailing comma (lenient)", () => {
193
+ expect(
194
+ parseJsonValue(`{
195
+ "a": 1,
196
+ "b": 2,
197
+ }`),
198
+ ).toMatch({
199
+ value: {
200
+ kind: "object",
201
+ keyValues: {
202
+ a: {
203
+ key: "a",
204
+ value: { kind: "literal", jsonCode: "1", type: "number" },
205
+ },
206
+ b: {
207
+ key: "b",
208
+ value: { kind: "literal", jsonCode: "2", type: "number" },
209
+ },
210
+ },
211
+ },
212
+ errors: [],
213
+ edits: [
214
+ { segment: { start: 1, end: 8 }, replacement: "\n " },
215
+ { segment: { start: 15, end: 22 }, replacement: "\n " },
216
+ { segment: { start: 28, end: 29 }, replacement: "" },
217
+ { segment: { start: 29, end: 34 }, replacement: "\n" },
218
+ ],
219
+ });
220
+ });
221
+
222
+ it("parses array with trailing comma (lenient)", () => {
223
+ expect(parseJsonValue(`[1, 2,]`)).toMatch({
224
+ value: {
225
+ kind: "array",
226
+ values: [
227
+ { kind: "literal", jsonCode: "1", type: "number" },
228
+ { kind: "literal", jsonCode: "2", type: "number" },
229
+ ],
230
+ },
231
+ errors: [],
232
+ edits: [
233
+ { segment: { start: 1, end: 1 }, replacement: "\n " },
234
+ { segment: { start: 3, end: 4 }, replacement: "\n " },
235
+ { segment: { start: 5, end: 6 }, replacement: "" },
236
+ { segment: { start: 6, end: 6 }, replacement: "\n" },
237
+ ],
238
+ });
239
+ });
240
+
241
+ it("parses array without commas (adds commas via edits)", () => {
242
+ expect(parseJsonValue("[1 2 3]")).toMatch({
243
+ value: {
244
+ kind: "array",
245
+ values: [
246
+ { kind: "literal", jsonCode: "1", type: "number" },
247
+ { kind: "literal", jsonCode: "2", type: "number" },
248
+ { kind: "literal", jsonCode: "3", type: "number" },
249
+ ],
250
+ },
251
+ errors: [],
252
+ edits: [
253
+ { segment: { start: 1, end: 1 }, replacement: "\n " },
254
+ { segment: { start: 2, end: 3 }, replacement: ",\n " },
255
+ { segment: { start: 4, end: 5 }, replacement: ",\n " },
256
+ { segment: { start: 6, end: 6 }, replacement: "\n" },
257
+ ],
258
+ });
259
+ });
260
+
261
+ it("parses object without commas (adds commas via edits)", () => {
262
+ expect(parseJsonValue('{"a": 1 "b": 2}')).toMatch({
263
+ value: {
264
+ kind: "object",
265
+ keyValues: {
266
+ a: {
267
+ key: "a",
268
+ value: { kind: "literal", jsonCode: "1", type: "number" },
269
+ },
270
+ b: {
271
+ key: "b",
272
+ value: { kind: "literal", jsonCode: "2", type: "number" },
273
+ },
274
+ },
275
+ },
276
+ errors: [],
277
+ edits: [
278
+ { segment: { start: 1, end: 1 }, replacement: "\n " },
279
+ { segment: { start: 7, end: 8 }, replacement: ",\n " },
280
+ { segment: { start: 14, end: 14 }, replacement: "\n" },
281
+ ],
282
+ });
283
+ });
284
+
285
+ it("parses object with key missing colon (no infinite loop)", () => {
286
+ const result = parseJsonValue(`
287
+ {
288
+ "x": 0,
289
+ ""
290
+ "y": 0
291
+ }`);
292
+ // Should parse without hanging
293
+ // Note: The empty key without colon causes skip() to consume "y": 0
294
+ expect(result).toMatch({
295
+ value: {
296
+ kind: "object",
297
+ keyValues: {
298
+ x: {
299
+ key: "x",
300
+ value: { kind: "literal", jsonCode: "0", type: "number" },
301
+ },
302
+ },
303
+ allKeys: [
304
+ {
305
+ key: "x",
306
+ },
307
+ {
308
+ key: "",
309
+ },
310
+ {
311
+ key: "y",
312
+ },
313
+ ],
314
+ },
315
+ errors: [
316
+ {
317
+ kind: "error",
318
+ message: "expected: ':'",
319
+ },
320
+ ],
321
+ });
322
+ });
323
+
324
+ it("parses object with double comma (no infinite loop)", () => {
325
+ const result = parseJsonValue('{"x": 1, , "y": 2}');
326
+ // Should parse without hanging - the double comma should be handled
327
+ expect(result.value).toMatch({
328
+ kind: "object",
329
+ keyValues: {
330
+ x: {
331
+ key: "x",
332
+ value: { kind: "literal", jsonCode: "1", type: "number" },
333
+ },
334
+ y: {
335
+ key: "y",
336
+ value: { kind: "literal", jsonCode: "2", type: "number" },
337
+ },
338
+ },
339
+ });
340
+ // Should have error for the unexpected comma
341
+ expect(result.errors.length > 0).toBe(true);
342
+ });
343
+
344
+ it("parses array with invalid element followed by comma (no infinite loop)", () => {
345
+ const result = parseJsonValue(`[
346
+ 1,
347
+ @
348
+ 2
349
+ ]`);
350
+ // Invalid token @ causes tokenization error
351
+ expect(result.value).toBe(undefined);
352
+ // Should have error for invalid token
353
+ expect(result.errors).toMatch([
354
+ {
355
+ kind: "error",
356
+ message: "not a token",
357
+ },
358
+ ]);
359
+ });
360
+ });