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.
- package/README.md +126 -0
- package/dist/codemirror/create_editor_state.d.ts +20 -0
- package/dist/codemirror/create_editor_state.d.ts.map +1 -0
- package/dist/codemirror/create_editor_state.js +252 -0
- package/dist/codemirror/create_editor_state.js.map +1 -0
- package/dist/codemirror/enter_key_handler.d.ts +8 -0
- package/dist/codemirror/enter_key_handler.d.ts.map +1 -0
- package/dist/codemirror/enter_key_handler.js +181 -0
- package/dist/codemirror/enter_key_handler.js.map +1 -0
- package/dist/codemirror/json_completion.d.ts +4 -0
- package/dist/codemirror/json_completion.d.ts.map +1 -0
- package/dist/codemirror/json_completion.js +150 -0
- package/dist/codemirror/json_completion.js.map +1 -0
- package/dist/codemirror/json_linter.d.ts +4 -0
- package/dist/codemirror/json_linter.d.ts.map +1 -0
- package/dist/codemirror/json_linter.js +277 -0
- package/dist/codemirror/json_linter.js.map +1 -0
- package/dist/codemirror/json_state.d.ts +16 -0
- package/dist/codemirror/json_state.d.ts.map +1 -0
- package/dist/codemirror/json_state.js +124 -0
- package/dist/codemirror/json_state.js.map +1 -0
- package/dist/codemirror/status_bar.d.ts +3 -0
- package/dist/codemirror/status_bar.d.ts.map +1 -0
- package/dist/codemirror/status_bar.js +123 -0
- package/dist/codemirror/status_bar.js.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -0
- package/dist/json/json_parser.d.ts +3 -0
- package/dist/json/json_parser.d.ts.map +1 -0
- package/dist/json/json_parser.js +414 -0
- package/dist/json/json_parser.js.map +1 -0
- package/dist/json/json_parser.test.d.ts +2 -0
- package/dist/json/json_parser.test.d.ts.map +1 -0
- package/dist/json/json_parser.test.js +337 -0
- package/dist/json/json_parser.test.js.map +1 -0
- package/dist/json/schema_validator.d.ts +3 -0
- package/dist/json/schema_validator.d.ts.map +1 -0
- package/dist/json/schema_validator.js +525 -0
- package/dist/json/schema_validator.js.map +1 -0
- package/dist/json/schema_validator.test.d.ts +2 -0
- package/dist/json/schema_validator.test.d.ts.map +1 -0
- package/dist/json/schema_validator.test.js +212 -0
- package/dist/json/schema_validator.test.js.map +1 -0
- package/dist/json/to_json.d.ts +6 -0
- package/dist/json/to_json.d.ts.map +1 -0
- package/dist/json/to_json.js +61 -0
- package/dist/json/to_json.js.map +1 -0
- package/dist/json/to_json.test.d.ts +2 -0
- package/dist/json/to_json.test.d.ts.map +1 -0
- package/dist/json/to_json.test.js +128 -0
- package/dist/json/to_json.test.js.map +1 -0
- package/dist/json/types.d.ts +170 -0
- package/dist/json/types.d.ts.map +1 -0
- package/dist/json/types.js +2 -0
- package/dist/json/types.js.map +1 -0
- package/package.json +85 -0
- package/src/codemirror/create_editor_state.ts +278 -0
- package/src/codemirror/enter_key_handler.ts +232 -0
- package/src/codemirror/json_completion.ts +182 -0
- package/src/codemirror/json_linter.ts +358 -0
- package/src/codemirror/json_state.ts +170 -0
- package/src/codemirror/status_bar.ts +137 -0
- package/src/index.ts +6 -0
- package/src/json/json_parser.test.ts +360 -0
- package/src/json/json_parser.ts +461 -0
- package/src/json/schema_validator.test.ts +230 -0
- package/src/json/schema_validator.ts +558 -0
- package/src/json/to_json.test.ts +150 -0
- package/src/json/to_json.ts +70 -0
- 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,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
|
+
});
|