toon-formatter 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/LICENSE +21 -0
- package/README.md +406 -0
- package/package.json +54 -0
- package/src/csv.js +92 -0
- package/src/index.js +124 -0
- package/src/json.js +297 -0
- package/src/utils.js +81 -0
- package/src/validator.js +218 -0
- package/src/xml.js +174 -0
- package/src/yaml.js +41 -0
- package/test/basic.test.js +139 -0
- package/test/converters.test.js +135 -0
package/src/xml.js
ADDED
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* XML ↔ TOON Converter
|
|
3
|
+
* Note: This module is designed for Node.js environments
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { jsonToToon, toonToJson } from './json.js';
|
|
7
|
+
import { encodeXmlReservedChars } from './utils.js';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Converts XML DOM to JSON object
|
|
11
|
+
* @param {Node} xml - XML DOM node
|
|
12
|
+
* @returns {Object|string|undefined} JSON representation
|
|
13
|
+
*/
|
|
14
|
+
function xmlToJsonObject(xml) {
|
|
15
|
+
let obj = {};
|
|
16
|
+
|
|
17
|
+
if (xml.nodeType === 1) { // Element node
|
|
18
|
+
if (xml.attributes && xml.attributes.length > 0) {
|
|
19
|
+
obj["@attributes"] = {};
|
|
20
|
+
for (let j = 0; j < xml.attributes.length; j++) {
|
|
21
|
+
const attribute = xml.attributes.item(j);
|
|
22
|
+
obj["@attributes"][attribute.nodeName] = attribute.nodeValue;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
} else if (xml.nodeType === 3) { // Text node
|
|
26
|
+
const trimmedText = xml.nodeValue.trim();
|
|
27
|
+
return trimmedText === "" ? undefined : trimmedText;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (xml.hasChildNodes && xml.hasChildNodes()) {
|
|
31
|
+
for (let i = 0; i < xml.childNodes.length; i++) {
|
|
32
|
+
const item = xml.childNodes.item(i);
|
|
33
|
+
const nodeName = item.nodeName;
|
|
34
|
+
const childJson = xmlToJsonObject(item);
|
|
35
|
+
|
|
36
|
+
if (childJson === undefined) continue;
|
|
37
|
+
|
|
38
|
+
if (obj[nodeName] === undefined) {
|
|
39
|
+
obj[nodeName] = childJson;
|
|
40
|
+
} else {
|
|
41
|
+
// Handle multiple children with the same tag name (create an array)
|
|
42
|
+
if (typeof obj[nodeName].push === "undefined") {
|
|
43
|
+
const old = obj[nodeName];
|
|
44
|
+
obj[nodeName] = [];
|
|
45
|
+
obj[nodeName].push(old);
|
|
46
|
+
}
|
|
47
|
+
obj[nodeName].push(childJson);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Clean up: If the object only contains text and no attributes/children, return the text directly
|
|
53
|
+
if (Object.keys(obj).length === 1 && obj['#text'] !== undefined) {
|
|
54
|
+
return obj['#text'];
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return obj;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Converts JSON object to XML string
|
|
62
|
+
* @param {Object} obj - JSON object
|
|
63
|
+
* @returns {string} XML string
|
|
64
|
+
*/
|
|
65
|
+
function jsonObjectToXml(obj) {
|
|
66
|
+
let xml = '';
|
|
67
|
+
|
|
68
|
+
for (const key in obj) {
|
|
69
|
+
if (!obj.hasOwnProperty(key)) continue;
|
|
70
|
+
|
|
71
|
+
const value = obj[key];
|
|
72
|
+
|
|
73
|
+
if (key === "#text") {
|
|
74
|
+
// Handle text content directly
|
|
75
|
+
xml += value;
|
|
76
|
+
}
|
|
77
|
+
else if (key === '@attributes' && typeof value === 'object') {
|
|
78
|
+
// Handle attributes: Convert { "@attributes": { "id": "1" } } to id="1"
|
|
79
|
+
let attrString = '';
|
|
80
|
+
for (const attrKey in value) {
|
|
81
|
+
attrString += ` ${attrKey}="${value[attrKey]}"`;
|
|
82
|
+
}
|
|
83
|
+
xml += attrString;
|
|
84
|
+
}
|
|
85
|
+
else if (Array.isArray(value)) {
|
|
86
|
+
// Handle arrays: Loop and create a tag for each item
|
|
87
|
+
value.forEach(item => {
|
|
88
|
+
if (typeof item === 'object') {
|
|
89
|
+
const innerContent = jsonObjectToXml(item);
|
|
90
|
+
const attrMatch = innerContent.match(/^(\s+[^\s=]+="[^"]*")*/);
|
|
91
|
+
const attrs = attrMatch ? attrMatch[0] : "";
|
|
92
|
+
const body = innerContent.slice(attrs.length);
|
|
93
|
+
|
|
94
|
+
xml += `<${key}${attrs}>${body}</${key}>`;
|
|
95
|
+
} else {
|
|
96
|
+
xml += `<${key}>${item}</${key}>`;
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
else if (typeof value === 'object' && value !== null) {
|
|
101
|
+
// Handle nested objects: Recurse and wrap in the current key's tag
|
|
102
|
+
const innerContent = jsonObjectToXml(value);
|
|
103
|
+
const attrMatch = innerContent.match(/^(\s+[^\s=]+="[^"]*")*/);
|
|
104
|
+
const attrs = attrMatch ? attrMatch[0] : "";
|
|
105
|
+
const body = innerContent.slice(attrs.length);
|
|
106
|
+
|
|
107
|
+
xml += `<${key}${attrs}>${body}</${key}>`;
|
|
108
|
+
}
|
|
109
|
+
else if (value !== null && value !== undefined) {
|
|
110
|
+
// Handle primitive values: Create a simple tag
|
|
111
|
+
xml += `<${key}>${value}</${key}>`;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return xml;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Converts XML to TOON format (Browser environment)
|
|
119
|
+
* @param {string} xmlString - XML formatted string
|
|
120
|
+
* @returns {string} TOON formatted string
|
|
121
|
+
* @throws {Error} If XML is invalid or DOMParser is not available
|
|
122
|
+
*/
|
|
123
|
+
export async function xmlToToon(xmlString) {
|
|
124
|
+
if (!xmlString || typeof xmlString !== 'string') {
|
|
125
|
+
throw new Error('Input must be a non-empty string');
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Check if we're in a browser environment
|
|
129
|
+
if (typeof DOMParser !== 'undefined') {
|
|
130
|
+
const parser = new DOMParser();
|
|
131
|
+
const xmlDoc = parser.parseFromString(
|
|
132
|
+
encodeXmlReservedChars(xmlString),
|
|
133
|
+
'application/xml'
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
const parserError = xmlDoc.querySelector('parsererror');
|
|
137
|
+
if (parserError) {
|
|
138
|
+
throw new Error(parserError.textContent);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const jsonObject = xmlToJsonObject(xmlDoc);
|
|
142
|
+
return jsonToToon(jsonObject);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Node.js environment - require xmldom
|
|
146
|
+
try {
|
|
147
|
+
const { DOMParser: NodeDOMParser } = await import('xmldom');
|
|
148
|
+
const parser = new NodeDOMParser();
|
|
149
|
+
const xmlDoc = parser.parseFromString(
|
|
150
|
+
encodeXmlReservedChars(xmlString),
|
|
151
|
+
'application/xml'
|
|
152
|
+
);
|
|
153
|
+
|
|
154
|
+
const jsonObject = xmlToJsonObject(xmlDoc);
|
|
155
|
+
return jsonToToon(jsonObject);
|
|
156
|
+
} catch (error) {
|
|
157
|
+
throw new Error('XML parsing requires DOMParser (browser) or xmldom package (Node.js). Install xmldom: npm install xmldom');
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Converts TOON to XML format
|
|
163
|
+
* @param {string} toonString - TOON formatted string
|
|
164
|
+
* @returns {string} XML formatted string
|
|
165
|
+
* @throws {Error} If TOON is invalid
|
|
166
|
+
*/
|
|
167
|
+
export function toonToXml(toonString) {
|
|
168
|
+
if (!toonString || typeof toonString !== 'string') {
|
|
169
|
+
throw new Error('Input must be a non-empty string');
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const jsonObject = toonToJson(toonString);
|
|
173
|
+
return jsonObjectToXml(jsonObject);
|
|
174
|
+
}
|
package/src/yaml.js
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* YAML ↔ TOON Converter
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import yaml from 'js-yaml';
|
|
6
|
+
import { jsonToToon, toonToJson } from './json.js';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Converts YAML to TOON format
|
|
10
|
+
* @param {string} yamlString - YAML formatted string
|
|
11
|
+
* @returns {string} TOON formatted string
|
|
12
|
+
* @throws {Error} If YAML is invalid
|
|
13
|
+
*/
|
|
14
|
+
export function yamlToToon(yamlString) {
|
|
15
|
+
if (!yamlString || typeof yamlString !== 'string') {
|
|
16
|
+
throw new Error('Input must be a non-empty string');
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const jsonObject = yaml.load(yamlString);
|
|
20
|
+
|
|
21
|
+
if (typeof jsonObject !== "object" || jsonObject === null) {
|
|
22
|
+
throw new Error("YAML parsing failed — cannot convert.");
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return jsonToToon(jsonObject);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Converts TOON to YAML format
|
|
30
|
+
* @param {string} toonString - TOON formatted string
|
|
31
|
+
* @returns {string} YAML formatted string
|
|
32
|
+
* @throws {Error} If TOON is invalid
|
|
33
|
+
*/
|
|
34
|
+
export function toonToYaml(toonString) {
|
|
35
|
+
if (!toonString || typeof toonString !== 'string') {
|
|
36
|
+
throw new Error('Input must be a non-empty string');
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const jsonObject = toonToJson(toonString);
|
|
40
|
+
return yaml.dump(jsonObject);
|
|
41
|
+
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Basic tests for TOON Converter
|
|
3
|
+
* Run with: node --test test/basic.test.js
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { test } from 'node:test';
|
|
7
|
+
import assert from 'node:assert';
|
|
8
|
+
import { jsonToToon, toonToJson } from '../src/json.js';
|
|
9
|
+
import { validateToonString } from '../src/validator.js';
|
|
10
|
+
|
|
11
|
+
test('JSON to TOON - Simple Object', () => {
|
|
12
|
+
const input = { name: "Alice", age: 30, active: true };
|
|
13
|
+
const result = jsonToToon(input);
|
|
14
|
+
|
|
15
|
+
assert.ok(result.includes('name: "Alice"'));
|
|
16
|
+
assert.ok(result.includes('age: 30'));
|
|
17
|
+
assert.ok(result.includes('active: true'));
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
test('JSON to TOON - Array of Primitives', () => {
|
|
21
|
+
const input = { numbers: [1, 2, 3, 4, 5] };
|
|
22
|
+
const result = jsonToToon(input);
|
|
23
|
+
|
|
24
|
+
assert.ok(result.includes('numbers[5]: 1, 2, 3, 4, 5'));
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test('JSON to TOON - Tabular Array', () => {
|
|
28
|
+
const input = {
|
|
29
|
+
users: [
|
|
30
|
+
{ id: 1, name: "Alice", active: true },
|
|
31
|
+
{ id: 2, name: "Bob", active: false }
|
|
32
|
+
]
|
|
33
|
+
};
|
|
34
|
+
const result = jsonToToon(input);
|
|
35
|
+
|
|
36
|
+
assert.ok(result.includes('users[2]{id,name,active}:'));
|
|
37
|
+
assert.ok(result.includes('1,"Alice",true'));
|
|
38
|
+
assert.ok(result.includes('2,"Bob",false'));
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test('TOON to JSON - Simple Object', () => {
|
|
42
|
+
const input = `name: "Alice"\nage: 30\nactive: true`;
|
|
43
|
+
const result = toonToJson(input);
|
|
44
|
+
|
|
45
|
+
assert.strictEqual(result.name, "Alice");
|
|
46
|
+
assert.strictEqual(result.age, 30);
|
|
47
|
+
assert.strictEqual(result.active, true);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test('TOON to JSON - Array of Primitives', () => {
|
|
51
|
+
const input = `numbers[5]: 1, 2, 3, 4, 5`;
|
|
52
|
+
const result = toonToJson(input);
|
|
53
|
+
|
|
54
|
+
assert.ok(Array.isArray(result.numbers));
|
|
55
|
+
assert.strictEqual(result.numbers.length, 5);
|
|
56
|
+
assert.deepStrictEqual(result.numbers, [1, 2, 3, 4, 5]);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test('TOON to JSON - Tabular Array', () => {
|
|
60
|
+
const input = `users[2]{id,name,active}:\n 1,"Alice",true\n 2,"Bob",false`;
|
|
61
|
+
const result = toonToJson(input);
|
|
62
|
+
|
|
63
|
+
assert.ok(Array.isArray(result.users));
|
|
64
|
+
assert.strictEqual(result.users.length, 2);
|
|
65
|
+
assert.strictEqual(result.users[0].id, 1);
|
|
66
|
+
assert.strictEqual(result.users[0].name, "Alice");
|
|
67
|
+
assert.strictEqual(result.users[0].active, true);
|
|
68
|
+
assert.strictEqual(result.users[1].id, 2);
|
|
69
|
+
assert.strictEqual(result.users[1].name, "Bob");
|
|
70
|
+
assert.strictEqual(result.users[1].active, false);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test('Round-trip Conversion - Object', () => {
|
|
74
|
+
const original = {
|
|
75
|
+
company: "TechCorp",
|
|
76
|
+
employees: [
|
|
77
|
+
{ name: "Alice", role: "Engineer" },
|
|
78
|
+
{ name: "Bob", role: "Designer" }
|
|
79
|
+
]
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
const toon = jsonToToon(original);
|
|
83
|
+
const result = toonToJson(toon);
|
|
84
|
+
|
|
85
|
+
assert.deepStrictEqual(result, original);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test('Validator - Valid TOON', () => {
|
|
89
|
+
const input = `name: "Alice"\nage: 30`;
|
|
90
|
+
const result = validateToonString(input);
|
|
91
|
+
|
|
92
|
+
assert.strictEqual(result.isValid, true);
|
|
93
|
+
assert.strictEqual(result.error, null);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
test('Validator - Invalid TOON (Array Size Mismatch)', () => {
|
|
97
|
+
const input = `items[3]: 1, 2`; // Declared 3, but only 2 items
|
|
98
|
+
const result = validateToonString(input);
|
|
99
|
+
|
|
100
|
+
assert.strictEqual(result.isValid, false);
|
|
101
|
+
assert.ok(result.error.includes('Array size mismatch'));
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
test('Validator - Valid Tabular Array', () => {
|
|
105
|
+
const input = `users[2]{id,name}:\n 1,"Alice"\n 2,"Bob"`;
|
|
106
|
+
const result = validateToonString(input);
|
|
107
|
+
|
|
108
|
+
assert.strictEqual(result.isValid, true);
|
|
109
|
+
assert.strictEqual(result.error, null);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
test('Edge Case - Empty Object', () => {
|
|
113
|
+
const input = {};
|
|
114
|
+
const result = jsonToToon(input);
|
|
115
|
+
|
|
116
|
+
assert.strictEqual(result.trim(), '');
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
test('Edge Case - Null Value', () => {
|
|
120
|
+
const input = { value: null };
|
|
121
|
+
const result = jsonToToon(input);
|
|
122
|
+
|
|
123
|
+
assert.ok(result.includes('value: null'));
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
test('Edge Case - Nested Objects', () => {
|
|
127
|
+
const input = {
|
|
128
|
+
level1: {
|
|
129
|
+
level2: {
|
|
130
|
+
level3: "deep"
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
const toon = jsonToToon(input);
|
|
136
|
+
const result = toonToJson(toon);
|
|
137
|
+
|
|
138
|
+
assert.deepStrictEqual(result, input);
|
|
139
|
+
});
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for YAML, XML, and CSV Converters
|
|
3
|
+
* Run with: node --test test/converters.test.js
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { test } from 'node:test';
|
|
7
|
+
import assert from 'node:assert';
|
|
8
|
+
import { yamlToToon, toonToYaml } from '../src/yaml.js';
|
|
9
|
+
import { xmlToToon, toonToXml } from '../src/xml.js';
|
|
10
|
+
import { csvToToon, csvToToonSync, toonToCsv } from '../src/csv.js';
|
|
11
|
+
|
|
12
|
+
// --- YAML Tests ---
|
|
13
|
+
|
|
14
|
+
test('YAML to TOON - Simple Object', () => {
|
|
15
|
+
const yaml = `
|
|
16
|
+
name: Alice
|
|
17
|
+
age: 30
|
|
18
|
+
`;
|
|
19
|
+
const toon = yamlToToon(yaml);
|
|
20
|
+
assert.ok(toon.includes('name: "Alice"'));
|
|
21
|
+
assert.ok(toon.includes('age: 30'));
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test('TOON to YAML - Simple Object', () => {
|
|
25
|
+
const toon = `name: "Alice"\nage: 30`;
|
|
26
|
+
const yaml = toonToYaml(toon);
|
|
27
|
+
assert.ok(yaml.includes('name: Alice'));
|
|
28
|
+
assert.ok(yaml.includes('age: 30'));
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test('YAML to TOON - Nested Structure', () => {
|
|
32
|
+
const yaml = `
|
|
33
|
+
user:
|
|
34
|
+
name: Alice
|
|
35
|
+
roles:
|
|
36
|
+
- admin
|
|
37
|
+
- editor
|
|
38
|
+
`;
|
|
39
|
+
const toon = yamlToToon(yaml);
|
|
40
|
+
assert.ok(toon.includes('user:'));
|
|
41
|
+
assert.ok(toon.includes('name: "Alice"'));
|
|
42
|
+
assert.ok(toon.includes('roles[2]:'));
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
// --- XML Tests ---
|
|
46
|
+
|
|
47
|
+
test('XML to TOON - Simple Element', async () => {
|
|
48
|
+
const xml = `<user><name>Alice</name><age>30</age></user>`;
|
|
49
|
+
const toon = await xmlToToon(xml);
|
|
50
|
+
|
|
51
|
+
// Note: XML conversion wraps based on root element
|
|
52
|
+
assert.ok(toon.includes('user:'));
|
|
53
|
+
assert.ok(toon.includes('name: "Alice"'));
|
|
54
|
+
assert.ok(toon.includes('age: "30"')); // XML text content is usually string
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test('TOON to XML - Simple Element', () => {
|
|
58
|
+
const toon = `user:\n name: "Alice"\n age: 30`;
|
|
59
|
+
const xml = toonToXml(toon);
|
|
60
|
+
|
|
61
|
+
assert.ok(xml.includes('<user>'));
|
|
62
|
+
assert.ok(xml.includes('<name>Alice</name>'));
|
|
63
|
+
assert.ok(xml.includes('<age>30</age>'));
|
|
64
|
+
assert.ok(xml.includes('</user>'));
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test('XML to TOON - Attributes', async () => {
|
|
68
|
+
const xml = `<item id="123" type="widget">Content</item>`;
|
|
69
|
+
const toon = await xmlToToon(xml);
|
|
70
|
+
|
|
71
|
+
assert.ok(toon.includes('item:'));
|
|
72
|
+
assert.ok(toon.includes('@attributes:'));
|
|
73
|
+
assert.ok(toon.includes('id: "123"'));
|
|
74
|
+
assert.ok(toon.includes('type: "widget"'));
|
|
75
|
+
assert.ok(toon.includes('#text: "Content"'));
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
// --- CSV Tests ---
|
|
79
|
+
|
|
80
|
+
test('CSV to TOON - Basic (Async)', async () => {
|
|
81
|
+
const csv = `name,age,active
|
|
82
|
+
Alice,30,true
|
|
83
|
+
Bob,25,false`;
|
|
84
|
+
|
|
85
|
+
const toon = await csvToToon(csv);
|
|
86
|
+
|
|
87
|
+
// Should detect as tabular array or array of objects
|
|
88
|
+
// Since root is array, it might be [2]{name,age,active}: ...
|
|
89
|
+
|
|
90
|
+
assert.ok(toon.includes('[2]{name,age,active}:'));
|
|
91
|
+
assert.ok(toon.includes('Alice'));
|
|
92
|
+
assert.ok(toon.includes('30'));
|
|
93
|
+
assert.ok(toon.includes('true'));
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
test('CSV to TOON - Basic (Sync)', () => {
|
|
97
|
+
const csv = `id,product
|
|
98
|
+
1,Apple
|
|
99
|
+
2,Banana`;
|
|
100
|
+
|
|
101
|
+
const toon = csvToToonSync(csv);
|
|
102
|
+
|
|
103
|
+
assert.ok(toon.includes('[2]{id,product}:'));
|
|
104
|
+
assert.ok(toon.includes('1,"Apple"'));
|
|
105
|
+
assert.ok(toon.includes('2,"Banana"'));
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
test('TOON to CSV - Basic', () => {
|
|
109
|
+
const toon = `
|
|
110
|
+
[2]{name,role}:
|
|
111
|
+
"Alice","Admin"
|
|
112
|
+
"Bob","User"
|
|
113
|
+
`;
|
|
114
|
+
const csv = toonToCsv(toon);
|
|
115
|
+
|
|
116
|
+
assert.ok(csv.includes('name,role'));
|
|
117
|
+
assert.ok(csv.includes('Alice,Admin'));
|
|
118
|
+
assert.ok(csv.includes('Bob,User'));
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
test('CSV Round Trip', async () => {
|
|
122
|
+
const originalCsv = `name,score
|
|
123
|
+
Alice,100
|
|
124
|
+
Bob,95`;
|
|
125
|
+
|
|
126
|
+
const toon = await csvToToon(originalCsv);
|
|
127
|
+
const finalCsv = toonToCsv(toon);
|
|
128
|
+
|
|
129
|
+
// Note: PapaParse might add/remove quotes or change spacing, so exact match isn't always guaranteed
|
|
130
|
+
// But content should be same
|
|
131
|
+
assert.ok(finalCsv.includes('name'));
|
|
132
|
+
assert.ok(finalCsv.includes('score'));
|
|
133
|
+
assert.ok(finalCsv.includes('Alice'));
|
|
134
|
+
assert.ok(finalCsv.includes('100'));
|
|
135
|
+
});
|