origamic 0.1.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/.prettierrc +4 -0
- package/eslint.config.mjs +37 -0
- package/package.json +34 -0
- package/pre_commit.sh +10 -0
- package/pre_commit_setup.sh +32 -0
- package/src/TODO +1 -0
- package/src/index.ts +1 -0
- package/src/parser.test.ts +1567 -0
- package/src/parser.ts +1359 -0
- package/src/types.ts +142 -0
- package/tsconfig.build.json +4 -0
- package/tsconfig.eslint.json +4 -0
- package/tsconfig.json +24 -0
|
@@ -0,0 +1,1567 @@
|
|
|
1
|
+
import { expect } from "buckwheat";
|
|
2
|
+
import { describe, it } from "mocha";
|
|
3
|
+
|
|
4
|
+
import { parseTemplateFile } from "./parser.js";
|
|
5
|
+
|
|
6
|
+
describe("parseTemplateFile", () => {
|
|
7
|
+
it("parses a template header, expressions, schema, and example", () => {
|
|
8
|
+
const parsed = parseTemplateFile(
|
|
9
|
+
[
|
|
10
|
+
"Foo/Bar:",
|
|
11
|
+
" Hello $name and ${ value : hide }.",
|
|
12
|
+
" $<schema>",
|
|
13
|
+
" interface Out { value: string; }",
|
|
14
|
+
" $</schema>",
|
|
15
|
+
" $<example>",
|
|
16
|
+
' {"value":"x$y"}',
|
|
17
|
+
" $</example>",
|
|
18
|
+
].join("\n"),
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
expect(parsed).toMatch({
|
|
22
|
+
errors: [],
|
|
23
|
+
value: {
|
|
24
|
+
templates: [
|
|
25
|
+
{
|
|
26
|
+
name: { text: "Foo" },
|
|
27
|
+
variantName: { text: "Bar" },
|
|
28
|
+
parameters: ["name", "value"],
|
|
29
|
+
pieces: [
|
|
30
|
+
{
|
|
31
|
+
kind: "text",
|
|
32
|
+
text: "Hello ",
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
kind: "expression",
|
|
36
|
+
expression: "name",
|
|
37
|
+
hide: false,
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
kind: "text",
|
|
41
|
+
text: " and ",
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
kind: "expression",
|
|
45
|
+
expression: "value",
|
|
46
|
+
hide: true,
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
kind: "text",
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
kind: "schema",
|
|
53
|
+
schema: {
|
|
54
|
+
declarations: [
|
|
55
|
+
{
|
|
56
|
+
kind: "interface",
|
|
57
|
+
name: { text: "Out" },
|
|
58
|
+
},
|
|
59
|
+
],
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
kind: "text",
|
|
64
|
+
text: "\n",
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
kind: "example",
|
|
68
|
+
example: {
|
|
69
|
+
value: {
|
|
70
|
+
kind: "object",
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
],
|
|
75
|
+
},
|
|
76
|
+
],
|
|
77
|
+
},
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it("requires end of line after ':'", () => {
|
|
82
|
+
const parsed = parseTemplateFile(["Foo: ", " $<schema/>"].join("\n"));
|
|
83
|
+
|
|
84
|
+
expect(parsed).toMatch({
|
|
85
|
+
errors: [
|
|
86
|
+
{
|
|
87
|
+
message: "Expected end of line after ':'",
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
message: "Template header line must not be indented",
|
|
91
|
+
},
|
|
92
|
+
],
|
|
93
|
+
value: {
|
|
94
|
+
templates: [],
|
|
95
|
+
},
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it("reports body indentation mismatch", () => {
|
|
100
|
+
const parsed = parseTemplateFile(
|
|
101
|
+
["Foo:", " first", " second", " $<schema/>"].join("\n"),
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
expect(parsed).toMatch({
|
|
105
|
+
errors: [
|
|
106
|
+
{
|
|
107
|
+
message: "Expected line to start with template indent ' '",
|
|
108
|
+
},
|
|
109
|
+
],
|
|
110
|
+
value: {
|
|
111
|
+
templates: [
|
|
112
|
+
{
|
|
113
|
+
name: { text: "Foo" },
|
|
114
|
+
parameters: [],
|
|
115
|
+
pieces: [
|
|
116
|
+
{
|
|
117
|
+
kind: "text",
|
|
118
|
+
text: "first\nsecond\n",
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
kind: "schema",
|
|
122
|
+
text: "",
|
|
123
|
+
},
|
|
124
|
+
],
|
|
125
|
+
},
|
|
126
|
+
],
|
|
127
|
+
},
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it("requires exactly one schema section", () => {
|
|
132
|
+
const none = parseTemplateFile(["Foo:", " plain text"].join("\n"));
|
|
133
|
+
|
|
134
|
+
const many = parseTemplateFile(
|
|
135
|
+
["Foo:", " $<schema/>", " $<schema/>"].join("\n"),
|
|
136
|
+
);
|
|
137
|
+
|
|
138
|
+
expect(none).toMatch({
|
|
139
|
+
errors: [
|
|
140
|
+
{
|
|
141
|
+
message: "Template must contain exactly one schema section",
|
|
142
|
+
},
|
|
143
|
+
],
|
|
144
|
+
value: {
|
|
145
|
+
templates: [
|
|
146
|
+
{
|
|
147
|
+
name: { text: "Foo" },
|
|
148
|
+
pieces: [
|
|
149
|
+
{
|
|
150
|
+
kind: "text",
|
|
151
|
+
text: "plain text",
|
|
152
|
+
},
|
|
153
|
+
],
|
|
154
|
+
},
|
|
155
|
+
],
|
|
156
|
+
},
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
expect(many).toMatch({
|
|
160
|
+
errors: [
|
|
161
|
+
{
|
|
162
|
+
message: "Template must contain exactly one schema section",
|
|
163
|
+
},
|
|
164
|
+
],
|
|
165
|
+
value: {
|
|
166
|
+
templates: [
|
|
167
|
+
{
|
|
168
|
+
name: { text: "Foo" },
|
|
169
|
+
pieces: [
|
|
170
|
+
{
|
|
171
|
+
kind: "schema",
|
|
172
|
+
},
|
|
173
|
+
{
|
|
174
|
+
kind: "text",
|
|
175
|
+
text: "\n",
|
|
176
|
+
},
|
|
177
|
+
{
|
|
178
|
+
kind: "schema",
|
|
179
|
+
},
|
|
180
|
+
],
|
|
181
|
+
},
|
|
182
|
+
],
|
|
183
|
+
},
|
|
184
|
+
});
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it("parses schema self-closing as valid schema section", () => {
|
|
188
|
+
const parsed = parseTemplateFile(
|
|
189
|
+
["Foo:", " before", " $<schema />", " after"].join("\n"),
|
|
190
|
+
);
|
|
191
|
+
|
|
192
|
+
expect(parsed).toMatch({
|
|
193
|
+
errors: [],
|
|
194
|
+
value: {
|
|
195
|
+
templates: [
|
|
196
|
+
{
|
|
197
|
+
name: { text: "Foo" },
|
|
198
|
+
parameters: [],
|
|
199
|
+
pieces: [
|
|
200
|
+
{ kind: "text", text: "before\n" },
|
|
201
|
+
{ kind: "schema", text: "" },
|
|
202
|
+
{ kind: "text", text: "\nafter" },
|
|
203
|
+
],
|
|
204
|
+
},
|
|
205
|
+
],
|
|
206
|
+
},
|
|
207
|
+
});
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
it("parses JSON example and reports JSON errors leniently", () => {
|
|
211
|
+
const parsed = parseTemplateFile(
|
|
212
|
+
[
|
|
213
|
+
"Foo:",
|
|
214
|
+
" $<schema/>",
|
|
215
|
+
" $<example>",
|
|
216
|
+
' {"a": [1, true, null, ]}',
|
|
217
|
+
" $</example>",
|
|
218
|
+
"",
|
|
219
|
+
"Bar:",
|
|
220
|
+
" $<schema/>",
|
|
221
|
+
].join("\n"),
|
|
222
|
+
);
|
|
223
|
+
|
|
224
|
+
expect(parsed).toMatch({
|
|
225
|
+
errors: [
|
|
226
|
+
{
|
|
227
|
+
message: "Invalid JSON value",
|
|
228
|
+
},
|
|
229
|
+
],
|
|
230
|
+
value: {
|
|
231
|
+
templates: [
|
|
232
|
+
{
|
|
233
|
+
name: { text: "Foo" },
|
|
234
|
+
pieces: [
|
|
235
|
+
{ kind: "schema", text: "" },
|
|
236
|
+
{ kind: "text", text: "\n" },
|
|
237
|
+
{
|
|
238
|
+
kind: "example",
|
|
239
|
+
example: {
|
|
240
|
+
value: null,
|
|
241
|
+
},
|
|
242
|
+
},
|
|
243
|
+
{
|
|
244
|
+
kind: "text",
|
|
245
|
+
text: "\n",
|
|
246
|
+
},
|
|
247
|
+
],
|
|
248
|
+
},
|
|
249
|
+
{
|
|
250
|
+
name: { text: "Bar" },
|
|
251
|
+
pieces: [{ kind: "schema", text: "" }],
|
|
252
|
+
},
|
|
253
|
+
],
|
|
254
|
+
},
|
|
255
|
+
});
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
it("keeps parsing after invalid placeholder", () => {
|
|
259
|
+
const parsed = parseTemplateFile(
|
|
260
|
+
[
|
|
261
|
+
"Foo:",
|
|
262
|
+
" bad: $-x",
|
|
263
|
+
" $<schema/>",
|
|
264
|
+
"",
|
|
265
|
+
"Bar:",
|
|
266
|
+
" ok $name",
|
|
267
|
+
" $<schema/>",
|
|
268
|
+
].join("\n"),
|
|
269
|
+
);
|
|
270
|
+
|
|
271
|
+
expect(parsed).toMatch({
|
|
272
|
+
errors: [
|
|
273
|
+
{
|
|
274
|
+
message: "Invalid '$' sequence",
|
|
275
|
+
},
|
|
276
|
+
],
|
|
277
|
+
value: {
|
|
278
|
+
templates: [
|
|
279
|
+
{
|
|
280
|
+
name: { text: "Foo" },
|
|
281
|
+
parameters: [],
|
|
282
|
+
pieces: [
|
|
283
|
+
{
|
|
284
|
+
kind: "text",
|
|
285
|
+
text: "bad: $-x\n",
|
|
286
|
+
},
|
|
287
|
+
{
|
|
288
|
+
kind: "schema",
|
|
289
|
+
text: "",
|
|
290
|
+
},
|
|
291
|
+
{
|
|
292
|
+
kind: "text",
|
|
293
|
+
text: "\n",
|
|
294
|
+
},
|
|
295
|
+
],
|
|
296
|
+
},
|
|
297
|
+
{
|
|
298
|
+
name: { text: "Bar" },
|
|
299
|
+
parameters: ["name"],
|
|
300
|
+
pieces: [
|
|
301
|
+
{
|
|
302
|
+
kind: "text",
|
|
303
|
+
text: "ok ",
|
|
304
|
+
},
|
|
305
|
+
{
|
|
306
|
+
kind: "expression",
|
|
307
|
+
expression: "name",
|
|
308
|
+
},
|
|
309
|
+
{
|
|
310
|
+
kind: "text",
|
|
311
|
+
text: "\n",
|
|
312
|
+
},
|
|
313
|
+
{
|
|
314
|
+
kind: "schema",
|
|
315
|
+
text: "",
|
|
316
|
+
},
|
|
317
|
+
],
|
|
318
|
+
},
|
|
319
|
+
],
|
|
320
|
+
},
|
|
321
|
+
});
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
it("returns empty result for empty file", () => {
|
|
325
|
+
const parsed = parseTemplateFile("");
|
|
326
|
+
|
|
327
|
+
expect(parsed).toMatch({
|
|
328
|
+
errors: [],
|
|
329
|
+
value: {
|
|
330
|
+
templates: [],
|
|
331
|
+
},
|
|
332
|
+
});
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
it("supports top-level # comments before and between templates", () => {
|
|
336
|
+
const parsed = parseTemplateFile(
|
|
337
|
+
[
|
|
338
|
+
"# comment before first template",
|
|
339
|
+
"",
|
|
340
|
+
"Foo:",
|
|
341
|
+
" $<schema/>",
|
|
342
|
+
"# comment between templates",
|
|
343
|
+
"Bar:",
|
|
344
|
+
" $<schema/>",
|
|
345
|
+
"# trailing comment",
|
|
346
|
+
].join("\n"),
|
|
347
|
+
);
|
|
348
|
+
|
|
349
|
+
expect(parsed).toMatch({
|
|
350
|
+
errors: [],
|
|
351
|
+
value: {
|
|
352
|
+
templates: [{ name: { text: "Foo" } }, { name: { text: "Bar" } }],
|
|
353
|
+
},
|
|
354
|
+
});
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
it("keeps indented # lines as template body text", () => {
|
|
358
|
+
const parsed = parseTemplateFile(
|
|
359
|
+
["Foo:", " # not a top-level comment", " $<schema/>"].join("\n"),
|
|
360
|
+
);
|
|
361
|
+
|
|
362
|
+
expect(parsed).toMatch({
|
|
363
|
+
errors: [],
|
|
364
|
+
value: {
|
|
365
|
+
templates: [
|
|
366
|
+
{
|
|
367
|
+
pieces: [
|
|
368
|
+
{ kind: "text", text: "# not a top-level comment\n" },
|
|
369
|
+
{ kind: "schema", text: "" },
|
|
370
|
+
],
|
|
371
|
+
},
|
|
372
|
+
],
|
|
373
|
+
},
|
|
374
|
+
});
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
it("reports header without identifier", () => {
|
|
378
|
+
const parsed = parseTemplateFile([":", "Foo:", " $<schema/>"].join("\n"));
|
|
379
|
+
|
|
380
|
+
expect(parsed).toMatch({
|
|
381
|
+
errors: [
|
|
382
|
+
{
|
|
383
|
+
message: "Expected template identifier",
|
|
384
|
+
},
|
|
385
|
+
],
|
|
386
|
+
value: {
|
|
387
|
+
templates: [
|
|
388
|
+
{
|
|
389
|
+
name: { text: "Foo" },
|
|
390
|
+
pieces: [{ kind: "schema" }],
|
|
391
|
+
},
|
|
392
|
+
],
|
|
393
|
+
},
|
|
394
|
+
});
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
it("reports missing variant identifier", () => {
|
|
398
|
+
const parsed = parseTemplateFile(
|
|
399
|
+
["Foo/:", "Bar:", " $<schema/>"].join("\n"),
|
|
400
|
+
);
|
|
401
|
+
|
|
402
|
+
expect(parsed).toMatch({
|
|
403
|
+
errors: [
|
|
404
|
+
{
|
|
405
|
+
message: "Expected variant identifier after '/'",
|
|
406
|
+
},
|
|
407
|
+
],
|
|
408
|
+
value: {
|
|
409
|
+
templates: [
|
|
410
|
+
{
|
|
411
|
+
name: { text: "Bar" },
|
|
412
|
+
},
|
|
413
|
+
],
|
|
414
|
+
},
|
|
415
|
+
});
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
it("reports missing colon in header", () => {
|
|
419
|
+
const parsed = parseTemplateFile(
|
|
420
|
+
["Foo/Bar", "Baz:", " $<schema/>"].join("\n"),
|
|
421
|
+
);
|
|
422
|
+
|
|
423
|
+
expect(parsed).toMatch({
|
|
424
|
+
errors: [
|
|
425
|
+
{
|
|
426
|
+
expected: ":",
|
|
427
|
+
},
|
|
428
|
+
],
|
|
429
|
+
value: {
|
|
430
|
+
templates: [
|
|
431
|
+
{
|
|
432
|
+
name: { text: "Baz" },
|
|
433
|
+
pieces: [{ kind: "schema" }],
|
|
434
|
+
},
|
|
435
|
+
],
|
|
436
|
+
},
|
|
437
|
+
});
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
it("allows empty lines in template body without indentation", () => {
|
|
441
|
+
const parsed = parseTemplateFile(
|
|
442
|
+
["Foo:", "", " line", "", " $<schema/>"].join("\n"),
|
|
443
|
+
);
|
|
444
|
+
|
|
445
|
+
expect(parsed).toMatch({
|
|
446
|
+
errors: [],
|
|
447
|
+
value: {
|
|
448
|
+
templates: [
|
|
449
|
+
{
|
|
450
|
+
name: { text: "Foo" },
|
|
451
|
+
pieces: [
|
|
452
|
+
{ kind: "text", text: "\nline\n\n" },
|
|
453
|
+
{ kind: "schema", text: "" },
|
|
454
|
+
],
|
|
455
|
+
},
|
|
456
|
+
],
|
|
457
|
+
},
|
|
458
|
+
});
|
|
459
|
+
});
|
|
460
|
+
|
|
461
|
+
it("preserves unindented empty lines as text line breaks within body", () => {
|
|
462
|
+
const parsed = parseTemplateFile(
|
|
463
|
+
["Foo:", " first", "", " second", "", "", " $<schema/>"].join("\n"),
|
|
464
|
+
);
|
|
465
|
+
|
|
466
|
+
expect(parsed).toMatch({
|
|
467
|
+
errors: [],
|
|
468
|
+
value: {
|
|
469
|
+
templates: [
|
|
470
|
+
{
|
|
471
|
+
pieces: [
|
|
472
|
+
{ kind: "text", text: "first\n\nsecond\n\n\n" },
|
|
473
|
+
{ kind: "schema", text: "" },
|
|
474
|
+
],
|
|
475
|
+
},
|
|
476
|
+
],
|
|
477
|
+
},
|
|
478
|
+
});
|
|
479
|
+
});
|
|
480
|
+
|
|
481
|
+
it("supports $$ as escaped dollar", () => {
|
|
482
|
+
const parsed = parseTemplateFile(
|
|
483
|
+
["Foo:", " price is $$5 and $$10", " $<schema/>"].join("\n"),
|
|
484
|
+
);
|
|
485
|
+
|
|
486
|
+
expect(parsed).toMatch({
|
|
487
|
+
errors: [],
|
|
488
|
+
value: {
|
|
489
|
+
templates: [
|
|
490
|
+
{
|
|
491
|
+
parameters: [],
|
|
492
|
+
pieces: [
|
|
493
|
+
{ kind: "text", text: "price is $5 and $10\n" },
|
|
494
|
+
{ kind: "schema", text: "" },
|
|
495
|
+
],
|
|
496
|
+
},
|
|
497
|
+
],
|
|
498
|
+
},
|
|
499
|
+
});
|
|
500
|
+
});
|
|
501
|
+
|
|
502
|
+
it("reports invalid braced placeholder modifiers", () => {
|
|
503
|
+
const parsed = parseTemplateFile(
|
|
504
|
+
["Foo:", " ${name:oops}", " $<schema/>"].join("\n"),
|
|
505
|
+
);
|
|
506
|
+
|
|
507
|
+
expect(parsed).toMatch({
|
|
508
|
+
errors: [
|
|
509
|
+
{
|
|
510
|
+
message: "Invalid expression placeholder",
|
|
511
|
+
},
|
|
512
|
+
],
|
|
513
|
+
value: {
|
|
514
|
+
templates: [
|
|
515
|
+
{
|
|
516
|
+
parameters: [],
|
|
517
|
+
pieces: [
|
|
518
|
+
{ kind: "text", text: "${name:oops}\n" },
|
|
519
|
+
{ kind: "schema", text: "" },
|
|
520
|
+
],
|
|
521
|
+
},
|
|
522
|
+
],
|
|
523
|
+
},
|
|
524
|
+
});
|
|
525
|
+
});
|
|
526
|
+
|
|
527
|
+
it("reports missing closing brace in braced placeholder", () => {
|
|
528
|
+
const parsed = parseTemplateFile(
|
|
529
|
+
["Foo:", " hello ${name", " $<schema/>"].join("\n"),
|
|
530
|
+
);
|
|
531
|
+
|
|
532
|
+
expect(parsed).toMatch({
|
|
533
|
+
errors: [
|
|
534
|
+
{
|
|
535
|
+
expected: "}",
|
|
536
|
+
},
|
|
537
|
+
],
|
|
538
|
+
value: {
|
|
539
|
+
templates: [
|
|
540
|
+
{
|
|
541
|
+
parameters: [],
|
|
542
|
+
pieces: [
|
|
543
|
+
{ kind: "text", text: "hello ${name\n" },
|
|
544
|
+
{ kind: "schema", text: "" },
|
|
545
|
+
],
|
|
546
|
+
},
|
|
547
|
+
],
|
|
548
|
+
},
|
|
549
|
+
});
|
|
550
|
+
});
|
|
551
|
+
|
|
552
|
+
it("supports spaced schema tags", () => {
|
|
553
|
+
const parsed = parseTemplateFile(
|
|
554
|
+
[
|
|
555
|
+
"Foo:",
|
|
556
|
+
" $< schema >",
|
|
557
|
+
" type Out = string;",
|
|
558
|
+
" $</schema >",
|
|
559
|
+
].join("\n"),
|
|
560
|
+
);
|
|
561
|
+
|
|
562
|
+
expect(parsed).toMatch({
|
|
563
|
+
errors: [],
|
|
564
|
+
value: {
|
|
565
|
+
templates: [
|
|
566
|
+
{
|
|
567
|
+
pieces: [
|
|
568
|
+
{
|
|
569
|
+
kind: "schema",
|
|
570
|
+
text: "\n type Out = string;\n",
|
|
571
|
+
schema: {
|
|
572
|
+
declarations: [{ kind: "type", name: { text: "Out" } }],
|
|
573
|
+
},
|
|
574
|
+
},
|
|
575
|
+
],
|
|
576
|
+
},
|
|
577
|
+
],
|
|
578
|
+
},
|
|
579
|
+
});
|
|
580
|
+
});
|
|
581
|
+
|
|
582
|
+
it("reports missing closing schema tag", () => {
|
|
583
|
+
const parsed = parseTemplateFile(
|
|
584
|
+
["Foo:", " before", " $<schema>", " interface Out {}"].join("\n"),
|
|
585
|
+
);
|
|
586
|
+
|
|
587
|
+
expect(parsed).toMatch({
|
|
588
|
+
errors: [
|
|
589
|
+
{
|
|
590
|
+
message: "Missing '$</schema>'",
|
|
591
|
+
},
|
|
592
|
+
],
|
|
593
|
+
value: {
|
|
594
|
+
templates: [
|
|
595
|
+
{
|
|
596
|
+
pieces: [
|
|
597
|
+
{ kind: "text", text: "before\n" },
|
|
598
|
+
{ kind: "schema", schema: null },
|
|
599
|
+
],
|
|
600
|
+
},
|
|
601
|
+
],
|
|
602
|
+
},
|
|
603
|
+
});
|
|
604
|
+
});
|
|
605
|
+
|
|
606
|
+
it("does not close example section on $</example> inside JSON string", () => {
|
|
607
|
+
const parsed = parseTemplateFile(
|
|
608
|
+
[
|
|
609
|
+
"Foo:",
|
|
610
|
+
" $<schema/>",
|
|
611
|
+
" $<example>",
|
|
612
|
+
' {"text": "value with $</example> and #</example> inside"}',
|
|
613
|
+
" $</example>",
|
|
614
|
+
].join("\n"),
|
|
615
|
+
);
|
|
616
|
+
|
|
617
|
+
expect(parsed).toMatch({
|
|
618
|
+
errors: [],
|
|
619
|
+
value: {
|
|
620
|
+
templates: [
|
|
621
|
+
{
|
|
622
|
+
pieces: [
|
|
623
|
+
{ kind: "schema", text: "" },
|
|
624
|
+
{ kind: "text", text: "\n" },
|
|
625
|
+
{
|
|
626
|
+
kind: "example",
|
|
627
|
+
example: {
|
|
628
|
+
value: {
|
|
629
|
+
kind: "object",
|
|
630
|
+
entries: [
|
|
631
|
+
{
|
|
632
|
+
key: { value: "text" },
|
|
633
|
+
value: {
|
|
634
|
+
kind: "string",
|
|
635
|
+
value:
|
|
636
|
+
"value with $</example> and #</example> inside",
|
|
637
|
+
},
|
|
638
|
+
},
|
|
639
|
+
],
|
|
640
|
+
},
|
|
641
|
+
},
|
|
642
|
+
},
|
|
643
|
+
],
|
|
644
|
+
},
|
|
645
|
+
],
|
|
646
|
+
},
|
|
647
|
+
});
|
|
648
|
+
});
|
|
649
|
+
|
|
650
|
+
it("does not close schema section on $</schema> inside type string literal", () => {
|
|
651
|
+
const parsed = parseTemplateFile(
|
|
652
|
+
[
|
|
653
|
+
"Foo:",
|
|
654
|
+
" $<schema>",
|
|
655
|
+
' type Marker = "$</schema> and #</schema>";',
|
|
656
|
+
" interface Out { value: Marker; }",
|
|
657
|
+
" $</schema>",
|
|
658
|
+
].join("\n"),
|
|
659
|
+
);
|
|
660
|
+
|
|
661
|
+
expect(parsed).toMatch({
|
|
662
|
+
errors: [],
|
|
663
|
+
value: {
|
|
664
|
+
templates: [
|
|
665
|
+
{
|
|
666
|
+
pieces: [
|
|
667
|
+
{
|
|
668
|
+
kind: "schema",
|
|
669
|
+
schema: {
|
|
670
|
+
declarations: [
|
|
671
|
+
{ kind: "type", name: { text: "Marker" } },
|
|
672
|
+
{ kind: "interface", name: { text: "Out" } },
|
|
673
|
+
],
|
|
674
|
+
},
|
|
675
|
+
},
|
|
676
|
+
],
|
|
677
|
+
},
|
|
678
|
+
],
|
|
679
|
+
},
|
|
680
|
+
});
|
|
681
|
+
});
|
|
682
|
+
|
|
683
|
+
it("does not close schema section on $</schema> inside TypeScript comments", () => {
|
|
684
|
+
const parsed = parseTemplateFile(
|
|
685
|
+
[
|
|
686
|
+
"Foo:",
|
|
687
|
+
" $<schema>",
|
|
688
|
+
" // line comment with $</schema> and #</schema>",
|
|
689
|
+
" /* block comment with $</schema> and #</schema> */",
|
|
690
|
+
" interface Out { value: string; }",
|
|
691
|
+
" $</schema>",
|
|
692
|
+
].join("\n"),
|
|
693
|
+
);
|
|
694
|
+
|
|
695
|
+
expect(parsed).toMatch({
|
|
696
|
+
errors: [],
|
|
697
|
+
value: {
|
|
698
|
+
templates: [
|
|
699
|
+
{
|
|
700
|
+
pieces: [
|
|
701
|
+
{
|
|
702
|
+
kind: "schema",
|
|
703
|
+
schema: {
|
|
704
|
+
declarations: [{ kind: "interface", name: { text: "Out" } }],
|
|
705
|
+
},
|
|
706
|
+
},
|
|
707
|
+
],
|
|
708
|
+
},
|
|
709
|
+
],
|
|
710
|
+
},
|
|
711
|
+
});
|
|
712
|
+
});
|
|
713
|
+
|
|
714
|
+
it("parses JSON example with object and array", () => {
|
|
715
|
+
const parsed = parseTemplateFile(
|
|
716
|
+
[
|
|
717
|
+
"Foo:",
|
|
718
|
+
" $<schema/>",
|
|
719
|
+
" $<example>",
|
|
720
|
+
' {"a": [1, 2, 3], "b": false}',
|
|
721
|
+
" $</example>",
|
|
722
|
+
].join("\n"),
|
|
723
|
+
);
|
|
724
|
+
|
|
725
|
+
expect(parsed).toMatch({
|
|
726
|
+
errors: [],
|
|
727
|
+
value: {
|
|
728
|
+
templates: [
|
|
729
|
+
{
|
|
730
|
+
pieces: [
|
|
731
|
+
{ kind: "schema", text: "" },
|
|
732
|
+
{ kind: "text", text: "\n" },
|
|
733
|
+
{
|
|
734
|
+
kind: "example",
|
|
735
|
+
example: {
|
|
736
|
+
value: {
|
|
737
|
+
kind: "object",
|
|
738
|
+
entries: [
|
|
739
|
+
{
|
|
740
|
+
key: { value: "a" },
|
|
741
|
+
value: {
|
|
742
|
+
kind: "array",
|
|
743
|
+
values: [
|
|
744
|
+
{ kind: "number", value: 1 },
|
|
745
|
+
{ kind: "number", value: 2 },
|
|
746
|
+
{ kind: "number", value: 3 },
|
|
747
|
+
],
|
|
748
|
+
},
|
|
749
|
+
},
|
|
750
|
+
{
|
|
751
|
+
key: { value: "b" },
|
|
752
|
+
value: { kind: "boolean", value: false },
|
|
753
|
+
},
|
|
754
|
+
],
|
|
755
|
+
},
|
|
756
|
+
},
|
|
757
|
+
},
|
|
758
|
+
],
|
|
759
|
+
},
|
|
760
|
+
],
|
|
761
|
+
},
|
|
762
|
+
});
|
|
763
|
+
});
|
|
764
|
+
|
|
765
|
+
it("continues parsing templates after bad JSON example", () => {
|
|
766
|
+
const parsed = parseTemplateFile(
|
|
767
|
+
[
|
|
768
|
+
"Foo:",
|
|
769
|
+
" $<schema/>",
|
|
770
|
+
" $<example>",
|
|
771
|
+
" [1, 2,",
|
|
772
|
+
" $</example>",
|
|
773
|
+
"Bar:",
|
|
774
|
+
" text $param",
|
|
775
|
+
" $<schema/>",
|
|
776
|
+
].join("\n"),
|
|
777
|
+
);
|
|
778
|
+
|
|
779
|
+
expect(parsed).toMatch({
|
|
780
|
+
errors: [
|
|
781
|
+
{
|
|
782
|
+
expected: "]",
|
|
783
|
+
},
|
|
784
|
+
],
|
|
785
|
+
value: {
|
|
786
|
+
templates: [
|
|
787
|
+
{
|
|
788
|
+
name: { text: "Foo" },
|
|
789
|
+
},
|
|
790
|
+
{
|
|
791
|
+
name: { text: "Bar" },
|
|
792
|
+
parameters: ["param"],
|
|
793
|
+
},
|
|
794
|
+
],
|
|
795
|
+
},
|
|
796
|
+
});
|
|
797
|
+
});
|
|
798
|
+
|
|
799
|
+
// --- expression hide ---
|
|
800
|
+
|
|
801
|
+
it("sets hide:true on ${ name : hide } expression", () => {
|
|
802
|
+
const parsed = parseTemplateFile(
|
|
803
|
+
["Foo:", " ${ x : hide }", " $<schema/>"].join("\n"),
|
|
804
|
+
);
|
|
805
|
+
|
|
806
|
+
expect(parsed).toMatch({
|
|
807
|
+
errors: [],
|
|
808
|
+
value: {
|
|
809
|
+
templates: [
|
|
810
|
+
{
|
|
811
|
+
parameters: ["x"],
|
|
812
|
+
pieces: [
|
|
813
|
+
{ kind: "expression", expression: "x", hide: true },
|
|
814
|
+
{ kind: "text", text: "\n" },
|
|
815
|
+
{ kind: "schema", text: "" },
|
|
816
|
+
],
|
|
817
|
+
},
|
|
818
|
+
],
|
|
819
|
+
},
|
|
820
|
+
});
|
|
821
|
+
});
|
|
822
|
+
|
|
823
|
+
it("sets hide:false on simple $name expression", () => {
|
|
824
|
+
const parsed = parseTemplateFile(
|
|
825
|
+
["Foo:", " $x and ${y}", " $<schema/>"].join("\n"),
|
|
826
|
+
);
|
|
827
|
+
|
|
828
|
+
expect(parsed).toMatch({
|
|
829
|
+
errors: [],
|
|
830
|
+
value: {
|
|
831
|
+
templates: [
|
|
832
|
+
{
|
|
833
|
+
parameters: ["x", "y"],
|
|
834
|
+
pieces: [
|
|
835
|
+
{ kind: "expression", expression: "x", hide: false },
|
|
836
|
+
{ kind: "text", text: " and " },
|
|
837
|
+
{ kind: "expression", expression: "y", hide: false },
|
|
838
|
+
{ kind: "text", text: "\n" },
|
|
839
|
+
{ kind: "schema", text: "" },
|
|
840
|
+
],
|
|
841
|
+
},
|
|
842
|
+
],
|
|
843
|
+
},
|
|
844
|
+
});
|
|
845
|
+
});
|
|
846
|
+
|
|
847
|
+
// --- schema tag ---
|
|
848
|
+
|
|
849
|
+
it("accepts $<schema> section tags", () => {
|
|
850
|
+
const parsed = parseTemplateFile(
|
|
851
|
+
[
|
|
852
|
+
"Foo:",
|
|
853
|
+
" $<schema>",
|
|
854
|
+
" interface Out { value: string; }",
|
|
855
|
+
" $</schema>",
|
|
856
|
+
].join("\n"),
|
|
857
|
+
);
|
|
858
|
+
|
|
859
|
+
expect(parsed).toMatch({
|
|
860
|
+
errors: [],
|
|
861
|
+
value: {
|
|
862
|
+
templates: [
|
|
863
|
+
{
|
|
864
|
+
pieces: [
|
|
865
|
+
{
|
|
866
|
+
kind: "schema",
|
|
867
|
+
text: "\n interface Out { value: string; }\n",
|
|
868
|
+
schema: {
|
|
869
|
+
declarations: [{ kind: "interface", name: { text: "Out" } }],
|
|
870
|
+
},
|
|
871
|
+
},
|
|
872
|
+
],
|
|
873
|
+
},
|
|
874
|
+
],
|
|
875
|
+
},
|
|
876
|
+
});
|
|
877
|
+
});
|
|
878
|
+
|
|
879
|
+
it("accepts $<schema/> self-closing", () => {
|
|
880
|
+
const parsed = parseTemplateFile(
|
|
881
|
+
["Foo:", " before $<schema/> after"].join("\n"),
|
|
882
|
+
);
|
|
883
|
+
|
|
884
|
+
expect(parsed).toMatch({
|
|
885
|
+
errors: [],
|
|
886
|
+
value: {
|
|
887
|
+
templates: [
|
|
888
|
+
{
|
|
889
|
+
pieces: [
|
|
890
|
+
{ kind: "text", text: "before " },
|
|
891
|
+
{ kind: "schema", text: "" },
|
|
892
|
+
{ kind: "text", text: " after" },
|
|
893
|
+
],
|
|
894
|
+
},
|
|
895
|
+
],
|
|
896
|
+
},
|
|
897
|
+
});
|
|
898
|
+
});
|
|
899
|
+
|
|
900
|
+
it("does not parse $<response> tags", () => {
|
|
901
|
+
const parsed = parseTemplateFile(["Foo:", " $<response/>"].join("\n"));
|
|
902
|
+
|
|
903
|
+
expect(parsed).toMatch({
|
|
904
|
+
errors: [
|
|
905
|
+
{ message: "Invalid '$' sequence" },
|
|
906
|
+
{ message: "Template must contain exactly one schema section" },
|
|
907
|
+
],
|
|
908
|
+
value: {
|
|
909
|
+
templates: [
|
|
910
|
+
{
|
|
911
|
+
pieces: [{ kind: "text", text: "$<response/>" }],
|
|
912
|
+
},
|
|
913
|
+
],
|
|
914
|
+
},
|
|
915
|
+
});
|
|
916
|
+
});
|
|
917
|
+
|
|
918
|
+
// --- schema hide attribute ---
|
|
919
|
+
|
|
920
|
+
it("$<schema hide> empties text but still parses schema tree", () => {
|
|
921
|
+
const parsed = parseTemplateFile(
|
|
922
|
+
[
|
|
923
|
+
"Foo:",
|
|
924
|
+
" $<schema hide>",
|
|
925
|
+
" interface Out { value: string; }",
|
|
926
|
+
" $</schema>",
|
|
927
|
+
].join("\n"),
|
|
928
|
+
);
|
|
929
|
+
|
|
930
|
+
expect(parsed).toMatch({
|
|
931
|
+
errors: [],
|
|
932
|
+
value: {
|
|
933
|
+
templates: [
|
|
934
|
+
{
|
|
935
|
+
pieces: [
|
|
936
|
+
{
|
|
937
|
+
kind: "schema",
|
|
938
|
+
text: "",
|
|
939
|
+
schema: {
|
|
940
|
+
declarations: [{ kind: "interface", name: { text: "Out" } }],
|
|
941
|
+
},
|
|
942
|
+
},
|
|
943
|
+
],
|
|
944
|
+
},
|
|
945
|
+
],
|
|
946
|
+
},
|
|
947
|
+
});
|
|
948
|
+
});
|
|
949
|
+
|
|
950
|
+
it("$<schema hide> also empties text but still parses schema tree", () => {
|
|
951
|
+
const parsed = parseTemplateFile(
|
|
952
|
+
[
|
|
953
|
+
"Foo:",
|
|
954
|
+
" $<schema hide>",
|
|
955
|
+
" type Result = string;",
|
|
956
|
+
" $</schema>",
|
|
957
|
+
].join("\n"),
|
|
958
|
+
);
|
|
959
|
+
|
|
960
|
+
expect(parsed).toMatch({
|
|
961
|
+
errors: [],
|
|
962
|
+
value: {
|
|
963
|
+
templates: [
|
|
964
|
+
{
|
|
965
|
+
pieces: [
|
|
966
|
+
{
|
|
967
|
+
kind: "schema",
|
|
968
|
+
text: "",
|
|
969
|
+
schema: {
|
|
970
|
+
declarations: [{ kind: "type", name: { text: "Result" } }],
|
|
971
|
+
},
|
|
972
|
+
},
|
|
973
|
+
],
|
|
974
|
+
},
|
|
975
|
+
],
|
|
976
|
+
},
|
|
977
|
+
});
|
|
978
|
+
});
|
|
979
|
+
|
|
980
|
+
// --- inline structs in schema ---
|
|
981
|
+
|
|
982
|
+
it("schema with inline struct field type", () => {
|
|
983
|
+
const parsed = parseTemplateFile(
|
|
984
|
+
[
|
|
985
|
+
"Foo:",
|
|
986
|
+
" $<schema>",
|
|
987
|
+
" type Foo = {",
|
|
988
|
+
" point: {",
|
|
989
|
+
" x: number;",
|
|
990
|
+
" y: number;",
|
|
991
|
+
" };",
|
|
992
|
+
" };",
|
|
993
|
+
" $</schema>",
|
|
994
|
+
].join("\n"),
|
|
995
|
+
);
|
|
996
|
+
|
|
997
|
+
expect(parsed).toMatch({
|
|
998
|
+
errors: [],
|
|
999
|
+
value: {
|
|
1000
|
+
templates: [
|
|
1001
|
+
{
|
|
1002
|
+
pieces: [
|
|
1003
|
+
{
|
|
1004
|
+
kind: "schema",
|
|
1005
|
+
schema: {
|
|
1006
|
+
declarations: [{ kind: "type", name: { text: "Foo" } }],
|
|
1007
|
+
},
|
|
1008
|
+
},
|
|
1009
|
+
],
|
|
1010
|
+
},
|
|
1011
|
+
],
|
|
1012
|
+
},
|
|
1013
|
+
});
|
|
1014
|
+
});
|
|
1015
|
+
|
|
1016
|
+
// --- union types in schema ---
|
|
1017
|
+
|
|
1018
|
+
it("schema with union type containing string literals", () => {
|
|
1019
|
+
const parsed = parseTemplateFile(
|
|
1020
|
+
[
|
|
1021
|
+
"Foo:",
|
|
1022
|
+
" $<schema>",
|
|
1023
|
+
' type Status = "active" | "inactive";',
|
|
1024
|
+
" $</schema>",
|
|
1025
|
+
].join("\n"),
|
|
1026
|
+
);
|
|
1027
|
+
|
|
1028
|
+
expect(parsed).toMatch({
|
|
1029
|
+
errors: [],
|
|
1030
|
+
value: {
|
|
1031
|
+
templates: [
|
|
1032
|
+
{
|
|
1033
|
+
pieces: [
|
|
1034
|
+
{
|
|
1035
|
+
kind: "schema",
|
|
1036
|
+
schema: {
|
|
1037
|
+
declarations: [{ kind: "type", name: { text: "Status" } }],
|
|
1038
|
+
},
|
|
1039
|
+
},
|
|
1040
|
+
],
|
|
1041
|
+
},
|
|
1042
|
+
],
|
|
1043
|
+
},
|
|
1044
|
+
});
|
|
1045
|
+
});
|
|
1046
|
+
|
|
1047
|
+
it("schema with union type containing struct members", () => {
|
|
1048
|
+
const parsed = parseTemplateFile(
|
|
1049
|
+
[
|
|
1050
|
+
"Foo:",
|
|
1051
|
+
" $<schema>",
|
|
1052
|
+
' type Shape = { kind: "circle"; radius: number } | { kind: "rect"; width: number; height: number };',
|
|
1053
|
+
" $</schema>",
|
|
1054
|
+
].join("\n"),
|
|
1055
|
+
);
|
|
1056
|
+
|
|
1057
|
+
expect(parsed).toMatch({
|
|
1058
|
+
errors: [],
|
|
1059
|
+
value: {
|
|
1060
|
+
templates: [
|
|
1061
|
+
{
|
|
1062
|
+
pieces: [
|
|
1063
|
+
{
|
|
1064
|
+
kind: "schema",
|
|
1065
|
+
schema: {
|
|
1066
|
+
declarations: [{ kind: "type", name: { text: "Shape" } }],
|
|
1067
|
+
},
|
|
1068
|
+
},
|
|
1069
|
+
],
|
|
1070
|
+
},
|
|
1071
|
+
],
|
|
1072
|
+
},
|
|
1073
|
+
});
|
|
1074
|
+
});
|
|
1075
|
+
|
|
1076
|
+
// --- leading pipe union syntax ---
|
|
1077
|
+
|
|
1078
|
+
it("schema with leading pipe union syntax", () => {
|
|
1079
|
+
const parsed = parseTemplateFile(
|
|
1080
|
+
[
|
|
1081
|
+
"Foo:",
|
|
1082
|
+
" $<schema>",
|
|
1083
|
+
" type Shape =",
|
|
1084
|
+
' | { kind: "circle"; radius: number }',
|
|
1085
|
+
' | { kind: "rect"; width: number; height: number };',
|
|
1086
|
+
" $</schema>",
|
|
1087
|
+
].join("\n"),
|
|
1088
|
+
);
|
|
1089
|
+
|
|
1090
|
+
expect(parsed).toMatch({
|
|
1091
|
+
errors: [],
|
|
1092
|
+
value: {
|
|
1093
|
+
templates: [
|
|
1094
|
+
{
|
|
1095
|
+
pieces: [
|
|
1096
|
+
{
|
|
1097
|
+
kind: "schema",
|
|
1098
|
+
schema: {
|
|
1099
|
+
declarations: [{ kind: "type", name: { text: "Shape" } }],
|
|
1100
|
+
},
|
|
1101
|
+
},
|
|
1102
|
+
],
|
|
1103
|
+
},
|
|
1104
|
+
],
|
|
1105
|
+
},
|
|
1106
|
+
});
|
|
1107
|
+
});
|
|
1108
|
+
|
|
1109
|
+
// --- multiline schema with correct dedenting ---
|
|
1110
|
+
|
|
1111
|
+
it("multiline schema section is correctly dedented", () => {
|
|
1112
|
+
const parsed = parseTemplateFile(
|
|
1113
|
+
[
|
|
1114
|
+
"Foo:",
|
|
1115
|
+
" $<schema>",
|
|
1116
|
+
" interface User {",
|
|
1117
|
+
" name: string;",
|
|
1118
|
+
" age: number;",
|
|
1119
|
+
" }",
|
|
1120
|
+
' type Role = "admin" | "member";',
|
|
1121
|
+
" $</schema>",
|
|
1122
|
+
].join("\n"),
|
|
1123
|
+
);
|
|
1124
|
+
|
|
1125
|
+
expect(parsed).toMatch({
|
|
1126
|
+
errors: [],
|
|
1127
|
+
value: {
|
|
1128
|
+
templates: [
|
|
1129
|
+
{
|
|
1130
|
+
pieces: [
|
|
1131
|
+
{
|
|
1132
|
+
kind: "schema",
|
|
1133
|
+
text: '\n interface User {\n name: string;\n age: number;\n }\n type Role = "admin" | "member";\n',
|
|
1134
|
+
schema: {
|
|
1135
|
+
declarations: [
|
|
1136
|
+
{ kind: "interface", name: { text: "User" } },
|
|
1137
|
+
{ kind: "type", name: { text: "Role" } },
|
|
1138
|
+
],
|
|
1139
|
+
},
|
|
1140
|
+
},
|
|
1141
|
+
],
|
|
1142
|
+
},
|
|
1143
|
+
],
|
|
1144
|
+
},
|
|
1145
|
+
});
|
|
1146
|
+
});
|
|
1147
|
+
|
|
1148
|
+
// --- multiline example with correct dedenting ---
|
|
1149
|
+
|
|
1150
|
+
it("multiline example section is correctly dedented", () => {
|
|
1151
|
+
const parsed = parseTemplateFile(
|
|
1152
|
+
[
|
|
1153
|
+
"Foo:",
|
|
1154
|
+
" $<schema/>",
|
|
1155
|
+
" $<example>",
|
|
1156
|
+
" {",
|
|
1157
|
+
' "name": "Alice",',
|
|
1158
|
+
' "age": 30',
|
|
1159
|
+
" }",
|
|
1160
|
+
" $</example>",
|
|
1161
|
+
].join("\n"),
|
|
1162
|
+
);
|
|
1163
|
+
|
|
1164
|
+
expect(parsed).toMatch({
|
|
1165
|
+
errors: [],
|
|
1166
|
+
value: {
|
|
1167
|
+
templates: [
|
|
1168
|
+
{
|
|
1169
|
+
pieces: [
|
|
1170
|
+
{ kind: "schema", text: "" },
|
|
1171
|
+
{ kind: "text", text: "\n" },
|
|
1172
|
+
{
|
|
1173
|
+
kind: "example",
|
|
1174
|
+
text: '\n {\n "name": "Alice",\n "age": 30\n }\n',
|
|
1175
|
+
example: {
|
|
1176
|
+
value: {
|
|
1177
|
+
kind: "object",
|
|
1178
|
+
entries: [
|
|
1179
|
+
{
|
|
1180
|
+
key: { value: "name" },
|
|
1181
|
+
value: { kind: "string", value: "Alice" },
|
|
1182
|
+
},
|
|
1183
|
+
{
|
|
1184
|
+
key: { value: "age" },
|
|
1185
|
+
value: { kind: "number", value: 30 },
|
|
1186
|
+
},
|
|
1187
|
+
],
|
|
1188
|
+
},
|
|
1189
|
+
},
|
|
1190
|
+
},
|
|
1191
|
+
],
|
|
1192
|
+
},
|
|
1193
|
+
],
|
|
1194
|
+
},
|
|
1195
|
+
});
|
|
1196
|
+
});
|
|
1197
|
+
|
|
1198
|
+
// --- JSON error coverage ---
|
|
1199
|
+
|
|
1200
|
+
it("reports empty JSON example section", () => {
|
|
1201
|
+
const parsed = parseTemplateFile(
|
|
1202
|
+
["Foo:", " $<schema/>", " $<example>", " $</example>"].join("\n"),
|
|
1203
|
+
);
|
|
1204
|
+
|
|
1205
|
+
expect(parsed).toMatch({
|
|
1206
|
+
errors: [{ message: "Expected JSON value" }],
|
|
1207
|
+
value: {
|
|
1208
|
+
templates: [
|
|
1209
|
+
{
|
|
1210
|
+
pieces: [
|
|
1211
|
+
{ kind: "schema" },
|
|
1212
|
+
{ kind: "text" },
|
|
1213
|
+
{ kind: "example", example: { value: null } },
|
|
1214
|
+
],
|
|
1215
|
+
},
|
|
1216
|
+
],
|
|
1217
|
+
},
|
|
1218
|
+
});
|
|
1219
|
+
});
|
|
1220
|
+
|
|
1221
|
+
it("reports unexpected trailing content after JSON value", () => {
|
|
1222
|
+
const parsed = parseTemplateFile(
|
|
1223
|
+
[
|
|
1224
|
+
"Foo:",
|
|
1225
|
+
" $<schema/>",
|
|
1226
|
+
" $<example>",
|
|
1227
|
+
" 42 extra",
|
|
1228
|
+
" $</example>",
|
|
1229
|
+
].join("\n"),
|
|
1230
|
+
);
|
|
1231
|
+
|
|
1232
|
+
expect(parsed).toMatch({
|
|
1233
|
+
errors: [{ message: "Unexpected trailing content after JSON value" }],
|
|
1234
|
+
value: {
|
|
1235
|
+
templates: [
|
|
1236
|
+
{
|
|
1237
|
+
pieces: [
|
|
1238
|
+
{ kind: "schema" },
|
|
1239
|
+
{ kind: "text" },
|
|
1240
|
+
{
|
|
1241
|
+
kind: "example",
|
|
1242
|
+
example: { value: { kind: "number", value: 42 } },
|
|
1243
|
+
},
|
|
1244
|
+
],
|
|
1245
|
+
},
|
|
1246
|
+
],
|
|
1247
|
+
},
|
|
1248
|
+
});
|
|
1249
|
+
});
|
|
1250
|
+
|
|
1251
|
+
it("reports missing colon in JSON object", () => {
|
|
1252
|
+
const parsed = parseTemplateFile(
|
|
1253
|
+
[
|
|
1254
|
+
"Foo:",
|
|
1255
|
+
" $<schema/>",
|
|
1256
|
+
" $<example>",
|
|
1257
|
+
' {"key" 1}',
|
|
1258
|
+
" $</example>",
|
|
1259
|
+
].join("\n"),
|
|
1260
|
+
);
|
|
1261
|
+
|
|
1262
|
+
expect(parsed).toMatch({
|
|
1263
|
+
errors: [{ expected: ":" }],
|
|
1264
|
+
value: { templates: [{}] },
|
|
1265
|
+
});
|
|
1266
|
+
});
|
|
1267
|
+
|
|
1268
|
+
it("reports missing comma or closing brace in JSON object", () => {
|
|
1269
|
+
const parsed = parseTemplateFile(
|
|
1270
|
+
[
|
|
1271
|
+
"Foo:",
|
|
1272
|
+
" $<schema/>",
|
|
1273
|
+
" $<example>",
|
|
1274
|
+
' {"a": 1 "b": 2}',
|
|
1275
|
+
" $</example>",
|
|
1276
|
+
].join("\n"),
|
|
1277
|
+
);
|
|
1278
|
+
|
|
1279
|
+
expect(parsed).toMatch({
|
|
1280
|
+
errors: [{ expected: "',' or '}'" }],
|
|
1281
|
+
value: { templates: [{}] },
|
|
1282
|
+
});
|
|
1283
|
+
});
|
|
1284
|
+
|
|
1285
|
+
it("reports EOF inside JSON object without closing brace", () => {
|
|
1286
|
+
const parsed = parseTemplateFile(
|
|
1287
|
+
[
|
|
1288
|
+
"Foo:",
|
|
1289
|
+
" $<schema/>",
|
|
1290
|
+
" $<example>",
|
|
1291
|
+
' {"a": 1',
|
|
1292
|
+
" $</example>",
|
|
1293
|
+
].join("\n"),
|
|
1294
|
+
);
|
|
1295
|
+
|
|
1296
|
+
expect(parsed).toMatch({
|
|
1297
|
+
errors: [{ expected: "',' or '}'" }],
|
|
1298
|
+
value: { templates: [{}] },
|
|
1299
|
+
});
|
|
1300
|
+
});
|
|
1301
|
+
|
|
1302
|
+
it("reports missing comma or closing bracket in JSON array", () => {
|
|
1303
|
+
const parsed = parseTemplateFile(
|
|
1304
|
+
[
|
|
1305
|
+
"Foo:",
|
|
1306
|
+
" $<schema/>",
|
|
1307
|
+
" $<example>",
|
|
1308
|
+
" [1 2]",
|
|
1309
|
+
" $</example>",
|
|
1310
|
+
].join("\n"),
|
|
1311
|
+
);
|
|
1312
|
+
|
|
1313
|
+
expect(parsed).toMatch({
|
|
1314
|
+
errors: [{ expected: "',' or ']'" }],
|
|
1315
|
+
value: { templates: [{}] },
|
|
1316
|
+
});
|
|
1317
|
+
});
|
|
1318
|
+
|
|
1319
|
+
it("reports EOF inside JSON array without closing bracket", () => {
|
|
1320
|
+
const parsed = parseTemplateFile(
|
|
1321
|
+
[
|
|
1322
|
+
"Foo:",
|
|
1323
|
+
" $<schema/>",
|
|
1324
|
+
" $<example>",
|
|
1325
|
+
" [1, 2",
|
|
1326
|
+
" $</example>",
|
|
1327
|
+
].join("\n"),
|
|
1328
|
+
);
|
|
1329
|
+
|
|
1330
|
+
expect(parsed).toMatch({
|
|
1331
|
+
errors: [{ expected: "',' or ']'" }],
|
|
1332
|
+
value: { templates: [{}] },
|
|
1333
|
+
});
|
|
1334
|
+
});
|
|
1335
|
+
|
|
1336
|
+
it("reports non-string key in JSON object", () => {
|
|
1337
|
+
const parsed = parseTemplateFile(
|
|
1338
|
+
[
|
|
1339
|
+
"Foo:",
|
|
1340
|
+
" $<schema/>",
|
|
1341
|
+
" $<example>",
|
|
1342
|
+
" {1: 2}",
|
|
1343
|
+
" $</example>",
|
|
1344
|
+
].join("\n"),
|
|
1345
|
+
);
|
|
1346
|
+
|
|
1347
|
+
expect(parsed).toMatch({
|
|
1348
|
+
errors: [{ expected: "JSON string" }],
|
|
1349
|
+
value: { templates: [{}] },
|
|
1350
|
+
});
|
|
1351
|
+
});
|
|
1352
|
+
|
|
1353
|
+
it("reports unterminated JSON string", () => {
|
|
1354
|
+
const parsed = parseTemplateFile(
|
|
1355
|
+
[
|
|
1356
|
+
"Foo:",
|
|
1357
|
+
" $<schema/>",
|
|
1358
|
+
" $<example>",
|
|
1359
|
+
' "hello',
|
|
1360
|
+
" $</example>",
|
|
1361
|
+
].join("\n"),
|
|
1362
|
+
);
|
|
1363
|
+
|
|
1364
|
+
expect(parsed).toMatch({
|
|
1365
|
+
errors: [{ expected: "closing quote for JSON string" }],
|
|
1366
|
+
value: { templates: [{}] },
|
|
1367
|
+
});
|
|
1368
|
+
});
|
|
1369
|
+
|
|
1370
|
+
it("reports invalid boolean keyword", () => {
|
|
1371
|
+
const parsed = parseTemplateFile(
|
|
1372
|
+
[
|
|
1373
|
+
"Foo:",
|
|
1374
|
+
" $<schema/>",
|
|
1375
|
+
" $<example>",
|
|
1376
|
+
" falsy",
|
|
1377
|
+
" $</example>",
|
|
1378
|
+
].join("\n"),
|
|
1379
|
+
);
|
|
1380
|
+
|
|
1381
|
+
expect(parsed).toMatch({
|
|
1382
|
+
errors: [{ expected: "'true' or 'false'" }],
|
|
1383
|
+
value: { templates: [{}] },
|
|
1384
|
+
});
|
|
1385
|
+
});
|
|
1386
|
+
|
|
1387
|
+
it("reports invalid null keyword", () => {
|
|
1388
|
+
const parsed = parseTemplateFile(
|
|
1389
|
+
["Foo:", " $<schema/>", " $<example>", " nul", " $</example>"].join(
|
|
1390
|
+
"\n",
|
|
1391
|
+
),
|
|
1392
|
+
);
|
|
1393
|
+
|
|
1394
|
+
expect(parsed).toMatch({
|
|
1395
|
+
errors: [{ expected: "'null'" }],
|
|
1396
|
+
value: { templates: [{}] },
|
|
1397
|
+
});
|
|
1398
|
+
});
|
|
1399
|
+
|
|
1400
|
+
it("parses ellipsis as a standalone JSON value", () => {
|
|
1401
|
+
const parsed = parseTemplateFile(
|
|
1402
|
+
["Foo:", " $<schema/>", " $<example>", " ...", " $</example>"].join(
|
|
1403
|
+
"\n",
|
|
1404
|
+
),
|
|
1405
|
+
);
|
|
1406
|
+
|
|
1407
|
+
expect(parsed).toMatch({
|
|
1408
|
+
errors: [],
|
|
1409
|
+
value: {
|
|
1410
|
+
templates: [
|
|
1411
|
+
{
|
|
1412
|
+
pieces: [
|
|
1413
|
+
{ kind: "schema" },
|
|
1414
|
+
{ kind: "text" },
|
|
1415
|
+
{ kind: "example", example: { value: { kind: "ellipsis" } } },
|
|
1416
|
+
],
|
|
1417
|
+
},
|
|
1418
|
+
],
|
|
1419
|
+
},
|
|
1420
|
+
});
|
|
1421
|
+
});
|
|
1422
|
+
|
|
1423
|
+
it("parses ellipsis wherever a JSON value is expected", () => {
|
|
1424
|
+
const parsed = parseTemplateFile(
|
|
1425
|
+
[
|
|
1426
|
+
"Foo:",
|
|
1427
|
+
" $<schema/>",
|
|
1428
|
+
" $<example>",
|
|
1429
|
+
' [1, ..., {"a": ...}]',
|
|
1430
|
+
" $</example>",
|
|
1431
|
+
].join("\n"),
|
|
1432
|
+
);
|
|
1433
|
+
|
|
1434
|
+
expect(parsed).toMatch({
|
|
1435
|
+
errors: [],
|
|
1436
|
+
value: {
|
|
1437
|
+
templates: [
|
|
1438
|
+
{
|
|
1439
|
+
pieces: [
|
|
1440
|
+
{ kind: "schema" },
|
|
1441
|
+
{ kind: "text" },
|
|
1442
|
+
{
|
|
1443
|
+
kind: "example",
|
|
1444
|
+
example: {
|
|
1445
|
+
value: {
|
|
1446
|
+
kind: "array",
|
|
1447
|
+
values: [
|
|
1448
|
+
{ kind: "number", value: 1 },
|
|
1449
|
+
{ kind: "ellipsis" },
|
|
1450
|
+
{
|
|
1451
|
+
kind: "object",
|
|
1452
|
+
entries: [
|
|
1453
|
+
{ key: { value: "a" }, value: { kind: "ellipsis" } },
|
|
1454
|
+
],
|
|
1455
|
+
},
|
|
1456
|
+
],
|
|
1457
|
+
},
|
|
1458
|
+
},
|
|
1459
|
+
},
|
|
1460
|
+
],
|
|
1461
|
+
},
|
|
1462
|
+
],
|
|
1463
|
+
},
|
|
1464
|
+
});
|
|
1465
|
+
});
|
|
1466
|
+
|
|
1467
|
+
it("parses object with trailing ellipsis before closing brace", () => {
|
|
1468
|
+
const parsed = parseTemplateFile(
|
|
1469
|
+
[
|
|
1470
|
+
"Foo:",
|
|
1471
|
+
" $<schema/>",
|
|
1472
|
+
" $<example>",
|
|
1473
|
+
' {"foo": true, ...}',
|
|
1474
|
+
" $</example>",
|
|
1475
|
+
].join("\n"),
|
|
1476
|
+
);
|
|
1477
|
+
|
|
1478
|
+
expect(parsed).toMatch({
|
|
1479
|
+
errors: [],
|
|
1480
|
+
value: {
|
|
1481
|
+
templates: [
|
|
1482
|
+
{
|
|
1483
|
+
pieces: [
|
|
1484
|
+
{ kind: "schema" },
|
|
1485
|
+
{ kind: "text" },
|
|
1486
|
+
{
|
|
1487
|
+
kind: "example",
|
|
1488
|
+
example: {
|
|
1489
|
+
value: {
|
|
1490
|
+
kind: "object",
|
|
1491
|
+
entries: [
|
|
1492
|
+
{
|
|
1493
|
+
key: { value: "foo" },
|
|
1494
|
+
value: { kind: "boolean", value: true },
|
|
1495
|
+
},
|
|
1496
|
+
],
|
|
1497
|
+
ellipsis: { kind: "ellipsis" },
|
|
1498
|
+
},
|
|
1499
|
+
},
|
|
1500
|
+
},
|
|
1501
|
+
],
|
|
1502
|
+
},
|
|
1503
|
+
],
|
|
1504
|
+
},
|
|
1505
|
+
});
|
|
1506
|
+
});
|
|
1507
|
+
|
|
1508
|
+
it("parses object that only contains ellipsis", () => {
|
|
1509
|
+
const parsed = parseTemplateFile(
|
|
1510
|
+
[
|
|
1511
|
+
"Foo:",
|
|
1512
|
+
" $<schema/>",
|
|
1513
|
+
" $<example>",
|
|
1514
|
+
" { ... }",
|
|
1515
|
+
" $</example>",
|
|
1516
|
+
].join("\n"),
|
|
1517
|
+
);
|
|
1518
|
+
|
|
1519
|
+
expect(parsed).toMatch({
|
|
1520
|
+
errors: [],
|
|
1521
|
+
value: {
|
|
1522
|
+
templates: [
|
|
1523
|
+
{
|
|
1524
|
+
pieces: [
|
|
1525
|
+
{ kind: "schema" },
|
|
1526
|
+
{ kind: "text" },
|
|
1527
|
+
{
|
|
1528
|
+
kind: "example",
|
|
1529
|
+
example: {
|
|
1530
|
+
value: {
|
|
1531
|
+
kind: "object",
|
|
1532
|
+
entries: [],
|
|
1533
|
+
ellipsis: { kind: "ellipsis" },
|
|
1534
|
+
},
|
|
1535
|
+
},
|
|
1536
|
+
},
|
|
1537
|
+
],
|
|
1538
|
+
},
|
|
1539
|
+
],
|
|
1540
|
+
},
|
|
1541
|
+
});
|
|
1542
|
+
});
|
|
1543
|
+
|
|
1544
|
+
it("parses JSON string, number, boolean, and null as standalone values", () => {
|
|
1545
|
+
const cases: Array<[string, object]> = [
|
|
1546
|
+
['"hello"', { kind: "string", value: "hello" }],
|
|
1547
|
+
["42.5", { kind: "number", value: 42.5 }],
|
|
1548
|
+
["true", { kind: "boolean", value: true }],
|
|
1549
|
+
["null", { kind: "null" }],
|
|
1550
|
+
];
|
|
1551
|
+
|
|
1552
|
+
for (const [input, expectedValue] of cases) {
|
|
1553
|
+
const parsed = parseTemplateFile(
|
|
1554
|
+
["Foo:", " $<schema/>", ` $<example>${input}$</example>`].join("\n"),
|
|
1555
|
+
);
|
|
1556
|
+
|
|
1557
|
+
expect(parsed.errors).toMatch([]);
|
|
1558
|
+
const examplePiece = parsed.value.templates[0]!.pieces.find(
|
|
1559
|
+
(p) => p.kind === "example",
|
|
1560
|
+
);
|
|
1561
|
+
expect(examplePiece).toMatch({
|
|
1562
|
+
kind: "example",
|
|
1563
|
+
example: { value: expectedValue },
|
|
1564
|
+
});
|
|
1565
|
+
}
|
|
1566
|
+
});
|
|
1567
|
+
});
|