@arkadia/data 0.1.7 → 0.1.8

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