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/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
+ });