skir-codemirror-plugin 1.0.2 → 1.0.3
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 +15 -6
- package/dist/codemirror/create_editor_state.d.ts +4 -2
- package/dist/codemirror/create_editor_state.d.ts.map +1 -1
- package/dist/codemirror/create_editor_state.js +178 -31
- package/dist/codemirror/create_editor_state.js.map +1 -1
- package/dist/codemirror/json_linter.d.ts.map +1 -1
- package/dist/codemirror/json_linter.js +67 -3
- package/dist/codemirror/json_linter.js.map +1 -1
- package/dist/codemirror/json_state.d.ts +5 -1
- package/dist/codemirror/json_state.d.ts.map +1 -1
- package/dist/codemirror/json_state.js +26 -1
- package/dist/codemirror/json_state.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/json/json_parser.js +35 -1
- package/dist/json/json_parser.js.map +1 -1
- package/dist/json/json_parser.test.js +48 -0
- package/dist/json/json_parser.test.js.map +1 -1
- package/dist/json/schema_validator.d.ts.map +1 -1
- package/dist/json/schema_validator.js +16 -11
- package/dist/json/schema_validator.js.map +1 -1
- package/dist/json/schema_validator.test.js +71 -0
- package/dist/json/schema_validator.test.js.map +1 -1
- package/dist/json/to_json.d.ts +1 -1
- package/dist/json/to_json.d.ts.map +1 -1
- package/dist/json/to_json.js +3 -3
- package/dist/json/to_json.js.map +1 -1
- package/dist/json/types.d.ts +4 -0
- package/dist/json/types.d.ts.map +1 -1
- package/dist/json/types.js.map +1 -1
- package/package.json +2 -3
- package/src/codemirror/create_editor_state.ts +272 -31
- package/src/codemirror/json_linter.ts +89 -4
- package/src/codemirror/json_state.ts +44 -1
- package/src/index.ts +1 -0
- package/src/json/json_parser.test.ts +51 -0
- package/src/json/json_parser.ts +37 -1
- package/src/json/schema_validator.test.ts +75 -0
- package/src/json/schema_validator.ts +20 -10
- package/src/json/to_json.ts +3 -3
- package/src/json/types.ts +7 -0
package/src/json/json_parser.ts
CHANGED
|
@@ -33,6 +33,7 @@ export function parseJsonValue(input: string): JsonParseResult {
|
|
|
33
33
|
interface JsonToken {
|
|
34
34
|
segment: Segment;
|
|
35
35
|
jsonCode: string;
|
|
36
|
+
indent: number;
|
|
36
37
|
}
|
|
37
38
|
|
|
38
39
|
interface JsonTokens {
|
|
@@ -43,16 +44,29 @@ interface JsonTokens {
|
|
|
43
44
|
function tokenize(input: string): JsonTokens | JsonError {
|
|
44
45
|
const tokens: JsonToken[] = [];
|
|
45
46
|
let pos = 0;
|
|
47
|
+
let lineIndent = 0;
|
|
48
|
+
let lineHasContent = false;
|
|
46
49
|
|
|
47
50
|
const whitespaceRegex = /[ \t\r\n]*/y;
|
|
48
51
|
const tokenRegex =
|
|
49
52
|
/([[\]{}:,]|(-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?)|false|true|null|("(((?=\\)\\(["\\/ bfnrt]|u[0-9a-fA-F]{4}))|[^"\\\0-\x1F\x7F]+)*")|$)/y;
|
|
50
53
|
|
|
51
54
|
while (true) {
|
|
55
|
+
const beforeWhitespacePos = pos;
|
|
52
56
|
whitespaceRegex.lastIndex = pos;
|
|
53
57
|
whitespaceRegex.exec(input);
|
|
54
58
|
pos = whitespaceRegex.lastIndex;
|
|
55
59
|
|
|
60
|
+
for (let i = beforeWhitespacePos; i < pos; ++i) {
|
|
61
|
+
const ch = input[i];
|
|
62
|
+
if (ch === "\n") {
|
|
63
|
+
lineIndent = 0;
|
|
64
|
+
lineHasContent = false;
|
|
65
|
+
} else if (!lineHasContent && (ch === " " || ch === "\t")) {
|
|
66
|
+
lineIndent++;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
56
70
|
tokenRegex.lastIndex = pos;
|
|
57
71
|
const tokenMatch = tokenRegex.exec(input);
|
|
58
72
|
if (tokenMatch) {
|
|
@@ -61,9 +75,16 @@ function tokenize(input: string): JsonTokens | JsonError {
|
|
|
61
75
|
start: pos,
|
|
62
76
|
end: pos + tokenText.length,
|
|
63
77
|
};
|
|
64
|
-
const token: JsonToken = {
|
|
78
|
+
const token: JsonToken = {
|
|
79
|
+
segment,
|
|
80
|
+
jsonCode: tokenText,
|
|
81
|
+
indent: lineIndent,
|
|
82
|
+
};
|
|
65
83
|
pos = tokenRegex.lastIndex;
|
|
66
84
|
tokens.push(token);
|
|
85
|
+
if (tokenText !== "") {
|
|
86
|
+
lineHasContent = true;
|
|
87
|
+
}
|
|
67
88
|
if (tokenText === "") {
|
|
68
89
|
return {
|
|
69
90
|
kind: "tokens",
|
|
@@ -96,6 +117,8 @@ class JsonParser {
|
|
|
96
117
|
const token = this.peekToken();
|
|
97
118
|
const firstChar = token.jsonCode ? token.jsonCode[0] : "";
|
|
98
119
|
|
|
120
|
+
const { indent } = token;
|
|
121
|
+
|
|
99
122
|
switch (firstChar) {
|
|
100
123
|
case "[":
|
|
101
124
|
return this.parseArray();
|
|
@@ -109,6 +132,7 @@ class JsonParser {
|
|
|
109
132
|
segment: token.segment,
|
|
110
133
|
jsonCode: "null",
|
|
111
134
|
type: "null",
|
|
135
|
+
indent,
|
|
112
136
|
};
|
|
113
137
|
case "f":
|
|
114
138
|
this.nextToken();
|
|
@@ -118,6 +142,7 @@ class JsonParser {
|
|
|
118
142
|
segment: token.segment,
|
|
119
143
|
jsonCode: "false",
|
|
120
144
|
type: "boolean",
|
|
145
|
+
indent,
|
|
121
146
|
};
|
|
122
147
|
case "t":
|
|
123
148
|
this.nextToken();
|
|
@@ -127,6 +152,7 @@ class JsonParser {
|
|
|
127
152
|
segment: token.segment,
|
|
128
153
|
jsonCode: "true",
|
|
129
154
|
type: "boolean",
|
|
155
|
+
indent,
|
|
130
156
|
};
|
|
131
157
|
case '"':
|
|
132
158
|
this.nextToken();
|
|
@@ -136,6 +162,7 @@ class JsonParser {
|
|
|
136
162
|
segment: token.segment,
|
|
137
163
|
jsonCode: token.jsonCode,
|
|
138
164
|
type: "string",
|
|
165
|
+
indent,
|
|
139
166
|
};
|
|
140
167
|
case "0":
|
|
141
168
|
case "1":
|
|
@@ -155,6 +182,7 @@ class JsonParser {
|
|
|
155
182
|
segment: token.segment,
|
|
156
183
|
jsonCode: token.jsonCode,
|
|
157
184
|
type: "number",
|
|
185
|
+
indent,
|
|
158
186
|
};
|
|
159
187
|
}
|
|
160
188
|
|
|
@@ -169,6 +197,7 @@ class JsonParser {
|
|
|
169
197
|
|
|
170
198
|
private parseArray(): JsonArray {
|
|
171
199
|
const leftBracket = this.nextToken();
|
|
200
|
+
const { indent } = leftBracket;
|
|
172
201
|
const values: JsonValue[] = [];
|
|
173
202
|
while (true) {
|
|
174
203
|
if (this.peekToken().jsonCode === "]") {
|
|
@@ -181,6 +210,7 @@ class JsonParser {
|
|
|
181
210
|
end: rightBracket.segment.end,
|
|
182
211
|
},
|
|
183
212
|
values,
|
|
213
|
+
indent,
|
|
184
214
|
};
|
|
185
215
|
}
|
|
186
216
|
if (this.peekToken().jsonCode === "}") {
|
|
@@ -198,6 +228,7 @@ class JsonParser {
|
|
|
198
228
|
end: wrongBracket.segment.end,
|
|
199
229
|
},
|
|
200
230
|
values,
|
|
231
|
+
indent,
|
|
201
232
|
};
|
|
202
233
|
}
|
|
203
234
|
if (this.peekToken().jsonCode === "") {
|
|
@@ -211,6 +242,7 @@ class JsonParser {
|
|
|
211
242
|
end: this.peekToken().segment.start,
|
|
212
243
|
},
|
|
213
244
|
values,
|
|
245
|
+
indent,
|
|
214
246
|
};
|
|
215
247
|
}
|
|
216
248
|
const value = this.parseValueOrSkip();
|
|
@@ -233,6 +265,7 @@ class JsonParser {
|
|
|
233
265
|
|
|
234
266
|
private parseObject(): JsonObject {
|
|
235
267
|
const leftBracket = this.nextToken();
|
|
268
|
+
const { indent } = leftBracket;
|
|
236
269
|
const keyValues: { [key: string]: JsonKeyValue } = {};
|
|
237
270
|
const allKeys: JsonKey[] = [];
|
|
238
271
|
while (true) {
|
|
@@ -247,6 +280,7 @@ class JsonParser {
|
|
|
247
280
|
},
|
|
248
281
|
keyValues,
|
|
249
282
|
allKeys,
|
|
283
|
+
indent,
|
|
250
284
|
};
|
|
251
285
|
}
|
|
252
286
|
if (this.peekToken().jsonCode === "]") {
|
|
@@ -265,6 +299,7 @@ class JsonParser {
|
|
|
265
299
|
},
|
|
266
300
|
keyValues,
|
|
267
301
|
allKeys,
|
|
302
|
+
indent,
|
|
268
303
|
};
|
|
269
304
|
}
|
|
270
305
|
if (this.peekToken().jsonCode === "") {
|
|
@@ -279,6 +314,7 @@ class JsonParser {
|
|
|
279
314
|
},
|
|
280
315
|
keyValues,
|
|
281
316
|
allKeys,
|
|
317
|
+
indent,
|
|
282
318
|
};
|
|
283
319
|
}
|
|
284
320
|
const keyToken = this.peekToken();
|
|
@@ -189,6 +189,81 @@ describe("schema_validator", () => {
|
|
|
189
189
|
]);
|
|
190
190
|
});
|
|
191
191
|
|
|
192
|
+
it("stores enum definition in type hint for UNKNOWN enum literal", () => {
|
|
193
|
+
const schema: TypeDefinition = {
|
|
194
|
+
type: { kind: "record", value: "MyEnum" },
|
|
195
|
+
records: [
|
|
196
|
+
{
|
|
197
|
+
kind: "enum",
|
|
198
|
+
id: "MyEnum",
|
|
199
|
+
variants: [
|
|
200
|
+
{ name: "First", number: 1 },
|
|
201
|
+
{ name: "Second", number: 2 },
|
|
202
|
+
],
|
|
203
|
+
},
|
|
204
|
+
],
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
const result = validateSchema(parse('"UNKNOWN"'), schema);
|
|
208
|
+
expect(result.errors).toMatch([]);
|
|
209
|
+
if (!result.rootTypeHint) {
|
|
210
|
+
throw new Error("Expected root type hint");
|
|
211
|
+
}
|
|
212
|
+
expect(result.rootTypeHint.enumDefinition).toMatch({
|
|
213
|
+
kind: "enum",
|
|
214
|
+
id: "MyEnum",
|
|
215
|
+
variants: [
|
|
216
|
+
{ name: "First", number: 1 },
|
|
217
|
+
{ name: "Second", number: 2 },
|
|
218
|
+
],
|
|
219
|
+
});
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
it("stores enum definition in nested UNKNOWN enum field hint", () => {
|
|
223
|
+
const schema: TypeDefinition = {
|
|
224
|
+
type: { kind: "record", value: "MyStruct" },
|
|
225
|
+
records: [
|
|
226
|
+
{
|
|
227
|
+
kind: "struct",
|
|
228
|
+
id: "MyStruct",
|
|
229
|
+
fields: [
|
|
230
|
+
{
|
|
231
|
+
name: "status",
|
|
232
|
+
number: 1,
|
|
233
|
+
type: { kind: "record", value: "MyEnum" },
|
|
234
|
+
},
|
|
235
|
+
],
|
|
236
|
+
},
|
|
237
|
+
{
|
|
238
|
+
kind: "enum",
|
|
239
|
+
id: "MyEnum",
|
|
240
|
+
variants: [
|
|
241
|
+
{ name: "First", number: 1 },
|
|
242
|
+
{ name: "Second", number: 2 },
|
|
243
|
+
],
|
|
244
|
+
},
|
|
245
|
+
],
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
const result = validateSchema(parse('{"status": "UNKNOWN"}'), schema);
|
|
249
|
+
expect(result.errors).toMatch([]);
|
|
250
|
+
if (!result.rootTypeHint) {
|
|
251
|
+
throw new Error("Expected root type hint");
|
|
252
|
+
}
|
|
253
|
+
if (result.rootTypeHint.childHints.length !== 1) {
|
|
254
|
+
throw new Error("Expected one child type hint");
|
|
255
|
+
}
|
|
256
|
+
const statusHint = result.rootTypeHint.childHints[0];
|
|
257
|
+
expect(statusHint.enumDefinition).toMatch({
|
|
258
|
+
kind: "enum",
|
|
259
|
+
id: "MyEnum",
|
|
260
|
+
variants: [
|
|
261
|
+
{ name: "First", number: 1 },
|
|
262
|
+
{ name: "Second", number: 2 },
|
|
263
|
+
],
|
|
264
|
+
});
|
|
265
|
+
});
|
|
266
|
+
|
|
192
267
|
it("validates enum (object with kind)", () => {
|
|
193
268
|
const schema: TypeDefinition = {
|
|
194
269
|
type: { kind: "record", value: "MyEnum" },
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { primitiveSerializer } from "skir-client";
|
|
2
2
|
import { toJson } from "./to_json";
|
|
3
3
|
import type {
|
|
4
|
+
EnumDefinition,
|
|
4
5
|
FieldDefinition,
|
|
5
6
|
Hint,
|
|
6
7
|
JsonError,
|
|
@@ -60,7 +61,9 @@ class SchemaValidator {
|
|
|
60
61
|
const { idToRecordDef, typeHintStack } = this;
|
|
61
62
|
value.expectedType = schema;
|
|
62
63
|
// For every call to pushTypeHint() there emust be one call to typeHintStack.pop()
|
|
63
|
-
const pushTypeHint = (
|
|
64
|
+
const pushTypeHint = (options?: {
|
|
65
|
+
enumDefinition?: EnumDefinition;
|
|
66
|
+
}): void => {
|
|
64
67
|
const typeDesc = getTypeDesc(optionalSchema ?? schema);
|
|
65
68
|
const typeDoc = getTypeDoc(schema, idToRecordDef);
|
|
66
69
|
const message = [typeDesc];
|
|
@@ -78,6 +81,7 @@ class SchemaValidator {
|
|
|
78
81
|
message: message.length === 1 ? message[0] : message,
|
|
79
82
|
valueContext: { value, path },
|
|
80
83
|
childHints: [],
|
|
84
|
+
enumDefinition: options?.enumDefinition,
|
|
81
85
|
};
|
|
82
86
|
if (typeHintStack.length) {
|
|
83
87
|
const topOfStack = typeHintStack[typeHintStack.length - 1];
|
|
@@ -196,8 +200,14 @@ class SchemaValidator {
|
|
|
196
200
|
} else if (value.kind === "literal" && value.type === "string") {
|
|
197
201
|
const name = JSON.parse(value.jsonCode);
|
|
198
202
|
const fieldDef = nameToVariantDef[name];
|
|
199
|
-
if (
|
|
200
|
-
|
|
203
|
+
if (fieldDef && fieldDef.type) {
|
|
204
|
+
this.errors.push({
|
|
205
|
+
kind: "error",
|
|
206
|
+
segment: value.segment,
|
|
207
|
+
message: "Expected: uppercase variant name",
|
|
208
|
+
});
|
|
209
|
+
} else if (name === "UNKNOWN" || fieldDef) {
|
|
210
|
+
pushTypeHint({ enumDefinition: recordDef });
|
|
201
211
|
typeHintStack.pop();
|
|
202
212
|
} else {
|
|
203
213
|
this.errors.push({
|
|
@@ -492,13 +502,13 @@ function isFloat(value: JsonValue): boolean {
|
|
|
492
502
|
}
|
|
493
503
|
if (value.type === "number") {
|
|
494
504
|
return true;
|
|
495
|
-
} else if (
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
505
|
+
} else if (
|
|
506
|
+
value.type === "string" &&
|
|
507
|
+
(value.jsonCode === '"NaN"' ||
|
|
508
|
+
value.jsonCode === '"Infinity"' ||
|
|
509
|
+
value.jsonCode === '"-Infinity"')
|
|
510
|
+
) {
|
|
511
|
+
return true;
|
|
502
512
|
} else {
|
|
503
513
|
return false;
|
|
504
514
|
}
|
package/src/json/to_json.ts
CHANGED
|
@@ -22,7 +22,7 @@ export function toJson(value: JsonValue): Json {
|
|
|
22
22
|
export function makeJsonTemplate(
|
|
23
23
|
type: TypeSignature,
|
|
24
24
|
idToRecordDef: { [id: string]: RecordDefinition },
|
|
25
|
-
|
|
25
|
+
nested?: "nested",
|
|
26
26
|
): Json {
|
|
27
27
|
switch (type.kind) {
|
|
28
28
|
case "array": {
|
|
@@ -34,13 +34,13 @@ export function makeJsonTemplate(
|
|
|
34
34
|
case "record": {
|
|
35
35
|
const recordDef = idToRecordDef[type.value]!;
|
|
36
36
|
if (recordDef.kind === "struct") {
|
|
37
|
-
if (
|
|
37
|
+
if (nested) {
|
|
38
38
|
return {};
|
|
39
39
|
}
|
|
40
40
|
return Object.fromEntries(
|
|
41
41
|
recordDef.fields.map((field) => [
|
|
42
42
|
field.name,
|
|
43
|
-
makeJsonTemplate(field.type!, idToRecordDef, "
|
|
43
|
+
makeJsonTemplate(field.type!, idToRecordDef, "nested"),
|
|
44
44
|
]),
|
|
45
45
|
);
|
|
46
46
|
} else {
|
package/src/json/types.ts
CHANGED
|
@@ -39,6 +39,7 @@ export interface JsonArray {
|
|
|
39
39
|
/// From '[' to ']' included.
|
|
40
40
|
readonly segment: Segment;
|
|
41
41
|
readonly values: JsonValue[];
|
|
42
|
+
readonly indent: number;
|
|
42
43
|
expectedType?: TypeSignature;
|
|
43
44
|
}
|
|
44
45
|
|
|
@@ -63,6 +64,7 @@ export interface JsonObject {
|
|
|
63
64
|
readonly keyValues: { [key: string]: JsonKeyValue };
|
|
64
65
|
/// Includes "broken" keys which produced a parsing error.
|
|
65
66
|
readonly allKeys: readonly JsonKey[];
|
|
67
|
+
readonly indent: number;
|
|
66
68
|
expectedType?: TypeSignature;
|
|
67
69
|
}
|
|
68
70
|
|
|
@@ -73,6 +75,7 @@ export interface JsonLiteral {
|
|
|
73
75
|
readonly segment: Segment;
|
|
74
76
|
readonly jsonCode: string;
|
|
75
77
|
readonly type: "boolean" | "null" | "number" | "string";
|
|
78
|
+
readonly indent: number;
|
|
76
79
|
expectedType?: TypeSignature;
|
|
77
80
|
}
|
|
78
81
|
|
|
@@ -223,6 +226,10 @@ export interface TypeHint {
|
|
|
223
226
|
readonly valueContext: JsonValueContext;
|
|
224
227
|
/// In order. All are included in 'valueContext.value.segment'.
|
|
225
228
|
readonly childHints: readonly TypeHint[];
|
|
229
|
+
/// Set if the expected type is an enum and the value is a literal string.
|
|
230
|
+
/// In that case, the editor can show a dropdown with the possible variants
|
|
231
|
+
/// of the enum.
|
|
232
|
+
readonly enumDefinition?: EnumDefinition;
|
|
226
233
|
}
|
|
227
234
|
|
|
228
235
|
export type Hint =
|