@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.
@@ -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
- // Check for `= ... {` (replaceProperties with preserveValue)
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
- const props = this.parsePropertiesBlock();
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
- // `= value` (setEq)
137
+ // = value
151
138
  const value = this.parseEqValue();
152
139
  this.skipWs();
153
- // Optionally followed by `{ ... }` or `{ statements }`
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 (ch === "@")
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.parseDoubleQuotedString() },
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 === "." || 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 rem = this.remaining();
344
- return offset < rem.length && isBareChar(rem[offset]);
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.parseScalarValue();
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();
@@ -1,4 +1,4 @@
1
- import { MOTLYValue, MOTLYError, MOTLYSchemaError, MOTLYValidationError } from "motly-ts-interface";
1
+ import { MOTLYValue, MOTLYError, MOTLYSchemaError, MOTLYValidationError } from "../../interface/src/types";
2
2
  /**
3
3
  * A stateful MOTLY parsing session.
4
4
  *
@@ -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
- this.value = (0, interpreter_1.execute)(stmts, this.value);
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
- this.schema = (0, interpreter_1.execute)(stmts, {});
45
- return [];
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 deepClone(this.value);
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 "motly-ts-interface";
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[];