@malloydata/motly-ts-parser 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/build/ast.d.ts +62 -0
- package/build/ast.js +2 -0
- package/build/index.d.ts +2 -0
- package/build/index.js +5 -0
- package/build/interpreter.d.ts +4 -0
- package/build/interpreter.js +236 -0
- package/build/parser.d.ts +3 -0
- package/build/parser.js +861 -0
- package/build/session.d.ts +45 -0
- package/build/session.js +135 -0
- package/build/validate.d.ts +3 -0
- package/build/validate.js +596 -0
- package/package.json +31 -0
package/build/parser.js
ADDED
|
@@ -0,0 +1,861 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.parse = parse;
|
|
4
|
+
class Parser {
|
|
5
|
+
constructor(input) {
|
|
6
|
+
this.input = input;
|
|
7
|
+
this.pos = 0;
|
|
8
|
+
}
|
|
9
|
+
// ── Helpers ──────────────────────────────────────────────────────
|
|
10
|
+
remaining() {
|
|
11
|
+
return this.input.substring(this.pos);
|
|
12
|
+
}
|
|
13
|
+
peekChar() {
|
|
14
|
+
return this.pos < this.input.length ? this.input[this.pos] : undefined;
|
|
15
|
+
}
|
|
16
|
+
advance(n) {
|
|
17
|
+
this.pos += n;
|
|
18
|
+
}
|
|
19
|
+
startsWith(s) {
|
|
20
|
+
return this.input.startsWith(s, this.pos);
|
|
21
|
+
}
|
|
22
|
+
eatChar(ch) {
|
|
23
|
+
if (this.peekChar() === ch) {
|
|
24
|
+
this.advance(1);
|
|
25
|
+
return true;
|
|
26
|
+
}
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
expectChar(ch) {
|
|
30
|
+
if (!this.eatChar(ch)) {
|
|
31
|
+
throw this.errorPoint(`Expected '${ch}'`);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
position() {
|
|
35
|
+
const consumed = this.input.substring(0, this.pos);
|
|
36
|
+
const line = (consumed.match(/\n/g) || []).length;
|
|
37
|
+
const lastNewline = consumed.lastIndexOf("\n");
|
|
38
|
+
const column = this.pos - (lastNewline === -1 ? 0 : lastNewline + 1);
|
|
39
|
+
return { line, column, offset: this.pos };
|
|
40
|
+
}
|
|
41
|
+
errorPoint(message) {
|
|
42
|
+
const pos = this.position();
|
|
43
|
+
return {
|
|
44
|
+
code: "tag-parse-syntax-error",
|
|
45
|
+
message,
|
|
46
|
+
begin: pos,
|
|
47
|
+
end: pos,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
errorSpan(message, begin) {
|
|
51
|
+
return {
|
|
52
|
+
code: "tag-parse-syntax-error",
|
|
53
|
+
message,
|
|
54
|
+
begin,
|
|
55
|
+
end: this.position(),
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
// ── Whitespace & Comments ───────────────────────────────────────
|
|
59
|
+
skipWs() {
|
|
60
|
+
for (;;) {
|
|
61
|
+
while (this.pos < this.input.length) {
|
|
62
|
+
const ch = this.input[this.pos];
|
|
63
|
+
if (ch === " " || ch === "\t" || ch === "\r" || ch === "\n") {
|
|
64
|
+
this.pos++;
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
break;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
if (this.peekChar() === "#") {
|
|
71
|
+
while (this.pos < this.input.length) {
|
|
72
|
+
const ch = this.input[this.pos];
|
|
73
|
+
if (ch === "\r" || ch === "\n")
|
|
74
|
+
break;
|
|
75
|
+
this.pos++;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
else {
|
|
79
|
+
break;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
// ── Statement Dispatch ──────────────────────────────────────────
|
|
84
|
+
parseStatements() {
|
|
85
|
+
const statements = [];
|
|
86
|
+
this.skipWs();
|
|
87
|
+
while (this.pos < this.input.length) {
|
|
88
|
+
statements.push(this.parseStatement());
|
|
89
|
+
this.skipWs();
|
|
90
|
+
}
|
|
91
|
+
return statements;
|
|
92
|
+
}
|
|
93
|
+
parseStatement() {
|
|
94
|
+
// -... (clearAll)
|
|
95
|
+
if (this.startsWith("-...")) {
|
|
96
|
+
this.advance(4);
|
|
97
|
+
return { kind: "clearAll" };
|
|
98
|
+
}
|
|
99
|
+
// -name (define deleted)
|
|
100
|
+
if (this.peekChar() === "-") {
|
|
101
|
+
this.advance(1);
|
|
102
|
+
const path = this.parsePropName();
|
|
103
|
+
return { kind: "define", path, deleted: true };
|
|
104
|
+
}
|
|
105
|
+
// Parse the property path
|
|
106
|
+
const path = this.parsePropName();
|
|
107
|
+
this.skipWs();
|
|
108
|
+
const ch = this.peekChar();
|
|
109
|
+
if (ch === "=") {
|
|
110
|
+
this.advance(1);
|
|
111
|
+
this.skipWs();
|
|
112
|
+
// Check for `= ... {` (replaceProperties with preserveValue)
|
|
113
|
+
if (this.startsWith("...")) {
|
|
114
|
+
const saved = this.pos;
|
|
115
|
+
this.advance(3);
|
|
116
|
+
this.skipWs();
|
|
117
|
+
if (this.peekChar() === "{") {
|
|
118
|
+
const props = this.parsePropertiesBlock();
|
|
119
|
+
return {
|
|
120
|
+
kind: "replaceProperties",
|
|
121
|
+
path,
|
|
122
|
+
properties: props,
|
|
123
|
+
preserveValue: true,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
this.pos = saved;
|
|
127
|
+
}
|
|
128
|
+
// Check for `= {` (replaceProperties without preserveValue)
|
|
129
|
+
if (this.peekChar() === "{") {
|
|
130
|
+
const props = this.parsePropertiesBlock();
|
|
131
|
+
return {
|
|
132
|
+
kind: "replaceProperties",
|
|
133
|
+
path,
|
|
134
|
+
properties: props,
|
|
135
|
+
preserveValue: false,
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
// `= value` (setEq)
|
|
139
|
+
const value = this.parseEqValue();
|
|
140
|
+
this.skipWs();
|
|
141
|
+
// Optionally followed by `{ ... }` or `{ statements }`
|
|
142
|
+
if (this.peekChar() === "{") {
|
|
143
|
+
const saved = this.pos;
|
|
144
|
+
this.advance(1);
|
|
145
|
+
this.skipWs();
|
|
146
|
+
if (this.startsWith("...")) {
|
|
147
|
+
const saved2 = this.pos;
|
|
148
|
+
this.advance(3);
|
|
149
|
+
this.skipWs();
|
|
150
|
+
if (this.peekChar() === "}") {
|
|
151
|
+
this.advance(1);
|
|
152
|
+
return {
|
|
153
|
+
kind: "setEq",
|
|
154
|
+
path,
|
|
155
|
+
value,
|
|
156
|
+
properties: null,
|
|
157
|
+
preserveProperties: true,
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
this.pos = saved2;
|
|
161
|
+
}
|
|
162
|
+
this.pos = saved;
|
|
163
|
+
const props = this.parsePropertiesBlock();
|
|
164
|
+
return {
|
|
165
|
+
kind: "setEq",
|
|
166
|
+
path,
|
|
167
|
+
value,
|
|
168
|
+
properties: props,
|
|
169
|
+
preserveProperties: false,
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
return {
|
|
173
|
+
kind: "setEq",
|
|
174
|
+
path,
|
|
175
|
+
value,
|
|
176
|
+
properties: null,
|
|
177
|
+
preserveProperties: false,
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
if (ch === ":") {
|
|
181
|
+
this.advance(1);
|
|
182
|
+
this.skipWs();
|
|
183
|
+
const props = this.parsePropertiesBlock();
|
|
184
|
+
return {
|
|
185
|
+
kind: "replaceProperties",
|
|
186
|
+
path,
|
|
187
|
+
properties: props,
|
|
188
|
+
preserveValue: false,
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
if (ch === "{") {
|
|
192
|
+
const props = this.parsePropertiesBlock();
|
|
193
|
+
return { kind: "updateProperties", path, properties: props };
|
|
194
|
+
}
|
|
195
|
+
return { kind: "define", path, deleted: false };
|
|
196
|
+
}
|
|
197
|
+
// ── Property Name (dotted path) ─────────────────────────────────
|
|
198
|
+
parsePropName() {
|
|
199
|
+
const first = this.parseIdentifier();
|
|
200
|
+
const path = [first];
|
|
201
|
+
while (this.peekChar() === ".") {
|
|
202
|
+
this.advance(1);
|
|
203
|
+
path.push(this.parseIdentifier());
|
|
204
|
+
}
|
|
205
|
+
return path;
|
|
206
|
+
}
|
|
207
|
+
parseIdentifier() {
|
|
208
|
+
if (this.peekChar() === "`") {
|
|
209
|
+
return this.parseBacktickString();
|
|
210
|
+
}
|
|
211
|
+
return this.parseBareString();
|
|
212
|
+
}
|
|
213
|
+
// ── Values ──────────────────────────────────────────────────────
|
|
214
|
+
parseEqValue() {
|
|
215
|
+
const ch = this.peekChar();
|
|
216
|
+
if (ch === "[")
|
|
217
|
+
return { kind: "array", elements: this.parseArray() };
|
|
218
|
+
if (ch === "@")
|
|
219
|
+
return { kind: "scalar", value: this.parseAtValue() };
|
|
220
|
+
if (ch === "$")
|
|
221
|
+
return { kind: "scalar", value: this.parseReference() };
|
|
222
|
+
if (ch === '"') {
|
|
223
|
+
if (this.startsWith('"""')) {
|
|
224
|
+
return {
|
|
225
|
+
kind: "scalar",
|
|
226
|
+
value: { kind: "string", value: this.parseTripleString() },
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
return {
|
|
230
|
+
kind: "scalar",
|
|
231
|
+
value: { kind: "string", value: this.parseDoubleQuotedString() },
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
if (ch === "'") {
|
|
235
|
+
if (this.startsWith("'''")) {
|
|
236
|
+
return {
|
|
237
|
+
kind: "scalar",
|
|
238
|
+
value: {
|
|
239
|
+
kind: "string",
|
|
240
|
+
value: this.parseTripleSingleQuotedString(),
|
|
241
|
+
},
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
return {
|
|
245
|
+
kind: "scalar",
|
|
246
|
+
value: { kind: "string", value: this.parseSingleQuotedString() },
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
if (ch !== undefined &&
|
|
250
|
+
(ch === "-" || (ch >= "0" && ch <= "9") || ch === ".")) {
|
|
251
|
+
return this.parseNumberOrString();
|
|
252
|
+
}
|
|
253
|
+
if (ch !== undefined && isBareChar(ch)) {
|
|
254
|
+
return {
|
|
255
|
+
kind: "scalar",
|
|
256
|
+
value: { kind: "string", value: this.parseBareString() },
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
throw this.errorPoint("Expected a value");
|
|
260
|
+
}
|
|
261
|
+
parseScalarValue() {
|
|
262
|
+
const ch = this.peekChar();
|
|
263
|
+
if (ch === "@")
|
|
264
|
+
return { kind: "scalar", value: this.parseAtValue() };
|
|
265
|
+
if (ch === "$")
|
|
266
|
+
return { kind: "scalar", value: this.parseReference() };
|
|
267
|
+
if (ch === '"') {
|
|
268
|
+
if (this.startsWith('"""')) {
|
|
269
|
+
return {
|
|
270
|
+
kind: "scalar",
|
|
271
|
+
value: { kind: "string", value: this.parseTripleString() },
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
return {
|
|
275
|
+
kind: "scalar",
|
|
276
|
+
value: { kind: "string", value: this.parseDoubleQuotedString() },
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
if (ch === "'") {
|
|
280
|
+
if (this.startsWith("'''")) {
|
|
281
|
+
return {
|
|
282
|
+
kind: "scalar",
|
|
283
|
+
value: {
|
|
284
|
+
kind: "string",
|
|
285
|
+
value: this.parseTripleSingleQuotedString(),
|
|
286
|
+
},
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
return {
|
|
290
|
+
kind: "scalar",
|
|
291
|
+
value: { kind: "string", value: this.parseSingleQuotedString() },
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
if (ch !== undefined &&
|
|
295
|
+
((ch >= "0" && ch <= "9") || ch === "." || ch === "-")) {
|
|
296
|
+
return this.parseNumberOrString();
|
|
297
|
+
}
|
|
298
|
+
if (ch !== undefined && isBareChar(ch)) {
|
|
299
|
+
return {
|
|
300
|
+
kind: "scalar",
|
|
301
|
+
value: { kind: "string", value: this.parseBareString() },
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
throw this.errorPoint("Expected a value");
|
|
305
|
+
}
|
|
306
|
+
/** Parse `@true`, `@false`, or `@date` */
|
|
307
|
+
parseAtValue() {
|
|
308
|
+
const begin = this.position();
|
|
309
|
+
this.expectChar("@");
|
|
310
|
+
if (this.startsWith("true") && !this.isBareCharAt(4)) {
|
|
311
|
+
this.advance(4);
|
|
312
|
+
return { kind: "boolean", value: true };
|
|
313
|
+
}
|
|
314
|
+
if (this.startsWith("false") && !this.isBareCharAt(5)) {
|
|
315
|
+
this.advance(5);
|
|
316
|
+
return { kind: "boolean", value: false };
|
|
317
|
+
}
|
|
318
|
+
const ch = this.peekChar();
|
|
319
|
+
if (ch !== undefined && ch >= "0" && ch <= "9") {
|
|
320
|
+
return this.parseDate(begin);
|
|
321
|
+
}
|
|
322
|
+
// Consume the bad token for a better span
|
|
323
|
+
const tokenStart = this.pos;
|
|
324
|
+
while (this.pos < this.input.length && isBareChar(this.input[this.pos])) {
|
|
325
|
+
this.pos++;
|
|
326
|
+
}
|
|
327
|
+
const token = this.pos > tokenStart ? this.input.substring(tokenStart, this.pos) : "";
|
|
328
|
+
throw this.errorSpan(`Illegal constant @${token}; expected @true, @false, or @date`, begin);
|
|
329
|
+
}
|
|
330
|
+
isBareCharAt(offset) {
|
|
331
|
+
const rem = this.remaining();
|
|
332
|
+
return offset < rem.length && isBareChar(rem[offset]);
|
|
333
|
+
}
|
|
334
|
+
parseDate(begin) {
|
|
335
|
+
const start = this.pos;
|
|
336
|
+
// YYYY-MM-DD
|
|
337
|
+
this.consumeDigits(4, begin);
|
|
338
|
+
this.expectChar("-");
|
|
339
|
+
this.consumeDigits(2, begin);
|
|
340
|
+
this.expectChar("-");
|
|
341
|
+
this.consumeDigits(2, begin);
|
|
342
|
+
// Optional time part: T HH:MM
|
|
343
|
+
if (this.peekChar() === "T") {
|
|
344
|
+
this.advance(1);
|
|
345
|
+
this.consumeDigits(2, begin);
|
|
346
|
+
this.expectChar(":");
|
|
347
|
+
this.consumeDigits(2, begin);
|
|
348
|
+
// Optional :SS
|
|
349
|
+
if (this.peekChar() === ":") {
|
|
350
|
+
this.advance(1);
|
|
351
|
+
this.consumeDigits(2, begin);
|
|
352
|
+
// Optional .fractional
|
|
353
|
+
if (this.peekChar() === ".") {
|
|
354
|
+
this.advance(1);
|
|
355
|
+
const fracStart = this.pos;
|
|
356
|
+
while (this.pos < this.input.length &&
|
|
357
|
+
this.input[this.pos] >= "0" &&
|
|
358
|
+
this.input[this.pos] <= "9") {
|
|
359
|
+
this.pos++;
|
|
360
|
+
}
|
|
361
|
+
if (this.pos === fracStart) {
|
|
362
|
+
throw this.errorSpan("Expected fractional digits in date", begin);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
// Optional timezone: Z or +/-HH:MM or +/-HHMM
|
|
367
|
+
const tzCh = this.peekChar();
|
|
368
|
+
if (tzCh === "Z") {
|
|
369
|
+
this.advance(1);
|
|
370
|
+
}
|
|
371
|
+
else if (tzCh === "+" || tzCh === "-") {
|
|
372
|
+
this.advance(1);
|
|
373
|
+
this.consumeDigits(2, begin);
|
|
374
|
+
if (this.peekChar() === ":") {
|
|
375
|
+
this.advance(1);
|
|
376
|
+
}
|
|
377
|
+
this.consumeDigits(2, begin);
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
const dateStr = this.input.substring(start, this.pos);
|
|
381
|
+
return { kind: "date", value: dateStr };
|
|
382
|
+
}
|
|
383
|
+
consumeDigits(count, begin) {
|
|
384
|
+
for (let i = 0; i < count; i++) {
|
|
385
|
+
const ch = this.peekChar();
|
|
386
|
+
if (ch === undefined || ch < "0" || ch > "9") {
|
|
387
|
+
throw this.errorSpan("Expected digit", begin);
|
|
388
|
+
}
|
|
389
|
+
this.advance(1);
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
// ── Numbers ─────────────────────────────────────────────────────
|
|
393
|
+
parseNumberOrString() {
|
|
394
|
+
const start = this.pos;
|
|
395
|
+
const begin = this.position();
|
|
396
|
+
const hasMinus = this.peekChar() === "-";
|
|
397
|
+
if (hasMinus)
|
|
398
|
+
this.advance(1);
|
|
399
|
+
let hasIntDigits = false;
|
|
400
|
+
let hasDot = false;
|
|
401
|
+
// Integer part
|
|
402
|
+
while (this.pos < this.input.length &&
|
|
403
|
+
this.input[this.pos] >= "0" &&
|
|
404
|
+
this.input[this.pos] <= "9") {
|
|
405
|
+
hasIntDigits = true;
|
|
406
|
+
this.advance(1);
|
|
407
|
+
}
|
|
408
|
+
// Decimal point
|
|
409
|
+
if (this.peekChar() === ".") {
|
|
410
|
+
hasDot = true;
|
|
411
|
+
this.advance(1);
|
|
412
|
+
const fracStart = this.pos;
|
|
413
|
+
while (this.pos < this.input.length &&
|
|
414
|
+
this.input[this.pos] >= "0" &&
|
|
415
|
+
this.input[this.pos] <= "9") {
|
|
416
|
+
this.advance(1);
|
|
417
|
+
}
|
|
418
|
+
if (this.pos === fracStart) {
|
|
419
|
+
this.pos = start;
|
|
420
|
+
return this.parseIntegerOrBare(start, hasMinus);
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
if (!hasIntDigits && !hasDot) {
|
|
424
|
+
this.pos = start;
|
|
425
|
+
if (hasMinus) {
|
|
426
|
+
throw this.errorPoint("Expected a value");
|
|
427
|
+
}
|
|
428
|
+
return {
|
|
429
|
+
kind: "scalar",
|
|
430
|
+
value: { kind: "string", value: this.parseBareString() },
|
|
431
|
+
};
|
|
432
|
+
}
|
|
433
|
+
// Exponent part
|
|
434
|
+
const expCh = this.peekChar();
|
|
435
|
+
if (expCh === "e" || expCh === "E") {
|
|
436
|
+
this.advance(1);
|
|
437
|
+
const signCh = this.peekChar();
|
|
438
|
+
if (signCh === "+" || signCh === "-")
|
|
439
|
+
this.advance(1);
|
|
440
|
+
const expStart = this.pos;
|
|
441
|
+
while (this.pos < this.input.length &&
|
|
442
|
+
this.input[this.pos] >= "0" &&
|
|
443
|
+
this.input[this.pos] <= "9") {
|
|
444
|
+
this.advance(1);
|
|
445
|
+
}
|
|
446
|
+
if (this.pos === expStart) {
|
|
447
|
+
throw this.errorSpan("Expected exponent digits", begin);
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
// Make sure the number isn't followed by bare-string characters
|
|
451
|
+
const nextCh = this.peekChar();
|
|
452
|
+
if (nextCh !== undefined &&
|
|
453
|
+
isBareChar(nextCh) &&
|
|
454
|
+
!(nextCh >= "0" && nextCh <= "9")) {
|
|
455
|
+
this.pos = start;
|
|
456
|
+
if (hasMinus) {
|
|
457
|
+
throw this.errorPoint("Expected a value");
|
|
458
|
+
}
|
|
459
|
+
return {
|
|
460
|
+
kind: "scalar",
|
|
461
|
+
value: { kind: "string", value: this.parseBareString() },
|
|
462
|
+
};
|
|
463
|
+
}
|
|
464
|
+
const fullStr = this.input.substring(start, this.pos);
|
|
465
|
+
const n = parseFloat(fullStr);
|
|
466
|
+
if (isNaN(n)) {
|
|
467
|
+
throw this.errorSpan(`Invalid number: ${fullStr}`, begin);
|
|
468
|
+
}
|
|
469
|
+
return { kind: "scalar", value: { kind: "number", value: n } };
|
|
470
|
+
}
|
|
471
|
+
parseIntegerOrBare(start, hasMinus) {
|
|
472
|
+
this.pos = start;
|
|
473
|
+
const begin = this.position();
|
|
474
|
+
if (hasMinus)
|
|
475
|
+
this.advance(1);
|
|
476
|
+
const digitStart = this.pos;
|
|
477
|
+
while (this.pos < this.input.length &&
|
|
478
|
+
this.input[this.pos] >= "0" &&
|
|
479
|
+
this.input[this.pos] <= "9") {
|
|
480
|
+
this.advance(1);
|
|
481
|
+
}
|
|
482
|
+
if (this.pos === digitStart) {
|
|
483
|
+
this.pos = start;
|
|
484
|
+
if (hasMinus) {
|
|
485
|
+
throw this.errorPoint("Expected a value");
|
|
486
|
+
}
|
|
487
|
+
return {
|
|
488
|
+
kind: "scalar",
|
|
489
|
+
value: { kind: "string", value: this.parseBareString() },
|
|
490
|
+
};
|
|
491
|
+
}
|
|
492
|
+
// Check if followed by bare chars
|
|
493
|
+
if (!hasMinus) {
|
|
494
|
+
const ch = this.peekChar();
|
|
495
|
+
if (ch !== undefined &&
|
|
496
|
+
isBareChar(ch) &&
|
|
497
|
+
!(ch >= "0" && ch <= "9")) {
|
|
498
|
+
this.pos = start;
|
|
499
|
+
return {
|
|
500
|
+
kind: "scalar",
|
|
501
|
+
value: { kind: "string", value: this.parseBareString() },
|
|
502
|
+
};
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
// Check for exponent
|
|
506
|
+
const expCh = this.peekChar();
|
|
507
|
+
if (expCh === "e" || expCh === "E") {
|
|
508
|
+
this.advance(1);
|
|
509
|
+
const signCh = this.peekChar();
|
|
510
|
+
if (signCh === "+" || signCh === "-")
|
|
511
|
+
this.advance(1);
|
|
512
|
+
const expStart = this.pos;
|
|
513
|
+
while (this.pos < this.input.length &&
|
|
514
|
+
this.input[this.pos] >= "0" &&
|
|
515
|
+
this.input[this.pos] <= "9") {
|
|
516
|
+
this.advance(1);
|
|
517
|
+
}
|
|
518
|
+
if (this.pos === expStart) {
|
|
519
|
+
throw this.errorSpan("Expected exponent digits", begin);
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
const fullStr = this.input.substring(start, this.pos);
|
|
523
|
+
const n = parseFloat(fullStr);
|
|
524
|
+
if (isNaN(n)) {
|
|
525
|
+
throw this.errorSpan(`Invalid number: ${fullStr}`, begin);
|
|
526
|
+
}
|
|
527
|
+
return { kind: "scalar", value: { kind: "number", value: n } };
|
|
528
|
+
}
|
|
529
|
+
// ── Strings ─────────────────────────────────────────────────────
|
|
530
|
+
parseBareString() {
|
|
531
|
+
const start = this.pos;
|
|
532
|
+
while (this.pos < this.input.length && isBareChar(this.input[this.pos])) {
|
|
533
|
+
this.pos++;
|
|
534
|
+
}
|
|
535
|
+
if (this.pos === start) {
|
|
536
|
+
throw this.errorPoint("Expected an identifier");
|
|
537
|
+
}
|
|
538
|
+
return this.input.substring(start, this.pos);
|
|
539
|
+
}
|
|
540
|
+
parseDoubleQuotedString() {
|
|
541
|
+
const begin = this.position();
|
|
542
|
+
this.expectChar('"');
|
|
543
|
+
let result = "";
|
|
544
|
+
for (;;) {
|
|
545
|
+
const ch = this.peekChar();
|
|
546
|
+
if (ch === undefined || ch === "\r" || ch === "\n") {
|
|
547
|
+
throw this.errorSpan("Unterminated string", begin);
|
|
548
|
+
}
|
|
549
|
+
if (ch === '"') {
|
|
550
|
+
this.advance(1);
|
|
551
|
+
return result;
|
|
552
|
+
}
|
|
553
|
+
if (ch === "\\") {
|
|
554
|
+
this.advance(1);
|
|
555
|
+
result += this.parseEscapeChar();
|
|
556
|
+
}
|
|
557
|
+
else {
|
|
558
|
+
this.advance(1);
|
|
559
|
+
result += ch;
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
parseSingleQuotedString() {
|
|
564
|
+
const begin = this.position();
|
|
565
|
+
this.expectChar("'");
|
|
566
|
+
let result = "";
|
|
567
|
+
for (;;) {
|
|
568
|
+
const ch = this.peekChar();
|
|
569
|
+
if (ch === undefined || ch === "\r" || ch === "\n") {
|
|
570
|
+
throw this.errorSpan("Unterminated string", begin);
|
|
571
|
+
}
|
|
572
|
+
if (ch === "'") {
|
|
573
|
+
this.advance(1);
|
|
574
|
+
return result;
|
|
575
|
+
}
|
|
576
|
+
if (ch === "\\") {
|
|
577
|
+
this.advance(1);
|
|
578
|
+
result += "\\";
|
|
579
|
+
const next = this.peekChar();
|
|
580
|
+
if (next === undefined || next === "\r" || next === "\n") {
|
|
581
|
+
throw this.errorSpan("Unterminated string", begin);
|
|
582
|
+
}
|
|
583
|
+
this.advance(1);
|
|
584
|
+
result += next;
|
|
585
|
+
}
|
|
586
|
+
else {
|
|
587
|
+
this.advance(1);
|
|
588
|
+
result += ch;
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
parseTripleSingleQuotedString() {
|
|
593
|
+
const begin = this.position();
|
|
594
|
+
if (!this.startsWith("'''")) {
|
|
595
|
+
throw this.errorPoint("Expected triple-single-quoted string");
|
|
596
|
+
}
|
|
597
|
+
this.advance(3);
|
|
598
|
+
let result = "";
|
|
599
|
+
for (;;) {
|
|
600
|
+
if (this.startsWith("'''")) {
|
|
601
|
+
this.advance(3);
|
|
602
|
+
return result;
|
|
603
|
+
}
|
|
604
|
+
const ch = this.peekChar();
|
|
605
|
+
if (ch === undefined) {
|
|
606
|
+
throw this.errorSpan("Unterminated triple-single-quoted string", begin);
|
|
607
|
+
}
|
|
608
|
+
if (ch === "\\") {
|
|
609
|
+
this.advance(1);
|
|
610
|
+
result += "\\";
|
|
611
|
+
const next = this.peekChar();
|
|
612
|
+
if (next === undefined) {
|
|
613
|
+
throw this.errorSpan("Unterminated triple-single-quoted string", begin);
|
|
614
|
+
}
|
|
615
|
+
this.advance(1);
|
|
616
|
+
result += next;
|
|
617
|
+
}
|
|
618
|
+
else {
|
|
619
|
+
this.advance(1);
|
|
620
|
+
result += ch;
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
parseBacktickString() {
|
|
625
|
+
const begin = this.position();
|
|
626
|
+
this.expectChar("`");
|
|
627
|
+
let result = "";
|
|
628
|
+
for (;;) {
|
|
629
|
+
const ch = this.peekChar();
|
|
630
|
+
if (ch === undefined || ch === "\r" || ch === "\n") {
|
|
631
|
+
throw this.errorSpan("Unterminated backtick string", begin);
|
|
632
|
+
}
|
|
633
|
+
if (ch === "`") {
|
|
634
|
+
this.advance(1);
|
|
635
|
+
return result;
|
|
636
|
+
}
|
|
637
|
+
if (ch === "\\") {
|
|
638
|
+
this.advance(1);
|
|
639
|
+
result += this.parseEscapeChar();
|
|
640
|
+
}
|
|
641
|
+
else {
|
|
642
|
+
this.advance(1);
|
|
643
|
+
result += ch;
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
parseTripleString() {
|
|
648
|
+
const begin = this.position();
|
|
649
|
+
if (!this.startsWith('"""')) {
|
|
650
|
+
throw this.errorPoint("Expected triple-quoted string");
|
|
651
|
+
}
|
|
652
|
+
this.advance(3);
|
|
653
|
+
let result = "";
|
|
654
|
+
for (;;) {
|
|
655
|
+
if (this.startsWith('"""')) {
|
|
656
|
+
this.advance(3);
|
|
657
|
+
return result;
|
|
658
|
+
}
|
|
659
|
+
const ch = this.peekChar();
|
|
660
|
+
if (ch === undefined) {
|
|
661
|
+
throw this.errorSpan("Unterminated triple-quoted string", begin);
|
|
662
|
+
}
|
|
663
|
+
if (ch === "\\") {
|
|
664
|
+
this.advance(1);
|
|
665
|
+
result += this.parseEscapeChar();
|
|
666
|
+
}
|
|
667
|
+
else {
|
|
668
|
+
this.advance(1);
|
|
669
|
+
result += ch;
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
parseEscapeChar() {
|
|
674
|
+
const ch = this.peekChar();
|
|
675
|
+
if (ch === undefined)
|
|
676
|
+
throw this.errorPoint("Unterminated escape sequence");
|
|
677
|
+
switch (ch) {
|
|
678
|
+
case "b":
|
|
679
|
+
this.advance(1);
|
|
680
|
+
return "\b";
|
|
681
|
+
case "f":
|
|
682
|
+
this.advance(1);
|
|
683
|
+
return "\f";
|
|
684
|
+
case "n":
|
|
685
|
+
this.advance(1);
|
|
686
|
+
return "\n";
|
|
687
|
+
case "r":
|
|
688
|
+
this.advance(1);
|
|
689
|
+
return "\r";
|
|
690
|
+
case "t":
|
|
691
|
+
this.advance(1);
|
|
692
|
+
return "\t";
|
|
693
|
+
case "u": {
|
|
694
|
+
const begin = this.position();
|
|
695
|
+
this.advance(1);
|
|
696
|
+
const start = this.pos;
|
|
697
|
+
for (let i = 0; i < 4; i++) {
|
|
698
|
+
const hch = this.peekChar();
|
|
699
|
+
if (hch === undefined || !isHexDigit(hch)) {
|
|
700
|
+
throw this.errorSpan("Expected 4 hex digits in \\uXXXX", begin);
|
|
701
|
+
}
|
|
702
|
+
this.advance(1);
|
|
703
|
+
}
|
|
704
|
+
const hex = this.input.substring(start, this.pos);
|
|
705
|
+
const codePoint = parseInt(hex, 16);
|
|
706
|
+
return String.fromCharCode(codePoint);
|
|
707
|
+
}
|
|
708
|
+
default:
|
|
709
|
+
this.advance(1);
|
|
710
|
+
return ch;
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
// ── Arrays ──────────────────────────────────────────────────────
|
|
714
|
+
parseArray() {
|
|
715
|
+
const begin = this.position();
|
|
716
|
+
this.expectChar("[");
|
|
717
|
+
this.skipWs();
|
|
718
|
+
if (this.eatChar("]"))
|
|
719
|
+
return [];
|
|
720
|
+
const elements = [];
|
|
721
|
+
elements.push(this.parseArrayElement());
|
|
722
|
+
for (;;) {
|
|
723
|
+
this.skipWs();
|
|
724
|
+
if (this.eatChar("]"))
|
|
725
|
+
return elements;
|
|
726
|
+
if (this.eatChar(",")) {
|
|
727
|
+
this.skipWs();
|
|
728
|
+
if (this.peekChar() === "]") {
|
|
729
|
+
this.advance(1);
|
|
730
|
+
return elements;
|
|
731
|
+
}
|
|
732
|
+
elements.push(this.parseArrayElement());
|
|
733
|
+
}
|
|
734
|
+
else if (this.pos >= this.input.length) {
|
|
735
|
+
throw this.errorSpan("Unclosed '['", begin);
|
|
736
|
+
}
|
|
737
|
+
else {
|
|
738
|
+
throw this.errorPoint("Expected ',' or ']' in array");
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
parseArrayElement() {
|
|
743
|
+
this.skipWs();
|
|
744
|
+
const ch = this.peekChar();
|
|
745
|
+
if (ch === "{") {
|
|
746
|
+
const props = this.parsePropertiesBlock();
|
|
747
|
+
return { value: null, properties: props };
|
|
748
|
+
}
|
|
749
|
+
if (ch === "[") {
|
|
750
|
+
const elements = this.parseArray();
|
|
751
|
+
return { value: { kind: "array", elements }, properties: null };
|
|
752
|
+
}
|
|
753
|
+
const value = this.parseScalarValue();
|
|
754
|
+
this.skipWs();
|
|
755
|
+
if (this.peekChar() === "{") {
|
|
756
|
+
const props = this.parsePropertiesBlock();
|
|
757
|
+
return { value, properties: props };
|
|
758
|
+
}
|
|
759
|
+
return { value, properties: null };
|
|
760
|
+
}
|
|
761
|
+
// ── Properties Block ────────────────────────────────────────────
|
|
762
|
+
parsePropertiesBlock() {
|
|
763
|
+
const begin = this.position();
|
|
764
|
+
this.expectChar("{");
|
|
765
|
+
this.skipWs();
|
|
766
|
+
if (this.startsWith("...")) {
|
|
767
|
+
const saved = this.pos;
|
|
768
|
+
this.advance(3);
|
|
769
|
+
this.skipWs();
|
|
770
|
+
if (this.peekChar() === "}") {
|
|
771
|
+
this.advance(1);
|
|
772
|
+
return [];
|
|
773
|
+
}
|
|
774
|
+
this.pos = saved;
|
|
775
|
+
}
|
|
776
|
+
const stmts = [];
|
|
777
|
+
for (;;) {
|
|
778
|
+
this.skipWs();
|
|
779
|
+
if (this.eatChar("}"))
|
|
780
|
+
return stmts;
|
|
781
|
+
if (this.pos >= this.input.length) {
|
|
782
|
+
throw this.errorSpan("Unclosed '{'", begin);
|
|
783
|
+
}
|
|
784
|
+
stmts.push(this.parseStatement());
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
// ── References ──────────────────────────────────────────────────
|
|
788
|
+
parseReference() {
|
|
789
|
+
this.expectChar("$");
|
|
790
|
+
let ups = 0;
|
|
791
|
+
while (this.peekChar() === "^") {
|
|
792
|
+
this.advance(1);
|
|
793
|
+
ups++;
|
|
794
|
+
}
|
|
795
|
+
const path = [];
|
|
796
|
+
const firstName = this.parseIdentifier();
|
|
797
|
+
path.push({ kind: "name", name: firstName });
|
|
798
|
+
if (this.peekChar() === "[") {
|
|
799
|
+
this.advance(1);
|
|
800
|
+
this.skipWs();
|
|
801
|
+
const idx = this.parseRefIndex();
|
|
802
|
+
path.push({ kind: "index", index: idx });
|
|
803
|
+
this.skipWs();
|
|
804
|
+
this.expectChar("]");
|
|
805
|
+
}
|
|
806
|
+
while (this.peekChar() === ".") {
|
|
807
|
+
this.advance(1);
|
|
808
|
+
const name = this.parseIdentifier();
|
|
809
|
+
path.push({ kind: "name", name });
|
|
810
|
+
if (this.peekChar() === "[") {
|
|
811
|
+
this.advance(1);
|
|
812
|
+
this.skipWs();
|
|
813
|
+
const idx = this.parseRefIndex();
|
|
814
|
+
path.push({ kind: "index", index: idx });
|
|
815
|
+
this.skipWs();
|
|
816
|
+
this.expectChar("]");
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
return { kind: "reference", ups, path };
|
|
820
|
+
}
|
|
821
|
+
parseRefIndex() {
|
|
822
|
+
const begin = this.position();
|
|
823
|
+
const start = this.pos;
|
|
824
|
+
while (this.pos < this.input.length &&
|
|
825
|
+
this.input[this.pos] >= "0" &&
|
|
826
|
+
this.input[this.pos] <= "9") {
|
|
827
|
+
this.pos++;
|
|
828
|
+
}
|
|
829
|
+
if (this.pos === start) {
|
|
830
|
+
throw this.errorPoint("Expected array index");
|
|
831
|
+
}
|
|
832
|
+
const idx = parseInt(this.input.substring(start, this.pos), 10);
|
|
833
|
+
if (isNaN(idx)) {
|
|
834
|
+
throw this.errorSpan("Invalid array index", begin);
|
|
835
|
+
}
|
|
836
|
+
return idx;
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
/** Check if a character is valid in a bare string / identifier. */
|
|
840
|
+
function isBareChar(ch) {
|
|
841
|
+
const code = ch.charCodeAt(0);
|
|
842
|
+
return ((code >= 0x30 && code <= 0x39) || // 0-9
|
|
843
|
+
(code >= 0x41 && code <= 0x5a) || // A-Z
|
|
844
|
+
(code >= 0x61 && code <= 0x7a) || // a-z
|
|
845
|
+
code === 0x5f || // _
|
|
846
|
+
(code >= 0x00c0 && code <= 0x024f) || // Latin Extended
|
|
847
|
+
(code >= 0x1e00 && code <= 0x1eff) // Latin Extended Additional
|
|
848
|
+
);
|
|
849
|
+
}
|
|
850
|
+
function isHexDigit(ch) {
|
|
851
|
+
const code = ch.charCodeAt(0);
|
|
852
|
+
return ((code >= 0x30 && code <= 0x39) || // 0-9
|
|
853
|
+
(code >= 0x41 && code <= 0x46) || // A-F
|
|
854
|
+
(code >= 0x61 && code <= 0x66) // a-f
|
|
855
|
+
);
|
|
856
|
+
}
|
|
857
|
+
/** Parse a MOTLY input string into a list of statements. */
|
|
858
|
+
function parse(input) {
|
|
859
|
+
const parser = new Parser(input);
|
|
860
|
+
return parser.parseStatements();
|
|
861
|
+
}
|