@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.
- package/.prettierrc +8 -0
- package/README.md +166 -112
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js.map +1 -1
- package/dist/core/Decoder.d.ts.map +1 -1
- package/dist/core/Decoder.js +123 -97
- package/dist/core/Decoder.js.map +1 -1
- package/dist/core/Encoder.d.ts +1 -2
- package/dist/core/Encoder.d.ts.map +1 -1
- package/dist/core/Encoder.js +74 -76
- package/dist/core/Encoder.js.map +1 -1
- package/dist/core/Parser.d.ts +1 -1
- package/dist/core/Parser.d.ts.map +1 -1
- package/dist/core/Parser.js +11 -11
- package/dist/core/Parser.js.map +1 -1
- package/dist/index.d.ts +4 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -8
- package/dist/index.js.map +1 -1
- package/dist/models/Meta.d.ts +3 -2
- package/dist/models/Meta.d.ts.map +1 -1
- package/dist/models/Meta.js +3 -3
- package/dist/models/Meta.js.map +1 -1
- package/dist/models/Node.d.ts +4 -3
- package/dist/models/Node.d.ts.map +1 -1
- package/dist/models/Node.js +29 -23
- package/dist/models/Node.js.map +1 -1
- package/dist/models/Schema.d.ts.map +1 -1
- package/dist/models/Schema.js +27 -21
- package/dist/models/Schema.js.map +1 -1
- package/eslint.config.mjs +42 -0
- package/package.json +11 -1
- package/scripts/verify-build.js +95 -92
- package/src/config.ts +75 -75
- package/src/core/Decoder.ts +984 -922
- package/src/core/Encoder.ts +364 -371
- package/src/core/Parser.ts +112 -112
- package/src/index.ts +18 -20
- package/src/models/Meta.ts +107 -107
- package/src/models/Node.ts +190 -185
- package/src/models/Schema.ts +198 -193
- package/tests/00.meta.test.ts +19 -25
- package/tests/00.node.test.ts +40 -48
- package/tests/00.primitive.test.ts +121 -95
- package/tests/00.schema.test.ts +28 -35
- package/tests/01.schema.test.ts +42 -52
- package/tests/02.data.test.ts +69 -75
- package/tests/03.errors.test.ts +53 -55
- package/tests/04.list.test.ts +192 -193
- package/tests/05.record.test.ts +54 -56
- package/tests/06.meta.test.ts +393 -389
- package/tests/utils.ts +47 -44
- package/tsconfig.json +27 -29
- package/vitest.config.ts +1 -1
package/src/core/Decoder.ts
CHANGED
|
@@ -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
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
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
|
-
|
|
105
|
-
|
|
131
|
+
// =========================================================
|
|
132
|
+
// ENTRY
|
|
133
|
+
// =========================================================
|
|
106
134
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
135
|
+
public decode(): DecodeResult {
|
|
136
|
+
this._dbg('decode() start');
|
|
137
|
+
this.parseMeta();
|
|
110
138
|
|
|
111
|
-
|
|
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
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
}
|
|
141
|
+
// 1. Schema Processing Loop
|
|
142
|
+
while (!this.eof()) {
|
|
143
|
+
const ch = this.peek();
|
|
146
144
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
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
|
-
//
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
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
|
-
|
|
156
|
+
// Named Schema @Name
|
|
157
|
+
if (ch === '@') {
|
|
158
|
+
const schema = this.parseSchemaAtRef();
|
|
167
159
|
this.parseMeta();
|
|
168
|
-
this.applyMeta(rootNode);
|
|
169
160
|
|
|
170
|
-
this.
|
|
171
|
-
|
|
161
|
+
const next = this.peek();
|
|
162
|
+
if (next === '@' || next === '<') continue;
|
|
172
163
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
warnings: this.warnings
|
|
178
|
-
};
|
|
164
|
+
rootSchemaContext = schema;
|
|
165
|
+
break;
|
|
166
|
+
}
|
|
167
|
+
break;
|
|
179
168
|
}
|
|
180
169
|
|
|
181
|
-
//
|
|
182
|
-
|
|
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
|
-
|
|
199
|
-
|
|
200
|
-
|
|
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
|
-
|
|
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
|
-
|
|
207
|
-
|
|
208
|
-
|
|
194
|
+
// Final prefix scan
|
|
195
|
+
this.parseMeta();
|
|
196
|
+
this.applyMeta(rootNode);
|
|
209
197
|
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
this.popSchema();
|
|
213
|
-
return s;
|
|
214
|
-
}
|
|
198
|
+
this.popNode(); // Just in case
|
|
199
|
+
this._dbg('decode() end');
|
|
215
200
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
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
|
-
|
|
225
|
-
|
|
226
|
-
|
|
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
|
-
|
|
298
|
-
|
|
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
|
-
|
|
331
|
-
|
|
332
|
-
|
|
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
|
-
|
|
238
|
+
if (!this.expect('<')) {
|
|
239
|
+
const s = this.createSchema(SchemaKind.ANY, typeName);
|
|
240
|
+
this.popSchema();
|
|
241
|
+
return s;
|
|
340
242
|
}
|
|
341
243
|
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
244
|
+
const schema = this.createSchema(SchemaKind.RECORD, typeName);
|
|
245
|
+
this.parseSchemaBodyContent(schema, '>');
|
|
246
|
+
this.popSchema();
|
|
345
247
|
|
|
346
|
-
|
|
347
|
-
|
|
248
|
+
this._dbg(`END parse_schema_body '>' ${typeNamePrefix}`);
|
|
249
|
+
return schema;
|
|
250
|
+
}
|
|
348
251
|
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
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
|
-
|
|
355
|
-
|
|
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
|
-
|
|
381
|
-
|
|
382
|
-
|
|
259
|
+
const ch = this.peek();
|
|
260
|
+
if (ch === endChar) {
|
|
261
|
+
this.advance(1);
|
|
262
|
+
break;
|
|
263
|
+
}
|
|
383
264
|
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
this.
|
|
388
|
-
|
|
389
|
-
|
|
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
|
-
|
|
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
|
-
|
|
406
|
-
|
|
274
|
+
const elementSchema = new Schema(SchemaKind.ANY);
|
|
275
|
+
this.parseSchemaBodyContent(elementSchema, ']');
|
|
276
|
+
schema.element = elementSchema;
|
|
407
277
|
|
|
408
|
-
|
|
409
|
-
this.
|
|
410
|
-
this.
|
|
278
|
+
this.parseMeta(schema);
|
|
279
|
+
if (this.peek() === endChar) this.advance(1);
|
|
280
|
+
this.applyMeta(schema);
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
411
283
|
|
|
412
|
-
|
|
413
|
-
|
|
284
|
+
if (ch === ',') {
|
|
285
|
+
this.applyMeta(fieldSchema || schema);
|
|
286
|
+
this.advance(1);
|
|
287
|
+
continue;
|
|
288
|
+
}
|
|
414
289
|
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
290
|
+
const name = this.parseIdent();
|
|
291
|
+
if (!name) {
|
|
292
|
+
this.addError('Expected identifier');
|
|
293
|
+
this.advance(1);
|
|
294
|
+
continue;
|
|
295
|
+
}
|
|
420
296
|
|
|
421
|
-
|
|
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
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
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
|
-
|
|
466
|
-
this.
|
|
467
|
-
this.
|
|
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
|
-
|
|
470
|
-
|
|
471
|
-
node.schema.kind = SchemaKind.RECORD;
|
|
472
|
-
node.schema.typeName = "any";
|
|
473
|
-
}
|
|
313
|
+
fieldSchema.name = name;
|
|
314
|
+
this.applyMeta(fieldSchema);
|
|
474
315
|
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
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
|
-
|
|
522
|
-
|
|
523
|
-
|
|
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
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
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
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
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
|
-
|
|
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
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
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
|
-
|
|
660
|
-
|
|
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
|
-
|
|
668
|
-
|
|
669
|
-
|
|
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
|
-
|
|
712
|
-
|
|
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
|
-
|
|
719
|
-
|
|
377
|
+
if (this.eof()) {
|
|
378
|
+
this.addError('Unexpected EOF while expecting a node');
|
|
379
|
+
return this.createNode(null);
|
|
720
380
|
}
|
|
721
381
|
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
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
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
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
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
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
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
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
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
466
|
+
break;
|
|
467
|
+
}
|
|
468
|
+
if (this.peek() === ',') {
|
|
469
|
+
this.applyMeta(childNode || node);
|
|
470
|
+
this.advance(1);
|
|
471
|
+
continue;
|
|
472
|
+
}
|
|
773
473
|
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
return this.createNode(val);
|
|
777
|
-
}
|
|
474
|
+
childNode = this.parseNode(node);
|
|
475
|
+
node.elements.push(childNode);
|
|
778
476
|
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
}
|
|
477
|
+
if (parentSchema.element && parentSchema.element.isAny && childNode.schema) {
|
|
478
|
+
parentSchema.element = childNode.schema;
|
|
479
|
+
}
|
|
783
480
|
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
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
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
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
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
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
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
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
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
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
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
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
|
-
|
|
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
|
-
|
|
863
|
-
|
|
864
|
-
}
|
|
657
|
+
while (!this.eof() && nesting > 0) {
|
|
658
|
+
const ch = this.text[this.i];
|
|
865
659
|
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
this.
|
|
869
|
-
this.
|
|
870
|
-
|
|
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
|
-
|
|
874
|
-
|
|
875
|
-
|
|
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
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
s.element.clearMeta();
|
|
734
|
+
this.skipWhitespace();
|
|
735
|
+
if (this.peek() === '=') {
|
|
736
|
+
this.advance(1);
|
|
737
|
+
val = this.parsePrimitiveValue();
|
|
883
738
|
}
|
|
884
|
-
this.
|
|
739
|
+
this.addWarning(`Implicit attribute '${key}'. Use '$${key}' instead.`);
|
|
885
740
|
|
|
886
|
-
|
|
887
|
-
|
|
741
|
+
meta.attr[key] = val;
|
|
742
|
+
continue;
|
|
743
|
+
}
|
|
888
744
|
|
|
889
|
-
|
|
890
|
-
|
|
745
|
+
this.addError(`Unexpected token in meta block: ${ch}`);
|
|
746
|
+
this.advance(1);
|
|
891
747
|
}
|
|
892
748
|
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
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
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
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
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
currentS = new Schema(SchemaKind.ANY);
|
|
908
|
-
this.pushSchema(currentS);
|
|
909
|
-
}
|
|
791
|
+
// =========================================================
|
|
792
|
+
// HELPERS & LOW-LEVEL PARSERS
|
|
793
|
+
// =========================================================
|
|
910
794
|
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
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
|
-
|
|
943
|
-
|
|
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
|
-
|
|
949
|
-
|
|
950
|
-
|
|
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
|
-
|
|
954
|
-
|
|
955
|
-
|
|
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
|
-
|
|
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
|
-
|
|
982
|
-
|
|
983
|
-
|
|
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
|
-
|
|
986
|
-
|
|
987
|
-
|
|
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
|
-
|
|
995
|
-
|
|
996
|
-
|
|
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
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
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
|
-
|
|
1006
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
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
|
-
|
|
1018
|
-
|
|
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
|
-
|
|
1054
|
-
|
|
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
|
+
}
|