@malloydata/motly-ts-parser 0.0.2 → 0.2.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/interface/src/types.d.ts +69 -0
- package/build/interface/src/types.js +12 -0
- package/build/{ast.d.ts → parser/src/ast.d.ts} +10 -2
- package/build/parser/src/clone.d.ts +3 -0
- package/build/parser/src/clone.js +35 -0
- package/build/parser/src/index.d.ts +3 -0
- package/build/parser/src/index.js +8 -0
- package/build/{interpreter.d.ts → parser/src/interpreter.d.ts} +2 -2
- package/build/parser/src/interpreter.js +325 -0
- package/build/{parser.js → parser/src/parser.js} +111 -129
- package/build/{session.d.ts → parser/src/session.d.ts} +1 -1
- package/build/{session.js → parser/src/session.js} +8 -35
- package/build/{validate.d.ts → parser/src/validate.d.ts} +1 -1
- package/build/{validate.js → parser/src/validate.js} +51 -65
- package/package.json +3 -4
- package/build/index.d.ts +0 -2
- package/build/index.js +0 -5
- package/build/interpreter.js +0 -236
- /package/build/{ast.js → parser/src/ast.js} +0 -0
- /package/build/{parser.d.ts → parser/src/parser.d.ts} +0 -0
|
@@ -7,9 +7,6 @@ class Parser {
|
|
|
7
7
|
this.pos = 0;
|
|
8
8
|
}
|
|
9
9
|
// ── Helpers ──────────────────────────────────────────────────────
|
|
10
|
-
remaining() {
|
|
11
|
-
return this.input.substring(this.pos);
|
|
12
|
-
}
|
|
13
10
|
peekChar() {
|
|
14
11
|
return this.pos < this.input.length ? this.input[this.pos] : undefined;
|
|
15
12
|
}
|
|
@@ -118,87 +115,40 @@ class Parser {
|
|
|
118
115
|
const path = this.parsePropName();
|
|
119
116
|
this.skipWs();
|
|
120
117
|
const ch = this.peekChar();
|
|
118
|
+
// Check := first (MUST check before : alone)
|
|
119
|
+
if (ch === ":" && this.startsWith(":=")) {
|
|
120
|
+
this.advance(2);
|
|
121
|
+
this.skipWs();
|
|
122
|
+
const value = this.parseEqValue();
|
|
123
|
+
this.skipWs();
|
|
124
|
+
if (this.peekChar() === "{") {
|
|
125
|
+
const props = this.parsePropertiesBlock();
|
|
126
|
+
return { kind: "assignBoth", path, value, properties: props };
|
|
127
|
+
}
|
|
128
|
+
return { kind: "assignBoth", path, value, properties: null };
|
|
129
|
+
}
|
|
121
130
|
if (ch === "=") {
|
|
122
131
|
this.advance(1);
|
|
123
132
|
this.skipWs();
|
|
124
|
-
//
|
|
125
|
-
if (this.startsWith("...")) {
|
|
126
|
-
const saved = this.pos;
|
|
127
|
-
this.advance(3);
|
|
128
|
-
this.skipWs();
|
|
129
|
-
if (this.peekChar() === "{") {
|
|
130
|
-
const props = this.parsePropertiesBlock();
|
|
131
|
-
return {
|
|
132
|
-
kind: "replaceProperties",
|
|
133
|
-
path,
|
|
134
|
-
properties: props,
|
|
135
|
-
preserveValue: true,
|
|
136
|
-
};
|
|
137
|
-
}
|
|
138
|
-
this.pos = saved;
|
|
139
|
-
}
|
|
140
|
-
// Check for `= {` (replaceProperties without preserveValue)
|
|
133
|
+
// = { is now a parse error (= requires a value)
|
|
141
134
|
if (this.peekChar() === "{") {
|
|
142
|
-
|
|
143
|
-
return {
|
|
144
|
-
kind: "replaceProperties",
|
|
145
|
-
path,
|
|
146
|
-
properties: props,
|
|
147
|
-
preserveValue: false,
|
|
148
|
-
};
|
|
135
|
+
throw this.errorPoint("'=' requires a value; use ': { ... }' to replace properties");
|
|
149
136
|
}
|
|
150
|
-
//
|
|
137
|
+
// = value
|
|
151
138
|
const value = this.parseEqValue();
|
|
152
139
|
this.skipWs();
|
|
153
|
-
//
|
|
140
|
+
// Optional { props } block (MERGE semantics)
|
|
154
141
|
if (this.peekChar() === "{") {
|
|
155
|
-
const saved = this.pos;
|
|
156
|
-
this.advance(1);
|
|
157
|
-
this.skipWs();
|
|
158
|
-
if (this.startsWith("...")) {
|
|
159
|
-
const saved2 = this.pos;
|
|
160
|
-
this.advance(3);
|
|
161
|
-
this.skipWs();
|
|
162
|
-
if (this.peekChar() === "}") {
|
|
163
|
-
this.advance(1);
|
|
164
|
-
return {
|
|
165
|
-
kind: "setEq",
|
|
166
|
-
path,
|
|
167
|
-
value,
|
|
168
|
-
properties: null,
|
|
169
|
-
preserveProperties: true,
|
|
170
|
-
};
|
|
171
|
-
}
|
|
172
|
-
this.pos = saved2;
|
|
173
|
-
}
|
|
174
|
-
this.pos = saved;
|
|
175
142
|
const props = this.parsePropertiesBlock();
|
|
176
|
-
return {
|
|
177
|
-
kind: "setEq",
|
|
178
|
-
path,
|
|
179
|
-
value,
|
|
180
|
-
properties: props,
|
|
181
|
-
preserveProperties: false,
|
|
182
|
-
};
|
|
143
|
+
return { kind: "setEq", path, value, properties: props };
|
|
183
144
|
}
|
|
184
|
-
return {
|
|
185
|
-
kind: "setEq",
|
|
186
|
-
path,
|
|
187
|
-
value,
|
|
188
|
-
properties: null,
|
|
189
|
-
preserveProperties: false,
|
|
190
|
-
};
|
|
145
|
+
return { kind: "setEq", path, value, properties: null };
|
|
191
146
|
}
|
|
192
147
|
if (ch === ":") {
|
|
193
148
|
this.advance(1);
|
|
194
149
|
this.skipWs();
|
|
195
150
|
const props = this.parsePropertiesBlock();
|
|
196
|
-
return {
|
|
197
|
-
kind: "replaceProperties",
|
|
198
|
-
path,
|
|
199
|
-
properties: props,
|
|
200
|
-
preserveValue: false,
|
|
201
|
-
};
|
|
151
|
+
return { kind: "replaceProperties", path, properties: props };
|
|
202
152
|
}
|
|
203
153
|
if (ch === "{") {
|
|
204
154
|
const props = this.parsePropertiesBlock();
|
|
@@ -223,55 +173,16 @@ class Parser {
|
|
|
223
173
|
return this.parseBareString();
|
|
224
174
|
}
|
|
225
175
|
// ── Values ──────────────────────────────────────────────────────
|
|
226
|
-
parseEqValue() {
|
|
176
|
+
parseEqValue(allowArrays = true) {
|
|
227
177
|
const ch = this.peekChar();
|
|
228
|
-
if (ch === "[")
|
|
178
|
+
if (allowArrays && ch === "[")
|
|
229
179
|
return { kind: "array", elements: this.parseArray() };
|
|
230
|
-
if (
|
|
231
|
-
return { kind: "scalar", value: this.parseAtValue() };
|
|
232
|
-
if (ch === "$")
|
|
233
|
-
return { kind: "scalar", value: this.parseReference() };
|
|
234
|
-
if (ch === '"') {
|
|
235
|
-
if (this.startsWith('"""')) {
|
|
236
|
-
return {
|
|
237
|
-
kind: "scalar",
|
|
238
|
-
value: { kind: "string", value: this.parseTripleString() },
|
|
239
|
-
};
|
|
240
|
-
}
|
|
180
|
+
if (this.startsWith("<<<")) {
|
|
241
181
|
return {
|
|
242
182
|
kind: "scalar",
|
|
243
|
-
value: { kind: "string", value: this.
|
|
183
|
+
value: { kind: "string", value: this.parseHeredoc() },
|
|
244
184
|
};
|
|
245
185
|
}
|
|
246
|
-
if (ch === "'") {
|
|
247
|
-
if (this.startsWith("'''")) {
|
|
248
|
-
return {
|
|
249
|
-
kind: "scalar",
|
|
250
|
-
value: {
|
|
251
|
-
kind: "string",
|
|
252
|
-
value: this.parseTripleSingleQuotedString(),
|
|
253
|
-
},
|
|
254
|
-
};
|
|
255
|
-
}
|
|
256
|
-
return {
|
|
257
|
-
kind: "scalar",
|
|
258
|
-
value: { kind: "string", value: this.parseSingleQuotedString() },
|
|
259
|
-
};
|
|
260
|
-
}
|
|
261
|
-
if (ch !== undefined &&
|
|
262
|
-
(ch === "-" || (ch >= "0" && ch <= "9") || ch === ".")) {
|
|
263
|
-
return this.parseNumberOrString();
|
|
264
|
-
}
|
|
265
|
-
if (ch !== undefined && isBareChar(ch)) {
|
|
266
|
-
return {
|
|
267
|
-
kind: "scalar",
|
|
268
|
-
value: { kind: "string", value: this.parseBareString() },
|
|
269
|
-
};
|
|
270
|
-
}
|
|
271
|
-
throw this.errorPoint("Expected a value");
|
|
272
|
-
}
|
|
273
|
-
parseScalarValue() {
|
|
274
|
-
const ch = this.peekChar();
|
|
275
186
|
if (ch === "@")
|
|
276
187
|
return { kind: "scalar", value: this.parseAtValue() };
|
|
277
188
|
if (ch === "$")
|
|
@@ -304,7 +215,7 @@ class Parser {
|
|
|
304
215
|
};
|
|
305
216
|
}
|
|
306
217
|
if (ch !== undefined &&
|
|
307
|
-
((ch >= "0" && ch <= "9") || ch === "."
|
|
218
|
+
(ch === "-" || (ch >= "0" && ch <= "9") || ch === ".")) {
|
|
308
219
|
return this.parseNumberOrString();
|
|
309
220
|
}
|
|
310
221
|
if (ch !== undefined && isBareChar(ch)) {
|
|
@@ -315,7 +226,7 @@ class Parser {
|
|
|
315
226
|
}
|
|
316
227
|
throw this.errorPoint("Expected a value");
|
|
317
228
|
}
|
|
318
|
-
/** Parse `@true`, `@false`, or `@date` */
|
|
229
|
+
/** Parse `@true`, `@false`, `@none`, `@env.NAME`, or `@date` */
|
|
319
230
|
parseAtValue() {
|
|
320
231
|
const begin = this.position();
|
|
321
232
|
this.expectChar("@");
|
|
@@ -327,6 +238,15 @@ class Parser {
|
|
|
327
238
|
this.advance(5);
|
|
328
239
|
return { kind: "boolean", value: false };
|
|
329
240
|
}
|
|
241
|
+
if (this.startsWith("none") && !this.isBareCharAt(4)) {
|
|
242
|
+
this.advance(4);
|
|
243
|
+
return { kind: "none" };
|
|
244
|
+
}
|
|
245
|
+
if (this.startsWith("env.")) {
|
|
246
|
+
this.advance(4);
|
|
247
|
+
const name = this.parseBareString();
|
|
248
|
+
return { kind: "env", name };
|
|
249
|
+
}
|
|
330
250
|
const ch = this.peekChar();
|
|
331
251
|
if (ch !== undefined && ch >= "0" && ch <= "9") {
|
|
332
252
|
return this.parseDate(begin);
|
|
@@ -337,11 +257,11 @@ class Parser {
|
|
|
337
257
|
this.pos++;
|
|
338
258
|
}
|
|
339
259
|
const token = this.pos > tokenStart ? this.input.substring(tokenStart, this.pos) : "";
|
|
340
|
-
throw this.errorSpan(`Illegal constant @${token}; expected @true, @false, or @date`, begin);
|
|
260
|
+
throw this.errorSpan(`Illegal constant @${token}; expected @true, @false, @none, @env.NAME, or @date`, begin);
|
|
341
261
|
}
|
|
342
262
|
isBareCharAt(offset) {
|
|
343
|
-
const
|
|
344
|
-
return
|
|
263
|
+
const absPos = this.pos + offset;
|
|
264
|
+
return absPos < this.input.length && isBareChar(this.input[absPos]);
|
|
345
265
|
}
|
|
346
266
|
parseDate(begin) {
|
|
347
267
|
const start = this.pos;
|
|
@@ -722,6 +642,79 @@ class Parser {
|
|
|
722
642
|
return ch;
|
|
723
643
|
}
|
|
724
644
|
}
|
|
645
|
+
// ── Heredoc ─────────────────────────────────────────────────────
|
|
646
|
+
parseHeredoc() {
|
|
647
|
+
const begin = this.position();
|
|
648
|
+
this.advance(3); // past <<<
|
|
649
|
+
// Skip spaces/tabs on the same line
|
|
650
|
+
while (this.pos < this.input.length) {
|
|
651
|
+
const ch = this.input[this.pos];
|
|
652
|
+
if (ch === " " || ch === "\t") {
|
|
653
|
+
this.pos++;
|
|
654
|
+
}
|
|
655
|
+
else {
|
|
656
|
+
break;
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
// Allow \r before \n
|
|
660
|
+
if (this.pos < this.input.length && this.input[this.pos] === "\r") {
|
|
661
|
+
this.advance(1);
|
|
662
|
+
}
|
|
663
|
+
// Expect newline
|
|
664
|
+
if (this.pos >= this.input.length || this.input[this.pos] !== "\n") {
|
|
665
|
+
throw this.errorSpan("Expected newline after <<<", begin);
|
|
666
|
+
}
|
|
667
|
+
this.advance(1);
|
|
668
|
+
// Collect lines until we find >>> on its own line
|
|
669
|
+
const lines = [];
|
|
670
|
+
let foundClose = false;
|
|
671
|
+
while (this.pos < this.input.length) {
|
|
672
|
+
// Read a line (break only on \n)
|
|
673
|
+
const lineStart = this.pos;
|
|
674
|
+
while (this.pos < this.input.length && this.input[this.pos] !== "\n") {
|
|
675
|
+
this.pos++;
|
|
676
|
+
}
|
|
677
|
+
// Strip trailing \r for CRLF compatibility
|
|
678
|
+
let lineContent = this.input.substring(lineStart, this.pos);
|
|
679
|
+
if (lineContent.endsWith("\r")) {
|
|
680
|
+
lineContent = lineContent.substring(0, lineContent.length - 1);
|
|
681
|
+
}
|
|
682
|
+
// Consume the \n
|
|
683
|
+
if (this.pos < this.input.length && this.input[this.pos] === "\n") {
|
|
684
|
+
this.advance(1);
|
|
685
|
+
}
|
|
686
|
+
// Check if this is the closing >>> line
|
|
687
|
+
if (lineContent.trim() === ">>>") {
|
|
688
|
+
foundClose = true;
|
|
689
|
+
break;
|
|
690
|
+
}
|
|
691
|
+
lines.push(lineContent);
|
|
692
|
+
}
|
|
693
|
+
if (!foundClose) {
|
|
694
|
+
throw this.errorSpan("Unterminated heredoc (expected >>>)", begin);
|
|
695
|
+
}
|
|
696
|
+
if (lines.length === 0) {
|
|
697
|
+
return "";
|
|
698
|
+
}
|
|
699
|
+
// Determine strip amount from first line containing a non-space character
|
|
700
|
+
let strip = 0;
|
|
701
|
+
for (const line of lines) {
|
|
702
|
+
const trimmed = line.trimStart();
|
|
703
|
+
if (trimmed.length > 0) {
|
|
704
|
+
strip = line.length - trimmed.length;
|
|
705
|
+
break;
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
// Strip indentation and join; whitespace-only lines become empty
|
|
709
|
+
const stripped = lines.map((line) => {
|
|
710
|
+
if (line.trimStart().length === 0)
|
|
711
|
+
return "";
|
|
712
|
+
if (strip <= line.length)
|
|
713
|
+
return line.substring(strip);
|
|
714
|
+
return line;
|
|
715
|
+
});
|
|
716
|
+
return stripped.join("\n") + "\n";
|
|
717
|
+
}
|
|
725
718
|
// ── Arrays ──────────────────────────────────────────────────────
|
|
726
719
|
parseArray() {
|
|
727
720
|
const begin = this.position();
|
|
@@ -762,7 +755,7 @@ class Parser {
|
|
|
762
755
|
const elements = this.parseArray();
|
|
763
756
|
return { value: { kind: "array", elements }, properties: null };
|
|
764
757
|
}
|
|
765
|
-
const value = this.
|
|
758
|
+
const value = this.parseEqValue(false);
|
|
766
759
|
this.skipWs();
|
|
767
760
|
if (this.peekChar() === "{") {
|
|
768
761
|
const props = this.parsePropertiesBlock();
|
|
@@ -774,17 +767,6 @@ class Parser {
|
|
|
774
767
|
parsePropertiesBlock() {
|
|
775
768
|
const begin = this.position();
|
|
776
769
|
this.expectChar("{");
|
|
777
|
-
this.skipWs();
|
|
778
|
-
if (this.startsWith("...")) {
|
|
779
|
-
const saved = this.pos;
|
|
780
|
-
this.advance(3);
|
|
781
|
-
this.skipWs();
|
|
782
|
-
if (this.peekChar() === "}") {
|
|
783
|
-
this.advance(1);
|
|
784
|
-
return [];
|
|
785
|
-
}
|
|
786
|
-
this.pos = saved;
|
|
787
|
-
}
|
|
788
770
|
const stmts = [];
|
|
789
771
|
for (;;) {
|
|
790
772
|
this.skipWsAndCommas();
|
|
@@ -4,6 +4,7 @@ exports.MOTLYSession = void 0;
|
|
|
4
4
|
const parser_1 = require("./parser");
|
|
5
5
|
const interpreter_1 = require("./interpreter");
|
|
6
6
|
const validate_1 = require("./validate");
|
|
7
|
+
const clone_1 = require("./clone");
|
|
7
8
|
/**
|
|
8
9
|
* A stateful MOTLY parsing session.
|
|
9
10
|
*
|
|
@@ -24,8 +25,8 @@ class MOTLYSession {
|
|
|
24
25
|
this.ensureAlive();
|
|
25
26
|
try {
|
|
26
27
|
const stmts = (0, parser_1.parse)(source);
|
|
27
|
-
|
|
28
|
-
return
|
|
28
|
+
const errors = (0, interpreter_1.execute)(stmts, this.value);
|
|
29
|
+
return errors;
|
|
29
30
|
}
|
|
30
31
|
catch (e) {
|
|
31
32
|
if (isMotlyError(e))
|
|
@@ -41,8 +42,10 @@ class MOTLYSession {
|
|
|
41
42
|
this.ensureAlive();
|
|
42
43
|
try {
|
|
43
44
|
const stmts = (0, parser_1.parse)(source);
|
|
44
|
-
|
|
45
|
-
|
|
45
|
+
const fresh = {};
|
|
46
|
+
const errors = (0, interpreter_1.execute)(stmts, fresh);
|
|
47
|
+
this.schema = fresh;
|
|
48
|
+
return errors;
|
|
46
49
|
}
|
|
47
50
|
catch (e) {
|
|
48
51
|
if (isMotlyError(e))
|
|
@@ -62,7 +65,7 @@ class MOTLYSession {
|
|
|
62
65
|
*/
|
|
63
66
|
getValue() {
|
|
64
67
|
this.ensureAlive();
|
|
65
|
-
return
|
|
68
|
+
return (0, clone_1.cloneValue)(this.value);
|
|
66
69
|
}
|
|
67
70
|
/**
|
|
68
71
|
* Validate the session's value against its stored schema.
|
|
@@ -103,33 +106,3 @@ function isMotlyError(e) {
|
|
|
103
106
|
"begin" in e &&
|
|
104
107
|
"end" in e);
|
|
105
108
|
}
|
|
106
|
-
function deepClone(value) {
|
|
107
|
-
const result = {};
|
|
108
|
-
if (value.deleted)
|
|
109
|
-
result.deleted = true;
|
|
110
|
-
if (value.eq !== undefined) {
|
|
111
|
-
if (value.eq instanceof Date) {
|
|
112
|
-
result.eq = new Date(value.eq.getTime());
|
|
113
|
-
}
|
|
114
|
-
else if (Array.isArray(value.eq)) {
|
|
115
|
-
result.eq = value.eq.map(cloneNode);
|
|
116
|
-
}
|
|
117
|
-
else {
|
|
118
|
-
result.eq = value.eq;
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
if (value.properties) {
|
|
122
|
-
const props = {};
|
|
123
|
-
for (const key of Object.keys(value.properties)) {
|
|
124
|
-
props[key] = cloneNode(value.properties[key]);
|
|
125
|
-
}
|
|
126
|
-
result.properties = props;
|
|
127
|
-
}
|
|
128
|
-
return result;
|
|
129
|
-
}
|
|
130
|
-
function cloneNode(node) {
|
|
131
|
-
if ("linkTo" in node) {
|
|
132
|
-
return { linkTo: node.linkTo };
|
|
133
|
-
}
|
|
134
|
-
return deepClone(node);
|
|
135
|
-
}
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import { MOTLYValue, MOTLYSchemaError, MOTLYValidationError } from "
|
|
1
|
+
import { MOTLYValue, MOTLYSchemaError, MOTLYValidationError } from "../../interface/src/types";
|
|
2
2
|
export declare function validateReferences(root: MOTLYValue): MOTLYValidationError[];
|
|
3
3
|
export declare function validateSchema(tag: MOTLYValue, schema: MOTLYValue): MOTLYSchemaError[];
|