@arkadia/data 0.1.7 → 0.1.8

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 (54) hide show
  1. package/.prettierrc +8 -0
  2. package/README.md +166 -112
  3. package/dist/config.d.ts.map +1 -1
  4. package/dist/config.js.map +1 -1
  5. package/dist/core/Decoder.d.ts.map +1 -1
  6. package/dist/core/Decoder.js +123 -97
  7. package/dist/core/Decoder.js.map +1 -1
  8. package/dist/core/Encoder.d.ts +1 -2
  9. package/dist/core/Encoder.d.ts.map +1 -1
  10. package/dist/core/Encoder.js +74 -76
  11. package/dist/core/Encoder.js.map +1 -1
  12. package/dist/core/Parser.d.ts +1 -1
  13. package/dist/core/Parser.d.ts.map +1 -1
  14. package/dist/core/Parser.js +11 -11
  15. package/dist/core/Parser.js.map +1 -1
  16. package/dist/index.d.ts +4 -4
  17. package/dist/index.d.ts.map +1 -1
  18. package/dist/index.js +7 -8
  19. package/dist/index.js.map +1 -1
  20. package/dist/models/Meta.d.ts +3 -2
  21. package/dist/models/Meta.d.ts.map +1 -1
  22. package/dist/models/Meta.js +3 -3
  23. package/dist/models/Meta.js.map +1 -1
  24. package/dist/models/Node.d.ts +4 -3
  25. package/dist/models/Node.d.ts.map +1 -1
  26. package/dist/models/Node.js +29 -23
  27. package/dist/models/Node.js.map +1 -1
  28. package/dist/models/Schema.d.ts.map +1 -1
  29. package/dist/models/Schema.js +27 -21
  30. package/dist/models/Schema.js.map +1 -1
  31. package/eslint.config.mjs +42 -0
  32. package/package.json +11 -1
  33. package/scripts/verify-build.js +95 -92
  34. package/src/config.ts +75 -75
  35. package/src/core/Decoder.ts +984 -922
  36. package/src/core/Encoder.ts +364 -371
  37. package/src/core/Parser.ts +112 -112
  38. package/src/index.ts +18 -20
  39. package/src/models/Meta.ts +107 -107
  40. package/src/models/Node.ts +190 -185
  41. package/src/models/Schema.ts +198 -193
  42. package/tests/00.meta.test.ts +19 -25
  43. package/tests/00.node.test.ts +40 -48
  44. package/tests/00.primitive.test.ts +121 -95
  45. package/tests/00.schema.test.ts +28 -35
  46. package/tests/01.schema.test.ts +42 -52
  47. package/tests/02.data.test.ts +69 -75
  48. package/tests/03.errors.test.ts +53 -55
  49. package/tests/04.list.test.ts +192 -193
  50. package/tests/05.record.test.ts +54 -56
  51. package/tests/06.meta.test.ts +393 -389
  52. package/tests/utils.ts +47 -44
  53. package/tsconfig.json +27 -29
  54. package/vitest.config.ts +1 -1
@@ -1,1057 +1,1119 @@
1
- import { Node } from '../models/Node';
2
- import { Schema, SchemaKind } from '../models/Schema';
3
1
  import { MetaInfo } from '../models/Meta';
2
+ import { Node, Primitive } from '../models/Node';
3
+ import { Schema, SchemaKind } from '../models/Schema';
4
4
 
5
5
  // --- ANSI Colors Helper ---
6
6
  class Ansi {
7
- static RESET = "\x1b[0m";
8
- static DIM = "\x1b[2m";
9
- static BOLD = "\x1b[1m";
10
- static CYAN = "\x1b[36m";
11
- static YELLOW = "\x1b[33m";
12
- static GREEN = "\x1b[32m";
13
- static RED = "\x1b[31m";
14
- static MAGENTA = "\x1b[35m";
7
+ static RESET = '\x1b[0m';
8
+ static DIM = '\x1b[2m';
9
+ static BOLD = '\x1b[1m';
10
+ static CYAN = '\x1b[36m';
11
+ static YELLOW = '\x1b[33m';
12
+ static GREEN = '\x1b[32m';
13
+ static RED = '\x1b[31m';
14
+ static MAGENTA = '\x1b[35m';
15
15
  }
16
16
 
17
+ // eslint-disable-next-line no-control-regex
17
18
  const ANSI_RE = /\x1b\[[0-9;]*m/g;
18
19
 
19
20
  // --- Error/Warning Data Structures ---
20
21
 
21
22
  export class DecodeError {
22
- message: string;
23
- position: number;
24
- context: string = "";
25
- schema: Schema | null = null;
26
- node: Node | null = null;
27
-
28
- constructor(message: string, position: number, schema: Schema | null = null, node: Node | null = null) {
29
- this.message = message;
30
- this.position = position;
31
- this.schema = schema;
32
- this.node = node;
33
- }
34
-
35
- toString(): string {
36
- return `[DecodeError] ${this.message} (at pos ${this.position})`;
37
- }
38
-
23
+ message: string;
24
+ position: number;
25
+ context: string = '';
26
+ schema: Schema | null = null;
27
+ node: Node | null = null;
28
+
29
+ constructor(
30
+ message: string,
31
+ position: number,
32
+ schema: Schema | null = null,
33
+ node: Node | null = null,
34
+ ) {
35
+ this.message = message;
36
+ this.position = position;
37
+ this.schema = schema;
38
+ this.node = node;
39
+ }
40
+
41
+ toString(): string {
42
+ return `[DecodeError] ${this.message} (at pos ${this.position})`;
43
+ }
39
44
  }
40
45
 
41
46
  export class DecodeWarning {
42
- message: string;
43
- position: number;
44
- schema: Schema | null;
45
- node: Node | null;
46
-
47
- constructor(message: string, position: number, schema: Schema | null = null, node: Node | null = null) {
48
- this.message = message;
49
- this.position = position;
50
- this.schema = schema;
51
- this.node = node;
52
- }
53
-
54
- toString(): string {
55
- return `[DecodeWarn] ${this.message} (at pos ${this.position})`;
56
- }
47
+ message: string;
48
+ position: number;
49
+ schema: Schema | null;
50
+ node: Node | null;
51
+
52
+ constructor(
53
+ message: string,
54
+ position: number,
55
+ schema: Schema | null = null,
56
+ node: Node | null = null,
57
+ ) {
58
+ this.message = message;
59
+ this.position = position;
60
+ this.schema = schema;
61
+ this.node = node;
62
+ }
63
+
64
+ toString(): string {
65
+ return `[DecodeWarn] ${this.message} (at pos ${this.position})`;
66
+ }
57
67
  }
58
68
 
59
69
  export interface DecodeResult {
60
- node: Node;
61
- schema: Schema | null;
62
- errors: DecodeError[];
63
- warnings: DecodeWarning[];
70
+ node: Node;
71
+ schema: Schema | null;
72
+ errors: DecodeError[];
73
+ warnings: DecodeWarning[];
64
74
  }
65
75
 
66
76
  // --- DECODER CLASS ---
67
77
 
68
78
  export class Decoder {
69
- private static PRIMITIVES = new Set(["string", "bool", "number", "null", "int", "float", "binary"]);
70
- private static PRIMITIVES_MAPPING: Record<string, string> = {
71
- "string": "string", "bool": "bool", "number": "number", "null": "null",
72
- "int": "number", "float": "number", "binary": "binary"
73
- };
74
- private static MAX_ERRORS = 50;
75
-
76
- private text: string;
77
- private debug: boolean;
78
-
79
- // Cursor State
80
- private i: number = 0;
81
- private line: number = 0;
82
- private col: number = 0;
83
-
84
- // Context State
85
- private pendingMeta: MetaInfo = new MetaInfo();
86
-
87
- // Hierarchy State
88
- private nodeStack: Node[] = [];
89
- private schemaStack: Schema[] = [];
90
- private errors: DecodeError[] = [];
91
- private warnings: DecodeWarning[] = [];
92
- private namedSchemas: Map<string, Schema> = new Map();
93
-
94
- constructor(text: string, schema: string = "", removeAnsiColors: boolean = false, debug: boolean = false) {
95
- let cleanText = schema + text;
96
- if (removeAnsiColors) {
97
- cleanText = cleanText.replace(ANSI_RE, '');
98
- }
99
- this.text = cleanText;
100
- this.debug = debug;
79
+ private static PRIMITIVES = new Set([
80
+ 'string',
81
+ 'bool',
82
+ 'number',
83
+ 'null',
84
+ 'int',
85
+ 'float',
86
+ 'binary',
87
+ ]);
88
+ private static PRIMITIVES_MAPPING: Record<string, string> = {
89
+ string: 'string',
90
+ bool: 'bool',
91
+ number: 'number',
92
+ null: 'null',
93
+ int: 'number',
94
+ float: 'number',
95
+ binary: 'binary',
96
+ };
97
+ private static MAX_ERRORS = 50;
98
+
99
+ private text: string;
100
+ private debug: boolean;
101
+
102
+ // Cursor State
103
+ private i: number = 0;
104
+ private line: number = 0;
105
+ private col: number = 0;
106
+
107
+ // Context State
108
+ private pendingMeta: MetaInfo = new MetaInfo();
109
+
110
+ // Hierarchy State
111
+ private nodeStack: Node[] = [];
112
+ private schemaStack: Schema[] = [];
113
+ private errors: DecodeError[] = [];
114
+ private warnings: DecodeWarning[] = [];
115
+ private namedSchemas: Map<string, Schema> = new Map();
116
+
117
+ constructor(
118
+ text: string,
119
+ schema: string = '',
120
+ removeAnsiColors: boolean = false,
121
+ debug: boolean = false,
122
+ ) {
123
+ let cleanText = schema + text;
124
+ if (removeAnsiColors) {
125
+ cleanText = cleanText.replace(ANSI_RE, '');
101
126
  }
127
+ this.text = cleanText;
128
+ this.debug = debug;
129
+ }
102
130
 
103
- // =========================================================
104
- // ENTRY
105
- // =========================================================
131
+ // =========================================================
132
+ // ENTRY
133
+ // =========================================================
106
134
 
107
- public decode(): DecodeResult {
108
- this._dbg("decode() start");
109
- this.parseMeta();
135
+ public decode(): DecodeResult {
136
+ this._dbg('decode() start');
137
+ this.parseMeta();
110
138
 
111
- let rootSchemaContext: Schema | null = null;
112
-
113
- // 1. Schema Processing Loop
114
- while (!this.eof()) {
115
- const ch = this.peek();
116
-
117
- // Inline Definition <x:int>
118
- if (ch === '<') {
119
- rootSchemaContext = this.parseSchemaBody();
120
- this.parseMeta();
121
-
122
- // Check lookahead for data start
123
- const next = this.peek();
124
- if (next === '(' || next === '{' || next === '[') break;
125
- continue;
126
- }
127
-
128
- // Named Schema @Name
129
- if (ch === '@') {
130
- const schema = this.parseSchemaAtRef();
131
- this.parseMeta();
132
-
133
- const next = this.peek();
134
- if (next === '@' || next === '<') continue;
135
-
136
- rootSchemaContext = schema;
137
- break;
138
- }
139
- break;
140
- }
139
+ let rootSchemaContext: Schema | null = null;
141
140
 
142
- // 2. Push Context
143
- if (rootSchemaContext) {
144
- this.pushSchema(rootSchemaContext);
145
- }
141
+ // 1. Schema Processing Loop
142
+ while (!this.eof()) {
143
+ const ch = this.peek();
146
144
 
147
- // 3. Parse Root Node
148
- let rootNode: Node;
149
- if (this.eof()) {
150
- rootNode = this.createNode(null);
151
- } else {
152
- rootNode = this.parseNode();
153
- }
145
+ // Inline Definition <x:int>
146
+ if (ch === '<') {
147
+ rootSchemaContext = this.parseSchemaBody();
148
+ this.parseMeta();
154
149
 
155
- // 4. Cleanup Context
156
- if (rootSchemaContext) {
157
- this.popSchema();
158
- // Link schema if node ended up generic
159
- if (!rootNode.schema || rootNode.schema.isAny) {
160
- rootNode.schema = rootSchemaContext;
161
- }
162
- } else {
163
- rootSchemaContext = rootNode.schema;
164
- }
150
+ // Check lookahead for data start
151
+ const next = this.peek();
152
+ if (next === '(' || next === '{' || next === '[') break;
153
+ continue;
154
+ }
165
155
 
166
- // Final prefix scan
156
+ // Named Schema @Name
157
+ if (ch === '@') {
158
+ const schema = this.parseSchemaAtRef();
167
159
  this.parseMeta();
168
- this.applyMeta(rootNode);
169
160
 
170
- this.popNode(); // Just in case
171
- this._dbg("decode() end");
161
+ const next = this.peek();
162
+ if (next === '@' || next === '<') continue;
172
163
 
173
- return {
174
- node: rootNode,
175
- schema: rootSchemaContext,
176
- errors: this.errors,
177
- warnings: this.warnings
178
- };
164
+ rootSchemaContext = schema;
165
+ break;
166
+ }
167
+ break;
179
168
  }
180
169
 
181
- // =========================================================
182
- // SCHEMA DEFINITION PARSING
183
- // =========================================================
184
-
185
- private parseSchemaAtRef(): Schema {
186
- this.advance(1); // @
187
- const typeName = this.parseIdent();
188
- this.skipWhitespace();
189
-
190
- if (this.peek() === '<') {
191
- this._dbg(`defining type ${typeName}`);
192
- const schema = this.parseSchemaBody(typeName);
193
- if (schema.isAny) schema.kind = SchemaKind.RECORD;
194
- this.namedSchemas.set(typeName, schema);
195
- return schema;
196
- }
170
+ // 2. Push Context
171
+ if (rootSchemaContext) {
172
+ this.pushSchema(rootSchemaContext);
173
+ }
197
174
 
198
- this._dbg(`referencing type ${typeName}`);
199
- if (this.namedSchemas.has(typeName)) {
200
- return this.namedSchemas.get(typeName)!;
201
- }
175
+ // 3. Parse Root Node
176
+ let rootNode: Node;
177
+ if (this.eof()) {
178
+ rootNode = this.createNode(null);
179
+ } else {
180
+ rootNode = this.parseNode();
181
+ }
202
182
 
203
- return new Schema(SchemaKind.RECORD, { typeName });
183
+ // 4. Cleanup Context
184
+ if (rootSchemaContext) {
185
+ this.popSchema();
186
+ // Link schema if node ended up generic
187
+ if (!rootNode.schema || rootNode.schema.isAny) {
188
+ rootNode.schema = rootSchemaContext;
189
+ }
190
+ } else {
191
+ rootSchemaContext = rootNode.schema;
204
192
  }
205
193
 
206
- private parseSchemaBody(typeName: string = ""): Schema {
207
- const typeNamePrefix = typeName ? `@${typeName}` : "";
208
- this._dbg(`START parse_schema_body '<' ${typeNamePrefix}`);
194
+ // Final prefix scan
195
+ this.parseMeta();
196
+ this.applyMeta(rootNode);
209
197
 
210
- if (!this.expect('<')) {
211
- const s = this.createSchema(SchemaKind.ANY, typeName);
212
- this.popSchema();
213
- return s;
214
- }
198
+ this.popNode(); // Just in case
199
+ this._dbg('decode() end');
215
200
 
216
- const schema = this.createSchema(SchemaKind.RECORD, typeName);
217
- this.parseSchemaBodyContent(schema, '>');
218
- this.popSchema();
219
-
220
- this._dbg(`END parse_schema_body '>' ${typeNamePrefix}`);
221
- return schema;
201
+ return {
202
+ node: rootNode,
203
+ schema: rootSchemaContext,
204
+ errors: this.errors,
205
+ warnings: this.warnings,
206
+ };
207
+ }
208
+
209
+ // =========================================================
210
+ // SCHEMA DEFINITION PARSING
211
+ // =========================================================
212
+
213
+ private parseSchemaAtRef(): Schema {
214
+ this.advance(1); // @
215
+ const typeName = this.parseIdent();
216
+ this.skipWhitespace();
217
+
218
+ if (this.peek() === '<') {
219
+ this._dbg(`defining type ${typeName}`);
220
+ const schema = this.parseSchemaBody(typeName);
221
+ if (schema.isAny) schema.kind = SchemaKind.RECORD;
222
+ this.namedSchemas.set(typeName, schema);
223
+ return schema;
222
224
  }
223
225
 
224
- private parseSchemaBodyContent(schema: Schema, endChar: string) {
225
- // Python style: iterate and parse meta into schema inside the loop
226
- let fieldSchema: Schema | null = null;
227
-
228
- while (!this.eof()) {
229
- this.parseMeta(schema); // Passes schema, so blocks /.../ apply to schema
230
-
231
- const ch = this.peek();
232
- if (ch === endChar) {
233
- this.advance(1);
234
- break;
235
- }
236
-
237
- // LIST Schema: < [ ... ] >
238
- if (ch === '[') {
239
- this.advance(1);
240
- this._dbg("LIST schema begin");
241
- schema.kind = SchemaKind.LIST;
242
- schema.clearFields(); // Python: schema._fields_list = []
243
-
244
- this.applyMeta(schema); // Apply any pending meta
245
-
246
- const elementSchema = new Schema(SchemaKind.ANY);
247
- this.parseSchemaBodyContent(elementSchema, ']');
248
- schema.element = elementSchema;
249
-
250
- this.parseMeta(schema);
251
- if (this.peek() === endChar) this.advance(1);
252
- this.applyMeta(schema);
253
- return;
254
- }
255
-
256
- if (ch === ',') {
257
- this.applyMeta(fieldSchema || schema);
258
- this.advance(1);
259
- continue;
260
- }
261
-
262
- const name = this.parseIdent();
263
- if (!name) {
264
- this.addError("Expected identifier");
265
- this.advance(1);
266
- continue;
267
- }
268
-
269
- this.skipWhitespace();
270
-
271
- // Detect Primitive List Definition [ int ]
272
- if (Decoder.PRIMITIVES.has(name) && this.peek() !== ':') {
273
- schema.kind = SchemaKind.PRIMITIVE;
274
- schema.typeName = Decoder.PRIMITIVES_MAPPING[name];
275
- continue;
276
- }
277
-
278
- if (this.peek() === ':') {
279
- this.advance(1);
280
- fieldSchema = this.parseSchemaType();
281
- } else {
282
- fieldSchema = new Schema(SchemaKind.PRIMITIVE, { typeName: "any" });
283
- }
284
-
285
- fieldSchema.name = name;
286
- this.applyMeta(fieldSchema);
287
-
288
- // Trailing comments handling
289
- this.parseMeta(schema);
290
- this.applyMeta(fieldSchema || schema);
291
-
292
- schema.addField(fieldSchema);
293
- }
294
- this.applyMeta(fieldSchema || schema);
226
+ this._dbg(`referencing type ${typeName}`);
227
+ if (this.namedSchemas.has(typeName)) {
228
+ return this.namedSchemas.get(typeName)!;
295
229
  }
296
230
 
297
- private parseSchemaType(): Schema {
298
- this.parseMeta(this.currentSchema);
299
- const ch = this.peek();
300
-
301
- if (ch === '[') {
302
- this.advance(1);
303
- const lst = new Schema(SchemaKind.LIST);
304
- this.applyMeta(lst);
305
- lst.element = this.parseSchemaType();
306
- this.expect(']');
307
- return lst;
308
- }
309
-
310
- if (ch === '@') {
311
- this.advance(1);
312
- const name = this.parseIdent();
313
- this.parseMeta(this.currentSchema);
314
-
315
- if (this.peek() === '<') {
316
- this._dbg(`Inline definition for @${name}`);
317
- const s = this.parseSchemaBody(name);
318
- if (s.isAny) s.kind = SchemaKind.RECORD;
319
- this.namedSchemas.set(name, s);
320
- return s;
321
- }
322
- if (this.namedSchemas.has(name)) return this.namedSchemas.get(name)!;
323
- return new Schema(SchemaKind.RECORD, { typeName: name });
324
- }
325
-
326
- if (ch === '<') {
327
- return this.parseSchemaBody();
328
- }
231
+ return new Schema(SchemaKind.RECORD, { typeName });
232
+ }
329
233
 
330
- const name = this.parseIdent();
331
- if (Decoder.PRIMITIVES.has(name)) {
332
- const s = new Schema(SchemaKind.PRIMITIVE, { typeName: Decoder.PRIMITIVES_MAPPING[name] });
333
- this.applyMeta(s);
334
- return s;
335
- }
336
- if (this.namedSchemas.has(name)) return this.namedSchemas.get(name)!;
337
- if (!name) return new Schema(SchemaKind.ANY);
234
+ private parseSchemaBody(typeName: string = ''): Schema {
235
+ const typeNamePrefix = typeName ? `@${typeName}` : '';
236
+ this._dbg(`START parse_schema_body '<' ${typeNamePrefix}`);
338
237
 
339
- return new Schema(SchemaKind.RECORD, { typeName: name });
238
+ if (!this.expect('<')) {
239
+ const s = this.createSchema(SchemaKind.ANY, typeName);
240
+ this.popSchema();
241
+ return s;
340
242
  }
341
243
 
342
- // =========================================================
343
- // NODE DISPATCHER
344
- // =========================================================
244
+ const schema = this.createSchema(SchemaKind.RECORD, typeName);
245
+ this.parseSchemaBodyContent(schema, '>');
246
+ this.popSchema();
345
247
 
346
- private parseNode(_parent: Node | null = null): Node {
347
- this.parseMeta(this.currentNode);
248
+ this._dbg(`END parse_schema_body '>' ${typeNamePrefix}`);
249
+ return schema;
250
+ }
348
251
 
349
- if (this.eof()) {
350
- this.addError("Unexpected EOF while expecting a node");
351
- return this.createNode(null);
352
- }
252
+ private parseSchemaBodyContent(schema: Schema, endChar: string) {
253
+ // Python style: iterate and parse meta into schema inside the loop
254
+ let fieldSchema: Schema | null = null;
353
255
 
354
- const ch = this.peek();
355
- let node: Node;
356
-
357
- if (ch === '@') node = this.parseNodeWithSchemaRef();
358
- else if (ch === '<') node = this.parseNodeWithInlineSchema();
359
- else if (ch === '[') node = this.parseList();
360
- else if (ch === '(') node = this.parsePositionalRecord();
361
- else if (ch === '{') node = this.parseNamedRecord();
362
- else if (ch === '"') {
363
- this._dbg("Dispatch: String");
364
- node = this.parseString();
365
- }
366
- else if ((ch && /\d/.test(ch)) || ch === '-') {
367
- this._dbg("Dispatch: Number");
368
- node = this.parseNumber();
369
- }
370
- else if ((ch && /[a-zA-Z_]/.test(ch))) {
371
- this._dbg("Dispatch: RawString/Ident");
372
- node = this.parseRawString();
373
- }
374
- else {
375
- this.addError(`Unexpected character '${ch}'`);
376
- this.advance(1);
377
- node = this.createNode(null);
378
- }
256
+ while (!this.eof()) {
257
+ this.parseMeta(schema); // Passes schema, so blocks /.../ apply to schema
379
258
 
380
- this.applyMeta(node);
381
- return node;
382
- }
259
+ const ch = this.peek();
260
+ if (ch === endChar) {
261
+ this.advance(1);
262
+ break;
263
+ }
383
264
 
384
- private parseNodeWithSchemaRef(): Node {
385
- this._dbg("Start Node with Ref (@)");
386
- const schema = this.parseSchemaAtRef();
387
- this.pushSchema(schema);
388
- const node = this.parseNode();
389
- this.popSchema();
390
- node.schema = schema;
391
- return node;
392
- }
265
+ // LIST Schema: < [ ... ] >
266
+ if (ch === '[') {
267
+ this.advance(1);
268
+ this._dbg('LIST schema begin');
269
+ schema.kind = SchemaKind.LIST;
270
+ schema.clearFields(); // Python: schema._fields_list = []
393
271
 
394
- private parseNodeWithInlineSchema(): Node {
395
- this._dbg("Start Node with Inline (<)");
396
- const schema = this.parseSchemaBody();
397
- this.pushSchema(schema);
398
- const node = this.parseNode();
399
- this.popSchema();
400
- node.schema = schema;
401
- return node;
402
- }
272
+ this.applyMeta(schema); // Apply any pending meta
403
273
 
404
- // =========================================================
405
- // STRUCTURE PARSERS
406
- // =========================================================
274
+ const elementSchema = new Schema(SchemaKind.ANY);
275
+ this.parseSchemaBodyContent(elementSchema, ']');
276
+ schema.element = elementSchema;
407
277
 
408
- private parseList(): Node {
409
- this._dbg("Start LIST [");
410
- this.advance(1); // [
278
+ this.parseMeta(schema);
279
+ if (this.peek() === endChar) this.advance(1);
280
+ this.applyMeta(schema);
281
+ return;
282
+ }
411
283
 
412
- const node = this.createNode();
413
- node.elements = [];
284
+ if (ch === ',') {
285
+ this.applyMeta(fieldSchema || schema);
286
+ this.advance(1);
287
+ continue;
288
+ }
414
289
 
415
- if (node.schema.kind !== SchemaKind.LIST) {
416
- node.schema.kind = SchemaKind.LIST;
417
- node.schema.typeName = "list";
418
- node.schema.element = new Schema(SchemaKind.ANY);
419
- }
290
+ const name = this.parseIdent();
291
+ if (!name) {
292
+ this.addError('Expected identifier');
293
+ this.advance(1);
294
+ continue;
295
+ }
420
296
 
421
- const parentSchema = node.schema;
422
- let childSchema = new Schema(SchemaKind.ANY);
423
- if (parentSchema && parentSchema.isList && parentSchema.element) {
424
- childSchema = parentSchema.element;
425
- }
297
+ this.skipWhitespace();
426
298
 
427
- let childNode: Node | null = null;
428
-
429
- while (true) {
430
- this.parseMeta(node); // Passes node, so blocks /.../ apply to list
431
- this.pushSchema(childSchema);
432
-
433
- if (this.eof()) {
434
- this.addError("Unexpected EOF: List not closed");
435
- break;
436
- }
437
-
438
- if (this.peek() === ']') {
439
- this.applyMeta(childNode || node);
440
- this.advance(1);
441
- break;
442
- }
443
- if (this.peek() === ',') {
444
- this.applyMeta(childNode || node);
445
- this.advance(1);
446
- continue;
447
- }
448
-
449
- childNode = this.parseNode(node);
450
- node.elements.push(childNode);
451
-
452
- if (parentSchema.element && parentSchema.element.isAny && childNode.schema) {
453
- parentSchema.element = childNode.schema;
454
- }
455
-
456
- this.applyMeta(childNode || node);
457
- this.popNode();
458
- this.popSchema();
459
- }
460
- this.popSchema();
461
- this._dbg("End LIST ]");
462
- return node;
463
- }
299
+ // Detect Primitive List Definition [ int ]
300
+ if (Decoder.PRIMITIVES.has(name) && this.peek() !== ':') {
301
+ schema.kind = SchemaKind.PRIMITIVE;
302
+ schema.typeName = Decoder.PRIMITIVES_MAPPING[name];
303
+ continue;
304
+ }
464
305
 
465
- private parsePositionalRecord(): Node {
466
- this._dbg("Start RECORD (");
467
- this.advance(1); // (
306
+ if (this.peek() === ':') {
307
+ this.advance(1);
308
+ fieldSchema = this.parseSchemaType();
309
+ } else {
310
+ fieldSchema = new Schema(SchemaKind.PRIMITIVE, { typeName: 'any' });
311
+ }
468
312
 
469
- const node = this.createNode();
470
- if (node.schema.kind !== SchemaKind.RECORD) {
471
- node.schema.kind = SchemaKind.RECORD;
472
- node.schema.typeName = "any";
473
- }
313
+ fieldSchema.name = name;
314
+ this.applyMeta(fieldSchema);
474
315
 
475
- let index = 0;
476
- const predefinedFields = node.schema.fields ? [...node.schema.fields] : [];
477
- let valNode: Node | null = null;
478
-
479
- while (!this.eof()) {
480
- this.parseMeta(node);
481
-
482
- if (this.peek() === ')') {
483
- this.applyMeta(valNode || node);
484
- this.advance(1);
485
- break;
486
- }
487
- if (this.peek() === ',') {
488
- this.applyMeta(valNode || node);
489
- this.advance(1);
490
- continue;
491
- }
492
-
493
- let fieldSchema = new Schema(SchemaKind.ANY);
494
- if (index < predefinedFields.length) {
495
- fieldSchema = predefinedFields[index];
496
- }
497
-
498
- this.pushSchema(fieldSchema);
499
- valNode = this.parseNode();
500
-
501
- if (index < predefinedFields.length) {
502
- const name = predefinedFields[index].name;
503
- node.fields[name] = valNode;
504
- } else {
505
- const name = `_${index}`;
506
- const inferred = new Schema(valNode.schema.kind, { typeName: valNode.schema.typeName || "any" });
507
- inferred.name = name;
508
- node.schema.addField(inferred);
509
- node.fields[name] = valNode;
510
- }
511
-
512
- this.applyMeta(valNode || node);
513
- this.popNode();
514
- this.popSchema();
515
- index++;
516
- }
517
- this._dbg("End RECORD )");
518
- return node;
519
- }
316
+ // Trailing comments handling
317
+ this.parseMeta(schema);
318
+ this.applyMeta(fieldSchema || schema);
520
319
 
521
- private parseNamedRecord(): Node {
522
- this._dbg("Start NAMED RECORD {");
523
- this.advance(1); // {
320
+ schema.addField(fieldSchema);
321
+ }
322
+ this.applyMeta(fieldSchema || schema);
323
+ }
324
+
325
+ private parseSchemaType(): Schema {
326
+ this.parseMeta(this.currentSchema);
327
+ const ch = this.peek();
328
+
329
+ if (ch === '[') {
330
+ this.advance(1);
331
+ const lst = new Schema(SchemaKind.LIST);
332
+ this.applyMeta(lst);
333
+ lst.element = this.parseSchemaType();
334
+ this.expect(']');
335
+ return lst;
336
+ }
524
337
 
525
- const node = this.createNode();
526
- node.fields = {};
527
-
528
- if (node.schema.kind !== SchemaKind.RECORD) {
529
- node.schema.kind = SchemaKind.RECORD;
530
- node.schema.typeName = "any";
531
- }
338
+ if (ch === '@') {
339
+ this.advance(1);
340
+ const name = this.parseIdent();
341
+ this.parseMeta(this.currentSchema);
532
342
 
533
- const currentSchema = node.schema;
534
- let valNode: Node | null = null;
535
-
536
- while (!this.eof()) {
537
- this.parseMeta(node);
538
-
539
- if (this.peek() === '}') {
540
- this.applyMeta(valNode || node);
541
- this.advance(1);
542
- break;
543
- }
544
- if (this.peek() === ',') {
545
- this.applyMeta(valNode || node);
546
- this.advance(1);
547
- continue;
548
- }
549
-
550
- let keyName = this.parseIdent();
551
- if (!keyName) {
552
- if (this.peek() === '"') {
553
- keyName = this.readQuotedString();
554
- } else {
555
- this.addError("Expected key in record");
556
- this.advance(1);
557
- continue;
558
- }
559
- }
560
-
561
- this.skipWhitespace();
562
- this.expect(':');
563
-
564
- let fieldSchema = new Schema(SchemaKind.ANY);
565
- if (currentSchema && currentSchema.isRecord) {
566
- const existing = currentSchema.getField(keyName);
567
- if (existing) fieldSchema = existing;
568
- }
569
-
570
- this.pushSchema(fieldSchema);
571
- valNode = this.parseNode();
572
-
573
- if (currentSchema.isRecord) {
574
- const existing = currentSchema.getField(keyName);
575
- if (existing && existing.isAny && !valNode.schema.isAny) {
576
- valNode.schema.name = keyName;
577
- currentSchema.replaceField(valNode.schema);
578
- } else if (!existing) {
579
- const inferred = new Schema(valNode.schema.kind, { typeName: valNode.schema.typeName || "any" });
580
- inferred.name = keyName;
581
- node.schema.addField(inferred);
582
- }
583
- }
584
-
585
- node.fields[keyName] = valNode;
586
- this.applyMeta(valNode || node);
587
- this.popNode();
588
- this.popSchema();
589
- }
590
- this._dbg("End NAMED RECORD }");
591
- return node;
343
+ if (this.peek() === '<') {
344
+ this._dbg(`Inline definition for @${name}`);
345
+ const s = this.parseSchemaBody(name);
346
+ if (s.isAny) s.kind = SchemaKind.RECORD;
347
+ this.namedSchemas.set(name, s);
348
+ return s;
349
+ }
350
+ if (this.namedSchemas.has(name)) return this.namedSchemas.get(name)!;
351
+ return new Schema(SchemaKind.RECORD, { typeName: name });
592
352
  }
593
353
 
594
- // =========================================================
595
- // PREFIX & META PARSING
596
- // =========================================================
597
-
598
- private parseMeta(obj: Node | Schema | null = null): void {
599
- while (!this.eof()) {
600
- this.skipWhitespace();
601
- const ch = this.peek();
602
- const nextCh = this.peekNext();
603
-
604
- if (ch === '/' && nextCh === '*') {
605
- this.pendingMeta.comments.push(this.parseCommentBlock());
606
- continue;
607
- }
608
-
609
- if (ch === '/' && nextCh !== '*') {
610
- this.parseMetaBlock(obj);
611
- continue;
612
- }
613
-
614
- if (ch === '$' || ch === '#' || ch === '!') {
615
- this.parseModifierInline();
616
- continue;
617
- }
618
- break;
619
- }
354
+ if (ch === '<') {
355
+ return this.parseSchemaBody();
620
356
  }
621
357
 
622
- private parseCommentBlock(): string {
623
- this._dbg("START block comment");
624
- this.advance(2);
625
- let nesting = 1;
626
- let content: string[] = [];
627
-
628
- while (!this.eof() && nesting > 0) {
629
- const ch = this.text[this.i];
630
-
631
- if (ch === '\\') {
632
- this.advance(1);
633
- if (!this.eof()) content.push(this.text[this.i]);
634
- this.advance(1);
635
- continue;
636
- }
637
-
638
- if (ch === '/' && this.peekNext() === '*') {
639
- nesting++;
640
- this.advance(2);
641
- content.push("/*");
642
- continue;
643
- }
644
- if (ch === '*' && this.peekNext() === '/') {
645
- nesting--;
646
- this.advance(2);
647
- if (nesting > 0) content.push("*/");
648
- continue;
649
- }
650
-
651
- content.push(ch);
652
- this.advance(1);
653
- }
654
-
655
- if (nesting > 0) this.addError("Unterminated comment");
656
- return content.join("").trim();
358
+ const name = this.parseIdent();
359
+ if (Decoder.PRIMITIVES.has(name)) {
360
+ const s = new Schema(SchemaKind.PRIMITIVE, { typeName: Decoder.PRIMITIVES_MAPPING[name] });
361
+ this.applyMeta(s);
362
+ return s;
657
363
  }
364
+ if (this.namedSchemas.has(name)) return this.namedSchemas.get(name)!;
365
+ if (!name) return new Schema(SchemaKind.ANY);
658
366
 
659
- private parseModifierInline(): void {
660
- const ch = this.peek();
661
- if (ch === '$') this.parseMetaAttribute(this.pendingMeta);
662
- else if (ch === '#') this.parseMetaTag(this.pendingMeta);
663
- else if (ch === '!') this.parseMetaFlag(this.pendingMeta);
664
- else this.advance(1);
665
- }
367
+ return new Schema(SchemaKind.RECORD, { typeName: name });
368
+ }
666
369
 
667
- private parseMetaBlock(obj: Node | Schema | null = null): MetaInfo {
668
- this.expect('/');
669
- this._dbg("START meta header /.../");
670
- const meta = new MetaInfo();
671
-
672
- while (!this.eof()) {
673
- this.skipWhitespace();
674
- const ch = this.peek();
675
- const nextCh = this.peekNext();
676
-
677
- if (ch === '/' && nextCh === '*') {
678
- meta.comments.push(this.parseCommentBlock());
679
- continue;
680
- }
681
-
682
- if (ch === '/') {
683
- this.advance(1);
684
- break;
685
- }
686
-
687
- if (ch === '$') { this.parseMetaAttribute(meta); continue; }
688
- if (ch === '#') { this.parseMetaTag(meta); continue; }
689
- if (ch === '!') { this.parseMetaFlag(meta); continue; }
690
-
691
- // Implicit Attribute (Legacy support: key=value without $)
692
- if (/[a-zA-Z0-9_]/.test(ch || '')) {
693
- const key = this.parseIdent();
694
- let val: any = true;
695
-
696
- this.skipWhitespace();
697
- if (this.peek() === '=') {
698
- this.advance(1);
699
- val = this.parsePrimitiveValue();
700
- }
701
- this.addWarning(`Implicit attribute '${key}'. Use '$${key}' instead.`);
702
-
703
- meta.attr[key] = val;
704
- continue;
705
- }
706
-
707
- this.addError(`Unexpected token in meta block: ${ch}`);
708
- this.advance(1);
709
- }
370
+ // =========================================================
371
+ // NODE DISPATCHER
372
+ // =========================================================
710
373
 
711
- if (obj) {
712
- obj.applyMeta(meta);
713
- } else {
714
- this.addWarning(`There is no parent to add the meta block '${meta}'`);
715
- this.pendingMeta.applyMeta(meta);
716
- }
374
+ private parseNode(_parent: Node | null = null): Node {
375
+ this.parseMeta(this.currentNode);
717
376
 
718
- this._dbg("END meta header");
719
- return meta;
377
+ if (this.eof()) {
378
+ this.addError('Unexpected EOF while expecting a node');
379
+ return this.createNode(null);
720
380
  }
721
381
 
722
- private parseMetaAttribute(meta: MetaInfo): void {
723
- this.advance(1); // $
724
- const key = this.parseIdent();
725
- let val: any = true;
726
- this.skipWhitespace();
727
- if (this.peek() === '=') {
728
- this.advance(1);
729
- val = this.parsePrimitiveValue();
730
- }
731
- meta.attr[key] = val;
732
- this._dbg(`Meta Attr: $${key}=${val}`);
382
+ const ch = this.peek();
383
+ let node: Node;
384
+
385
+ if (ch === '@') node = this.parseNodeWithSchemaRef();
386
+ else if (ch === '<') node = this.parseNodeWithInlineSchema();
387
+ else if (ch === '[') node = this.parseList();
388
+ else if (ch === '(') node = this.parsePositionalRecord();
389
+ else if (ch === '{') node = this.parseNamedRecord();
390
+ else if (ch === '"') {
391
+ this._dbg('Dispatch: String');
392
+ node = this.parseString();
393
+ } else if ((ch && /\d/.test(ch)) || ch === '-') {
394
+ this._dbg('Dispatch: Number');
395
+ node = this.parseNumber();
396
+ } else if (ch && /[a-zA-Z_]/.test(ch)) {
397
+ this._dbg('Dispatch: RawString/Ident');
398
+ node = this.parseRawString();
399
+ } else {
400
+ this.addError(`Unexpected character '${ch}'`);
401
+ this.advance(1);
402
+ node = this.createNode(null);
733
403
  }
734
404
 
735
- private parseMetaTag(meta: MetaInfo): void {
736
- this.advance(1); // #
737
- const tag = this.parseIdent();
738
- meta.tags.push(tag);
739
- this._dbg(`Meta Tag: #${tag}`);
405
+ this.applyMeta(node);
406
+ return node;
407
+ }
408
+
409
+ private parseNodeWithSchemaRef(): Node {
410
+ this._dbg('Start Node with Ref (@)');
411
+ const schema = this.parseSchemaAtRef();
412
+ this.pushSchema(schema);
413
+ const node = this.parseNode();
414
+ this.popSchema();
415
+ node.schema = schema;
416
+ return node;
417
+ }
418
+
419
+ private parseNodeWithInlineSchema(): Node {
420
+ this._dbg('Start Node with Inline (<)');
421
+ const schema = this.parseSchemaBody();
422
+ this.pushSchema(schema);
423
+ const node = this.parseNode();
424
+ this.popSchema();
425
+ node.schema = schema;
426
+ return node;
427
+ }
428
+
429
+ // =========================================================
430
+ // STRUCTURE PARSERS
431
+ // =========================================================
432
+
433
+ private parseList(): Node {
434
+ this._dbg('Start LIST [');
435
+ this.advance(1); // [
436
+
437
+ const node = this.createNode();
438
+ node.elements = [];
439
+
440
+ if (node.schema.kind !== SchemaKind.LIST) {
441
+ node.schema.kind = SchemaKind.LIST;
442
+ node.schema.typeName = 'list';
443
+ node.schema.element = new Schema(SchemaKind.ANY);
740
444
  }
741
445
 
742
- private parseMetaFlag(meta: MetaInfo): void {
743
- this.advance(1); // !
744
- const flag = this.parseIdent();
745
- if (flag === 'required') {
746
- meta.required = true;
747
- this._dbg("Meta Flag: !required");
748
- } else {
749
- this.addWarning(`Unknown flag: !${flag}`);
750
- }
446
+ const parentSchema = node.schema;
447
+ let childSchema = new Schema(SchemaKind.ANY);
448
+ if (parentSchema && parentSchema.isList && parentSchema.element) {
449
+ childSchema = parentSchema.element;
751
450
  }
752
451
 
753
- // =========================================================
754
- // HELPERS & LOW-LEVEL PARSERS
755
- // =========================================================
452
+ let childNode: Node | null = null;
756
453
 
757
- private parseIdent(): string {
758
- this.skipWhitespace();
759
- const start = this.i;
760
- if (this.eof()) return "";
761
-
762
- const ch = this.text[this.i];
763
- if (!(/[a-zA-Z_]/.test(ch))) return "";
764
-
454
+ while (true) {
455
+ this.parseMeta(node); // Passes node, so blocks /.../ apply to list
456
+ this.pushSchema(childSchema);
457
+
458
+ if (this.eof()) {
459
+ this.addError('Unexpected EOF: List not closed');
460
+ break;
461
+ }
462
+
463
+ if (this.peek() === ']') {
464
+ this.applyMeta(childNode || node);
765
465
  this.advance(1);
766
- while (!this.eof()) {
767
- const c = this.text[this.i];
768
- if (/[a-zA-Z0-9_]/.test(c)) this.advance(1);
769
- else break;
770
- }
771
- return this.text.substring(start, this.i);
772
- }
466
+ break;
467
+ }
468
+ if (this.peek() === ',') {
469
+ this.applyMeta(childNode || node);
470
+ this.advance(1);
471
+ continue;
472
+ }
773
473
 
774
- private parseString(): Node {
775
- const val = this.readQuotedString();
776
- return this.createNode(val);
777
- }
474
+ childNode = this.parseNode(node);
475
+ node.elements.push(childNode);
778
476
 
779
- private parseNumber(): Node {
780
- const val = this.readNumber();
781
- return this.createNode(val);
782
- }
477
+ if (parentSchema.element && parentSchema.element.isAny && childNode.schema) {
478
+ parentSchema.element = childNode.schema;
479
+ }
783
480
 
784
- private parseRawString(): Node {
785
- const raw = this.parseIdent();
786
- let val: any = raw;
787
- if (raw === "true") val = true;
788
- else if (raw === "false") val = false;
789
- else if (raw === "null") val = null;
790
- return this.createNode(val);
481
+ this.applyMeta(childNode || node);
482
+ this.popNode();
483
+ this.popSchema();
484
+ }
485
+ this.popSchema();
486
+ this._dbg('End LIST ]');
487
+ return node;
488
+ }
489
+
490
+ private parsePositionalRecord(): Node {
491
+ this._dbg('Start RECORD (');
492
+ this.advance(1); // (
493
+
494
+ const node = this.createNode();
495
+ if (node.schema.kind !== SchemaKind.RECORD) {
496
+ node.schema.kind = SchemaKind.RECORD;
497
+ node.schema.typeName = 'any';
791
498
  }
792
499
 
793
- private parsePrimitiveValue(): any {
794
- const ch = this.peek();
795
- if (!ch) return null;
796
- if (ch === '"') return this.readQuotedString();
797
- if (/\d/.test(ch) || ch === '-') return this.readNumber();
798
-
799
- const raw = this.parseIdent();
800
- if (raw === "true") return true;
801
- if (raw === "false") return false;
802
- if (raw === "null") return null;
803
- return raw;
500
+ let index = 0;
501
+ const predefinedFields = node.schema.fields ? [...node.schema.fields] : [];
502
+ let valNode: Node | null = null;
503
+
504
+ while (!this.eof()) {
505
+ this.parseMeta(node);
506
+
507
+ if (this.peek() === ')') {
508
+ this.applyMeta(valNode || node);
509
+ this.advance(1);
510
+ break;
511
+ }
512
+ if (this.peek() === ',') {
513
+ this.applyMeta(valNode || node);
514
+ this.advance(1);
515
+ continue;
516
+ }
517
+
518
+ let fieldSchema = new Schema(SchemaKind.ANY);
519
+ if (index < predefinedFields.length) {
520
+ fieldSchema = predefinedFields[index];
521
+ }
522
+
523
+ this.pushSchema(fieldSchema);
524
+ valNode = this.parseNode();
525
+
526
+ if (index < predefinedFields.length) {
527
+ const name = predefinedFields[index].name;
528
+ node.fields[name] = valNode;
529
+ } else {
530
+ const name = `_${index}`;
531
+ const inferred = new Schema(valNode.schema.kind, {
532
+ typeName: valNode.schema.typeName || 'any',
533
+ });
534
+ inferred.name = name;
535
+ node.schema.addField(inferred);
536
+ node.fields[name] = valNode;
537
+ }
538
+
539
+ this.applyMeta(valNode || node);
540
+ this.popNode();
541
+ this.popSchema();
542
+ index++;
804
543
  }
544
+ this._dbg('End RECORD )');
545
+ return node;
546
+ }
805
547
 
806
- private readQuotedString(): string {
807
- this.expect('"');
808
- let res = "";
809
- while (!this.eof()) {
810
- const ch = this.text[this.i];
811
- if (ch === '"') break;
812
-
813
- if (ch === '\\') {
814
- this.advance(1);
815
- if (this.eof()) break;
816
- const esc = this.text[this.i];
817
- if (esc === 'n') res += '\n';
818
- else if (esc === 't') res += '\t';
819
- else if (esc === 'r') res += '\r';
820
- else if (esc === '"') res += '"';
821
- else if (esc === '\\') res += '\\';
822
- else res += esc;
823
- this.advance(1);
824
- } else {
825
- res += ch;
826
- this.advance(1);
827
- }
828
- }
829
- this.expect('"');
830
- return res;
548
+ private parseNamedRecord(): Node {
549
+ this._dbg('Start NAMED RECORD {');
550
+ this.advance(1); // {
551
+
552
+ const node = this.createNode();
553
+ node.fields = {};
554
+
555
+ if (node.schema.kind !== SchemaKind.RECORD) {
556
+ node.schema.kind = SchemaKind.RECORD;
557
+ node.schema.typeName = 'any';
831
558
  }
832
559
 
833
- private readNumber(): number {
834
- const start = this.i;
835
- if (this.peek() === '-') this.advance(1);
836
- while (/\d/.test(this.peek() || '')) this.advance(1);
837
-
838
- if (this.peek() === '.') {
839
- this.advance(1);
840
- while (/\d/.test(this.peek() || '')) this.advance(1);
560
+ const currentSchema = node.schema;
561
+ let valNode: Node | null = null;
562
+
563
+ while (!this.eof()) {
564
+ this.parseMeta(node);
565
+
566
+ if (this.peek() === '}') {
567
+ this.applyMeta(valNode || node);
568
+ this.advance(1);
569
+ break;
570
+ }
571
+ if (this.peek() === ',') {
572
+ this.applyMeta(valNode || node);
573
+ this.advance(1);
574
+ continue;
575
+ }
576
+
577
+ let keyName = this.parseIdent();
578
+ if (!keyName) {
579
+ if (this.peek() === '"') {
580
+ keyName = this.readQuotedString();
581
+ } else {
582
+ this.addError('Expected key in record');
583
+ this.advance(1);
584
+ continue;
841
585
  }
842
-
843
- if (['e', 'E'].includes(this.peek() || '')) {
844
- this.advance(1);
845
- if (['+', '-'].includes(this.peek() || '')) this.advance(1);
846
- while (/\d/.test(this.peek() || '')) this.advance(1);
586
+ }
587
+
588
+ this.skipWhitespace();
589
+ this.expect(':');
590
+
591
+ let fieldSchema = new Schema(SchemaKind.ANY);
592
+ if (currentSchema && currentSchema.isRecord) {
593
+ const existing = currentSchema.getField(keyName);
594
+ if (existing) fieldSchema = existing;
595
+ }
596
+
597
+ this.pushSchema(fieldSchema);
598
+ valNode = this.parseNode();
599
+
600
+ if (currentSchema.isRecord) {
601
+ const existing = currentSchema.getField(keyName);
602
+ if (existing && existing.isAny && !valNode.schema.isAny) {
603
+ valNode.schema.name = keyName;
604
+ currentSchema.replaceField(valNode.schema);
605
+ } else if (!existing) {
606
+ const inferred = new Schema(valNode.schema.kind, {
607
+ typeName: valNode.schema.typeName || 'any',
608
+ });
609
+ inferred.name = keyName;
610
+ node.schema.addField(inferred);
847
611
  }
612
+ }
848
613
 
849
- const raw = this.text.substring(start, this.i);
850
- const num = parseFloat(raw);
851
- if (isNaN(num)) {
852
- this.addError(`Invalid number format: ${raw}`);
853
- return 0;
854
- }
855
- return num;
614
+ node.fields[keyName] = valNode;
615
+ this.applyMeta(valNode || node);
616
+ this.popNode();
617
+ this.popSchema();
856
618
  }
619
+ this._dbg('End NAMED RECORD }');
620
+ return node;
621
+ }
622
+
623
+ // =========================================================
624
+ // PREFIX & META PARSING
625
+ // =========================================================
626
+
627
+ private parseMeta(obj: Node | Schema | null = null): void {
628
+ while (!this.eof()) {
629
+ this.skipWhitespace();
630
+ const ch = this.peek();
631
+ const nextCh = this.peekNext();
632
+
633
+ if (ch === '/' && nextCh === '*') {
634
+ this.pendingMeta.comments.push(this.parseCommentBlock());
635
+ continue;
636
+ }
637
+
638
+ if (ch === '/' && nextCh !== '*') {
639
+ this.parseMetaBlock(obj);
640
+ continue;
641
+ }
642
+
643
+ if (ch === '$' || ch === '#' || ch === '!') {
644
+ this.parseModifierInline();
645
+ continue;
646
+ }
647
+ break;
648
+ }
649
+ }
857
650
 
858
- // =========================================================
859
- // STACK & STATE HELPERS
860
- // =========================================================
651
+ private parseCommentBlock(): string {
652
+ this._dbg('START block comment');
653
+ this.advance(2);
654
+ let nesting = 1;
655
+ const content: string[] = [];
861
656
 
862
- private get currentSchema(): Schema | null {
863
- return this.schemaStack.length > 0 ? this.schemaStack[this.schemaStack.length - 1] : null;
864
- }
657
+ while (!this.eof() && nesting > 0) {
658
+ const ch = this.text[this.i];
865
659
 
866
- private createSchema(kind: SchemaKind, typeName: string = ""): Schema {
867
- const s = new Schema(kind, { typeName });
868
- this.applyMeta(s);
869
- this.pushSchema(s);
870
- return s;
660
+ if (ch === '\\') {
661
+ this.advance(1);
662
+ if (!this.eof()) content.push(this.text[this.i]);
663
+ this.advance(1);
664
+ continue;
665
+ }
666
+
667
+ if (ch === '/' && this.peekNext() === '*') {
668
+ nesting++;
669
+ this.advance(2);
670
+ content.push('/*');
671
+ continue;
672
+ }
673
+ if (ch === '*' && this.peekNext() === '/') {
674
+ nesting--;
675
+ this.advance(2);
676
+ if (nesting > 0) content.push('*/');
677
+ continue;
678
+ }
679
+
680
+ content.push(ch);
681
+ this.advance(1);
871
682
  }
872
683
 
873
- private pushSchema(s: Schema): void {
874
- this.schemaStack.push(s);
875
- this._dbg(`PUSH SCHEMA ${s.toString().substring(0, 30)}...`);
876
- }
684
+ if (nesting > 0) this.addError('Unterminated comment');
685
+ return content.join('').trim();
686
+ }
687
+
688
+ private parseModifierInline(): void {
689
+ const ch = this.peek();
690
+ if (ch === '$') this.parseMetaAttribute(this.pendingMeta);
691
+ else if (ch === '#') this.parseMetaTag(this.pendingMeta);
692
+ else if (ch === '!') this.parseMetaFlag(this.pendingMeta);
693
+ else this.advance(1);
694
+ }
695
+
696
+ private parseMetaBlock(obj: Node | Schema | null = null): MetaInfo {
697
+ this.expect('/');
698
+ this._dbg('START meta header /.../');
699
+ const meta = new MetaInfo();
700
+
701
+ while (!this.eof()) {
702
+ this.skipWhitespace();
703
+ const ch = this.peek();
704
+ const nextCh = this.peekNext();
705
+
706
+ if (ch === '/' && nextCh === '*') {
707
+ meta.comments.push(this.parseCommentBlock());
708
+ continue;
709
+ }
710
+
711
+ if (ch === '/') {
712
+ this.advance(1);
713
+ break;
714
+ }
715
+
716
+ if (ch === '$') {
717
+ this.parseMetaAttribute(meta);
718
+ continue;
719
+ }
720
+ if (ch === '#') {
721
+ this.parseMetaTag(meta);
722
+ continue;
723
+ }
724
+ if (ch === '!') {
725
+ this.parseMetaFlag(meta);
726
+ continue;
727
+ }
728
+
729
+ // Implicit Attribute (Legacy support: key=value without $)
730
+ if (/[a-zA-Z0-9_]/.test(ch || '')) {
731
+ const key = this.parseIdent();
732
+ let val: Primitive = true;
877
733
 
878
- private popSchema(): Schema | null {
879
- const s = this.schemaStack.pop() || null;
880
- if(s && s.isList && s.element) {
881
- s.applyMeta(s.element)
882
- s.element.clearMeta();
734
+ this.skipWhitespace();
735
+ if (this.peek() === '=') {
736
+ this.advance(1);
737
+ val = this.parsePrimitiveValue();
883
738
  }
884
- this._dbg(`POP SCHEMA ${s ? s.toString().substring(0, 30) : 'null'}...`);
739
+ this.addWarning(`Implicit attribute '${key}'. Use '$${key}' instead.`);
885
740
 
886
- return s;
887
- }
741
+ meta.attr[key] = val;
742
+ continue;
743
+ }
888
744
 
889
- private get currentNode(): Node | null {
890
- return this.nodeStack.length > 0 ? this.nodeStack[this.nodeStack.length - 1] : null;
745
+ this.addError(`Unexpected token in meta block: ${ch}`);
746
+ this.advance(1);
891
747
  }
892
748
 
893
- private pushNode(n: Node): void {
894
- this.nodeStack.push(n);
895
- this._dbg(`PUSH NODE ${n.toString().substring(0, 30)}...`);
749
+ if (obj) {
750
+ obj.applyMeta(meta);
751
+ } else {
752
+ this.addWarning(`There is no parent to add the meta block '${meta}'`);
753
+ this.pendingMeta.applyMeta(meta);
896
754
  }
897
755
 
898
- private popNode(): Node | null {
899
- const n = this.nodeStack.pop() || null;
900
- this._dbg(`POP NODE ${n ? n.toString().substring(0, 30) : 'null'}...`);
901
- return n;
756
+ this._dbg('END meta header');
757
+ return meta;
758
+ }
759
+
760
+ private parseMetaAttribute(meta: MetaInfo): void {
761
+ this.advance(1); // $
762
+ const key = this.parseIdent();
763
+ let val: Primitive = true;
764
+ this.skipWhitespace();
765
+ if (this.peek() === '=') {
766
+ this.advance(1);
767
+ val = this.parsePrimitiveValue();
902
768
  }
769
+ meta.attr[key] = val;
770
+ this._dbg(`Meta Attr: $${key}=${val}`);
771
+ }
772
+
773
+ private parseMetaTag(meta: MetaInfo): void {
774
+ this.advance(1); // #
775
+ const tag = this.parseIdent();
776
+ meta.tags.push(tag);
777
+ this._dbg(`Meta Tag: #${tag}`);
778
+ }
779
+
780
+ private parseMetaFlag(meta: MetaInfo): void {
781
+ this.advance(1); // !
782
+ const flag = this.parseIdent();
783
+ if (flag === 'required') {
784
+ meta.required = true;
785
+ this._dbg('Meta Flag: !required');
786
+ } else {
787
+ this.addWarning(`Unknown flag: !${flag}`);
788
+ }
789
+ }
903
790
 
904
- private createNode(value: any = null): Node {
905
- let currentS = this.currentSchema;
906
- if (!currentS) {
907
- currentS = new Schema(SchemaKind.ANY);
908
- this.pushSchema(currentS);
909
- }
791
+ // =========================================================
792
+ // HELPERS & LOW-LEVEL PARSERS
793
+ // =========================================================
910
794
 
911
- let finalS = currentS;
912
-
913
- if (value !== null) {
914
- let inferred: Schema | null = null;
915
- if (typeof value === 'boolean') inferred = new Schema(SchemaKind.PRIMITIVE, { typeName: "bool" });
916
- else if (typeof value === 'number') inferred = new Schema(SchemaKind.PRIMITIVE, { typeName: "number" });
917
- else if (typeof value === 'string') inferred = new Schema(SchemaKind.PRIMITIVE, { typeName: "string" });
918
-
919
- if (inferred) {
920
- let compatible = false;
921
- if (currentS.kind === SchemaKind.ANY) {
922
- compatible = true;
923
- finalS = inferred;
924
- } else if (currentS.typeName === inferred.typeName) {
925
- compatible = true;
926
- } else if (currentS.typeName === "number" && (inferred.typeName === "int" || inferred.typeName === "float")) {
927
- compatible = true;
928
- }
929
-
930
- if (!compatible) {
931
- finalS = inferred;
932
- }
933
- }
934
- } else {
935
- if (currentS.isRecord || currentS.isList) {
936
- finalS = currentS;
937
- } else {
938
- finalS = new Schema(SchemaKind.PRIMITIVE, { typeName: "null" });
939
- }
940
- }
795
+ private parseIdent(): string {
796
+ this.skipWhitespace();
797
+ const start = this.i;
798
+ if (this.eof()) return '';
941
799
 
942
- const node = new Node(finalS, { value });
943
- this.applyMeta(node);
944
- this.pushNode(node);
945
- return node;
946
- }
800
+ const ch = this.text[this.i];
801
+ if (!/[a-zA-Z_]/.test(ch)) return '';
947
802
 
948
- private applyMeta(obj: Node | Schema): void {
949
- obj.applyMeta(this.pendingMeta);
950
- this.pendingMeta = new MetaInfo();
803
+ this.advance(1);
804
+ while (!this.eof()) {
805
+ const c = this.text[this.i];
806
+ if (/[a-zA-Z0-9_]/.test(c)) this.advance(1);
807
+ else break;
951
808
  }
809
+ return this.text.substring(start, this.i);
810
+ }
811
+
812
+ private parseString(): Node {
813
+ const val = this.readQuotedString();
814
+ return this.createNode(val);
815
+ }
816
+
817
+ private parseNumber(): Node {
818
+ const val = this.readNumber();
819
+ return this.createNode(val);
820
+ }
821
+
822
+ private parseRawString(): Node {
823
+ const raw = this.parseIdent();
824
+ let val: Primitive = raw;
825
+ if (raw === 'true') val = true;
826
+ else if (raw === 'false') val = false;
827
+ else if (raw === 'null') val = null;
828
+ return this.createNode(val);
829
+ }
830
+
831
+ private parsePrimitiveValue(): Primitive {
832
+ const ch = this.peek();
833
+ if (!ch) return null;
834
+ if (ch === '"') return this.readQuotedString();
835
+ if (/\d/.test(ch) || ch === '-') return this.readNumber();
836
+
837
+ const raw = this.parseIdent();
838
+ if (raw === 'true') return true;
839
+ if (raw === 'false') return false;
840
+ if (raw === 'null') return null;
841
+ return raw;
842
+ }
843
+
844
+ private readQuotedString(): string {
845
+ this.expect('"');
846
+ let res = '';
847
+
848
+ while (!this.eof()) {
849
+ const ch = this.text[this.i];
850
+
851
+ // End of string found
852
+ if (ch === '"') {
853
+ break;
854
+ }
855
+
856
+ // Escape sequence start
857
+ if (ch === '\\') {
858
+ this.advance(1); // Skip the backslash
952
859
 
953
- private advance(n: number = 1): string {
954
- let lastChar = "";
955
- for (let k = 0; k < n; k++) {
956
- if (this.i >= this.text.length) break;
957
- const c = this.text[this.i];
958
- lastChar = c;
959
- if (c === '\n') {
960
- this.line++;
961
- this.col = 1;
962
- } else {
963
- this.col++;
964
- }
965
- this.i++;
860
+ if (this.eof()) {
861
+ this.addError('Unexpected EOF inside string escape');
862
+ break;
966
863
  }
967
- return lastChar;
968
- }
969
864
 
970
- private skipWhitespace(): void {
971
- while (!this.eof()) {
972
- const ch = this.peek();
973
- if (ch && /\s/.test(ch)) {
974
- this.advance(1);
975
- } else {
976
- break;
977
- }
978
- }
979
- }
865
+ const esc = this.text[this.i];
980
866
 
981
- private eof(): boolean { return this.i >= this.text.length; }
982
- private peek(): string | null { return this.eof() ? null : this.text[this.i]; }
983
- private peekNext(): string | null { return (this.i + 1 < this.text.length) ? this.text[this.i + 1] : null; }
867
+ if (esc === 'n') res += '\n';
868
+ else if (esc === 't') res += '\t';
869
+ else if (esc === 'r') res += '\r';
870
+ else if (esc === '"') res += '"';
871
+ else if (esc === '\\') res += '\\';
872
+ else res += esc; // Fallback: append character literally
984
873
 
985
- private expect(ch: string): boolean {
986
- if (this.peek() !== ch) {
987
- this.addError(`Expected '${ch}', got '${this.peek()}'`);
988
- return false;
989
- }
990
- this.advance(1);
991
- return true;
992
- }
874
+ this.advance(1); // Move past the escaped char
875
+ continue;
876
+ }
993
877
 
994
- private addError(msg: string): void {
995
- if (this.errors.length >= Decoder.MAX_ERRORS) return;
996
- this._dbg(`ERROR: ${msg}`);
997
- this.errors.push(new DecodeError(msg, this.i, this.currentSchema, this.currentNode));
878
+ // Normal character
879
+ res += ch;
880
+ this.advance(1);
998
881
  }
999
882
 
1000
- private addWarning(msg: string): void {
1001
- this._dbg(`WARNING: ${msg}`);
1002
- this.warnings.push(new DecodeWarning(msg, this.i, this.currentSchema, this.currentNode));
883
+ this.expect('"');
884
+ return res;
885
+ }
886
+
887
+ private readNumber(): number {
888
+ const start = this.i;
889
+ if (this.peek() === '-') this.advance(1);
890
+ while (/\d/.test(this.peek() || '')) this.advance(1);
891
+
892
+ if (this.peek() === '.') {
893
+ this.advance(1);
894
+ while (/\d/.test(this.peek() || '')) this.advance(1);
1003
895
  }
1004
896
 
1005
- private _dbg(msg: string): void {
1006
- if (!this.debug) return;
897
+ if (['e', 'E'].includes(this.peek() || '')) {
898
+ this.advance(1);
899
+ if (['+', '-'].includes(this.peek() || '')) this.advance(1);
900
+ while (/\d/.test(this.peek() || '')) this.advance(1);
901
+ }
1007
902
 
1008
- const locStr = `${this.line + 1}:${this.col + 1}`;
903
+ const raw = this.text.substring(start, this.i);
904
+ const num = parseFloat(raw);
905
+ if (isNaN(num)) {
906
+ this.addError(`Invalid number format: ${raw}`);
907
+ return 0;
908
+ }
909
+ return num;
910
+ }
911
+
912
+ // =========================================================
913
+ // STACK & STATE HELPERS
914
+ // =========================================================
915
+
916
+ private get currentSchema(): Schema | null {
917
+ return this.schemaStack.length > 0 ? this.schemaStack[this.schemaStack.length - 1] : null;
918
+ }
919
+
920
+ private createSchema(kind: SchemaKind, typeName: string = ''): Schema {
921
+ const s = new Schema(kind, { typeName });
922
+ this.applyMeta(s);
923
+ this.pushSchema(s);
924
+ return s;
925
+ }
926
+
927
+ private pushSchema(s: Schema): void {
928
+ this.schemaStack.push(s);
929
+ this._dbg(`PUSH SCHEMA ${s.toString().substring(0, 30)}...`);
930
+ }
931
+
932
+ private popSchema(): Schema | null {
933
+ const s = this.schemaStack.pop() || null;
934
+ if (s && s.isList && s.element) {
935
+ s.applyMeta(s.element);
936
+ s.element.clearMeta();
937
+ }
938
+ this._dbg(`POP SCHEMA ${s ? s.toString().substring(0, 30) : 'null'}...`);
939
+
940
+ return s;
941
+ }
942
+
943
+ private get currentNode(): Node | null {
944
+ return this.nodeStack.length > 0 ? this.nodeStack[this.nodeStack.length - 1] : null;
945
+ }
946
+
947
+ private pushNode(n: Node): void {
948
+ this.nodeStack.push(n);
949
+ this._dbg(`PUSH NODE ${n.toString().substring(0, 30)}...`);
950
+ }
951
+
952
+ private popNode(): Node | null {
953
+ const n = this.nodeStack.pop() || null;
954
+ this._dbg(`POP NODE ${n ? n.toString().substring(0, 30) : 'null'}...`);
955
+ return n;
956
+ }
957
+
958
+ private createNode(value: Primitive = null): Node {
959
+ let currentS = this.currentSchema;
960
+ if (!currentS) {
961
+ currentS = new Schema(SchemaKind.ANY);
962
+ this.pushSchema(currentS);
963
+ }
1009
964
 
1010
- const depth = this.nodeStack.length;
1011
- let treePrefix = "";
1012
- if (depth > 0) {
1013
- // Python: "│ " * (depth - 1)
1014
- treePrefix = Ansi.DIM + "│ ".repeat(depth - 1) + "├─ " + Ansi.RESET;
965
+ let finalS = currentS;
966
+
967
+ if (value !== null) {
968
+ let inferred: Schema | null = null;
969
+ if (typeof value === 'boolean')
970
+ inferred = new Schema(SchemaKind.PRIMITIVE, { typeName: 'bool' });
971
+ else if (typeof value === 'number')
972
+ inferred = new Schema(SchemaKind.PRIMITIVE, { typeName: 'number' });
973
+ else if (typeof value === 'string')
974
+ inferred = new Schema(SchemaKind.PRIMITIVE, { typeName: 'string' });
975
+
976
+ if (inferred) {
977
+ let compatible = false;
978
+ if (currentS.kind === SchemaKind.ANY) {
979
+ compatible = true;
980
+ finalS = inferred;
981
+ } else if (currentS.typeName === inferred.typeName) {
982
+ compatible = true;
983
+ } else if (
984
+ currentS.typeName === 'number' &&
985
+ (inferred.typeName === 'int' || inferred.typeName === 'float')
986
+ ) {
987
+ compatible = true;
1015
988
  }
1016
989
 
1017
- const start = Math.max(0, this.i - 10);
1018
- const end = Math.min(this.text.length, this.i + 11);
1019
-
1020
- // Raw Before: replace newlines, pad start
1021
- const rawBefore = this.text.substring(start, this.i)
1022
- .padStart(10)
1023
- .replace(/\n/g, "↩︎");
1024
-
1025
- // Raw Current: handle EOF, replace whitespace
1026
- // Note: undefined check needed if i is out of bounds (EOF)
1027
- const charAtI = this.text[this.i] || " ";
1028
- const rawCurrent = charAtI
1029
- .replace(/\n/g, "↩︎")
1030
- .replace(/ /g, "·")
1031
- .replace(/\t/g, "→");
1032
-
1033
- // Raw After: replace newlines, pad end
1034
- const rawAfter = this.text.substring(this.i + 1, end)
1035
- .padEnd(10)
1036
- .replace(/\n/g, "↩︎");
1037
-
1038
- const context = `${Ansi.DIM}${rawBefore}${Ansi.RESET}${Ansi.YELLOW}${rawCurrent}${Ansi.RESET}${Ansi.DIM}${rawAfter}${Ansi.RESET}`;
1039
-
1040
- let msgColor = Ansi.RESET;
1041
- if (msg.includes("ERROR")) {
1042
- msgColor = Ansi.RED;
1043
- } else if (msg.includes("WARNING")) {
1044
- msgColor = Ansi.YELLOW;
1045
- } else if (msg.includes("→")) {
1046
- msgColor = Ansi.GREEN;
1047
- } else if (msg.includes("PUSH") || msg.includes("POP")) {
1048
- msgColor = Ansi.MAGENTA;
1049
- } else if (msg.includes("START") || msg.includes("END")) {
1050
- msgColor = Ansi.DIM;
990
+ if (!compatible) {
991
+ finalS = inferred;
1051
992
  }
993
+ }
994
+ } else {
995
+ if (currentS.isRecord || currentS.isList) {
996
+ finalS = currentS;
997
+ } else {
998
+ finalS = new Schema(SchemaKind.PRIMITIVE, { typeName: 'null' });
999
+ }
1000
+ }
1052
1001
 
1053
- console.log(
1054
- `${Ansi.CYAN}|${locStr.padStart(8)}|${Ansi.RESET}${Ansi.DIM} ${Ansi.RESET}${context}${Ansi.DIM}${Ansi.CYAN}|${Ansi.RESET} ${Ansi.YELLOW}${treePrefix}${Ansi.YELLOW}@${depth}${Ansi.RESET} ${Ansi.DIM}|${Ansi.RESET} ${msgColor}${msg}${Ansi.RESET}`
1055
- );
1002
+ const node = new Node(finalS, { value });
1003
+ this.applyMeta(node);
1004
+ this.pushNode(node);
1005
+ return node;
1006
+ }
1007
+
1008
+ private applyMeta(obj: Node | Schema): void {
1009
+ obj.applyMeta(this.pendingMeta);
1010
+ this.pendingMeta = new MetaInfo();
1011
+ }
1012
+
1013
+ private advance(n: number = 1): string {
1014
+ let lastChar = '';
1015
+ for (let k = 0; k < n; k++) {
1016
+ if (this.i >= this.text.length) break;
1017
+ const c = this.text[this.i];
1018
+ lastChar = c;
1019
+ if (c === '\n') {
1020
+ this.line++;
1021
+ this.col = 1;
1022
+ } else {
1023
+ this.col++;
1024
+ }
1025
+ this.i++;
1056
1026
  }
1057
- }
1027
+ return lastChar;
1028
+ }
1029
+
1030
+ private skipWhitespace(): void {
1031
+ while (!this.eof()) {
1032
+ const ch = this.peek();
1033
+ if (ch && /\s/.test(ch)) {
1034
+ this.advance(1);
1035
+ } else {
1036
+ break;
1037
+ }
1038
+ }
1039
+ }
1040
+
1041
+ private eof(): boolean {
1042
+ return this.i >= this.text.length;
1043
+ }
1044
+ private peek(): string | null {
1045
+ return this.eof() ? null : this.text[this.i];
1046
+ }
1047
+ private peekNext(): string | null {
1048
+ return this.i + 1 < this.text.length ? this.text[this.i + 1] : null;
1049
+ }
1050
+
1051
+ private expect(ch: string): boolean {
1052
+ if (this.peek() !== ch) {
1053
+ this.addError(`Expected '${ch}', got '${this.peek()}'`);
1054
+ return false;
1055
+ }
1056
+ this.advance(1);
1057
+ return true;
1058
+ }
1059
+
1060
+ private addError(msg: string): void {
1061
+ if (this.errors.length >= Decoder.MAX_ERRORS) return;
1062
+ this._dbg(`ERROR: ${msg}`);
1063
+ this.errors.push(new DecodeError(msg, this.i, this.currentSchema, this.currentNode));
1064
+ }
1065
+
1066
+ private addWarning(msg: string): void {
1067
+ this._dbg(`WARNING: ${msg}`);
1068
+ this.warnings.push(new DecodeWarning(msg, this.i, this.currentSchema, this.currentNode));
1069
+ }
1070
+
1071
+ private _dbg(msg: string): void {
1072
+ if (!this.debug) return;
1073
+
1074
+ const locStr = `${this.line + 1}:${this.col + 1}`;
1075
+
1076
+ const depth = this.nodeStack.length;
1077
+ let treePrefix = '';
1078
+ if (depth > 0) {
1079
+ // Python: "│ " * (depth - 1)
1080
+ treePrefix = Ansi.DIM + '│ '.repeat(depth - 1) + '├─ ' + Ansi.RESET;
1081
+ }
1082
+
1083
+ const start = Math.max(0, this.i - 10);
1084
+ const end = Math.min(this.text.length, this.i + 11);
1085
+
1086
+ // Raw Before: replace newlines, pad start
1087
+ const rawBefore = this.text.substring(start, this.i).padStart(10).replace(/\n/g, '↩︎');
1088
+
1089
+ // Raw Current: handle EOF, replace whitespace
1090
+ // Note: undefined check needed if i is out of bounds (EOF)
1091
+ const charAtI = this.text[this.i] || ' ';
1092
+ const rawCurrent = charAtI.replace(/\n/g, '↩︎').replace(/ /g, '·').replace(/\t/g, '→');
1093
+
1094
+ // Raw After: replace newlines, pad end
1095
+ const rawAfter = this.text
1096
+ .substring(this.i + 1, end)
1097
+ .padEnd(10)
1098
+ .replace(/\n/g, '↩︎');
1099
+
1100
+ const context = `${Ansi.DIM}${rawBefore}${Ansi.RESET}${Ansi.YELLOW}${rawCurrent}${Ansi.RESET}${Ansi.DIM}${rawAfter}${Ansi.RESET}`;
1101
+
1102
+ let msgColor = Ansi.RESET;
1103
+ if (msg.includes('ERROR')) {
1104
+ msgColor = Ansi.RED;
1105
+ } else if (msg.includes('WARNING')) {
1106
+ msgColor = Ansi.YELLOW;
1107
+ } else if (msg.includes('→')) {
1108
+ msgColor = Ansi.GREEN;
1109
+ } else if (msg.includes('PUSH') || msg.includes('POP')) {
1110
+ msgColor = Ansi.MAGENTA;
1111
+ } else if (msg.includes('START') || msg.includes('END')) {
1112
+ msgColor = Ansi.DIM;
1113
+ }
1114
+
1115
+ console.log(
1116
+ `${Ansi.CYAN}|${locStr.padStart(8)}|${Ansi.RESET}${Ansi.DIM} ${Ansi.RESET}${context}${Ansi.DIM}${Ansi.CYAN}|${Ansi.RESET} ${Ansi.YELLOW}${treePrefix}${Ansi.YELLOW}@${depth}${Ansi.RESET} ${Ansi.DIM}|${Ansi.RESET} ${msgColor}${msg}${Ansi.RESET}`,
1117
+ );
1118
+ }
1119
+ }