@liquidmetal-ai/drizzle 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. package/.changeset/README.md +4 -0
  2. package/.changeset/config.json +11 -0
  3. package/.changeset/odd-plums-dress.md +7 -0
  4. package/.turbo/turbo-build.log +6 -0
  5. package/dist/appify/build.d.ts +130 -0
  6. package/dist/appify/build.d.ts.map +1 -0
  7. package/dist/appify/build.js +703 -0
  8. package/dist/appify/build.test.d.ts +2 -0
  9. package/dist/appify/build.test.d.ts.map +1 -0
  10. package/dist/appify/build.test.js +111 -0
  11. package/dist/appify/index.d.ts +8 -0
  12. package/dist/appify/index.d.ts.map +1 -0
  13. package/dist/appify/index.js +28 -0
  14. package/dist/appify/index.test.d.ts +2 -0
  15. package/dist/appify/index.test.d.ts.map +1 -0
  16. package/dist/appify/index.test.js +40 -0
  17. package/dist/appify/parse.d.ts +151 -0
  18. package/dist/appify/parse.d.ts.map +1 -0
  19. package/dist/appify/parse.js +579 -0
  20. package/dist/appify/parse.test.d.ts +2 -0
  21. package/dist/appify/parse.test.d.ts.map +1 -0
  22. package/dist/appify/parse.test.js +319 -0
  23. package/dist/appify/validate.d.ts +22 -0
  24. package/dist/appify/validate.d.ts.map +1 -0
  25. package/dist/appify/validate.js +346 -0
  26. package/dist/appify/validate.test.d.ts +2 -0
  27. package/dist/appify/validate.test.d.ts.map +1 -0
  28. package/dist/appify/validate.test.js +327 -0
  29. package/dist/codestore.d.ts +34 -0
  30. package/dist/codestore.d.ts.map +1 -0
  31. package/dist/codestore.js +54 -0
  32. package/dist/codestore.test.d.ts +2 -0
  33. package/dist/codestore.test.d.ts.map +1 -0
  34. package/dist/codestore.test.js +84 -0
  35. package/dist/liquidmetal/v1alpha1/catalog_connect.d.ts +147 -0
  36. package/dist/liquidmetal/v1alpha1/catalog_connect.d.ts.map +1 -0
  37. package/dist/liquidmetal/v1alpha1/catalog_connect.js +150 -0
  38. package/dist/liquidmetal/v1alpha1/catalog_pb.d.ts +965 -0
  39. package/dist/liquidmetal/v1alpha1/catalog_pb.d.ts.map +1 -0
  40. package/dist/liquidmetal/v1alpha1/catalog_pb.js +1486 -0
  41. package/dist/liquidmetal/v1alpha1/rainbow_auth_connect.d.ts +49 -0
  42. package/dist/liquidmetal/v1alpha1/rainbow_auth_connect.d.ts.map +1 -0
  43. package/dist/liquidmetal/v1alpha1/rainbow_auth_connect.js +52 -0
  44. package/dist/liquidmetal/v1alpha1/rainbow_auth_pb.d.ts +271 -0
  45. package/dist/liquidmetal/v1alpha1/rainbow_auth_pb.d.ts.map +1 -0
  46. package/dist/liquidmetal/v1alpha1/rainbow_auth_pb.js +381 -0
  47. package/dist/liquidmetal/v1alpha1/raindrop_pb.d.ts +38 -0
  48. package/dist/liquidmetal/v1alpha1/raindrop_pb.d.ts.map +1 -0
  49. package/dist/liquidmetal/v1alpha1/raindrop_pb.js +52 -0
  50. package/dist/unsafe/codestore.d.ts +10 -0
  51. package/dist/unsafe/codestore.d.ts.map +1 -0
  52. package/dist/unsafe/codestore.js +23 -0
  53. package/dist/unsafe/codestore.test.d.ts +2 -0
  54. package/dist/unsafe/codestore.test.d.ts.map +1 -0
  55. package/dist/unsafe/codestore.test.js +27 -0
  56. package/eslint.config.mjs +4 -0
  57. package/package.json +45 -0
  58. package/src/appify/build.test.ts +116 -0
  59. package/src/appify/build.ts +783 -0
  60. package/src/appify/index.test.ts +43 -0
  61. package/src/appify/index.ts +33 -0
  62. package/src/appify/parse.test.ts +337 -0
  63. package/src/appify/parse.ts +744 -0
  64. package/src/appify/validate.test.ts +341 -0
  65. package/src/appify/validate.ts +435 -0
  66. package/src/codestore.test.ts +98 -0
  67. package/src/codestore.ts +93 -0
  68. package/src/liquidmetal/v1alpha1/catalog_connect.ts +153 -0
  69. package/src/liquidmetal/v1alpha1/catalog_pb.ts +1827 -0
  70. package/src/liquidmetal/v1alpha1/rainbow_auth_connect.ts +55 -0
  71. package/src/liquidmetal/v1alpha1/rainbow_auth_pb.ts +476 -0
  72. package/src/liquidmetal/v1alpha1/raindrop_pb.ts +63 -0
  73. package/src/unsafe/codestore.test.ts +34 -0
  74. package/src/unsafe/codestore.ts +29 -0
  75. package/tsconfig.json +31 -0
  76. package/tsconfig.tsbuildinfo +1 -0
@@ -0,0 +1,744 @@
1
+ type TokenBase = {
2
+ value: string;
3
+ start: number;
4
+ end: number;
5
+ line: number;
6
+ column: number;
7
+ };
8
+
9
+ export type TokenNumber = TokenBase & {
10
+ type: 'number';
11
+ };
12
+
13
+ export type TokenOperator = TokenBase & {
14
+ type: 'operator';
15
+ };
16
+
17
+ export type TokenParenthesis = TokenBase & {
18
+ type: 'parenthesis';
19
+ };
20
+
21
+ export type TokenWhitespace = TokenBase & {
22
+ type: 'whitespace';
23
+ };
24
+
25
+ export type TokenBoolean = TokenBase & {
26
+ type: 'boolean';
27
+ };
28
+
29
+ export type TokenString = TokenBase & {
30
+ type: 'string';
31
+ };
32
+
33
+ export type TokenIdentifier = TokenBase & {
34
+ type: 'identifier';
35
+ };
36
+
37
+ export type TokenComment = TokenBase & {
38
+ type: 'comment';
39
+ };
40
+
41
+ export type TokenNewline = TokenBase & {
42
+ type: 'newline';
43
+ };
44
+
45
+ export type TokenUnknown = TokenBase & {
46
+ type: 'unknown';
47
+ };
48
+
49
+ export type TokenCurly = TokenBase & {
50
+ type: 'curly';
51
+ };
52
+
53
+ export type TokenBracket = TokenBase & {
54
+ type: 'bracket';
55
+ };
56
+
57
+ export type TokenComma = TokenBase & {
58
+ type: 'comma';
59
+ };
60
+
61
+ export type TokenEnd = TokenBase & {
62
+ type: 'end';
63
+ };
64
+
65
+ type Token =
66
+ | TokenNumber
67
+ | TokenOperator
68
+ | TokenParenthesis
69
+ | TokenWhitespace
70
+ | TokenBoolean
71
+ | TokenString
72
+ | TokenIdentifier
73
+ | TokenComment
74
+ | TokenNewline
75
+ | TokenUnknown
76
+ | TokenCurly
77
+ | TokenBracket
78
+ | TokenEnd
79
+ | TokenComma;
80
+
81
+ type TokenType = Token['type'];
82
+
83
+ const TOKEN_PATTERNS: { type: TokenType; pattern: RegExp }[] = [
84
+ { type: 'number', pattern: /^\d+(\.\d+)?/ },
85
+ { type: 'comment', pattern: /^\/\/[^\n]*/ },
86
+ { type: 'operator', pattern: /^[+\-*/%=]/ },
87
+ { type: 'whitespace', pattern: /^[ \t]+/ },
88
+ { type: 'boolean', pattern: /^(true|false)/ },
89
+ { type: 'string', pattern: /^'(?:[^'\\\n]|\\.)*'/ },
90
+ { type: 'string', pattern: /^"(?:[^"\\\n]|\\.)*"/ },
91
+ { type: 'identifier', pattern: /^[a-zA-Z_]\w*/ },
92
+ { type: 'newline', pattern: /^\n/ },
93
+ { type: 'curly', pattern: /^[{}]/ },
94
+ { type: 'comma', pattern: /^,/ },
95
+ // eslint-disable-next-line no-useless-escape
96
+ { type: 'bracket', pattern: /^[\[\]]/ },
97
+ { type: 'parenthesis', pattern: /^[()]/ },
98
+ { type: 'unknown', pattern: /^./ },
99
+ ];
100
+
101
+ export class Tokenizer {
102
+ cursor = 0;
103
+ line = 1;
104
+ column = 1;
105
+
106
+ constructor(private input: string) {}
107
+
108
+ // Get the next token from the input. The optional ignore parameter
109
+ // can be used to skip certain token types. This could possibly be
110
+ // removed, but whitespace might be useful at some point.
111
+ next(ignore: TokenType[] = ['whitespace']): Token {
112
+ if (this.eof()) {
113
+ return {
114
+ type: 'end',
115
+ value: '',
116
+ start: this.cursor,
117
+ end: this.cursor,
118
+ line: this.line,
119
+ column: this.column,
120
+ };
121
+ }
122
+
123
+ // Try to match a token in order of precedence.
124
+ for (const { type, pattern } of TOKEN_PATTERNS) {
125
+ const result = this.input.slice(this.cursor).match(pattern);
126
+ if (result) {
127
+ const token = {
128
+ type,
129
+ value: result[0],
130
+ start: this.cursor,
131
+ end: this.cursor + result[0].length,
132
+ line: this.line,
133
+ column: this.column,
134
+ };
135
+ this.cursor += result[0].length;
136
+ if (type === 'newline') {
137
+ this.line++;
138
+ this.column = 1;
139
+ } else {
140
+ this.column += result[0].length;
141
+ }
142
+ // Skip any ignored tokens.
143
+ if (ignore.includes(token.type)) {
144
+ return this.next(ignore);
145
+ }
146
+ return token;
147
+ }
148
+ }
149
+
150
+ // This should be unreachable? Maybe?
151
+ throw new Error(`Unknown token at ${this.line}:${this.column}`);
152
+ }
153
+
154
+ peek(ignore: TokenType[] = ['whitespace']): Token {
155
+ const cursor = this.cursor;
156
+ const line = this.line;
157
+ const column = this.column;
158
+ const token = this.next(ignore);
159
+ this.cursor = cursor;
160
+ this.line = line;
161
+ this.column = column;
162
+ return token;
163
+ }
164
+
165
+ eof(): boolean {
166
+ return this.cursor >= this.input.length;
167
+ }
168
+ }
169
+
170
+ export type NodeType = Node['type'] | TokenType;
171
+
172
+ export type Node =
173
+ | StanzaNode
174
+ | AssignmentNode
175
+ | BlockNode
176
+ | ExpressionNode
177
+ | EmptyNode
178
+ | ArrayNode
179
+ | ObjectNode
180
+ | Token;
181
+
182
+ type BaseNode = {
183
+ line: number;
184
+ column: number;
185
+ start: number;
186
+ end: number;
187
+ };
188
+
189
+ export type StanzaNode = BaseNode & {
190
+ type: 'stanza';
191
+ name: string;
192
+ args: (TokenString | TokenNumber | TokenBoolean)[];
193
+ block: BlockNode | null;
194
+ };
195
+
196
+ export type BlockNode = BaseNode & {
197
+ type: 'block';
198
+ children: (Node | TokenNewline | TokenComment)[];
199
+ };
200
+
201
+ export type AssignmentNode = BaseNode & {
202
+ type: 'assignment';
203
+ operator: TokenOperator;
204
+ key: Extract<Token, { type: 'identifier' }>;
205
+ value: ExpressionNode | Extract<Token, { type: 'string' | 'number' | 'boolean' }> | ArrayNode | ObjectNode;
206
+ };
207
+
208
+ export type ArrayNode = BaseNode & {
209
+ type: 'array';
210
+ elements: (ExpressionNode | Extract<Token, { type: 'string' | 'number' | 'boolean' }> | ArrayNode | ObjectNode)[];
211
+ };
212
+
213
+ export type ObjectNode = BaseNode & {
214
+ type: 'object';
215
+ properties: AssignmentNode[];
216
+ };
217
+
218
+ export type ExpressionNode = BaseNode & {
219
+ type: 'expression';
220
+ operator: TokenOperator;
221
+ left: Extract<Token, { type: 'string' | 'number' | 'boolean' }>;
222
+ right: ExpressionNode | Extract<Token, { type: 'string' | 'number' | 'boolean' }>;
223
+ };
224
+
225
+ export type EmptyNode = BaseNode & {
226
+ type: 'empty';
227
+ };
228
+
229
+ type ParserError = {
230
+ expected?: NodeType[];
231
+ received?: NodeType;
232
+ message: string;
233
+ line: number;
234
+ column: number;
235
+ start: number;
236
+ end: number;
237
+ };
238
+
239
+ // Parser class that takes a tokenizer and produces an AST. This
240
+ // completely consumes the tokenizer.
241
+ export class Parser {
242
+ errors: ParserError[] = [];
243
+
244
+ constructor(private tokenizer: Tokenizer) {}
245
+
246
+ // Parse the input and return the AST. The optional node parameter
247
+ // is used when this is recursed.
248
+ parse(root?: BlockNode): BlockNode {
249
+ if (root === undefined) {
250
+ root = {
251
+ type: 'block',
252
+ children: [],
253
+ line: this.tokenizer.line,
254
+ column: this.tokenizer.column,
255
+ start: this.tokenizer.cursor,
256
+ end: 0,
257
+ };
258
+ }
259
+ while (!this.tokenizer.eof()) {
260
+ const token = this.tokenizer.peek();
261
+ switch (token.type) {
262
+ case 'identifier':
263
+ root.children.push(this.parseIdentifier());
264
+ break;
265
+ case 'newline':
266
+ root.children.push(this.tokenizer.next() as TokenNewline);
267
+ break;
268
+ case 'comment':
269
+ root.children.push(this.tokenizer.next() as TokenComment);
270
+ break;
271
+ case 'curly':
272
+ if (token.value === '}') {
273
+ root.end = this.tokenizer.cursor;
274
+ return root;
275
+ } else {
276
+ // wtf?
277
+ this.errors.push({
278
+ expected: ['identifier'],
279
+ received: 'curly',
280
+ message: `Unexpected ${token.value} at ${token.line}:${token.column}`,
281
+ line: token.line,
282
+ column: token.column,
283
+ start: token.start,
284
+ end: token.end,
285
+ });
286
+ this.tokenizer.next();
287
+ }
288
+ break;
289
+ case 'end':
290
+ root.end = this.tokenizer.cursor;
291
+ return root;
292
+ default:
293
+ this.errors.push({
294
+ expected: ['identifier', 'newline', 'comment'],
295
+ received: token.type,
296
+ message: `Unexpected ${token.type} at ${token.line}:${token.column}`,
297
+ line: token.line,
298
+ column: token.column,
299
+ start: token.start,
300
+ end: token.end,
301
+ });
302
+ root.end = this.tokenizer.cursor;
303
+ return root;
304
+ }
305
+ }
306
+ // We should never reach this point, but if we do, we're at the
307
+ // end.
308
+ root.end = this.tokenizer.cursor;
309
+ return root;
310
+ }
311
+
312
+ // Parse an identifier, which could be a stanza, assignment.
313
+ parseIdentifier(): Node {
314
+ const token = this.tokenizer.next();
315
+ // Assert token is an identifier.
316
+ if (token.type !== 'identifier') {
317
+ // should be unreachable
318
+ throw new Error(`Expected identifier but got ${token.type} at ${token.line}:${token.column}`);
319
+ }
320
+
321
+ const next = this.tokenizer.peek();
322
+ if (next.type === 'curly' && next.value === '{') {
323
+ return this.parseStanza(token);
324
+ }
325
+ if (['string', 'number', 'boolean'].includes(next.type)) {
326
+ return this.parseStanza(token);
327
+ }
328
+ if (next.type === 'operator' && next.value === '=') {
329
+ return this.parseAssignment(token);
330
+ }
331
+ this.errors.push({
332
+ expected: ['curly', 'string', 'operator'],
333
+ received: next.type,
334
+ message: `Expected {, argument or operator at ${next.line}:${next.column}`,
335
+ line: next.line,
336
+ column: next.column,
337
+ start: next.start,
338
+ end: next.end,
339
+ });
340
+ return { type: 'empty', line: next.line, column: next.column, start: next.start, end: next.end };
341
+ }
342
+
343
+ // Parse a stanza, which is a block of assignments and other
344
+ // stanzas.
345
+ parseStanza(token: TokenIdentifier): Node {
346
+ const stanza: StanzaNode = {
347
+ type: 'stanza',
348
+ name: token.value,
349
+ args: [],
350
+ block: null,
351
+ line: token.line,
352
+ column: token.column,
353
+ start: token.start,
354
+ end: token.end,
355
+ };
356
+ let next = this.tokenizer.peek();
357
+ while (next.type !== 'curly') {
358
+ if (['string', 'number', 'boolean'].includes(next.type)) {
359
+ stanza.args.push(this.tokenizer.next() as TokenString | TokenNumber | TokenBoolean);
360
+ next = this.tokenizer.peek();
361
+ continue;
362
+ }
363
+ this.errors.push({
364
+ expected: ['string', 'number', 'boolean'],
365
+ received: next.type,
366
+ message: `Expected argument at ${next.line}:${next.column}`,
367
+ line: next.line,
368
+ column: next.column,
369
+ start: next.start,
370
+ end: next.end,
371
+ });
372
+ this.tokenizer.next();
373
+ next = this.tokenizer.peek();
374
+ }
375
+ this.expect('curly', '{');
376
+ stanza.block = this.parse();
377
+ this.expect('curly', '}');
378
+ stanza.end = this.tokenizer.cursor;
379
+ return stanza;
380
+ }
381
+
382
+ // Parse an assignment, which is an assignment to an expression.
383
+ parseAssignment(token: Token): AssignmentNode {
384
+ if (token.type !== 'identifier') {
385
+ throw new Error(`Expected identifier but got ${token.type} at ${token.line}:${token.column}`);
386
+ }
387
+ const key = token;
388
+ const operator = this.expect('operator', '=');
389
+ const value = this.parseExpression();
390
+ return {
391
+ type: 'assignment',
392
+ operator,
393
+ key,
394
+ value,
395
+ line: key.line,
396
+ column: key.column,
397
+ start: key.start,
398
+ end: value.end,
399
+ };
400
+ }
401
+
402
+ // Parse an expression. We're not building a calculator (yet), so we
403
+ // just accept a value on the right for now.
404
+ parseExpression(
405
+ ignore?: TokenType[],
406
+ ): TokenNumber | TokenString | TokenBoolean | ArrayNode | ObjectNode | ExpressionNode {
407
+ const token = this.tokenizer.next(ignore || ['whitespace']);
408
+ switch (token.type) {
409
+ case 'string':
410
+ case 'number':
411
+ case 'boolean':
412
+ return token;
413
+ case 'bracket':
414
+ if (token.value === '[') {
415
+ return this.parseArray();
416
+ }
417
+ break;
418
+ case 'curly':
419
+ if (token.value === '{') {
420
+ return this.parseObject();
421
+ } else {
422
+ break;
423
+ }
424
+ default:
425
+ break;
426
+ }
427
+ // Recover by calling it a string.
428
+ this.errors.push({
429
+ expected: ['expression'],
430
+ received: token.type,
431
+ message: `Expected expression but got ${token.type} at ${token.line}:${token.column}`,
432
+ line: token.line,
433
+ column: token.column,
434
+ start: token.start,
435
+ end: token.end,
436
+ });
437
+ return {
438
+ type: 'string',
439
+ value: '"<error>"',
440
+ line: token.line,
441
+ column: token.column,
442
+ start: token.start,
443
+ end: token.end,
444
+ };
445
+ }
446
+
447
+ parseArray(): ArrayNode {
448
+ // the first thing we expect are expression.
449
+ const elements = [];
450
+ // This whole ignoring newlines thing is really just context.
451
+ let next = this.tokenizer.peek(['whitespace', 'newline']);
452
+ while (true) {
453
+ elements.push(this.parseExpression(['whitespace', 'newline']));
454
+ next = this.tokenizer.peek(['whitespace', 'newline']);
455
+ if (next.type === 'bracket' && next.value === ']') {
456
+ this.tokenizer.next(['whitespace', 'newline']);
457
+ return { type: 'array', elements, line: next.line, column: next.column, start: next.start, end: next.end };
458
+ }
459
+ if (next.type === 'comma') {
460
+ this.tokenizer.next();
461
+ next = this.tokenizer.peek(['whitespace', 'newline']);
462
+ // Special case: allow an ending comma because it's nice.
463
+ if (next.type === 'bracket' && next.value === ']') {
464
+ this.tokenizer.next(['whitespace', 'newline']);
465
+ return { type: 'array', elements, line: next.line, column: next.column, start: next.start, end: next.end };
466
+ }
467
+ continue;
468
+ }
469
+ // Should have been a closing bracket or a comma, so we "close" and return for recovery.
470
+ this.errors.push({
471
+ expected: ['bracket', 'comma'],
472
+ received: next.type,
473
+ message: `Expected ] or , but got ${next.type} at ${next.line}:${next.column}`,
474
+ line: next.line,
475
+ column: next.column,
476
+ start: next.start,
477
+ end: next.end,
478
+ });
479
+ return { type: 'array', elements, line: next.line, column: next.column, start: next.start, end: next.end };
480
+ }
481
+ }
482
+
483
+ parseObject(): ObjectNode {
484
+ const properties = [];
485
+ let next = this.tokenizer.peek(['whitespace', 'newline']);
486
+ while (true) {
487
+ if (next.type === 'end') {
488
+ this.errors.push({
489
+ expected: ['identifier'],
490
+ received: 'end',
491
+ message: `Unexpected end of input at ${next.line}:${next.column}`,
492
+ line: next.line,
493
+ column: next.column,
494
+ start: next.start,
495
+ end: next.end,
496
+ });
497
+ break;
498
+ }
499
+ if (next.type === 'curly' && next.value === '}') {
500
+ this.tokenizer.next(['whitespace', 'newline']);
501
+ break;
502
+ }
503
+ if (next.type === 'identifier') {
504
+ properties.push(this.parseAssignment(this.tokenizer.next(['whitespace', 'newline'])));
505
+ next = this.tokenizer.peek(['whitespace', 'newline']);
506
+ continue;
507
+ }
508
+ const token = this.tokenizer.next(['whitespace', 'newline']);
509
+ next = this.tokenizer.peek(['whitespace', 'newline']);
510
+ this.errors.push({
511
+ expected: ['identifier'],
512
+ received: next.type,
513
+ message: `Expected identifier but got ${token.type} at ${next.line}:${next.column}`,
514
+ line: next.line,
515
+ column: next.column,
516
+ start: next.start,
517
+ end: next.end,
518
+ });
519
+ }
520
+ return { type: 'object', properties, line: next.line, column: next.column, start: next.start, end: next.end };
521
+ }
522
+
523
+ // Expect a token of a certain type, optionally with a certain
524
+ // value.
525
+ expect<T extends TokenType>(type: T, value?: string): Extract<Token, { type: T }> {
526
+ const token = this.tokenizer.peek();
527
+ if (token.type === 'end' && type !== 'end') {
528
+ this.errors.push({
529
+ expected: [type],
530
+ received: 'end',
531
+ message: `Unexpected end of input at ${token.line}:${token.column}`,
532
+ line: token.line,
533
+ column: token.column,
534
+ start: token.start,
535
+ end: token.end,
536
+ });
537
+ return this.tokenizer.next() as Extract<Token, { type: T }>;
538
+ }
539
+ if (token.type !== type) {
540
+ this.errors.push({
541
+ expected: [type],
542
+ received: token.type,
543
+ message: `Expected ${type} but got ${token.type} at ${token.line}:${token.column}`,
544
+ line: token.line,
545
+ column: token.column,
546
+ start: token.start,
547
+ end: token.end,
548
+ });
549
+ // Naively try to recover by returning a working token.
550
+ return {
551
+ type: type,
552
+ value: token.value,
553
+ start: token.start,
554
+ end: token.end,
555
+ line: token.line,
556
+ column: token.column,
557
+ } as Token as Extract<Token, { type: T }>;
558
+ }
559
+ if (value && token.value !== value) {
560
+ this.errors.push({
561
+ message: `Expected ${value} but got ${token.value} at ${token.line}:${token.column}`,
562
+ line: token.line,
563
+ column: token.column,
564
+ start: token.start,
565
+ end: token.end,
566
+ });
567
+ // Again, naively try to recover with a working token.
568
+ return {
569
+ type,
570
+ value,
571
+ start: token.start,
572
+ end: token.end,
573
+ line: token.line,
574
+ column: token.column,
575
+ } as Token as Extract<Token, { type: T }>;
576
+ }
577
+ // We match, so consume the token.
578
+ return this.tokenizer.next() as Extract<Token, { type: T }>;
579
+ }
580
+ }
581
+
582
+ type TranslationError = {
583
+ message: string;
584
+ line: number;
585
+ column: number;
586
+ };
587
+
588
+ // JSONTranslator takes an AST and translates it into a JSON object
589
+ // (any).
590
+ export class JSONTranslator {
591
+ errors: TranslationError[];
592
+
593
+ constructor() {
594
+ this.errors = [];
595
+ }
596
+
597
+ // Translate the AST into a JSON serializable object.
598
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
599
+ translate(ast: BlockNode): any {
600
+ return this.translateRoot(ast);
601
+ }
602
+
603
+ // Start at the root and translate the children.
604
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
605
+ translateRoot(node: BlockNode): any {
606
+ const obj: any = {}; // eslint-disable-line @typescript-eslint/no-explicit-any
607
+ for (const child of node.children) {
608
+ if (child.type === 'stanza') {
609
+ if (!obj[child.name]) {
610
+ obj[child.name] = [];
611
+ }
612
+ const stanza = this.translateStanza(child);
613
+ obj[child.name].push(stanza);
614
+ } else {
615
+ this.errors.push({
616
+ message: `unexpected ${child.type} at ${child.line}:${child.column}`,
617
+ line: 1,
618
+ column: 1,
619
+ });
620
+ }
621
+ }
622
+ return obj;
623
+ }
624
+
625
+ // translateStanza takes a stanza node and translates it into a JSON
626
+ // object. This looks remarkably like translateRoot, and maybe
627
+ // there's some kind of generalized recursion we could use, but it's
628
+ // late.
629
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
630
+ translateStanza(node: StanzaNode): any {
631
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
632
+ const obj: { [key: string]: any } = {};
633
+ if (node.args && node.args[0]) {
634
+ obj['name'] = node.args[0].value.slice(1, -1);
635
+ }
636
+ const children = node.block ? node.block.children : [];
637
+ for (const child of children) {
638
+ if (child.type === 'assignment') {
639
+ // Remember we skipped expressions, so...
640
+ if (child.value.type === 'string') {
641
+ obj[child.key.value] = child.value.value.slice(1, -1);
642
+ } else if (child.value.type === 'number') {
643
+ obj[child.key.value] = parseFloat(child.value.value);
644
+ } else if (child.value.type === 'boolean') {
645
+ obj[child.key.value] = child.value.value === 'true';
646
+ } else if (child.value.type === 'array') {
647
+ obj[child.key.value] = child.value.elements.map((el) => {
648
+ if (el.type === 'string') {
649
+ return el.value.slice(1, -1);
650
+ } else if (el.type === 'number') {
651
+ return parseFloat(el.value);
652
+ } else if (el.type === 'boolean') {
653
+ return el.value === 'true';
654
+ }
655
+ return null;
656
+ });
657
+ }
658
+ }
659
+ if (child.type === 'stanza') {
660
+ if (!obj[child.name]) {
661
+ obj[child.name] = [];
662
+ }
663
+ obj[child.name].push(this.translateStanza(child));
664
+ }
665
+ }
666
+ return obj;
667
+ }
668
+ }
669
+
670
+ // fmt is an ugly mess of string concatenation.
671
+ export function fmt(ast: Node | Token | null, indent: number = 0, sibling?: Node | Token): string {
672
+ if (ast === null) {
673
+ return '';
674
+ }
675
+ const INDENT = ' '.repeat(indent);
676
+ let str = '';
677
+ // Just spit out token values as they are encountered.
678
+ switch (ast.type) {
679
+ case 'stanza':
680
+ str += `${INDENT}${ast.name} `;
681
+ for (const arg of ast.args) {
682
+ str += `${arg.value} `;
683
+ }
684
+ str += '{';
685
+ str += fmt(ast.block, indent + 2);
686
+ if (!ast.block?.children.every((child) => child.type === 'newline')) {
687
+ str += `${INDENT}`;
688
+ }
689
+ str += '}';
690
+ break;
691
+ case 'block':
692
+ if (ast.children.every((child) => child.type === 'newline')) {
693
+ // Do nothing.
694
+ } else {
695
+ for (const child of ast.children) {
696
+ str += fmt(child, indent, sibling);
697
+ sibling = child;
698
+ }
699
+ }
700
+ break;
701
+ case 'assignment':
702
+ str += `${INDENT}${fmt(ast.key)} = ${fmt(ast.value)}`;
703
+ break;
704
+ case 'expression':
705
+ str += `${fmt(ast.left)} ${ast.operator.value} ${fmt(ast.right)}`;
706
+ break;
707
+ case 'comment':
708
+ if (sibling?.type !== 'newline') {
709
+ str += ` `;
710
+ } else {
711
+ str += `${INDENT}`;
712
+ }
713
+ str += `${ast.value}`;
714
+ break;
715
+ case 'empty':
716
+ break;
717
+ case 'array':
718
+ // eslint-disable-next-line no-case-declarations
719
+ const oneLine = `[${ast.elements.map((el) => fmt(el)).join(', ')}]`;
720
+ if (oneLine.length > 80) {
721
+ str += `[`;
722
+ for (const el of ast.elements) {
723
+ str += `\n${fmt(el, indent + 2)}`;
724
+ }
725
+ str += `\n${INDENT}]`;
726
+ } else {
727
+ return oneLine;
728
+ }
729
+ break;
730
+ case 'object':
731
+ str += '{';
732
+ if (ast.properties.length > 0) {
733
+ str += '\n';
734
+ for (const assign of ast.properties) {
735
+ str += `${fmt(assign, indent + 2)}\n`;
736
+ }
737
+ str += `\n${INDENT}}`;
738
+ }
739
+ break;
740
+ default:
741
+ str += `${ast?.value}`;
742
+ }
743
+ return str;
744
+ }