@arkadia/data 0.1.9 → 0.1.11

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.
@@ -12,7 +12,7 @@ class Colors {
12
12
  static TYPE = '\x1b[96m'; // cyan
13
13
  static KEY = '\x1b[93m'; // yellow
14
14
  static SCHEMA = '\x1b[91m'; // red (for @TypeName)
15
- static TAG = '\x1b[91m';
15
+ static TAG = '\x1b[35m';
16
16
  static ATTR = '\x1b[93m';
17
17
  }
18
18
 
@@ -78,7 +78,8 @@ export class Encoder {
78
78
 
79
79
  // Avoid printing internal/default type names
80
80
  if (schema.typeName && schema.isRecord && !schema.isAny) {
81
- prefix = this.c(`@${schema.typeName}`, Colors.SCHEMA);
81
+ const escapedName = this.escapeIdent(schema.typeName);
82
+ prefix = this.c(`@${escapedName}`, Colors.SCHEMA);
82
83
  }
83
84
 
84
85
  // Przygotowanie meta (ale jeszcze nie użycie, bo w liście może się zmienić)
@@ -90,7 +91,8 @@ export class Encoder {
90
91
  const metaPrefix = includeMeta ? this.metaInline(schema) : '';
91
92
  // Python: return ind + ((meta_prefix + " ") if meta_prefix else "") + self._c(...)
92
93
  const metaStr = metaPrefix ? metaPrefix + ' ' : '';
93
- return ind + metaStr + this.c(schema.typeName, Colors.TYPE);
94
+ const escapedType = this.escapeIdent(schema.typeName);
95
+ return ind + metaStr + this.c(escapedType, Colors.TYPE);
94
96
  }
95
97
 
96
98
  // --- LIST ---
@@ -156,7 +158,7 @@ export class Encoder {
156
158
  }
157
159
 
158
160
  // 3. Field Name
159
- str += this.c(field.name, Colors.KEY);
161
+ str += this.c(this.escapeIdent(field.name), Colors.KEY);
160
162
 
161
163
  // 4. Field Type
162
164
  const fieldType = this.encodeSchema(field, 0, false).trim();
@@ -196,13 +198,13 @@ export class Encoder {
196
198
  }
197
199
 
198
200
  private getTypeLabel(schema: Schema): string {
199
- if (schema.isPrimitive) return schema.typeName || 'any';
201
+ if (schema.isPrimitive) return this.escapeIdent(schema.typeName || 'any');
200
202
  if (schema.isList) {
201
203
  const inner = schema.element ? this.getTypeLabel(schema.element) : 'any';
202
204
  return `[${inner}]`;
203
205
  }
204
206
  if (schema.isRecord && schema.typeName && schema.typeName !== 'any') {
205
- return schema.typeName;
207
+ return this.escapeIdent(schema.typeName);
206
208
  }
207
209
  return 'any';
208
210
  }
@@ -240,15 +242,20 @@ export class Encoder {
240
242
 
241
243
  // 2. Modifiers
242
244
  if ((obj as Schema).required) {
243
- items.push(this.c('!required', Colors.TAG));
245
+ items.push(this.c('$required', Colors.TAG));
244
246
  }
245
247
 
246
248
  // 3. Attributes & Tags
247
249
  if (this.config.includeMeta) {
248
250
  const currentAttr = obj.attr || {};
249
251
  for (const [k, v] of Object.entries(currentAttr)) {
250
- const valStr = this.primitiveValue(v);
251
- items.push(this.c(`$${k}=`, Colors.ATTR) + valStr);
252
+ const escapedKey = this.escapeIdent(k);
253
+ if (typeof v === 'boolean' && v === true) {
254
+ items.push(this.c(`$${escapedKey}`, Colors.ATTR));
255
+ } else {
256
+ const valStr = this.primitiveValue(v);
257
+ items.push(this.c(`$${escapedKey}=`, Colors.ATTR) + valStr);
258
+ }
252
259
  }
253
260
 
254
261
  const currentTags = obj.tags || [];
@@ -265,7 +272,7 @@ export class Encoder {
265
272
 
266
273
  if (wrapped) {
267
274
  const wrappedContent =
268
- this.c(`/${pad}`, Colors.SCHEMA) + content + this.c(`${pad}/`, Colors.SCHEMA);
275
+ this.c(`//${pad}`, Colors.SCHEMA) + content + this.c(`${pad}//`, Colors.SCHEMA);
269
276
  return this.config.compact ? wrappedContent + ' ' : ' ' + wrappedContent + ' ';
270
277
  }
271
278
 
@@ -321,92 +328,111 @@ export class Encoder {
321
328
  return this.c(`"${content}"`, Colors.STRING);
322
329
  }
323
330
 
324
- // -------------------------------------------------------------
325
- // LIST
326
- // -------------------------------------------------------------
331
+ /**
332
+ * Wraps identifier in backticks if it contains spaces or special characters.
333
+ * Regex: [a-zA-Z_][a-zA-Z0-9_]*
334
+ */
335
+ private escapeIdent(name: string): string {
336
+ if (!name) return '';
327
337
 
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)}`;
338
+ // Standard identifier pattern
339
+ const pattern = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
340
+
341
+ if (pattern.test(name)) {
342
+ return name;
333
343
  }
334
- return header;
335
- }
336
344
 
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} `);
345
+ // If it doesn't match, wrap it in backticks
346
+ return `\`${name}\``;
341
347
  }
342
348
 
349
+ // -------------------------------------------------------------
350
+ // LIST
351
+ // -------------------------------------------------------------
343
352
  private encodeList(node: Node, indent: number, includeSchema: boolean = false): string {
344
353
  const ind = ' '.repeat(indent);
345
354
  const childIndent = indent + this.config.indent;
355
+ const isPrompt = this.config.promptOutput;
346
356
 
357
+ if (this.config.includeArraySize) {
358
+ node.attr['size'] = node.elements.length;
359
+ }
347
360
  const innerMeta = this.metaWrapped(node);
348
361
 
349
- // 1. Generate Header Schema (if requested)
362
+ // 1. Generate Header Schema (Standard only)
350
363
  let schemaHeader = '';
351
- if (includeSchema && node.schema && node.schema.element) {
364
+ if (!isPrompt && includeSchema && node.schema && node.schema.element) {
352
365
  schemaHeader = this.encodeSchema(node.schema.element, 0).trim();
353
- }
354
- if (schemaHeader) {
355
- schemaHeader = schemaHeader + ' ';
366
+ if (schemaHeader) schemaHeader += ' ';
356
367
  }
357
368
 
358
369
  const expectedChild = node.schema ? node.schema.element : null;
359
370
 
371
+ // --- PROMPT MODE (Full Structural Expansion) ---
372
+ if (isPrompt) {
373
+ let res = ind + '[\n';
374
+ if (innerMeta) {
375
+ res += ' '.repeat(childIndent) + innerMeta.trim() + '\n';
376
+ }
377
+
378
+ if (node.schema && node.schema.element) {
379
+ // Force recursion into the element's structure using a Dummy Node
380
+ const dummy = new Node(node.schema.element, { value: null });
381
+
382
+ // We trim() the blueprint because we manually handle child indentation
383
+ const blueprint = this.encode(dummy, childIndent, false).trim();
384
+
385
+ res += ' '.repeat(childIndent) + blueprint + ',\n';
386
+ res += ' '.repeat(childIndent) + '... /* repeat pattern for additional items */\n';
387
+ } else {
388
+ res += ' '.repeat(childIndent) + '/* any content */\n';
389
+ }
390
+
391
+ res += ind + ']';
392
+ return res;
393
+ }
394
+
360
395
  // --- COMPACT MODE ---
361
396
  if (this.config.compact) {
362
397
  const items: string[] = [];
363
-
364
398
  for (const el of node.elements) {
365
- // IMPORTANT: We disable schema inclusion for elements to avoid duplication <...>
366
- // unless types mismatch drastically.
367
399
  let val = this.encode(el, 0, false).trim();
368
-
369
- // Check compatibility & Inject override if needed
370
400
  if (!this.schemasAreCompatible(el.schema, expectedChild)) {
371
401
  const label = el.schema ? this.getTypeLabel(el.schema) : 'any';
372
402
  const tag = this.c(`<${label}>`, Colors.SCHEMA);
373
403
  val = `${tag} ${val}`;
374
404
  }
375
-
376
405
  items.push(val);
377
406
  }
378
-
379
407
  return ind + '[' + innerMeta + schemaHeader + items.join(',') + ']';
380
408
  }
381
409
 
382
410
  // --- PRETTY MODE ---
383
- const header = this.listHeader(node);
384
- const out: string[] = [ind + header];
385
-
411
+ let res = ind + '[\n';
386
412
  if (innerMeta) {
387
- out.push(' '.repeat(childIndent) + innerMeta.trim());
413
+ res += ' '.repeat(childIndent) + innerMeta.trim() + '\n';
388
414
  }
389
-
390
415
  if (schemaHeader) {
391
- out.push(' '.repeat(childIndent) + schemaHeader.trim());
416
+ res += ' '.repeat(childIndent) + schemaHeader.trim() + '\n';
392
417
  }
393
418
 
419
+ const elementLines: string[] = [];
394
420
  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
421
+ let val = this.encode(el, 0, false).trim();
399
422
  if (!this.schemasAreCompatible(el.schema, expectedChild)) {
400
423
  const label = el.schema ? this.getTypeLabel(el.schema) : 'any';
401
424
  const tag = this.c(`<${label}>`, Colors.SCHEMA);
402
425
  val = `${tag} ${val}`;
403
426
  }
427
+ elementLines.push(' '.repeat(childIndent) + val);
428
+ }
404
429
 
405
- out.push(' '.repeat(childIndent) + val);
430
+ if (elementLines.length > 0) {
431
+ res += elementLines.join(',\n') + '\n';
406
432
  }
407
433
 
408
- out.push(ind + ']');
409
- return this.joinLines(out, '\n');
434
+ res += ind + ']';
435
+ return res;
410
436
  }
411
437
 
412
438
  // -------------------------------------------------------------
@@ -414,23 +440,57 @@ export class Encoder {
414
440
  // -------------------------------------------------------------
415
441
  private encodeRecord(node: Node, indent: number): string {
416
442
  const innerMeta = this.metaWrapped(node);
417
-
418
443
  const parts: string[] = [];
419
- if (node.schema.fields && node.schema.fields.length > 0) {
444
+ const isPrompt = this.config.promptOutput;
445
+ const baseInd = ' '.repeat(indent);
446
+ const childInd = ' '.repeat(indent + this.config.indent);
447
+
448
+ if (node.schema && node.schema.fields && node.schema.fields.length > 0) {
420
449
  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);
450
+ if (isPrompt) {
451
+ // --- PROMPT MODE: Type Label or Structural Expansion ---
452
+ const fieldName = this.escapeIdent(fieldDef.name);
453
+ let fieldValStructure: string;
454
+
455
+ if (fieldDef.isPrimitive) {
456
+ // For primitives, use the type name (e.g., "number")
457
+ fieldValStructure = this.c(this.getTypeLabel(fieldDef), Colors.TYPE);
458
+ } else {
459
+ // For structures (Records/Lists), recurse to get { } or [ ]
460
+ const dummyField = new Node(fieldDef, { value: null });
461
+ fieldValStructure = this.encode(dummyField, indent + this.config.indent, false).trim();
462
+ }
463
+
464
+ let line = `${this.c(fieldName, Colors.KEY)}: ${fieldValStructure}`;
465
+
466
+ if (fieldDef.comments && fieldDef.comments.length > 0) {
467
+ const comment = fieldDef.comments[0].trim();
468
+ line += ` ${this.c(`/* ${comment} */`, Colors.NULL)}`;
469
+ }
470
+ parts.push(line);
426
471
  } else {
427
- parts.push(this.c('null', Colors.NULL));
472
+ // --- STANDARD MODE (Data values) ---
473
+ const fieldNode = node.fields[fieldDef.name];
474
+ if (fieldNode) {
475
+ let val = this.encode(fieldNode, 0, false).trim();
476
+ val = this.applyTypeTag(val, fieldNode.schema, fieldDef);
477
+ parts.push(val);
478
+ } else {
479
+ parts.push(this.c('null', Colors.NULL));
480
+ }
428
481
  }
429
482
  }
430
- const sep = this.config.compact ? ',' : ', ';
431
- return '(' + innerMeta + parts.join(sep) + ')';
432
483
  } else {
433
- return '(' + innerMeta + this.c('null', Colors.NULL) + ')';
484
+ parts.push(this.c('null', Colors.NULL));
485
+ }
486
+
487
+ if (isPrompt) {
488
+ const body = parts.join(',\n' + childInd);
489
+ const metaStr = innerMeta ? childInd + innerMeta.trim() + '\n' : '';
490
+ return '{\n' + metaStr + childInd + body + '\n' + baseInd + '}';
434
491
  }
492
+
493
+ const sep = this.config.compact ? ',' : ', ';
494
+ return '(' + innerMeta + parts.join(sep) + ')';
435
495
  }
436
496
  }
@@ -53,7 +53,7 @@ export class Meta {
53
53
 
54
54
  /**
55
55
  * A temporary container (DTO) holding parsed metadata from a / ... / block.
56
- * It contains BOTH Schema constraints (!required) and Node attributes ($key=val).
56
+ * It contains BOTH Schema constraints ($required) and Node attributes ($key=val).
57
57
  */
58
58
  export class MetaInfo extends Meta {
59
59
  required: boolean;
@@ -91,14 +91,14 @@ export class MetaInfo extends Meta {
91
91
 
92
92
  /**
93
93
  * Debug representation mimicking the actual ADF format style.
94
- * Example: <MetaInfo !required #tag $key=val >
94
+ * Example: <MetaInfo $required #tag $key=val >
95
95
  */
96
96
  toString(): string {
97
97
  const parts: string[] = [];
98
98
 
99
99
  // 1. Flags
100
100
  if (this.required) {
101
- parts.push('!required');
101
+ parts.push('$required');
102
102
  }
103
103
 
104
104
  // 2. Tags
@@ -192,7 +192,7 @@ export class Schema extends Meta {
192
192
  // 3. Details
193
193
  const details: string[] = [];
194
194
 
195
- if (this.required) details.push('!required');
195
+ if (this.required) details.push('$required');
196
196
 
197
197
  const attrKeys = Object.keys(this.attr);
198
198
  if (attrKeys.length > 0)
@@ -1,4 +1,4 @@
1
- import { describe, it, expect } from 'vitest';
1
+ import { describe, expect, it } from 'vitest';
2
2
  import { MetaInfo } from '../src/index';
3
3
 
4
4
  describe('AK Data Meta', () => {
@@ -9,7 +9,7 @@ describe('AK Data Meta', () => {
9
9
  tags: ['tag1', 'tag2'],
10
10
  required: true,
11
11
  });
12
- const expected = '<MetaInfo !required #tag1 #tag2 $foo="bar" /* This is a comme.. */>';
12
+ const expected = '<MetaInfo $required #tag1 #tag2 $foo="bar" /* This is a comme.. */>';
13
13
  // const expectedJSON
14
14
  const expected_val = {
15
15
  comments: ['This is a comment'],
@@ -131,4 +131,42 @@ describe('String Escaping', () => {
131
131
  // Encoder must escape the backslash: "C:\\Program Files"
132
132
  assertRoundtrip(node, '<path:string>("C:\\\\Program Files")', false);
133
133
  });
134
+
135
+ it('should handle deeply nested escaped names and metadata keys', () => {
136
+ /**
137
+ * Validates that backticks work in nested schemas and metadata keys.
138
+ * This tests:
139
+ * 1. Schema name with spaces and symbols: @`User ID+`
140
+ * 2. Metadata keys with symbols: $`attributes*`
141
+ * 3. Field names with spaces and symbols: `ID of the user`
142
+ * 4. Standard identifiers: is_user (no backticks)
143
+ */
144
+ const text = `
145
+ /* Use backticks for Schema names and Attributes with spaces */
146
+ @\`User ID+\` <
147
+ // $\`attributes*\`="32" //
148
+ \`Is User?\`: bool,
149
+ $\`Attribute Special*\` \`ID of the user\`: string,
150
+ \`is - special?\`: bool,
151
+ is_user: bool
152
+ >
153
+
154
+ {
155
+ // $\`numbers of ids\`=4 //
156
+ $\`attr*\`=52 \`Is User?\`: true,
157
+ \`ID of the user\`: "ID",
158
+ \`is - special?\`: false,
159
+ is_user: false
160
+ }`;
161
+
162
+ // The expected output follows the AK Data rules:
163
+ // - Metadata wrapped in // ... //
164
+ // - Record data in ( ... )
165
+ // - Booleans and Numbers as literals
166
+ // - Identifiers escaped only if necessary
167
+ const expected =
168
+ `@\`User ID+\`<///*Use backticks for Schema names and Attributes with spaces*/ $\`attributes*\`="32"// \`Is User?\`:bool,$\`Attribute Special*\` \`ID of the user\`:string,\`is - special?\`:bool,is_user:bool>(//$\`numbers of ids\`=4// $\`attr*\`=52 true,"ID",false,false)`.trim();
169
+
170
+ assertRoundtrip(text, expected, false);
171
+ });
134
172
  });
@@ -1,4 +1,4 @@
1
- import { describe, it, expect } from 'vitest';
1
+ import { describe, expect, it } from 'vitest';
2
2
  import { Schema, SchemaKind } from '../src/index';
3
3
 
4
4
  describe('AI Schema test', () => {
@@ -11,7 +11,7 @@ describe('AI Schema test', () => {
11
11
  required: true,
12
12
  });
13
13
  const expected =
14
- '<Schema(DICT) name="TestSchema" !required attr=["foo"] tags=[tag1, tag2] comments=1>';
14
+ '<Schema(DICT) name="TestSchema" $required attr=["foo"] tags=[tag1, tag2] comments=1>';
15
15
  const expected_val = {
16
16
  comments: ['This is a comment'],
17
17
  attr: {
@@ -1,5 +1,5 @@
1
- import { describe, it, expect } from 'vitest';
2
- import { encode, decode, parse, SchemaKind } from '../src/index';
1
+ import { describe, expect, it } from 'vitest';
2
+ import { decode, encode, parse, SchemaKind } from '../src/index';
3
3
  import { assertRoundtrip } from './utils';
4
4
 
5
5
  describe('AK List Handling', () => {
@@ -221,4 +221,27 @@ describe('AK List Handling', () => {
221
221
  const expected = '<[[number]]>[[2,3,4],[5,6,7]]';
222
222
  assertRoundtrip(node, expected, false);
223
223
  });
224
+
225
+ it('should no compact with include_array_size', () => {
226
+ const akdText = `<[number]>[1,2,<string> "3"]`;
227
+
228
+ const result = decode(akdText, { debug: false });
229
+ const node = result.node;
230
+ expect(result.errors).toHaveLength(0);
231
+
232
+ // 'int' normalizes to 'number' in the TS implementation
233
+ const expected = `<[number]>
234
+ [
235
+ // $size=3 //
236
+ 1,
237
+ 2,
238
+ <string> "3"
239
+ ]`;
240
+ const output = encode(node, {
241
+ includeArraySize: true,
242
+ compact: false,
243
+ });
244
+
245
+ expect(output).toBe(expected);
246
+ });
224
247
  });