@arkadia/ai-data-format 1.0.0
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/dist/config.d.ts +75 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +28 -0
- package/dist/config.js.map +1 -0
- package/dist/core/Decoder.d.ts +88 -0
- package/dist/core/Decoder.d.ts.map +1 -0
- package/dist/core/Decoder.js +968 -0
- package/dist/core/Decoder.js.map +1 -0
- package/dist/core/Encoder.d.ts +26 -0
- package/dist/core/Encoder.d.ts.map +1 -0
- package/dist/core/Encoder.js +368 -0
- package/dist/core/Encoder.js.map +1 -0
- package/dist/core/Parser.d.ts +6 -0
- package/dist/core/Parser.d.ts.map +1 -0
- package/dist/core/Parser.js +132 -0
- package/dist/core/Parser.js.map +1 -0
- package/dist/index.d.ts +22 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +46 -0
- package/dist/index.js.map +1 -0
- package/dist/models/Meta.d.ts +48 -0
- package/dist/models/Meta.d.ts.map +1 -0
- package/dist/models/Meta.js +113 -0
- package/dist/models/Meta.js.map +1 -0
- package/dist/models/Node.d.ts +42 -0
- package/dist/models/Node.d.ts.map +1 -0
- package/dist/models/Node.js +179 -0
- package/dist/models/Node.js.map +1 -0
- package/dist/models/Schema.d.ts +55 -0
- package/dist/models/Schema.d.ts.map +1 -0
- package/dist/models/Schema.js +175 -0
- package/dist/models/Schema.js.map +1 -0
- package/package.json +32 -0
- package/src/config.ts +102 -0
- package/src/core/Decoder.ts +1074 -0
- package/src/core/Encoder.ts +443 -0
- package/src/core/Parser.ts +150 -0
- package/src/index.ts +46 -0
- package/src/models/Meta.ts +135 -0
- package/src/models/Node.ts +212 -0
- package/src/models/Schema.ts +222 -0
- package/tests/00.meta.test.ts +31 -0
- package/tests/00.node.test.ts +54 -0
- package/tests/00.primitive.test.ts +108 -0
- package/tests/00.schema.test.ts +41 -0
- package/tests/01.schema.test.ts +70 -0
- package/tests/02.data.test.ts +89 -0
- package/tests/03.errors.test.ts +71 -0
- package/tests/04.list.test.ts +225 -0
- package/tests/05.record.test.ts +82 -0
- package/tests/06.meta.test.ts +506 -0
- package/tests/utils.ts +69 -0
- package/tsconfig.json +46 -0
- package/vitest.config.ts +9 -0
|
@@ -0,0 +1,443 @@
|
|
|
1
|
+
import { Node } from '../models/Node';
|
|
2
|
+
import { Schema } from '../models/Schema';
|
|
3
|
+
import { Meta } from '../models/Meta';
|
|
4
|
+
import { EncoderConfig } from '../config';
|
|
5
|
+
|
|
6
|
+
class Colors {
|
|
7
|
+
static RESET = "\x1b[0m";
|
|
8
|
+
static STRING = "\x1b[92m"; // green
|
|
9
|
+
static NUMBER = "\x1b[94m"; // blue
|
|
10
|
+
static BOOL = "\x1b[95m"; // magenta
|
|
11
|
+
static NULL = "\x1b[90m"; // gray
|
|
12
|
+
static TYPE = "\x1b[96m"; // cyan
|
|
13
|
+
static KEY = "\x1b[93m"; // yellow
|
|
14
|
+
static SCHEMA = "\x1b[91m"; // red (for @TypeName)
|
|
15
|
+
static TAG = "\x1b[91m";
|
|
16
|
+
static ATTR = "\x1b[93m";
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export class Encoder {
|
|
20
|
+
private config: EncoderConfig;
|
|
21
|
+
|
|
22
|
+
constructor(config: EncoderConfig) {
|
|
23
|
+
this.config = config;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// -------------------------------------------------------------
|
|
27
|
+
// PUBLIC ENTRY
|
|
28
|
+
// -------------------------------------------------------------
|
|
29
|
+
|
|
30
|
+
public encode(node: Node, indentLevel: number = 0, includeSchema: boolean = true): string {
|
|
31
|
+
const baseIndent = this.config.startIndent + indentLevel;
|
|
32
|
+
let schemaPrefix = "";
|
|
33
|
+
|
|
34
|
+
// 2. Prepare Schema Header (e.g. <test: string>)
|
|
35
|
+
const shouldRenderSchema = (
|
|
36
|
+
includeSchema &&
|
|
37
|
+
node.schema !== null &&
|
|
38
|
+
this.config.includeSchema
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
if (shouldRenderSchema) {
|
|
42
|
+
let sTxt = this.encodeSchema(node.schema!, baseIndent).trim();
|
|
43
|
+
|
|
44
|
+
// Jeśli wynik nie jest pusty i nie zaczyna się od < ani @, dodaj nawiasy < >
|
|
45
|
+
if (sTxt && !sTxt.startsWith('<') && !sTxt.startsWith('@')) {
|
|
46
|
+
sTxt = `<${sTxt}>`;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (sTxt) {
|
|
50
|
+
if (this.config.compact) {
|
|
51
|
+
schemaPrefix = sTxt;
|
|
52
|
+
} else {
|
|
53
|
+
// Newline formatting for schema header
|
|
54
|
+
schemaPrefix = sTxt + "\n" + " ".repeat(baseIndent);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// 3. Encode Data
|
|
60
|
+
let data = "";
|
|
61
|
+
if (node.isList) {
|
|
62
|
+
data = this.encodeList(node, baseIndent, false);
|
|
63
|
+
} else if (node.isPrimitive) {
|
|
64
|
+
data = this.encodePrimitiveNode(node);
|
|
65
|
+
} else if (node.isRecord) {
|
|
66
|
+
data = this.encodeRecord(node, baseIndent);
|
|
67
|
+
} else {
|
|
68
|
+
data = this.c("null", Colors.NULL);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Final Assembly: Meta -> Schema -> Data
|
|
72
|
+
return `${schemaPrefix}${data}`;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
public encodeSchema(schema: Schema, indent: number, includeMeta: boolean = true): string {
|
|
76
|
+
if (!schema) return "";
|
|
77
|
+
|
|
78
|
+
const ind = " ".repeat(indent);
|
|
79
|
+
let prefix = "";
|
|
80
|
+
|
|
81
|
+
const pad = this.config.compact ? "" : " ";
|
|
82
|
+
|
|
83
|
+
// Avoid printing internal/default type names
|
|
84
|
+
if (
|
|
85
|
+
schema.typeName &&
|
|
86
|
+
schema.isRecord &&
|
|
87
|
+
!schema.isAny
|
|
88
|
+
) {
|
|
89
|
+
prefix = this.c(`@${schema.typeName}`, Colors.SCHEMA);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Przygotowanie meta (ale jeszcze nie użycie, bo w liście może się zmienić)
|
|
93
|
+
// W Pythonie: meta = self._meta_wrapped(schema) if include_meta else ""
|
|
94
|
+
// Tutaj obliczamy dynamicznie tam gdzie potrzeba, bo w liście modyfikujemy obiekt schema.
|
|
95
|
+
|
|
96
|
+
// --- PRIMITIVE ---
|
|
97
|
+
if (schema.isPrimitive) {
|
|
98
|
+
const metaPrefix = includeMeta ? this.metaInline(schema) : "";
|
|
99
|
+
// Python: return ind + ((meta_prefix + " ") if meta_prefix else "") + self._c(...)
|
|
100
|
+
const metaStr = metaPrefix ? metaPrefix + " " : "";
|
|
101
|
+
return ind + metaStr + this.c(schema.typeName, Colors.TYPE);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// --- LIST ---
|
|
105
|
+
// --- DEBUG ---
|
|
106
|
+
if (schema.isList) {
|
|
107
|
+
if(schema.element) {
|
|
108
|
+
schema.applyMeta(schema.element)
|
|
109
|
+
schema.element.clearMeta()
|
|
110
|
+
}
|
|
111
|
+
const listMeta = includeMeta ? this.metaWrapped(schema) : "";
|
|
112
|
+
|
|
113
|
+
// Special Case: List of Records < [ ... ] >
|
|
114
|
+
if (schema.element && schema.element.isRecord) {
|
|
115
|
+
// We reuse the _record_fields logic but wrap in <[ ... ]>
|
|
116
|
+
const innerFields = this.encodeSchemaFields(schema.element);
|
|
117
|
+
|
|
118
|
+
// FIX: Use 'pad' variable to remove spaces in compact mode
|
|
119
|
+
return ind + prefix + "<" + pad + "[" + listMeta + innerFields + pad + "]" + pad + ">";
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Standard List [Type]
|
|
123
|
+
const inner = this.encodeSchema(schema.element!, 0, false).trim();
|
|
124
|
+
return ind + "[" + listMeta + this.c(inner, Colors.TYPE) + "]";
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// --- RECORD ---
|
|
128
|
+
if (schema.isRecord) {
|
|
129
|
+
// Get Record-level meta (e.g. < / $ver=1 / ... >)
|
|
130
|
+
const recordMeta = includeMeta ? this.metaWrapped(schema) : "";
|
|
131
|
+
|
|
132
|
+
if (!schema.fields || schema.fields.length === 0) {
|
|
133
|
+
// If the record is generic (no fields, no specific type name, no meta),
|
|
134
|
+
// return an empty string to avoid printing "<>" or "<any>" before the "{...}".
|
|
135
|
+
if (!prefix && !recordMeta && schema.isAny) {
|
|
136
|
+
return "";
|
|
137
|
+
}
|
|
138
|
+
return ind + prefix + "<" + pad + recordMeta + "any" + pad + ">";
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Encode Fields
|
|
142
|
+
const innerFields = this.encodeSchemaFields(schema);
|
|
143
|
+
|
|
144
|
+
// FIX: Use 'pad' variable to remove spaces in compact mode
|
|
145
|
+
return ind + prefix + "<" + pad + recordMeta + innerFields + pad + ">";
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Fallback (ANY)
|
|
149
|
+
const meta = includeMeta ? this.metaWrapped(schema) : "";
|
|
150
|
+
return ind + `<${meta}any>`;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// --- Helper to deduplicate field encoding logic ---
|
|
154
|
+
private encodeSchemaFields(schema: Schema): string {
|
|
155
|
+
const parts: string[] = [];
|
|
156
|
+
const pad = this.config.compact ? "" : " ";
|
|
157
|
+
|
|
158
|
+
for (const field of schema.fields) {
|
|
159
|
+
let str = "";
|
|
160
|
+
|
|
161
|
+
const metaPrefix = this.metaInline(field);
|
|
162
|
+
if (metaPrefix) {
|
|
163
|
+
str += metaPrefix + " ";
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// 3. Field Name
|
|
167
|
+
str += this.c(field.name, Colors.KEY);
|
|
168
|
+
|
|
169
|
+
// 4. Field Type
|
|
170
|
+
const fieldType = this.encodeSchema(field, 0, false).trim();
|
|
171
|
+
|
|
172
|
+
// Logic to decide when to print the type signature
|
|
173
|
+
const isStructure = !field.isPrimitive;
|
|
174
|
+
const isExplicitPrimitive = (this.config.includeType && field.typeName !== "any");
|
|
175
|
+
|
|
176
|
+
if (fieldType && (isStructure || isExplicitPrimitive)) {
|
|
177
|
+
str += `:${pad}${this.c(fieldType, Colors.TYPE)}`;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
parts.push(str);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const sep = `,${pad}`;
|
|
184
|
+
return parts.join(sep);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
// -------------------------------------------------------------
|
|
189
|
+
// HELPER: SCHEMA COMPATIBILITY CHECK
|
|
190
|
+
// -------------------------------------------------------------
|
|
191
|
+
|
|
192
|
+
private schemasAreCompatible(nodeSchema: Schema | null, expectedSchema: Schema | null): boolean {
|
|
193
|
+
if (!expectedSchema || expectedSchema.isAny) return true;
|
|
194
|
+
if (!nodeSchema) return true;
|
|
195
|
+
|
|
196
|
+
// Check general kind
|
|
197
|
+
if (nodeSchema.kind !== expectedSchema.kind) return false;
|
|
198
|
+
|
|
199
|
+
// Check specific primitive type name (e.g. int vs string)
|
|
200
|
+
if (nodeSchema.isPrimitive && expectedSchema.isPrimitive) {
|
|
201
|
+
return nodeSchema.typeName === expectedSchema.typeName;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
return true;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
private getTypeLabel(schema: Schema): string {
|
|
208
|
+
if (schema.isPrimitive) return schema.typeName || "any";
|
|
209
|
+
if (schema.isList) {
|
|
210
|
+
const inner = schema.element ? this.getTypeLabel(schema.element) : "any";
|
|
211
|
+
return `[${inner}]`;
|
|
212
|
+
}
|
|
213
|
+
if (schema.isRecord && schema.typeName && schema.typeName !== "any") {
|
|
214
|
+
return schema.typeName;
|
|
215
|
+
}
|
|
216
|
+
return "any";
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
private applyTypeTag(valStr: string, nodeSchema: Schema | null, expectedSchema: Schema | null): string {
|
|
220
|
+
if (this.schemasAreCompatible(nodeSchema, expectedSchema)) {
|
|
221
|
+
return valStr;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Mismatch detected -> Wrap with tag
|
|
225
|
+
const label = nodeSchema ? this.getTypeLabel(nodeSchema) : "any";
|
|
226
|
+
const tag = this.c(`<${label}>`, Colors.SCHEMA);
|
|
227
|
+
return `${tag} ${valStr}`;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
// -------------------------------------------------------------
|
|
232
|
+
// META AND COMMENTS (Unified Logic)
|
|
233
|
+
// -------------------------------------------------------------
|
|
234
|
+
|
|
235
|
+
private buildMetaString(obj: Meta, wrapped: boolean = false): string {
|
|
236
|
+
const items: string[] = [];
|
|
237
|
+
const pad = this.config.compact ? "" : " ";
|
|
238
|
+
|
|
239
|
+
// 1. Comments
|
|
240
|
+
if (this.config.includeComments && obj.comments) {
|
|
241
|
+
for (const c of obj.comments) {
|
|
242
|
+
const cleaned = c.trim();
|
|
243
|
+
items.push(this.c(`/*${pad}${cleaned}${pad}*/`, Colors.NULL));
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// 2. Modifiers
|
|
248
|
+
// W Pythonie: getattr(obj, 'required', False)
|
|
249
|
+
if ((obj as any).required) {
|
|
250
|
+
items.push(this.c("!required", Colors.TAG));
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// 3. Attributes & Tags
|
|
254
|
+
if (this.config.includeMeta) {
|
|
255
|
+
const currentAttr = obj.attr || {};
|
|
256
|
+
for (const [k, v] of Object.entries(currentAttr)) {
|
|
257
|
+
const valStr = this.primitiveValue(v);
|
|
258
|
+
items.push(this.c(`$${k}=`, Colors.ATTR) + valStr);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const currentTags = obj.tags || [];
|
|
262
|
+
for (const t of currentTags) {
|
|
263
|
+
items.push(this.c(`#${t}`, Colors.TAG));
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
if (items.length === 0) {
|
|
268
|
+
return "";
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const content = items.join(" ");
|
|
272
|
+
|
|
273
|
+
if (wrapped) {
|
|
274
|
+
const wrappedContent = this.c(`/${pad}`, Colors.SCHEMA) + content + this.c(`${pad}/`, Colors.SCHEMA);
|
|
275
|
+
return this.config.compact ? wrappedContent + " " : " " + wrappedContent + " ";
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// Inline: /* c1 */ !req $a=1
|
|
279
|
+
return content;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
private metaInline(obj: Meta): string {
|
|
283
|
+
return this.buildMetaString(obj, false);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
private metaWrapped(obj: Meta): string {
|
|
287
|
+
return this.buildMetaString(obj, true);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// -------------------------------------------------------------
|
|
291
|
+
// HELPERS
|
|
292
|
+
// -------------------------------------------------------------
|
|
293
|
+
private c(text: string, color: string): string {
|
|
294
|
+
if (!this.config.colorize) return text;
|
|
295
|
+
return `${color}${text}${Colors.RESET}`;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
private escapeNewlines(text: string): string {
|
|
299
|
+
return text.replace(/\r\n/g, "\\n").replace(/\r/g, "\\n").replace(/\n/g, "\\n");
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
private encodePrimitiveNode(node: Node): string {
|
|
303
|
+
const innerMeta = this.metaInline(node);
|
|
304
|
+
const str = this.primitiveValue(node.value);
|
|
305
|
+
return (innerMeta ? innerMeta + " " : "") + str;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
private primitiveValue(v: any): string {
|
|
309
|
+
if (typeof v === 'string') return this.encodeString(v);
|
|
310
|
+
if (v === true) return this.c("true", Colors.BOOL);
|
|
311
|
+
if (v === false) return this.c("false", Colors.BOOL);
|
|
312
|
+
if (v === null || v === undefined) return this.c("null", Colors.NULL);
|
|
313
|
+
return this.c(String(v), Colors.NUMBER);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
private encodeString(v: string): string {
|
|
317
|
+
let content = v;
|
|
318
|
+
if (this.config.escapeNewLines) {
|
|
319
|
+
content = this.escapeNewlines(content);
|
|
320
|
+
}
|
|
321
|
+
content = content.replace(/"/g, '\\"');
|
|
322
|
+
return this.c(`"${content}"`, Colors.STRING);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// -------------------------------------------------------------
|
|
326
|
+
// LIST
|
|
327
|
+
// -------------------------------------------------------------
|
|
328
|
+
|
|
329
|
+
private listHeader(node: Node): string {
|
|
330
|
+
let header = "[";
|
|
331
|
+
if (this.config.includeArraySize) {
|
|
332
|
+
const size = node.elements.length;
|
|
333
|
+
header += `${this.c('$size', Colors.KEY)}=${this.c(String(size), Colors.NUMBER)}${this.c(':', Colors.TYPE)}`;
|
|
334
|
+
}
|
|
335
|
+
return header;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
private joinLines(items: string[], sep: string): string {
|
|
339
|
+
if (this.config.compact) return items.join(sep);
|
|
340
|
+
if (sep === "\n") return items.join(sep);
|
|
341
|
+
return items.join(`${sep} `);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
private encodeList(node: Node, indent: number, includeSchema: boolean = false): string {
|
|
345
|
+
const ind = " ".repeat(indent);
|
|
346
|
+
const childIndent = indent + this.config.indent;
|
|
347
|
+
|
|
348
|
+
const innerMeta = this.metaWrapped(node);
|
|
349
|
+
|
|
350
|
+
// 1. Generate Header Schema (if requested)
|
|
351
|
+
let schemaHeader = "";
|
|
352
|
+
if (includeSchema && node.schema && node.schema.element) {
|
|
353
|
+
schemaHeader = this.encodeSchema(node.schema.element, 0).trim();
|
|
354
|
+
}
|
|
355
|
+
if (schemaHeader) {
|
|
356
|
+
schemaHeader = schemaHeader + " ";
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
const expectedChild = node.schema ? node.schema.element : null;
|
|
360
|
+
|
|
361
|
+
// --- COMPACT MODE ---
|
|
362
|
+
if (this.config.compact) {
|
|
363
|
+
const items: string[] = [];
|
|
364
|
+
|
|
365
|
+
for (const el of node.elements) {
|
|
366
|
+
// IMPORTANT: We disable schema inclusion for elements to avoid duplication <...>
|
|
367
|
+
// unless types mismatch drastically.
|
|
368
|
+
let val = this.encode(el, 0, false).trim();
|
|
369
|
+
|
|
370
|
+
// Check compatibility & Inject override if needed
|
|
371
|
+
if (!this.schemasAreCompatible(el.schema, expectedChild)) {
|
|
372
|
+
const label = el.schema ? this.getTypeLabel(el.schema) : "any";
|
|
373
|
+
const tag = this.c(`<${label}>`, Colors.SCHEMA);
|
|
374
|
+
val = `${tag} ${val}`;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
items.push(val);
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
return ind + "[" + innerMeta + schemaHeader + items.join(",") + "]";
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// --- PRETTY MODE ---
|
|
384
|
+
const header = this.listHeader(node);
|
|
385
|
+
const out: string[] = [ind + header];
|
|
386
|
+
|
|
387
|
+
if (innerMeta) {
|
|
388
|
+
out.push(" ".repeat(childIndent) + innerMeta.trim());
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
if (schemaHeader) {
|
|
392
|
+
out.push(" ".repeat(childIndent) + schemaHeader.trim());
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
for (const el of node.elements) {
|
|
396
|
+
// IMPORTANT: Disable schema for children
|
|
397
|
+
let val = this.encode(
|
|
398
|
+
el, childIndent - this.config.startIndent, false
|
|
399
|
+
).trim();
|
|
400
|
+
|
|
401
|
+
// Check compatibility & Inject override if needed
|
|
402
|
+
if (!this.schemasAreCompatible(el.schema, expectedChild)) {
|
|
403
|
+
const label = el.schema ? this.getTypeLabel(el.schema) : "any";
|
|
404
|
+
const tag = this.c(`<${label}>`, Colors.SCHEMA);
|
|
405
|
+
val = `${tag} ${val}`;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
out.push(" ".repeat(childIndent) + val);
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
out.push(ind + "]");
|
|
412
|
+
return this.joinLines(out, "\n");
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
|
|
416
|
+
// -------------------------------------------------------------
|
|
417
|
+
// RECORD
|
|
418
|
+
// -------------------------------------------------------------
|
|
419
|
+
private encodeRecord(node: Node, indent: number): string {
|
|
420
|
+
const innerMeta = this.metaWrapped(node);
|
|
421
|
+
|
|
422
|
+
const parts: string[] = [];
|
|
423
|
+
if (node.schema.fields && node.schema.fields.length > 0) {
|
|
424
|
+
for (const fieldDef of node.schema.fields) {
|
|
425
|
+
const fieldNode = node.fields[fieldDef.name];
|
|
426
|
+
if (fieldNode) {
|
|
427
|
+
let val = this.encode(
|
|
428
|
+
fieldNode, indent - this.config.startIndent, false
|
|
429
|
+
).trim();
|
|
430
|
+
val = this.applyTypeTag(val, fieldNode.schema, fieldDef);
|
|
431
|
+
parts.push(val);
|
|
432
|
+
} else {
|
|
433
|
+
parts.push(this.c("null", Colors.NULL));
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
const sep = this.config.compact ? "," : ", ";
|
|
437
|
+
return "(" + innerMeta + parts.join(sep) + ")";
|
|
438
|
+
} else {
|
|
439
|
+
return "(" + innerMeta + this.c("null", Colors.NULL) + ")";
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
}
|
|
443
|
+
}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { Node } from '../models/Node';
|
|
2
|
+
import { Schema, SchemaKind } from '../models/Schema';
|
|
3
|
+
|
|
4
|
+
export class EncodingError extends Error {
|
|
5
|
+
constructor(message: string) {
|
|
6
|
+
super(message);
|
|
7
|
+
this.name = "EncodingError";
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
// --------------------------------------------------------------
|
|
12
|
+
// Helper: Type Wrapper
|
|
13
|
+
// --------------------------------------------------------------
|
|
14
|
+
const TYPE_STRING = "string";
|
|
15
|
+
const TYPE_NUMBER = "number";
|
|
16
|
+
const TYPE_BOOL = "bool";
|
|
17
|
+
const TYPE_NULL = "null";
|
|
18
|
+
// const TYPE_ANY = "any";
|
|
19
|
+
|
|
20
|
+
// --------------------------------------------------------------
|
|
21
|
+
// Primitive -> Node(primitive)
|
|
22
|
+
// --------------------------------------------------------------
|
|
23
|
+
|
|
24
|
+
function parsePrimitive(v: any): Node {
|
|
25
|
+
let schema: Schema;
|
|
26
|
+
|
|
27
|
+
if (typeof v === 'string') {
|
|
28
|
+
schema = new Schema(SchemaKind.PRIMITIVE, { typeName: TYPE_STRING });
|
|
29
|
+
} else if (typeof v === 'boolean') {
|
|
30
|
+
schema = new Schema(SchemaKind.PRIMITIVE, { typeName: TYPE_BOOL });
|
|
31
|
+
} else if (typeof v === 'number') {
|
|
32
|
+
schema = new Schema(SchemaKind.PRIMITIVE, { typeName: TYPE_NUMBER });
|
|
33
|
+
} else if (v === null || v === undefined) {
|
|
34
|
+
schema = new Schema(SchemaKind.PRIMITIVE, { typeName: TYPE_NULL });
|
|
35
|
+
} else {
|
|
36
|
+
throw new EncodingError(`Unsupported primitive: ${v}`);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return new Node(schema, { value: v });
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// --------------------------------------------------------------
|
|
43
|
+
// List -> Node(list)
|
|
44
|
+
// --------------------------------------------------------------
|
|
45
|
+
|
|
46
|
+
function parseList(arr: any[]): Node {
|
|
47
|
+
// 1. EMPTY LIST
|
|
48
|
+
if (arr.length === 0) {
|
|
49
|
+
const elementSchema = new Schema(SchemaKind.PRIMITIVE, { typeName: "any" });
|
|
50
|
+
const listSchema = new Schema(SchemaKind.LIST, { element: elementSchema });
|
|
51
|
+
return new Node(listSchema, { elements: [] });
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// 2. PARSE ALL ITEMS
|
|
55
|
+
const parsedItems: Node[] = arr.map(v => parseDataToNode(v));
|
|
56
|
+
|
|
57
|
+
// Use the first element as the baseline
|
|
58
|
+
const firstSchema = parsedItems[0].schema;
|
|
59
|
+
const isListOfRecords = (firstSchema.kind === SchemaKind.RECORD);
|
|
60
|
+
|
|
61
|
+
let unifiedElementSchema: Schema;
|
|
62
|
+
|
|
63
|
+
// 3. DETERMINE ELEMENT SCHEMA
|
|
64
|
+
if (isListOfRecords) {
|
|
65
|
+
// RECORDS: Create a unified schema containing ALL fields from ALL items.
|
|
66
|
+
unifiedElementSchema = new Schema(SchemaKind.RECORD, { typeName: "record" });
|
|
67
|
+
const seenFields = new Set<string>();
|
|
68
|
+
|
|
69
|
+
for (const item of parsedItems) {
|
|
70
|
+
// Skip non-record items if mixed list (or handle as error depending on strictness)
|
|
71
|
+
if (item.schema.kind === SchemaKind.RECORD) {
|
|
72
|
+
for (const field of item.schema.fields) {
|
|
73
|
+
if (!seenFields.has(field.name)) {
|
|
74
|
+
unifiedElementSchema.addField(field);
|
|
75
|
+
seenFields.add(field.name);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
} else {
|
|
81
|
+
// PRIMITIVES: Simply take the schema of the first element.
|
|
82
|
+
// We assume the list is homogeneous based on the first item.
|
|
83
|
+
unifiedElementSchema = firstSchema;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// 4. FINALIZE
|
|
87
|
+
const listSchema = new Schema(SchemaKind.LIST, {
|
|
88
|
+
typeName: "list",
|
|
89
|
+
element: unifiedElementSchema
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
return new Node(listSchema, { elements: parsedItems });
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// --------------------------------------------------------------
|
|
96
|
+
// Dict -> Node(record)
|
|
97
|
+
// --------------------------------------------------------------
|
|
98
|
+
|
|
99
|
+
function parseDict(obj: Record<string, any>): Node {
|
|
100
|
+
/**
|
|
101
|
+
* JSON objects -> named records.
|
|
102
|
+
*/
|
|
103
|
+
const fieldsData: Record<string, Node> = {};
|
|
104
|
+
const schema = new Schema(SchemaKind.RECORD);
|
|
105
|
+
|
|
106
|
+
for (const [key, rawValue] of Object.entries(obj)) {
|
|
107
|
+
const childNode = parseDataToNode(rawValue);
|
|
108
|
+
|
|
109
|
+
// Assign field name to the child's schema so it knows it is a field
|
|
110
|
+
childNode.schema.name = key;
|
|
111
|
+
|
|
112
|
+
fieldsData[key] = childNode;
|
|
113
|
+
schema.addField(childNode.schema);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return new Node(schema, { fields: fieldsData });
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// --------------------------------------------------------------
|
|
120
|
+
// Main entrypoint
|
|
121
|
+
// --------------------------------------------------------------
|
|
122
|
+
|
|
123
|
+
export function parseDataToNode(value: any): Node {
|
|
124
|
+
if (value instanceof Node) {
|
|
125
|
+
return value;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// 1. Primitive Check
|
|
129
|
+
if (
|
|
130
|
+
value === null ||
|
|
131
|
+
value === undefined ||
|
|
132
|
+
typeof value === 'string' ||
|
|
133
|
+
typeof value === 'number' ||
|
|
134
|
+
typeof value === 'boolean'
|
|
135
|
+
) {
|
|
136
|
+
return parsePrimitive(value);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// 2. List Check
|
|
140
|
+
if (Array.isArray(value)) {
|
|
141
|
+
return parseList(value);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// 3. Object Check (Dict)
|
|
145
|
+
if (typeof value === 'object') {
|
|
146
|
+
return parseDict(value);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
throw new EncodingError(`Unsupported structure type: ${typeof value}`);
|
|
150
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { EncoderConfig, DEFAULT_CONFIG } from './config';
|
|
2
|
+
import { Encoder } from './core/Encoder';
|
|
3
|
+
import { Decoder, DecodeResult } from './core/Decoder';
|
|
4
|
+
import { parseDataToNode } from './core/Parser';
|
|
5
|
+
import { Node } from './models/Node';
|
|
6
|
+
import { Schema, SchemaKind } from './models/Schema';
|
|
7
|
+
import { MetaInfo, Meta, MetaProps } from './models/Meta';
|
|
8
|
+
|
|
9
|
+
// Re-export types and classes
|
|
10
|
+
export { Node, Schema, SchemaKind, DecodeResult, EncoderConfig,
|
|
11
|
+
Meta, MetaInfo, MetaProps };
|
|
12
|
+
|
|
13
|
+
// =============================================================
|
|
14
|
+
// PUBLIC API
|
|
15
|
+
// =============================================================
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Encode input data into valid **AI.Data** format.
|
|
19
|
+
*/
|
|
20
|
+
export function encode(data: any | Node, config: Partial<EncoderConfig> = {}): string {
|
|
21
|
+
let node: Node;
|
|
22
|
+
node = parseDataToNode(data);
|
|
23
|
+
const finalConfig: EncoderConfig = { ...DEFAULT_CONFIG, ...config };
|
|
24
|
+
const encoder = new Encoder(finalConfig);
|
|
25
|
+
return encoder.encode(node);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Decode AI.Data format text into a Node structure.
|
|
30
|
+
*/
|
|
31
|
+
export function decode(
|
|
32
|
+
text: string,
|
|
33
|
+
options: { removeAnsiColors?: boolean; debug?: boolean } = {},
|
|
34
|
+
schema: string = ""
|
|
35
|
+
): DecodeResult {
|
|
36
|
+
const { removeAnsiColors = false, debug = false } = options;
|
|
37
|
+
const decoder = new Decoder(text, schema, removeAnsiColors, debug);
|
|
38
|
+
return decoder.decode();
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Convert raw JavaScript object/value into a Node.
|
|
43
|
+
*/
|
|
44
|
+
export function parse(data: any): Node {
|
|
45
|
+
return parseDataToNode(data);
|
|
46
|
+
}
|