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/CHANGELOG.md +39 -1
- package/README.md +152 -1
- package/package.json +14 -7
- package/src/csv.js +19 -50
- package/src/csv_formatter/index.js +496 -0
- package/src/csv_formatter/validator.js +36 -0
- package/src/index.js +9 -1
- package/src/json.js +117 -67
- package/src/json_formatter/csv.js +145 -0
- package/src/json_formatter/index.js +525 -0
- package/src/json_formatter/validator.js +29 -0
- package/src/json_formatter/xml.js +206 -0
- package/src/json_formatter/yaml.js +81 -0
- package/src/utils.js +262 -64
- package/src/xml.js +24 -79
- package/src/xml_formatter/csv.js +122 -0
- package/src/xml_formatter/index.js +488 -0
- package/src/xml_formatter/validator.js +53 -0
- package/src/yaml_formatter/csv.js +101 -0
- package/src/yaml_formatter/index.js +542 -0
- package/src/yaml_formatter/validator.js +31 -0
- package/src/yaml_formatter/xml.js +116 -0
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
|
-
*
|
|
10
|
-
* @param {*} data
|
|
11
|
-
* @param {string} key
|
|
12
|
-
* @param {number} depth
|
|
13
|
-
* @returns {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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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
|
-
// ----
|
|
94
|
+
// ---- YAML-STYLE ARRAY (nested objects or mixed types) ----
|
|
90
95
|
const lines = [];
|
|
91
96
|
lines.push(`${indent}${key}[${length}]:`);
|
|
92
|
-
|
|
93
|
-
|
|
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(
|
|
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
|
|
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>}
|
|
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
|
+
}
|