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/utils.js
CHANGED
|
@@ -11,11 +11,28 @@ export function encodeXmlReservedChars(rawXmlString) {
|
|
|
11
11
|
if (typeof rawXmlString !== 'string') {
|
|
12
12
|
return '';
|
|
13
13
|
}
|
|
14
|
+
// Replace & with & but not if it's already an entity
|
|
15
|
+
return rawXmlString.replace(/&(?!#|\w+;)/g, '&');
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Sanitizes a string for use as an XML tag name.
|
|
20
|
+
* @param {string} name
|
|
21
|
+
* @returns {string} Sanitized name
|
|
22
|
+
*/
|
|
23
|
+
export function sanitizeTagName(name) {
|
|
24
|
+
if (typeof name !== 'string' || !name) {
|
|
25
|
+
return '_';
|
|
26
|
+
}
|
|
14
27
|
|
|
15
|
-
let
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
28
|
+
let sanitized = name;
|
|
29
|
+
// If name starts with non-letter/underscore (e.g. digit), prepend underscore
|
|
30
|
+
if (/^[^a-zA-Z_]/.test(sanitized)) {
|
|
31
|
+
sanitized = '_' + sanitized;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Replace invalid chars with underscore
|
|
35
|
+
return sanitized.replace(/[^a-zA-Z0-9_.]/g, '_');
|
|
19
36
|
}
|
|
20
37
|
|
|
21
38
|
/**
|
|
@@ -56,10 +73,16 @@ export function parseValue(val) {
|
|
|
56
73
|
if (val === 'null') return null;
|
|
57
74
|
if (val === '') return ""; // Empty string
|
|
58
75
|
|
|
59
|
-
// Number check
|
|
60
|
-
if (!isNaN(Number(val)) && val !== '' && !val.startsWith('0') && val !== '0') return Number(val);
|
|
76
|
+
// Number check
|
|
61
77
|
if (val === '0') return 0;
|
|
62
|
-
|
|
78
|
+
|
|
79
|
+
// If it starts with 0 but is not a decimal, it's a string (e.g. "0123")
|
|
80
|
+
if (val.startsWith('0') && !val.startsWith('0.') && val.length > 1) {
|
|
81
|
+
// String
|
|
82
|
+
} else {
|
|
83
|
+
const num = Number(val);
|
|
84
|
+
if (!isNaN(num) && val !== '') return num;
|
|
85
|
+
}
|
|
63
86
|
|
|
64
87
|
// String unquoting
|
|
65
88
|
if (val.startsWith('"') && val.endsWith('"')) {
|
|
@@ -76,8 +99,10 @@ export function parseValue(val) {
|
|
|
76
99
|
*/
|
|
77
100
|
export function formatValue(v) {
|
|
78
101
|
if (v === null) return "null";
|
|
102
|
+
if (v === true) return "true";
|
|
103
|
+
if (v === false) return "false";
|
|
79
104
|
if (typeof v === "string") return `"${v.replace(/"/g, '\\"')}"`;
|
|
80
|
-
return v;
|
|
105
|
+
return String(v);
|
|
81
106
|
}
|
|
82
107
|
|
|
83
108
|
/**
|
|
@@ -88,62 +113,84 @@ export function formatValue(v) {
|
|
|
88
113
|
export function extractJsonFromString(text) {
|
|
89
114
|
if (!text || typeof text !== 'string') return null;
|
|
90
115
|
|
|
91
|
-
let
|
|
116
|
+
let searchStart = 0;
|
|
92
117
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
118
|
+
while (searchStart < text.length) {
|
|
119
|
+
let startIndex = -1;
|
|
120
|
+
|
|
121
|
+
// Find first potential start
|
|
122
|
+
for (let i = searchStart; i < text.length; i++) {
|
|
123
|
+
if (text[i] === '{' || text[i] === '[') {
|
|
124
|
+
// Ignore if preceded by non-whitespace (e.g. key[2]),
|
|
125
|
+
// unless it's a closing bracket/brace or XML tag end
|
|
126
|
+
if (i > 0 && /\S/.test(text[i - 1]) && !/[\}\]>]/.test(text[i - 1])) {
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
startIndex = i;
|
|
130
|
+
break;
|
|
99
131
|
}
|
|
100
|
-
startIndex = i;
|
|
101
|
-
break;
|
|
102
132
|
}
|
|
103
|
-
}
|
|
104
133
|
|
|
105
|
-
|
|
134
|
+
if (startIndex === -1) return null;
|
|
106
135
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
136
|
+
let balance = 0;
|
|
137
|
+
let inQuote = false;
|
|
138
|
+
let escape = false;
|
|
110
139
|
|
|
111
|
-
|
|
112
|
-
|
|
140
|
+
for (let i = startIndex; i < text.length; i++) {
|
|
141
|
+
const char = text[i];
|
|
113
142
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
if (char === '\\') {
|
|
120
|
-
escape = true;
|
|
121
|
-
continue;
|
|
122
|
-
}
|
|
143
|
+
if (escape) {
|
|
144
|
+
escape = false;
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
123
147
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
148
|
+
if (char === '\\') {
|
|
149
|
+
escape = true;
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
128
152
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
} else if (char === '}' || char === ']') {
|
|
133
|
-
balance--;
|
|
153
|
+
if (char === '"') {
|
|
154
|
+
inQuote = !inQuote;
|
|
155
|
+
continue;
|
|
134
156
|
}
|
|
135
157
|
|
|
136
|
-
if (
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
158
|
+
if (!inQuote) {
|
|
159
|
+
if (char === '{' || char === '[') {
|
|
160
|
+
balance++;
|
|
161
|
+
} else if (char === '}' || char === ']') {
|
|
162
|
+
balance--;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (balance === 0) {
|
|
166
|
+
const candidate = text.substring(startIndex, i + 1);
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
// Avoid matching TOON arrays (e.g. [3]: 1, 2, 3)
|
|
170
|
+
if (/^\s*\[\d+\]/.test(candidate)) {
|
|
171
|
+
searchStart = i + 1;
|
|
172
|
+
startIndex = -1;
|
|
173
|
+
break; // Break inner for loop, restart while loop from searchStart
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
try {
|
|
177
|
+
JSON.parse(candidate);
|
|
178
|
+
return candidate;
|
|
179
|
+
} catch (e) {
|
|
180
|
+
// If balanced but not valid JSON (like {id,name}),
|
|
181
|
+
// it's likely a false start. Abandon this startIndex and try next.
|
|
182
|
+
searchStart = startIndex + 1;
|
|
183
|
+
startIndex = -1;
|
|
184
|
+
break; // Break inner for loop, restart while loop from searchStart
|
|
185
|
+
}
|
|
144
186
|
}
|
|
145
187
|
}
|
|
146
188
|
}
|
|
189
|
+
|
|
190
|
+
if (startIndex !== -1) {
|
|
191
|
+
// Reached end without balancing for this startIndex
|
|
192
|
+
searchStart = startIndex + 1;
|
|
193
|
+
}
|
|
147
194
|
}
|
|
148
195
|
|
|
149
196
|
return null;
|
|
@@ -157,23 +204,22 @@ export function extractJsonFromString(text) {
|
|
|
157
204
|
export function extractXmlFromString(text) {
|
|
158
205
|
if (!text || typeof text !== 'string') return null;
|
|
159
206
|
|
|
160
|
-
// Find first start tag
|
|
161
|
-
const startTagRegex = /<([a-zA-Z0-9_:-]+)(?:\s[^>]*)
|
|
207
|
+
// Find first start tag (including self-closing)
|
|
208
|
+
const startTagRegex = /<([a-zA-Z0-9_:-]+)(?:\s[^>]*)?\/?>/;
|
|
162
209
|
const match = text.match(startTagRegex);
|
|
163
210
|
|
|
164
211
|
if (!match) return null;
|
|
165
212
|
|
|
166
213
|
const startIndex = match.index;
|
|
167
214
|
const rootTagName = match[1];
|
|
168
|
-
|
|
169
215
|
const fullMatch = match[0];
|
|
216
|
+
|
|
170
217
|
if (fullMatch.endsWith('/>')) {
|
|
171
218
|
return fullMatch;
|
|
172
219
|
}
|
|
173
220
|
|
|
174
221
|
let balance = 0;
|
|
175
|
-
|
|
176
|
-
const tagRegex = /<\/?([a-zA-Z0-9_:-]+)(?:\s[^>]*)?\/?\>/g;
|
|
222
|
+
const tagRegex = /<\/?([a-zA-Z0-9_:-]+)(?:\s[^>]*)?\/?>/g;
|
|
177
223
|
tagRegex.lastIndex = startIndex;
|
|
178
224
|
|
|
179
225
|
let matchTag;
|
|
@@ -190,7 +236,7 @@ export function extractXmlFromString(text) {
|
|
|
190
236
|
}
|
|
191
237
|
|
|
192
238
|
if (balance === 0) {
|
|
193
|
-
return text.substring(startIndex,
|
|
239
|
+
return text.substring(startIndex, tagRegex.lastIndex);
|
|
194
240
|
}
|
|
195
241
|
}
|
|
196
242
|
|
|
@@ -208,25 +254,53 @@ export function extractCsvFromString(text) {
|
|
|
208
254
|
const lines = text.split('\n');
|
|
209
255
|
let startLineIndex = -1;
|
|
210
256
|
|
|
211
|
-
|
|
257
|
+
const isJsonLike = (line) => {
|
|
258
|
+
const trimmed = line.trim();
|
|
259
|
+
return /^"[^"]+"\s*:/.test(trimmed) || /^[\{\[]/.test(trimmed) || /^[\}\]],?$/.test(trimmed);
|
|
260
|
+
};
|
|
261
|
+
const isYamlLike = (line) => {
|
|
262
|
+
const trimmed = line.trim();
|
|
263
|
+
return /^- /.test(trimmed) || /^[^",]+:\s/.test(trimmed);
|
|
264
|
+
};
|
|
265
|
+
const isXmlLike = (line) => {
|
|
266
|
+
const trimmed = line.trim();
|
|
267
|
+
return trimmed.startsWith('<') && trimmed.includes('>');
|
|
268
|
+
};
|
|
269
|
+
const isToonStructure = (line) => {
|
|
270
|
+
const trimmed = line.trim();
|
|
271
|
+
return /^.*?\[\d+\].*:\s*$/.test(trimmed);
|
|
272
|
+
};
|
|
273
|
+
|
|
274
|
+
let i = 0;
|
|
275
|
+
while (i < lines.length) {
|
|
212
276
|
const line = lines[i];
|
|
277
|
+
|
|
278
|
+
if (isToonStructure(line)) {
|
|
279
|
+
const countMatch = line.match(/\[(\d+)\]/);
|
|
280
|
+
const count = countMatch ? parseInt(countMatch[1], 10) : 0;
|
|
281
|
+
i += count + 1;
|
|
282
|
+
continue;
|
|
283
|
+
}
|
|
284
|
+
|
|
213
285
|
const commaCount = (line.match(/,/g) || []).length;
|
|
214
286
|
if (commaCount > 0) {
|
|
215
|
-
|
|
216
|
-
|
|
287
|
+
if (!isJsonLike(line) && !isYamlLike(line) && !isXmlLike(line)) {
|
|
288
|
+
startLineIndex = i;
|
|
289
|
+
break;
|
|
290
|
+
}
|
|
217
291
|
}
|
|
292
|
+
i++;
|
|
218
293
|
}
|
|
219
294
|
|
|
220
295
|
if (startLineIndex === -1) return null;
|
|
221
296
|
|
|
222
297
|
const resultLines = [];
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
const line = lines[i];
|
|
298
|
+
for (let j = startLineIndex; j < lines.length; j++) {
|
|
299
|
+
const line = lines[j];
|
|
226
300
|
if (line.trim() === '') continue;
|
|
227
301
|
|
|
228
302
|
const commaCount = (line.match(/,/g) || []).length;
|
|
229
|
-
if (commaCount === 0) {
|
|
303
|
+
if (commaCount === 0 || isJsonLike(line) || isYamlLike(line) || isXmlLike(line)) {
|
|
230
304
|
break;
|
|
231
305
|
}
|
|
232
306
|
resultLines.push(line);
|
|
@@ -234,10 +308,134 @@ export function extractCsvFromString(text) {
|
|
|
234
308
|
|
|
235
309
|
const result = resultLines.join('\n').trim();
|
|
236
310
|
|
|
237
|
-
// Avoid matching TOON arrays
|
|
311
|
+
// Avoid matching TOON arrays
|
|
238
312
|
if (/^\s*(\w+)?\[\d+\]/.test(result)) {
|
|
239
313
|
return null;
|
|
240
314
|
}
|
|
241
315
|
|
|
316
|
+
// Final check for JSON-like start
|
|
317
|
+
if (/^[\{\[]/.test(result)) {
|
|
318
|
+
return null;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
return result;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* Flattens a JSON object/list recursivey.
|
|
326
|
+
* @param {*} obj
|
|
327
|
+
* @param {string} prefix
|
|
328
|
+
* @param {Object} result
|
|
329
|
+
* @returns {Object}
|
|
330
|
+
*/
|
|
331
|
+
export function flattenObject(obj, prefix = '', result = {}) {
|
|
332
|
+
if (obj === null || typeof obj === 'undefined') {
|
|
333
|
+
result[prefix] = null;
|
|
334
|
+
return result;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// Try parsing string if it looks like JSON
|
|
338
|
+
if (typeof obj === 'string') {
|
|
339
|
+
const trimmed = obj.trim();
|
|
340
|
+
if ((trimmed.startsWith('{') && trimmed.endsWith('}')) || (trimmed.startsWith('[') && trimmed.endsWith(']'))) {
|
|
341
|
+
try {
|
|
342
|
+
const parsed = JSON.parse(obj);
|
|
343
|
+
return flattenObject(parsed, prefix, result);
|
|
344
|
+
} catch (e) { }
|
|
345
|
+
}
|
|
346
|
+
result[prefix] = obj;
|
|
347
|
+
return result;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
if (Array.isArray(obj)) {
|
|
351
|
+
for (let i = 0; i < obj.length; i++) {
|
|
352
|
+
const newKey = prefix ? `${prefix}.${i}` : `${i}`;
|
|
353
|
+
flattenObject(obj[i], newKey, result);
|
|
354
|
+
}
|
|
355
|
+
} else if (typeof obj === 'object') {
|
|
356
|
+
for (const key in obj) {
|
|
357
|
+
const newKey = prefix ? `${prefix}.${key}` : key;
|
|
358
|
+
flattenObject(obj[key], newKey, result);
|
|
359
|
+
}
|
|
360
|
+
} else {
|
|
361
|
+
result[prefix] = obj;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
return result;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* Unflattens a JSON object (reverses flattening).
|
|
369
|
+
* @param {Object} data
|
|
370
|
+
* @returns {Object}
|
|
371
|
+
*/
|
|
372
|
+
export function unflattenObject(data) {
|
|
373
|
+
if (typeof data !== 'object' || data === null || Array.isArray(data)) {
|
|
374
|
+
return data;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
const hasDot = Object.keys(data).some(k => k.includes('.'));
|
|
378
|
+
if (!hasDot) return data;
|
|
379
|
+
|
|
380
|
+
const result = {};
|
|
381
|
+
for (const key in data) {
|
|
382
|
+
const parts = key.split('.');
|
|
383
|
+
let current = result;
|
|
384
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
385
|
+
const part = parts[i];
|
|
386
|
+
if (!(part in current)) {
|
|
387
|
+
current[part] = {};
|
|
388
|
+
}
|
|
389
|
+
current = current[part];
|
|
390
|
+
}
|
|
391
|
+
current[parts[parts.length - 1]] = data[key];
|
|
392
|
+
}
|
|
242
393
|
return result;
|
|
243
394
|
}
|
|
395
|
+
|
|
396
|
+
/**
|
|
397
|
+
* Helper to build an XML tag string from a key and value object.
|
|
398
|
+
* @param {string} key
|
|
399
|
+
* @param {*} value
|
|
400
|
+
* @returns {string}
|
|
401
|
+
*/
|
|
402
|
+
export function buildTag(key, value) {
|
|
403
|
+
const sanitizedKey = sanitizeTagName(key);
|
|
404
|
+
|
|
405
|
+
if (value === null || typeof value === 'undefined') {
|
|
406
|
+
return `<${sanitizedKey} />`;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
if (typeof value === 'object' && !Array.isArray(value)) {
|
|
410
|
+
let attrs = '';
|
|
411
|
+
let content = '';
|
|
412
|
+
|
|
413
|
+
if (value['@attributes']) {
|
|
414
|
+
for (const k in value['@attributes']) {
|
|
415
|
+
attrs += ` ${k}="${value['@attributes'][k]}"`;
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
for (const k in value) {
|
|
420
|
+
if (k === '@attributes') continue;
|
|
421
|
+
if (k === '#text') {
|
|
422
|
+
content += String(value[k]);
|
|
423
|
+
} else {
|
|
424
|
+
const val = value[k];
|
|
425
|
+
if (Array.isArray(val)) {
|
|
426
|
+
val.forEach(item => {
|
|
427
|
+
content += buildTag(k, item);
|
|
428
|
+
});
|
|
429
|
+
} else {
|
|
430
|
+
content += buildTag(k, val);
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
return `<${sanitizedKey}${attrs}>${content}</${sanitizedKey}>`;
|
|
435
|
+
} else if (Array.isArray(value)) {
|
|
436
|
+
// This shouldn't happen if called correctly via parent, but for safety:
|
|
437
|
+
return value.map(item => buildTag(key, item)).join('');
|
|
438
|
+
} else {
|
|
439
|
+
return `<${sanitizedKey}>${value}</${sanitizedKey}>`;
|
|
440
|
+
}
|
|
441
|
+
}
|
package/src/xml.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { jsonToToonSync, toonToJsonSync } from './json.js';
|
|
7
|
-
import { encodeXmlReservedChars, extractXmlFromString } from './utils.js';
|
|
7
|
+
import { encodeXmlReservedChars, extractXmlFromString, buildTag } from './utils.js';
|
|
8
8
|
|
|
9
9
|
/**
|
|
10
10
|
* Converts XML DOM to JSON object
|
|
@@ -31,6 +31,10 @@ function xmlToJsonObject(xml) {
|
|
|
31
31
|
for (let i = 0; i < xml.childNodes.length; i++) {
|
|
32
32
|
const item = xml.childNodes.item(i);
|
|
33
33
|
const nodeName = item.nodeName;
|
|
34
|
+
|
|
35
|
+
// Skip comment nodes
|
|
36
|
+
if (item.nodeType === 8) continue;
|
|
37
|
+
|
|
34
38
|
const childJson = xmlToJsonObject(item);
|
|
35
39
|
|
|
36
40
|
if (childJson === undefined) continue;
|
|
@@ -38,8 +42,7 @@ function xmlToJsonObject(xml) {
|
|
|
38
42
|
if (obj[nodeName] === undefined) {
|
|
39
43
|
obj[nodeName] = childJson;
|
|
40
44
|
} else {
|
|
41
|
-
|
|
42
|
-
if (typeof obj[nodeName].push === "undefined") {
|
|
45
|
+
if (!Array.isArray(obj[nodeName])) {
|
|
43
46
|
const old = obj[nodeName];
|
|
44
47
|
obj[nodeName] = [];
|
|
45
48
|
obj[nodeName].push(old);
|
|
@@ -49,71 +52,15 @@ function xmlToJsonObject(xml) {
|
|
|
49
52
|
}
|
|
50
53
|
}
|
|
51
54
|
|
|
52
|
-
//
|
|
53
|
-
|
|
55
|
+
// Special case: if object only has #text and no attributes/children, return text directly
|
|
56
|
+
const keys = Object.keys(obj);
|
|
57
|
+
if (keys.length === 1 && keys[0] === '#text' && !obj['@attributes']) {
|
|
54
58
|
return obj['#text'];
|
|
55
59
|
}
|
|
56
60
|
|
|
57
61
|
return obj;
|
|
58
62
|
}
|
|
59
63
|
|
|
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
64
|
/**
|
|
118
65
|
* Internal core function to convert pure XML string to TOON (Sync)
|
|
119
66
|
* @param {string} xmlString
|
|
@@ -134,21 +81,17 @@ function parseXmlToToonSync(xmlString) {
|
|
|
134
81
|
'application/xml'
|
|
135
82
|
);
|
|
136
83
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
throw new Error(parserError.textContent);
|
|
143
|
-
}
|
|
144
|
-
} else {
|
|
145
|
-
// xmldom environment - check documentElement
|
|
146
|
-
if (xmlDoc.documentElement && xmlDoc.documentElement.nodeName === 'parsererror') {
|
|
147
|
-
throw new Error(xmlDoc.documentElement.textContent || 'XML parsing error');
|
|
148
|
-
}
|
|
84
|
+
const parserError = xmlDoc.querySelector ? xmlDoc.querySelector('parsererror') :
|
|
85
|
+
(xmlDoc.documentElement && xmlDoc.documentElement.nodeName === 'parsererror' ? xmlDoc.documentElement : null);
|
|
86
|
+
|
|
87
|
+
if (parserError) {
|
|
88
|
+
throw new Error(parserError.textContent || 'XML parsing error');
|
|
149
89
|
}
|
|
150
90
|
|
|
151
|
-
const
|
|
91
|
+
const rootElement = xmlDoc.documentElement;
|
|
92
|
+
const jsonObject = {};
|
|
93
|
+
jsonObject[rootElement.nodeName] = xmlToJsonObject(rootElement);
|
|
94
|
+
|
|
152
95
|
return jsonToToonSync(jsonObject);
|
|
153
96
|
}
|
|
154
97
|
|
|
@@ -194,9 +137,7 @@ export async function xmlToToon(xmlString) {
|
|
|
194
137
|
try {
|
|
195
138
|
const { DOMParser: NodeDOMParser } = await import('xmldom');
|
|
196
139
|
global.DOMParser = NodeDOMParser;
|
|
197
|
-
} catch (e) {
|
|
198
|
-
// Ignore if import fails, xmlToToonSync will throw appropriate error
|
|
199
|
-
}
|
|
140
|
+
} catch (e) { }
|
|
200
141
|
}
|
|
201
142
|
return xmlToToonSync(xmlString);
|
|
202
143
|
}
|
|
@@ -213,7 +154,11 @@ export function toonToXmlSync(toonString) {
|
|
|
213
154
|
}
|
|
214
155
|
|
|
215
156
|
const jsonObject = toonToJsonSync(toonString);
|
|
216
|
-
|
|
157
|
+
let xml = "";
|
|
158
|
+
for (const k in jsonObject) {
|
|
159
|
+
xml += buildTag(k, jsonObject[k]);
|
|
160
|
+
}
|
|
161
|
+
return xml;
|
|
217
162
|
}
|
|
218
163
|
|
|
219
164
|
/**
|