toon-formatter 2.0.0 → 2.1.1

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/src/json.js CHANGED
@@ -6,43 +6,22 @@ import { formatValue, parseValue, splitByDelimiter, extractJsonFromString } from
6
6
  import { validateToonStringSync } from './validator.js';
7
7
 
8
8
  /**
9
- * Converts JSON to TOON format (Synchronous)
10
- * @param {*} data - JSON data to convert
11
- * @param {string} key - Current key name (for recursion)
12
- * @param {number} depth - Current indentation depth
13
- * @returns {string} TOON formatted string
9
+ * Internal core parser for JSON to TOON conversion.
10
+ * @param {*} data
11
+ * @param {string} key
12
+ * @param {number} depth
13
+ * @returns {string}
14
14
  */
15
- export function jsonToToonSync(data, key = '', depth = 0) {
16
- // Handle String Input (Potential JSON string or Mixed Text)
17
- if (typeof data === 'string' && key === '' && depth === 0) {
18
- let convertedText = data;
19
- let iterationCount = 0;
20
- const maxIterations = 100;
21
-
22
- while (iterationCount < maxIterations) {
23
- const jsonString = extractJsonFromString(convertedText);
24
- if (!jsonString) break;
25
-
26
- try {
27
- const jsonObject = JSON.parse(jsonString);
28
- // Recursively call jsonToToonSync with the object
29
- const toonString = jsonToToonSync(jsonObject);
30
- const toonOutput = toonString.trim();
31
- convertedText = convertedText.replace(jsonString, toonOutput);
32
- iterationCount++;
33
- } catch (e) {
34
- break;
35
- }
36
- }
37
- return convertedText;
38
- }
39
-
15
+ function jsonToToonParser(data, key = '', depth = 0) {
40
16
  const indent = ' '.repeat(depth);
41
17
  const nextIndent = ' '.repeat(depth + 1);
42
18
 
43
19
  // ---- Primitive ----
44
20
  if (data === null || typeof data !== 'object') {
45
- return `${indent}${key}: ${formatValue(data)}`;
21
+ if (key) {
22
+ return `${indent}${key}: ${formatValue(data)}`;
23
+ }
24
+ return `${indent}${formatValue(data)}`;
46
25
  }
47
26
 
48
27
  // ---- Array ----
@@ -61,37 +40,75 @@ export function jsonToToonSync(data, key = '', depth = 0) {
61
40
  }
62
41
 
63
42
  // ---- Array of objects ----
43
+ const firstItem = data[0];
44
+ if (typeof firstItem === 'object' && firstItem !== null && !Array.isArray(firstItem)) {
45
+ const fields = Object.keys(firstItem);
46
+
47
+ // Collect all potential fields from ALL rows to be sure, or just from first row
48
+ // To match Python, we check fields from first row
49
+ let isTabular = true;
50
+ for (const row of data) {
51
+ if (typeof row !== 'object' || row === null || Array.isArray(row)) {
52
+ isTabular = false;
53
+ break;
54
+ }
64
55
 
65
- // Determine if all fields in object are primitives
66
- const firstObj = data[0];
67
- const fields = Object.keys(firstObj);
68
- const isTabular = data.every(row =>
69
- fields.every(f =>
70
- row[f] === null ||
71
- ['string', 'number', 'boolean'].includes(typeof row[f])
72
- )
73
- );
74
-
75
- // ---- TABULAR ARRAY (structured array) ----
76
- if (isTabular) {
77
- const header = fields.join(',');
78
- const lines = [];
79
- lines.push(`${indent}${key}[${length}]{${header}}:`);
80
-
81
- data.forEach(row => {
82
- const rowVals = fields.map(f => formatValue(row[f]));
83
- lines.push(`${nextIndent}${rowVals.join(',')}`);
84
- });
85
-
86
- return lines.join('\n');
56
+ // If this row has more keys than the first row, it might not be tabular
57
+ // but let's stick to Python logic: check if all values of 'fields' in this row are primitive
58
+ for (const f of fields) {
59
+ const val = row[f];
60
+ if (val !== null && typeof val === 'object') {
61
+ isTabular = false;
62
+ break;
63
+ }
64
+ }
65
+
66
+ // AND we should probably check if this row contains any extra non-primitive keys
67
+ if (isTabular) {
68
+ for (const k in row) {
69
+ if (!fields.includes(k) && typeof row[k] === 'object' && row[k] !== null) {
70
+ isTabular = false;
71
+ break;
72
+ }
73
+ }
74
+ }
75
+
76
+ if (!isTabular) break;
77
+ }
78
+
79
+ // ---- TABULAR ARRAY (structured array) ----
80
+ if (isTabular) {
81
+ const header = fields.join(',');
82
+ const lines = [];
83
+ lines.push(`${indent}${key}[${length}]{${header}}:`);
84
+
85
+ data.forEach(row => {
86
+ const rowVals = fields.map(f => formatValue(row[f]));
87
+ lines.push(`${nextIndent}${rowVals.join(',')}`);
88
+ });
89
+
90
+ return lines.join('\n');
91
+ }
87
92
  }
88
93
 
89
- // ---- STANDARD ARRAY OF OBJECTS ----
94
+ // ---- YAML-STYLE ARRAY (nested objects or mixed types) ----
90
95
  const lines = [];
91
96
  lines.push(`${indent}${key}[${length}]:`);
92
- data.forEach(item => {
93
- lines.push(jsonToToonSync(item, '', depth + 1));
97
+
98
+ data.forEach(row => {
99
+ lines.push(`${nextIndent}-`); // item marker
100
+ if (typeof row === 'object' && row !== null && !Array.isArray(row)) {
101
+ for (const f in row) {
102
+ lines.push(jsonToToonParser(row[f], f, depth + 2));
103
+ }
104
+ } else if (Array.isArray(row)) {
105
+ lines.push(jsonToToonParser(row, '', depth + 2));
106
+ } else {
107
+ // Primitive in array
108
+ lines.push(`${' '.repeat(depth + 2)}${formatValue(row)}`);
109
+ }
94
110
  });
111
+
95
112
  return lines.join('\n');
96
113
  }
97
114
 
@@ -101,13 +118,53 @@ export function jsonToToonSync(data, key = '', depth = 0) {
101
118
  lines.push(`${indent}${key}:`);
102
119
  }
103
120
 
121
+ const childDepth = key ? depth + 1 : depth;
104
122
  Object.keys(data).forEach(k => {
105
- lines.push(jsonToToonSync(data[k], k, key ? depth + 1 : depth));
123
+ lines.push(jsonToToonParser(data[k], k, childDepth));
106
124
  });
107
125
 
108
126
  return lines.join('\n');
109
127
  }
110
128
 
129
+ /**
130
+ * Converts JSON to TOON format (Sync)
131
+ * @param {*} data - JSON data to convert
132
+ * @returns {string} TOON formatted string
133
+ */
134
+ export function jsonToToonSync(data) {
135
+ // Handle String Input (Potential JSON string or Mixed Text)
136
+ if (typeof data === 'string') {
137
+ let convertedText = data;
138
+ let iterationCount = 0;
139
+ const maxIterations = 100;
140
+ let foundAnyJson = false;
141
+
142
+ while (iterationCount < maxIterations) {
143
+ const jsonString = extractJsonFromString(convertedText);
144
+ if (!jsonString) break;
145
+
146
+ foundAnyJson = true;
147
+ try {
148
+ const jsonObject = JSON.parse(jsonString);
149
+ const toonString = jsonToToonParser(jsonObject);
150
+ const toonOutput = toonString.trim();
151
+ convertedText = convertedText.replace(jsonString, toonOutput);
152
+ iterationCount++;
153
+ } catch (e) {
154
+ break;
155
+ }
156
+ }
157
+
158
+ if (!foundAnyJson) {
159
+ return jsonToToonParser(data);
160
+ }
161
+
162
+ return convertedText;
163
+ }
164
+
165
+ return jsonToToonParser(data);
166
+ }
167
+
111
168
  /**
112
169
  * Converts JSON to TOON format (Async)
113
170
  * @param {*} data - JSON data to convert
@@ -139,7 +196,7 @@ export function toonToJsonSync(toonString, returnJson = false) {
139
196
  const firstLine = lines.find(l => l.trim() !== '');
140
197
  if (!firstLine) return returnJson ? '{}' : {}; // Empty document
141
198
 
142
- // Root Array detection: [N]... at start of line
199
+ // Root Array detection
143
200
  if (firstLine.trim().startsWith('[')) {
144
201
  root = [];
145
202
  stack.push({ obj: root, indent: 0, isRootArray: true });
@@ -229,7 +286,6 @@ export function toonToJsonSync(toonString, returnJson = false) {
229
286
  const arrayMatch = content.match(/^\[(\d+)(.*?)\](?:\{(.*?)\})?:\s*(.*)$/);
230
287
 
231
288
  if (arrayMatch) {
232
- const length = parseInt(arrayMatch[1], 10);
233
289
  const delimChar = arrayMatch[2] || ',';
234
290
  const delimiter = delimChar === '\\t' ? '\t' : (delimChar === '|' ? '|' : ',');
235
291
  const fieldsStr = arrayMatch[3];
@@ -276,10 +332,8 @@ export function toonToJsonSync(toonString, returnJson = false) {
276
332
 
277
333
  // --- Key-Value or Array Header Handling ---
278
334
  const arrayHeaderMatch = trimmed.match(/^(.+?)\[(\d+)(.*?)\](?:\{(.*?)\})?:\s*(.*)$/);
279
-
280
335
  if (arrayHeaderMatch) {
281
336
  const key = arrayHeaderMatch[1].trim();
282
- const length = parseInt(arrayHeaderMatch[2], 10);
283
337
  const delimChar = arrayHeaderMatch[3];
284
338
  const fieldsStr = arrayHeaderMatch[4];
285
339
  const valueStr = arrayHeaderMatch[5];
@@ -289,10 +343,7 @@ export function toonToJsonSync(toonString, returnJson = false) {
289
343
  else if (delimChar === '|') delimiter = '|';
290
344
 
291
345
  const newArray = [];
292
-
293
- if (!Array.isArray(parent)) {
294
- parent[key] = newArray;
295
- }
346
+ parent[key] = newArray;
296
347
 
297
348
  if (fieldsStr) {
298
349
  tabularHeaders = fieldsStr.split(',').map(s => s.trim());
@@ -308,7 +359,6 @@ export function toonToJsonSync(toonString, returnJson = false) {
308
359
  continue;
309
360
  }
310
361
 
311
- // Standard Key-Value: key: value
312
362
  const kvMatch = trimmed.match(/^(.+?):\s*(.*)$/);
313
363
  if (kvMatch) {
314
364
  const key = kvMatch[1].trim();
@@ -332,7 +382,7 @@ export function toonToJsonSync(toonString, returnJson = false) {
332
382
  * Converts TOON to JSON format (Async)
333
383
  * @param {string} toonString - TOON formatted string
334
384
  * @param {boolean} [returnJson=false] - If true, returns JSON string; if false, returns object
335
- * @returns {Promise<Object|string>} Parsed JSON data (object or string)
385
+ * @returns {Promise<Object|string>} JSON object or JSON string
336
386
  */
337
387
  export async function toonToJson(toonString, returnJson = false) {
338
388
  return toonToJsonSync(toonString, returnJson);
@@ -0,0 +1,145 @@
1
+ /**
2
+ * CSV <-> JSON Converter (for JsonConverter)
3
+ */
4
+
5
+ import Papa from 'papaparse';
6
+ import { extractCsvFromString, extractJsonFromString, flattenObject, unflattenObject } from '../utils.js';
7
+
8
+ /**
9
+ * Convert CSV string to JSON object (Array of rows) (Sync)
10
+ * @param {string} csvString
11
+ * @returns {Array<Object>|string} JSON object or mixed text
12
+ */
13
+ export function csvToJsonSync(csvString) {
14
+ if (!csvString || typeof csvString !== 'string') {
15
+ throw new Error('Input must be a non-empty string');
16
+ }
17
+
18
+ let convertedText = csvString;
19
+ let iterationCount = 0;
20
+ const maxIterations = 100;
21
+ let wasModified = false;
22
+
23
+ // Check if pure CSV first
24
+ const firstExtract = extractCsvFromString(csvString);
25
+ if (firstExtract === csvString.trim()) {
26
+ const json = parseCsvDirectly(csvString);
27
+ return Array.isArray(json) ? json.map(row => unflattenObject(row)) : unflattenObject(json);
28
+ }
29
+
30
+ while (iterationCount < maxIterations) {
31
+ const csvBlock = extractCsvFromString(convertedText);
32
+ if (!csvBlock) break;
33
+
34
+ try {
35
+ const jsonObject = parseCsvDirectly(csvBlock);
36
+ const processedJson = Array.isArray(jsonObject) ? jsonObject.map(row => unflattenObject(row)) : unflattenObject(jsonObject);
37
+ const jsonOutput = JSON.stringify(processedJson);
38
+ convertedText = convertedText.replace(csvBlock, jsonOutput);
39
+ wasModified = true;
40
+ iterationCount++;
41
+ } catch (e) {
42
+ break;
43
+ }
44
+ }
45
+
46
+ if (wasModified) return convertedText;
47
+
48
+ try {
49
+ const json = parseCsvDirectly(csvString);
50
+ return Array.isArray(json) ? json.map(row => unflattenObject(row)) : unflattenObject(json);
51
+ } catch (e) {
52
+ return csvString;
53
+ }
54
+ }
55
+
56
+ function parseCsvDirectly(csvString) {
57
+ const results = Papa.parse(csvString, {
58
+ header: true,
59
+ dynamicTyping: true,
60
+ skipEmptyLines: true,
61
+ });
62
+
63
+ if (results.errors && results.errors.length > 0) {
64
+ throw new Error(`CSV parsing error: ${results.errors[0].message}`);
65
+ }
66
+
67
+ const jsonObject = results.data;
68
+ if (typeof jsonObject !== "object" || jsonObject === null) {
69
+ throw new Error("CSV parsing failed — cannot convert.");
70
+ }
71
+ return jsonObject;
72
+ }
73
+
74
+ /**
75
+ * Convert CSV string to JSON object (Array of rows) (Async)
76
+ * @param {string} csvString
77
+ * @returns {Promise<Array<Object>>} JSON object
78
+ */
79
+ export async function csvToJson(csvString) {
80
+ const res = csvToJsonSync(csvString);
81
+ if (typeof res === 'string' && res.trim().startsWith('[') || res.trim().startsWith('{')) {
82
+ try {
83
+ return JSON.parse(res);
84
+ } catch (e) { }
85
+ }
86
+ return res;
87
+ }
88
+
89
+ /**
90
+ * Convert JSON object to CSV string (Sync)
91
+ * @param {Array<Object>|Object|string} data
92
+ * @returns {string} CSV string
93
+ */
94
+ export function jsonToCsvSync(data) {
95
+ if (typeof data === 'string') {
96
+ let convertedText = data;
97
+ let iterationCount = 0;
98
+ const maxIterations = 100;
99
+ let wasModified = false;
100
+
101
+ const firstExtract = extractJsonFromString(data);
102
+ if (firstExtract && firstExtract === data.trim()) {
103
+ try {
104
+ const obj = JSON.parse(firstExtract);
105
+ const flatData = Array.isArray(obj) ? obj.map(row => flattenObject(row)) : [flattenObject(obj)];
106
+ return Papa.unparse(flatData, { header: true });
107
+ } catch (e) { }
108
+ }
109
+
110
+ while (iterationCount < maxIterations) {
111
+ const jsonString = extractJsonFromString(convertedText);
112
+ if (!jsonString) break;
113
+ try {
114
+ const jsonObject = JSON.parse(jsonString);
115
+ const flatData = Array.isArray(jsonObject) ? jsonObject.map(row => flattenObject(row)) : [flattenObject(jsonObject)];
116
+ const csvOutput = Papa.unparse(flatData, { header: true });
117
+ convertedText = convertedText.replace(jsonString, csvOutput);
118
+ wasModified = true;
119
+ iterationCount++;
120
+ } catch (e) { break; }
121
+ }
122
+
123
+ if (wasModified) return convertedText;
124
+
125
+ try {
126
+ const obj = JSON.parse(data);
127
+ const flatData = Array.isArray(obj) ? obj.map(row => flattenObject(row)) : [flattenObject(obj)];
128
+ return Papa.unparse(flatData, { header: true });
129
+ } catch (e) { return data; }
130
+ }
131
+
132
+ const flatData = Array.isArray(data) ? data.map(row => flattenObject(row)) : [flattenObject(data)];
133
+ return Papa.unparse(flatData, {
134
+ header: true,
135
+ });
136
+ }
137
+
138
+ /**
139
+ * Convert JSON object (Array) to CSV string (Async)
140
+ * @param {Array<Object>} jsonObject
141
+ * @returns {Promise<string>} CSV string
142
+ */
143
+ export async function jsonToCsv(jsonObject) {
144
+ return jsonToCsvSync(jsonObject);
145
+ }