@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.
- package/.changeset/README.md +4 -0
- package/.changeset/config.json +11 -0
- package/.changeset/odd-plums-dress.md +7 -0
- package/.turbo/turbo-build.log +6 -0
- package/dist/appify/build.d.ts +130 -0
- package/dist/appify/build.d.ts.map +1 -0
- package/dist/appify/build.js +703 -0
- package/dist/appify/build.test.d.ts +2 -0
- package/dist/appify/build.test.d.ts.map +1 -0
- package/dist/appify/build.test.js +111 -0
- package/dist/appify/index.d.ts +8 -0
- package/dist/appify/index.d.ts.map +1 -0
- package/dist/appify/index.js +28 -0
- package/dist/appify/index.test.d.ts +2 -0
- package/dist/appify/index.test.d.ts.map +1 -0
- package/dist/appify/index.test.js +40 -0
- package/dist/appify/parse.d.ts +151 -0
- package/dist/appify/parse.d.ts.map +1 -0
- package/dist/appify/parse.js +579 -0
- package/dist/appify/parse.test.d.ts +2 -0
- package/dist/appify/parse.test.d.ts.map +1 -0
- package/dist/appify/parse.test.js +319 -0
- package/dist/appify/validate.d.ts +22 -0
- package/dist/appify/validate.d.ts.map +1 -0
- package/dist/appify/validate.js +346 -0
- package/dist/appify/validate.test.d.ts +2 -0
- package/dist/appify/validate.test.d.ts.map +1 -0
- package/dist/appify/validate.test.js +327 -0
- package/dist/codestore.d.ts +34 -0
- package/dist/codestore.d.ts.map +1 -0
- package/dist/codestore.js +54 -0
- package/dist/codestore.test.d.ts +2 -0
- package/dist/codestore.test.d.ts.map +1 -0
- package/dist/codestore.test.js +84 -0
- package/dist/liquidmetal/v1alpha1/catalog_connect.d.ts +147 -0
- package/dist/liquidmetal/v1alpha1/catalog_connect.d.ts.map +1 -0
- package/dist/liquidmetal/v1alpha1/catalog_connect.js +150 -0
- package/dist/liquidmetal/v1alpha1/catalog_pb.d.ts +965 -0
- package/dist/liquidmetal/v1alpha1/catalog_pb.d.ts.map +1 -0
- package/dist/liquidmetal/v1alpha1/catalog_pb.js +1486 -0
- package/dist/liquidmetal/v1alpha1/rainbow_auth_connect.d.ts +49 -0
- package/dist/liquidmetal/v1alpha1/rainbow_auth_connect.d.ts.map +1 -0
- package/dist/liquidmetal/v1alpha1/rainbow_auth_connect.js +52 -0
- package/dist/liquidmetal/v1alpha1/rainbow_auth_pb.d.ts +271 -0
- package/dist/liquidmetal/v1alpha1/rainbow_auth_pb.d.ts.map +1 -0
- package/dist/liquidmetal/v1alpha1/rainbow_auth_pb.js +381 -0
- package/dist/liquidmetal/v1alpha1/raindrop_pb.d.ts +38 -0
- package/dist/liquidmetal/v1alpha1/raindrop_pb.d.ts.map +1 -0
- package/dist/liquidmetal/v1alpha1/raindrop_pb.js +52 -0
- package/dist/unsafe/codestore.d.ts +10 -0
- package/dist/unsafe/codestore.d.ts.map +1 -0
- package/dist/unsafe/codestore.js +23 -0
- package/dist/unsafe/codestore.test.d.ts +2 -0
- package/dist/unsafe/codestore.test.d.ts.map +1 -0
- package/dist/unsafe/codestore.test.js +27 -0
- package/eslint.config.mjs +4 -0
- package/package.json +45 -0
- package/src/appify/build.test.ts +116 -0
- package/src/appify/build.ts +783 -0
- package/src/appify/index.test.ts +43 -0
- package/src/appify/index.ts +33 -0
- package/src/appify/parse.test.ts +337 -0
- package/src/appify/parse.ts +744 -0
- package/src/appify/validate.test.ts +341 -0
- package/src/appify/validate.ts +435 -0
- package/src/codestore.test.ts +98 -0
- package/src/codestore.ts +93 -0
- package/src/liquidmetal/v1alpha1/catalog_connect.ts +153 -0
- package/src/liquidmetal/v1alpha1/catalog_pb.ts +1827 -0
- package/src/liquidmetal/v1alpha1/rainbow_auth_connect.ts +55 -0
- package/src/liquidmetal/v1alpha1/rainbow_auth_pb.ts +476 -0
- package/src/liquidmetal/v1alpha1/raindrop_pb.ts +63 -0
- package/src/unsafe/codestore.test.ts +34 -0
- package/src/unsafe/codestore.ts +29 -0
- package/tsconfig.json +31 -0
- package/tsconfig.tsbuildinfo +1 -0
|
@@ -0,0 +1,579 @@
|
|
|
1
|
+
const TOKEN_PATTERNS = [
|
|
2
|
+
{ type: 'number', pattern: /^\d+(\.\d+)?/ },
|
|
3
|
+
{ type: 'comment', pattern: /^\/\/[^\n]*/ },
|
|
4
|
+
{ type: 'operator', pattern: /^[+\-*/%=]/ },
|
|
5
|
+
{ type: 'whitespace', pattern: /^[ \t]+/ },
|
|
6
|
+
{ type: 'boolean', pattern: /^(true|false)/ },
|
|
7
|
+
{ type: 'string', pattern: /^'(?:[^'\\\n]|\\.)*'/ },
|
|
8
|
+
{ type: 'string', pattern: /^"(?:[^"\\\n]|\\.)*"/ },
|
|
9
|
+
{ type: 'identifier', pattern: /^[a-zA-Z_]\w*/ },
|
|
10
|
+
{ type: 'newline', pattern: /^\n/ },
|
|
11
|
+
{ type: 'curly', pattern: /^[{}]/ },
|
|
12
|
+
{ type: 'comma', pattern: /^,/ },
|
|
13
|
+
// eslint-disable-next-line no-useless-escape
|
|
14
|
+
{ type: 'bracket', pattern: /^[\[\]]/ },
|
|
15
|
+
{ type: 'parenthesis', pattern: /^[()]/ },
|
|
16
|
+
{ type: 'unknown', pattern: /^./ },
|
|
17
|
+
];
|
|
18
|
+
export class Tokenizer {
|
|
19
|
+
input;
|
|
20
|
+
cursor = 0;
|
|
21
|
+
line = 1;
|
|
22
|
+
column = 1;
|
|
23
|
+
constructor(input) {
|
|
24
|
+
this.input = input;
|
|
25
|
+
}
|
|
26
|
+
// Get the next token from the input. The optional ignore parameter
|
|
27
|
+
// can be used to skip certain token types. This could possibly be
|
|
28
|
+
// removed, but whitespace might be useful at some point.
|
|
29
|
+
next(ignore = ['whitespace']) {
|
|
30
|
+
if (this.eof()) {
|
|
31
|
+
return {
|
|
32
|
+
type: 'end',
|
|
33
|
+
value: '',
|
|
34
|
+
start: this.cursor,
|
|
35
|
+
end: this.cursor,
|
|
36
|
+
line: this.line,
|
|
37
|
+
column: this.column,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
// Try to match a token in order of precedence.
|
|
41
|
+
for (const { type, pattern } of TOKEN_PATTERNS) {
|
|
42
|
+
const result = this.input.slice(this.cursor).match(pattern);
|
|
43
|
+
if (result) {
|
|
44
|
+
const token = {
|
|
45
|
+
type,
|
|
46
|
+
value: result[0],
|
|
47
|
+
start: this.cursor,
|
|
48
|
+
end: this.cursor + result[0].length,
|
|
49
|
+
line: this.line,
|
|
50
|
+
column: this.column,
|
|
51
|
+
};
|
|
52
|
+
this.cursor += result[0].length;
|
|
53
|
+
if (type === 'newline') {
|
|
54
|
+
this.line++;
|
|
55
|
+
this.column = 1;
|
|
56
|
+
}
|
|
57
|
+
else {
|
|
58
|
+
this.column += result[0].length;
|
|
59
|
+
}
|
|
60
|
+
// Skip any ignored tokens.
|
|
61
|
+
if (ignore.includes(token.type)) {
|
|
62
|
+
return this.next(ignore);
|
|
63
|
+
}
|
|
64
|
+
return token;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
// This should be unreachable? Maybe?
|
|
68
|
+
throw new Error(`Unknown token at ${this.line}:${this.column}`);
|
|
69
|
+
}
|
|
70
|
+
peek(ignore = ['whitespace']) {
|
|
71
|
+
const cursor = this.cursor;
|
|
72
|
+
const line = this.line;
|
|
73
|
+
const column = this.column;
|
|
74
|
+
const token = this.next(ignore);
|
|
75
|
+
this.cursor = cursor;
|
|
76
|
+
this.line = line;
|
|
77
|
+
this.column = column;
|
|
78
|
+
return token;
|
|
79
|
+
}
|
|
80
|
+
eof() {
|
|
81
|
+
return this.cursor >= this.input.length;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
// Parser class that takes a tokenizer and produces an AST. This
|
|
85
|
+
// completely consumes the tokenizer.
|
|
86
|
+
export class Parser {
|
|
87
|
+
tokenizer;
|
|
88
|
+
errors = [];
|
|
89
|
+
constructor(tokenizer) {
|
|
90
|
+
this.tokenizer = tokenizer;
|
|
91
|
+
}
|
|
92
|
+
// Parse the input and return the AST. The optional node parameter
|
|
93
|
+
// is used when this is recursed.
|
|
94
|
+
parse(root) {
|
|
95
|
+
if (root === undefined) {
|
|
96
|
+
root = {
|
|
97
|
+
type: 'block',
|
|
98
|
+
children: [],
|
|
99
|
+
line: this.tokenizer.line,
|
|
100
|
+
column: this.tokenizer.column,
|
|
101
|
+
start: this.tokenizer.cursor,
|
|
102
|
+
end: 0,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
while (!this.tokenizer.eof()) {
|
|
106
|
+
const token = this.tokenizer.peek();
|
|
107
|
+
switch (token.type) {
|
|
108
|
+
case 'identifier':
|
|
109
|
+
root.children.push(this.parseIdentifier());
|
|
110
|
+
break;
|
|
111
|
+
case 'newline':
|
|
112
|
+
root.children.push(this.tokenizer.next());
|
|
113
|
+
break;
|
|
114
|
+
case 'comment':
|
|
115
|
+
root.children.push(this.tokenizer.next());
|
|
116
|
+
break;
|
|
117
|
+
case 'curly':
|
|
118
|
+
if (token.value === '}') {
|
|
119
|
+
root.end = this.tokenizer.cursor;
|
|
120
|
+
return root;
|
|
121
|
+
}
|
|
122
|
+
else {
|
|
123
|
+
// wtf?
|
|
124
|
+
this.errors.push({
|
|
125
|
+
expected: ['identifier'],
|
|
126
|
+
received: 'curly',
|
|
127
|
+
message: `Unexpected ${token.value} at ${token.line}:${token.column}`,
|
|
128
|
+
line: token.line,
|
|
129
|
+
column: token.column,
|
|
130
|
+
start: token.start,
|
|
131
|
+
end: token.end,
|
|
132
|
+
});
|
|
133
|
+
this.tokenizer.next();
|
|
134
|
+
}
|
|
135
|
+
break;
|
|
136
|
+
case 'end':
|
|
137
|
+
root.end = this.tokenizer.cursor;
|
|
138
|
+
return root;
|
|
139
|
+
default:
|
|
140
|
+
this.errors.push({
|
|
141
|
+
expected: ['identifier', 'newline', 'comment'],
|
|
142
|
+
received: token.type,
|
|
143
|
+
message: `Unexpected ${token.type} at ${token.line}:${token.column}`,
|
|
144
|
+
line: token.line,
|
|
145
|
+
column: token.column,
|
|
146
|
+
start: token.start,
|
|
147
|
+
end: token.end,
|
|
148
|
+
});
|
|
149
|
+
root.end = this.tokenizer.cursor;
|
|
150
|
+
return root;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
// We should never reach this point, but if we do, we're at the
|
|
154
|
+
// end.
|
|
155
|
+
root.end = this.tokenizer.cursor;
|
|
156
|
+
return root;
|
|
157
|
+
}
|
|
158
|
+
// Parse an identifier, which could be a stanza, assignment.
|
|
159
|
+
parseIdentifier() {
|
|
160
|
+
const token = this.tokenizer.next();
|
|
161
|
+
// Assert token is an identifier.
|
|
162
|
+
if (token.type !== 'identifier') {
|
|
163
|
+
// should be unreachable
|
|
164
|
+
throw new Error(`Expected identifier but got ${token.type} at ${token.line}:${token.column}`);
|
|
165
|
+
}
|
|
166
|
+
const next = this.tokenizer.peek();
|
|
167
|
+
if (next.type === 'curly' && next.value === '{') {
|
|
168
|
+
return this.parseStanza(token);
|
|
169
|
+
}
|
|
170
|
+
if (['string', 'number', 'boolean'].includes(next.type)) {
|
|
171
|
+
return this.parseStanza(token);
|
|
172
|
+
}
|
|
173
|
+
if (next.type === 'operator' && next.value === '=') {
|
|
174
|
+
return this.parseAssignment(token);
|
|
175
|
+
}
|
|
176
|
+
this.errors.push({
|
|
177
|
+
expected: ['curly', 'string', 'operator'],
|
|
178
|
+
received: next.type,
|
|
179
|
+
message: `Expected {, argument or operator at ${next.line}:${next.column}`,
|
|
180
|
+
line: next.line,
|
|
181
|
+
column: next.column,
|
|
182
|
+
start: next.start,
|
|
183
|
+
end: next.end,
|
|
184
|
+
});
|
|
185
|
+
return { type: 'empty', line: next.line, column: next.column, start: next.start, end: next.end };
|
|
186
|
+
}
|
|
187
|
+
// Parse a stanza, which is a block of assignments and other
|
|
188
|
+
// stanzas.
|
|
189
|
+
parseStanza(token) {
|
|
190
|
+
const stanza = {
|
|
191
|
+
type: 'stanza',
|
|
192
|
+
name: token.value,
|
|
193
|
+
args: [],
|
|
194
|
+
block: null,
|
|
195
|
+
line: token.line,
|
|
196
|
+
column: token.column,
|
|
197
|
+
start: token.start,
|
|
198
|
+
end: token.end,
|
|
199
|
+
};
|
|
200
|
+
let next = this.tokenizer.peek();
|
|
201
|
+
while (next.type !== 'curly') {
|
|
202
|
+
if (['string', 'number', 'boolean'].includes(next.type)) {
|
|
203
|
+
stanza.args.push(this.tokenizer.next());
|
|
204
|
+
next = this.tokenizer.peek();
|
|
205
|
+
continue;
|
|
206
|
+
}
|
|
207
|
+
this.errors.push({
|
|
208
|
+
expected: ['string', 'number', 'boolean'],
|
|
209
|
+
received: next.type,
|
|
210
|
+
message: `Expected argument at ${next.line}:${next.column}`,
|
|
211
|
+
line: next.line,
|
|
212
|
+
column: next.column,
|
|
213
|
+
start: next.start,
|
|
214
|
+
end: next.end,
|
|
215
|
+
});
|
|
216
|
+
this.tokenizer.next();
|
|
217
|
+
next = this.tokenizer.peek();
|
|
218
|
+
}
|
|
219
|
+
this.expect('curly', '{');
|
|
220
|
+
stanza.block = this.parse();
|
|
221
|
+
this.expect('curly', '}');
|
|
222
|
+
stanza.end = this.tokenizer.cursor;
|
|
223
|
+
return stanza;
|
|
224
|
+
}
|
|
225
|
+
// Parse an assignment, which is an assignment to an expression.
|
|
226
|
+
parseAssignment(token) {
|
|
227
|
+
if (token.type !== 'identifier') {
|
|
228
|
+
throw new Error(`Expected identifier but got ${token.type} at ${token.line}:${token.column}`);
|
|
229
|
+
}
|
|
230
|
+
const key = token;
|
|
231
|
+
const operator = this.expect('operator', '=');
|
|
232
|
+
const value = this.parseExpression();
|
|
233
|
+
return {
|
|
234
|
+
type: 'assignment',
|
|
235
|
+
operator,
|
|
236
|
+
key,
|
|
237
|
+
value,
|
|
238
|
+
line: key.line,
|
|
239
|
+
column: key.column,
|
|
240
|
+
start: key.start,
|
|
241
|
+
end: value.end,
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
// Parse an expression. We're not building a calculator (yet), so we
|
|
245
|
+
// just accept a value on the right for now.
|
|
246
|
+
parseExpression(ignore) {
|
|
247
|
+
const token = this.tokenizer.next(ignore || ['whitespace']);
|
|
248
|
+
switch (token.type) {
|
|
249
|
+
case 'string':
|
|
250
|
+
case 'number':
|
|
251
|
+
case 'boolean':
|
|
252
|
+
return token;
|
|
253
|
+
case 'bracket':
|
|
254
|
+
if (token.value === '[') {
|
|
255
|
+
return this.parseArray();
|
|
256
|
+
}
|
|
257
|
+
break;
|
|
258
|
+
case 'curly':
|
|
259
|
+
if (token.value === '{') {
|
|
260
|
+
return this.parseObject();
|
|
261
|
+
}
|
|
262
|
+
else {
|
|
263
|
+
break;
|
|
264
|
+
}
|
|
265
|
+
default:
|
|
266
|
+
break;
|
|
267
|
+
}
|
|
268
|
+
// Recover by calling it a string.
|
|
269
|
+
this.errors.push({
|
|
270
|
+
expected: ['expression'],
|
|
271
|
+
received: token.type,
|
|
272
|
+
message: `Expected expression but got ${token.type} at ${token.line}:${token.column}`,
|
|
273
|
+
line: token.line,
|
|
274
|
+
column: token.column,
|
|
275
|
+
start: token.start,
|
|
276
|
+
end: token.end,
|
|
277
|
+
});
|
|
278
|
+
return {
|
|
279
|
+
type: 'string',
|
|
280
|
+
value: '"<error>"',
|
|
281
|
+
line: token.line,
|
|
282
|
+
column: token.column,
|
|
283
|
+
start: token.start,
|
|
284
|
+
end: token.end,
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
parseArray() {
|
|
288
|
+
// the first thing we expect are expression.
|
|
289
|
+
const elements = [];
|
|
290
|
+
// This whole ignoring newlines thing is really just context.
|
|
291
|
+
let next = this.tokenizer.peek(['whitespace', 'newline']);
|
|
292
|
+
while (true) {
|
|
293
|
+
elements.push(this.parseExpression(['whitespace', 'newline']));
|
|
294
|
+
next = this.tokenizer.peek(['whitespace', 'newline']);
|
|
295
|
+
if (next.type === 'bracket' && next.value === ']') {
|
|
296
|
+
this.tokenizer.next(['whitespace', 'newline']);
|
|
297
|
+
return { type: 'array', elements, line: next.line, column: next.column, start: next.start, end: next.end };
|
|
298
|
+
}
|
|
299
|
+
if (next.type === 'comma') {
|
|
300
|
+
this.tokenizer.next();
|
|
301
|
+
next = this.tokenizer.peek(['whitespace', 'newline']);
|
|
302
|
+
// Special case: allow an ending comma because it's nice.
|
|
303
|
+
if (next.type === 'bracket' && next.value === ']') {
|
|
304
|
+
this.tokenizer.next(['whitespace', 'newline']);
|
|
305
|
+
return { type: 'array', elements, line: next.line, column: next.column, start: next.start, end: next.end };
|
|
306
|
+
}
|
|
307
|
+
continue;
|
|
308
|
+
}
|
|
309
|
+
// Should have been a closing bracket or a comma, so we "close" and return for recovery.
|
|
310
|
+
this.errors.push({
|
|
311
|
+
expected: ['bracket', 'comma'],
|
|
312
|
+
received: next.type,
|
|
313
|
+
message: `Expected ] or , but got ${next.type} at ${next.line}:${next.column}`,
|
|
314
|
+
line: next.line,
|
|
315
|
+
column: next.column,
|
|
316
|
+
start: next.start,
|
|
317
|
+
end: next.end,
|
|
318
|
+
});
|
|
319
|
+
return { type: 'array', elements, line: next.line, column: next.column, start: next.start, end: next.end };
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
parseObject() {
|
|
323
|
+
const properties = [];
|
|
324
|
+
let next = this.tokenizer.peek(['whitespace', 'newline']);
|
|
325
|
+
while (true) {
|
|
326
|
+
if (next.type === 'end') {
|
|
327
|
+
this.errors.push({
|
|
328
|
+
expected: ['identifier'],
|
|
329
|
+
received: 'end',
|
|
330
|
+
message: `Unexpected end of input at ${next.line}:${next.column}`,
|
|
331
|
+
line: next.line,
|
|
332
|
+
column: next.column,
|
|
333
|
+
start: next.start,
|
|
334
|
+
end: next.end,
|
|
335
|
+
});
|
|
336
|
+
break;
|
|
337
|
+
}
|
|
338
|
+
if (next.type === 'curly' && next.value === '}') {
|
|
339
|
+
this.tokenizer.next(['whitespace', 'newline']);
|
|
340
|
+
break;
|
|
341
|
+
}
|
|
342
|
+
if (next.type === 'identifier') {
|
|
343
|
+
properties.push(this.parseAssignment(this.tokenizer.next(['whitespace', 'newline'])));
|
|
344
|
+
next = this.tokenizer.peek(['whitespace', 'newline']);
|
|
345
|
+
continue;
|
|
346
|
+
}
|
|
347
|
+
const token = this.tokenizer.next(['whitespace', 'newline']);
|
|
348
|
+
next = this.tokenizer.peek(['whitespace', 'newline']);
|
|
349
|
+
this.errors.push({
|
|
350
|
+
expected: ['identifier'],
|
|
351
|
+
received: next.type,
|
|
352
|
+
message: `Expected identifier but got ${token.type} at ${next.line}:${next.column}`,
|
|
353
|
+
line: next.line,
|
|
354
|
+
column: next.column,
|
|
355
|
+
start: next.start,
|
|
356
|
+
end: next.end,
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
return { type: 'object', properties, line: next.line, column: next.column, start: next.start, end: next.end };
|
|
360
|
+
}
|
|
361
|
+
// Expect a token of a certain type, optionally with a certain
|
|
362
|
+
// value.
|
|
363
|
+
expect(type, value) {
|
|
364
|
+
const token = this.tokenizer.peek();
|
|
365
|
+
if (token.type === 'end' && type !== 'end') {
|
|
366
|
+
this.errors.push({
|
|
367
|
+
expected: [type],
|
|
368
|
+
received: 'end',
|
|
369
|
+
message: `Unexpected end of input at ${token.line}:${token.column}`,
|
|
370
|
+
line: token.line,
|
|
371
|
+
column: token.column,
|
|
372
|
+
start: token.start,
|
|
373
|
+
end: token.end,
|
|
374
|
+
});
|
|
375
|
+
return this.tokenizer.next();
|
|
376
|
+
}
|
|
377
|
+
if (token.type !== type) {
|
|
378
|
+
this.errors.push({
|
|
379
|
+
expected: [type],
|
|
380
|
+
received: token.type,
|
|
381
|
+
message: `Expected ${type} but got ${token.type} at ${token.line}:${token.column}`,
|
|
382
|
+
line: token.line,
|
|
383
|
+
column: token.column,
|
|
384
|
+
start: token.start,
|
|
385
|
+
end: token.end,
|
|
386
|
+
});
|
|
387
|
+
// Naively try to recover by returning a working token.
|
|
388
|
+
return {
|
|
389
|
+
type: type,
|
|
390
|
+
value: token.value,
|
|
391
|
+
start: token.start,
|
|
392
|
+
end: token.end,
|
|
393
|
+
line: token.line,
|
|
394
|
+
column: token.column,
|
|
395
|
+
};
|
|
396
|
+
}
|
|
397
|
+
if (value && token.value !== value) {
|
|
398
|
+
this.errors.push({
|
|
399
|
+
message: `Expected ${value} but got ${token.value} at ${token.line}:${token.column}`,
|
|
400
|
+
line: token.line,
|
|
401
|
+
column: token.column,
|
|
402
|
+
start: token.start,
|
|
403
|
+
end: token.end,
|
|
404
|
+
});
|
|
405
|
+
// Again, naively try to recover with a working token.
|
|
406
|
+
return {
|
|
407
|
+
type,
|
|
408
|
+
value,
|
|
409
|
+
start: token.start,
|
|
410
|
+
end: token.end,
|
|
411
|
+
line: token.line,
|
|
412
|
+
column: token.column,
|
|
413
|
+
};
|
|
414
|
+
}
|
|
415
|
+
// We match, so consume the token.
|
|
416
|
+
return this.tokenizer.next();
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
// JSONTranslator takes an AST and translates it into a JSON object
|
|
420
|
+
// (any).
|
|
421
|
+
export class JSONTranslator {
|
|
422
|
+
errors;
|
|
423
|
+
constructor() {
|
|
424
|
+
this.errors = [];
|
|
425
|
+
}
|
|
426
|
+
// Translate the AST into a JSON serializable object.
|
|
427
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
428
|
+
translate(ast) {
|
|
429
|
+
return this.translateRoot(ast);
|
|
430
|
+
}
|
|
431
|
+
// Start at the root and translate the children.
|
|
432
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
433
|
+
translateRoot(node) {
|
|
434
|
+
const obj = {}; // eslint-disable-line @typescript-eslint/no-explicit-any
|
|
435
|
+
for (const child of node.children) {
|
|
436
|
+
if (child.type === 'stanza') {
|
|
437
|
+
if (!obj[child.name]) {
|
|
438
|
+
obj[child.name] = [];
|
|
439
|
+
}
|
|
440
|
+
const stanza = this.translateStanza(child);
|
|
441
|
+
obj[child.name].push(stanza);
|
|
442
|
+
}
|
|
443
|
+
else {
|
|
444
|
+
this.errors.push({
|
|
445
|
+
message: `unexpected ${child.type} at ${child.line}:${child.column}`,
|
|
446
|
+
line: 1,
|
|
447
|
+
column: 1,
|
|
448
|
+
});
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
return obj;
|
|
452
|
+
}
|
|
453
|
+
// translateStanza takes a stanza node and translates it into a JSON
|
|
454
|
+
// object. This looks remarkably like translateRoot, and maybe
|
|
455
|
+
// there's some kind of generalized recursion we could use, but it's
|
|
456
|
+
// late.
|
|
457
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
458
|
+
translateStanza(node) {
|
|
459
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
460
|
+
const obj = {};
|
|
461
|
+
if (node.args && node.args[0]) {
|
|
462
|
+
obj['name'] = node.args[0].value.slice(1, -1);
|
|
463
|
+
}
|
|
464
|
+
const children = node.block ? node.block.children : [];
|
|
465
|
+
for (const child of children) {
|
|
466
|
+
if (child.type === 'assignment') {
|
|
467
|
+
// Remember we skipped expressions, so...
|
|
468
|
+
if (child.value.type === 'string') {
|
|
469
|
+
obj[child.key.value] = child.value.value.slice(1, -1);
|
|
470
|
+
}
|
|
471
|
+
else if (child.value.type === 'number') {
|
|
472
|
+
obj[child.key.value] = parseFloat(child.value.value);
|
|
473
|
+
}
|
|
474
|
+
else if (child.value.type === 'boolean') {
|
|
475
|
+
obj[child.key.value] = child.value.value === 'true';
|
|
476
|
+
}
|
|
477
|
+
else if (child.value.type === 'array') {
|
|
478
|
+
obj[child.key.value] = child.value.elements.map((el) => {
|
|
479
|
+
if (el.type === 'string') {
|
|
480
|
+
return el.value.slice(1, -1);
|
|
481
|
+
}
|
|
482
|
+
else if (el.type === 'number') {
|
|
483
|
+
return parseFloat(el.value);
|
|
484
|
+
}
|
|
485
|
+
else if (el.type === 'boolean') {
|
|
486
|
+
return el.value === 'true';
|
|
487
|
+
}
|
|
488
|
+
return null;
|
|
489
|
+
});
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
if (child.type === 'stanza') {
|
|
493
|
+
if (!obj[child.name]) {
|
|
494
|
+
obj[child.name] = [];
|
|
495
|
+
}
|
|
496
|
+
obj[child.name].push(this.translateStanza(child));
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
return obj;
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
// fmt is an ugly mess of string concatenation.
|
|
503
|
+
export function fmt(ast, indent = 0, sibling) {
|
|
504
|
+
if (ast === null) {
|
|
505
|
+
return '';
|
|
506
|
+
}
|
|
507
|
+
const INDENT = ' '.repeat(indent);
|
|
508
|
+
let str = '';
|
|
509
|
+
// Just spit out token values as they are encountered.
|
|
510
|
+
switch (ast.type) {
|
|
511
|
+
case 'stanza':
|
|
512
|
+
str += `${INDENT}${ast.name} `;
|
|
513
|
+
for (const arg of ast.args) {
|
|
514
|
+
str += `${arg.value} `;
|
|
515
|
+
}
|
|
516
|
+
str += '{';
|
|
517
|
+
str += fmt(ast.block, indent + 2);
|
|
518
|
+
if (!ast.block?.children.every((child) => child.type === 'newline')) {
|
|
519
|
+
str += `${INDENT}`;
|
|
520
|
+
}
|
|
521
|
+
str += '}';
|
|
522
|
+
break;
|
|
523
|
+
case 'block':
|
|
524
|
+
if (ast.children.every((child) => child.type === 'newline')) {
|
|
525
|
+
// Do nothing.
|
|
526
|
+
}
|
|
527
|
+
else {
|
|
528
|
+
for (const child of ast.children) {
|
|
529
|
+
str += fmt(child, indent, sibling);
|
|
530
|
+
sibling = child;
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
break;
|
|
534
|
+
case 'assignment':
|
|
535
|
+
str += `${INDENT}${fmt(ast.key)} = ${fmt(ast.value)}`;
|
|
536
|
+
break;
|
|
537
|
+
case 'expression':
|
|
538
|
+
str += `${fmt(ast.left)} ${ast.operator.value} ${fmt(ast.right)}`;
|
|
539
|
+
break;
|
|
540
|
+
case 'comment':
|
|
541
|
+
if (sibling?.type !== 'newline') {
|
|
542
|
+
str += ` `;
|
|
543
|
+
}
|
|
544
|
+
else {
|
|
545
|
+
str += `${INDENT}`;
|
|
546
|
+
}
|
|
547
|
+
str += `${ast.value}`;
|
|
548
|
+
break;
|
|
549
|
+
case 'empty':
|
|
550
|
+
break;
|
|
551
|
+
case 'array':
|
|
552
|
+
// eslint-disable-next-line no-case-declarations
|
|
553
|
+
const oneLine = `[${ast.elements.map((el) => fmt(el)).join(', ')}]`;
|
|
554
|
+
if (oneLine.length > 80) {
|
|
555
|
+
str += `[`;
|
|
556
|
+
for (const el of ast.elements) {
|
|
557
|
+
str += `\n${fmt(el, indent + 2)}`;
|
|
558
|
+
}
|
|
559
|
+
str += `\n${INDENT}]`;
|
|
560
|
+
}
|
|
561
|
+
else {
|
|
562
|
+
return oneLine;
|
|
563
|
+
}
|
|
564
|
+
break;
|
|
565
|
+
case 'object':
|
|
566
|
+
str += '{';
|
|
567
|
+
if (ast.properties.length > 0) {
|
|
568
|
+
str += '\n';
|
|
569
|
+
for (const assign of ast.properties) {
|
|
570
|
+
str += `${fmt(assign, indent + 2)}\n`;
|
|
571
|
+
}
|
|
572
|
+
str += `\n${INDENT}}`;
|
|
573
|
+
}
|
|
574
|
+
break;
|
|
575
|
+
default:
|
|
576
|
+
str += `${ast?.value}`;
|
|
577
|
+
}
|
|
578
|
+
return str;
|
|
579
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"parse.test.d.ts","sourceRoot":"","sources":["../../src/appify/parse.test.ts"],"names":[],"mappings":""}
|