Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e8f81bb584 | |||
| 4179b4010a | |||
| 15ac52b6a0 | |||
| c778f3d280 | |||
| d0af3a63bc | |||
| bf73d2670b | |||
| 361613f507 | |||
| 681c89d5af |
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@ztimson/utils",
|
"name": "@ztimson/utils",
|
||||||
"version": "0.28.14",
|
"version": "0.29.1",
|
||||||
"description": "Utility library",
|
"description": "Utility library",
|
||||||
"author": "Zak Timson",
|
"author": "Zak Timson",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|||||||
47
src/csv.ts
47
src/csv.ts
@@ -14,41 +14,55 @@ import {LETTER_LIST} from './string.ts';
|
|||||||
export function fromCsv<T = any>(csv: string, hasHeaders = true): T[] {
|
export function fromCsv<T = any>(csv: string, hasHeaders = true): T[] {
|
||||||
function parseLine(line: string): (string | null)[] {
|
function parseLine(line: string): (string | null)[] {
|
||||||
const columns: string[] = [];
|
const columns: string[] = [];
|
||||||
let current = '', inQuotes = false;
|
let current = '', inQuotes = false, quoteChar: string | null = null;
|
||||||
for (let i = 0; i < line.length; i++) {
|
for (let i = 0; i < line.length; i++) {
|
||||||
const char = line[i];
|
const char = line[i];
|
||||||
const nextChar = line[i + 1];
|
const nextChar = line[i + 1];
|
||||||
if (char === '"') {
|
if ((char === '"' || char === "'") && !inQuotes) {
|
||||||
if (inQuotes && nextChar === '"') {
|
inQuotes = true;
|
||||||
current += '"'; // Handle escaped quotes
|
quoteChar = char;
|
||||||
|
} else if (char === quoteChar && inQuotes) {
|
||||||
|
if (nextChar === quoteChar) {
|
||||||
|
current += quoteChar; // Handle escaped quotes
|
||||||
i++;
|
i++;
|
||||||
} else inQuotes = !inQuotes;
|
} else {
|
||||||
|
inQuotes = false;
|
||||||
|
quoteChar = null;
|
||||||
|
}
|
||||||
} else if (char === ',' && !inQuotes) {
|
} else if (char === ',' && !inQuotes) {
|
||||||
columns.push(current.trim()); // Trim column values
|
columns.push(current.trim());
|
||||||
current = '';
|
current = '';
|
||||||
} else current += char;
|
} else current += char;
|
||||||
}
|
}
|
||||||
columns.push(current.trim()); // Trim last column value
|
columns.push(current.trim());
|
||||||
return columns.map(col => col.replace(/^"|"$/g, '').replace(/""/g, '"'));
|
return columns.map(col => {
|
||||||
|
// Remove surrounding quotes (both " and ')
|
||||||
|
col = col.replace(/^["']|["']$/g, '');
|
||||||
|
// Unescape doubled quotes
|
||||||
|
return col.replace(/""/g, '"').replace(/''/g, "'");
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Normalize line endings and split rows
|
|
||||||
const rows = [];
|
const rows = [];
|
||||||
let currentRow = '', inQuotes = false;
|
let currentRow = '', inQuotes = false, quoteChar: string | null = null;
|
||||||
for (const char of csv.replace(/\r\n/g, '\n')) { // Normalize \r\n to \n
|
for (const char of csv.replace(/\r\n/g, '\n')) {
|
||||||
if (char === '"') inQuotes = !inQuotes;
|
if ((char === '"' || char === "'") && !inQuotes) {
|
||||||
|
inQuotes = true;
|
||||||
|
quoteChar = char;
|
||||||
|
} else if (char === quoteChar && inQuotes) {
|
||||||
|
inQuotes = false;
|
||||||
|
quoteChar = null;
|
||||||
|
}
|
||||||
if (char === '\n' && !inQuotes) {
|
if (char === '\n' && !inQuotes) {
|
||||||
rows.push(currentRow.trim()); // Trim row
|
rows.push(currentRow.trim());
|
||||||
currentRow = '';
|
currentRow = '';
|
||||||
} else currentRow += char;
|
} else currentRow += char;
|
||||||
}
|
}
|
||||||
if (currentRow) rows.push(currentRow.trim()); // Trim last row
|
if (currentRow) rows.push(currentRow.trim());
|
||||||
|
|
||||||
// Extract headers
|
|
||||||
let headers: any = hasHeaders ? rows.splice(0, 1)[0] : null;
|
let headers: any = hasHeaders ? rows.splice(0, 1)[0] : null;
|
||||||
if (headers) headers = headers.match(/(?:[^,"']+|"(?:[^"]|"")*"|'(?:[^']|'')*')+/g)?.map((h: any) => h.trim());
|
if (headers) headers = headers.match(/(?:[^,"']+|"(?:[^"]|"")*"|'(?:[^']|'')*')+/g)?.map((h: any) => h.trim());
|
||||||
|
|
||||||
// Parse rows
|
|
||||||
return <T[]>rows.map(r => {
|
return <T[]>rows.map(r => {
|
||||||
const props = parseLine(r);
|
const props = parseLine(r);
|
||||||
const h = headers || (Array(props.length).fill(null).map((_, i) => {
|
const h = headers || (Array(props.length).fill(null).map((_, i) => {
|
||||||
@@ -65,7 +79,6 @@ export function fromCsv<T = any>(csv: string, hasHeaders = true): T[] {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Convert an array of objects to a CSV string
|
* Convert an array of objects to a CSV string
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
export * from 'var-persist';
|
||||||
|
|
||||||
export * from './arg-parser';
|
export * from './arg-parser';
|
||||||
export * from './array';
|
export * from './array';
|
||||||
export * from './aset';
|
export * from './aset';
|
||||||
@@ -23,4 +25,4 @@ export * from './template';
|
|||||||
export * from './time';
|
export * from './time';
|
||||||
export * from './tts';
|
export * from './tts';
|
||||||
export * from './types';
|
export * from './types';
|
||||||
export * from 'var-persist';
|
export * from './xml';
|
||||||
|
|||||||
@@ -27,6 +27,31 @@ export function camelCase(str?: string): string {
|
|||||||
return pascal.charAt(0).toLowerCase() + pascal.slice(1);
|
return pascal.charAt(0).toLowerCase() + pascal.slice(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decode HTML escaped characters
|
||||||
|
* @param html HTML to clean up
|
||||||
|
* @returns {any}
|
||||||
|
*/
|
||||||
|
export function decodeHtml(html: string) {
|
||||||
|
return html
|
||||||
|
.replace(/ /g, '\u00A0')
|
||||||
|
.replace(/"/g, '"')
|
||||||
|
.replace(/'/g, "'")
|
||||||
|
.replace(/</g, '<')
|
||||||
|
.replace(/>/g, '>')
|
||||||
|
.replace(/¢/g, '¢')
|
||||||
|
.replace(/£/g, '£')
|
||||||
|
.replace(/¥/g, '¥')
|
||||||
|
.replace(/€/g, '€')
|
||||||
|
.replace(/©/g, '©')
|
||||||
|
.replace(/®/g, '®')
|
||||||
|
.replace(/™/g, '™')
|
||||||
|
.replace(/×/g, '×')
|
||||||
|
.replace(/÷/g, '÷')
|
||||||
|
.replace(/&#(\d+);/g, (match, dec) => String.fromCharCode(dec))
|
||||||
|
.replace(/&#x([0-9a-fA-F]+);/g, (match, hex) => String.fromCharCode(parseInt(hex, 16)))
|
||||||
|
.replace(/&/g, '&'); // Always last!
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Convert number of bytes into a human-readable size
|
* Convert number of bytes into a human-readable size
|
||||||
|
|||||||
199
src/xml.ts
Normal file
199
src/xml.ts
Normal file
@@ -0,0 +1,199 @@
|
|||||||
|
/**
|
||||||
|
* Parses an XML string into a structured JavaScript object.
|
||||||
|
* @param {string} xml - The XML string to parse
|
||||||
|
* @returns {Object} An object with `tag`, `attributes`, and `children` properties
|
||||||
|
*/
|
||||||
|
/**
|
||||||
|
* Parses an XML string into a structured JavaScript object (fast-xml-parser format).
|
||||||
|
* @param {string} xml - The XML string to parse
|
||||||
|
* @returns {Object} An object with tag names as keys and text content or nested objects as values
|
||||||
|
*/
|
||||||
|
export function fromXml(xml: string) {
|
||||||
|
xml = xml.trim();
|
||||||
|
let pos = 0;
|
||||||
|
|
||||||
|
function parseNode(): any {
|
||||||
|
skipWhitespace();
|
||||||
|
if(xml[pos] !== '<') return parseText();
|
||||||
|
pos++; // skip <
|
||||||
|
|
||||||
|
if(xml[pos] === '?') {
|
||||||
|
const declaration = parseDeclaration();
|
||||||
|
return { ['?' + declaration]: '', ...parseNode() };
|
||||||
|
}
|
||||||
|
|
||||||
|
if(xml[pos] === '!') {
|
||||||
|
parseComment();
|
||||||
|
return parseNode();
|
||||||
|
}
|
||||||
|
|
||||||
|
const tagName = parseTagName();
|
||||||
|
const attributes = parseAttributes();
|
||||||
|
skipWhitespace();
|
||||||
|
|
||||||
|
if(xml[pos] === '/' && xml[pos + 1] === '>') {
|
||||||
|
pos += 2; // skip />
|
||||||
|
return { [tagName]: '' };
|
||||||
|
}
|
||||||
|
|
||||||
|
pos++; // skip >
|
||||||
|
const children: any[] = [];
|
||||||
|
let textContent = '';
|
||||||
|
|
||||||
|
while(pos < xml.length) {
|
||||||
|
skipWhitespace();
|
||||||
|
if(xml[pos] === '<' && xml[pos + 1] === '/') {
|
||||||
|
pos += 2; // skip </
|
||||||
|
parseTagName(); // skip closing tag name
|
||||||
|
skipWhitespace();
|
||||||
|
pos++; // skip >
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
const startPos = pos;
|
||||||
|
const child = parseNode();
|
||||||
|
if(typeof child === 'string') {
|
||||||
|
textContent += child;
|
||||||
|
} else if(child) {
|
||||||
|
children.push(child);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If only text content, return simple value
|
||||||
|
if(children.length === 0 && textContent) {
|
||||||
|
const value = isNumeric(textContent) ? Number(textContent) : textContent;
|
||||||
|
return { [tagName]: value };
|
||||||
|
}
|
||||||
|
|
||||||
|
// If only text with no children
|
||||||
|
if(children.length === 0) {
|
||||||
|
return { [tagName]: '' };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Merge children into object
|
||||||
|
const result: any = {};
|
||||||
|
for(const child of children) {
|
||||||
|
for(const [key, value] of Object.entries(child)) {
|
||||||
|
if(result[key]) {
|
||||||
|
// Convert to array if duplicate tags
|
||||||
|
if(!Array.isArray(result[key])) {
|
||||||
|
result[key] = [result[key]];
|
||||||
|
}
|
||||||
|
result[key].push(value);
|
||||||
|
} else {
|
||||||
|
result[key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { [tagName]: result };
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseTagName() {
|
||||||
|
let name = '';
|
||||||
|
while (pos < xml.length && /[a-zA-Z0-9_:-]/.test(xml[pos])) name += xml[pos++];
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseAttributes() {
|
||||||
|
const attrs: any = {};
|
||||||
|
while (pos < xml.length) {
|
||||||
|
skipWhitespace();
|
||||||
|
if (xml[pos] === '>' || xml[pos] === '/') break;
|
||||||
|
const name = parseTagName();
|
||||||
|
skipWhitespace();
|
||||||
|
if (xml[pos] === '=') {
|
||||||
|
pos++;
|
||||||
|
skipWhitespace();
|
||||||
|
const quote = xml[pos++];
|
||||||
|
let value = '';
|
||||||
|
while (xml[pos] !== quote) value += xml[pos++];
|
||||||
|
pos++; // skip closing quote
|
||||||
|
attrs[name] = escapeXml(value, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return attrs;
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseText() {
|
||||||
|
let text = '';
|
||||||
|
while (pos < xml.length && xml[pos] !== '<') text += xml[pos++];
|
||||||
|
text = text.trim();
|
||||||
|
return text ? escapeXml(text, true) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseDeclaration() {
|
||||||
|
pos++; // skip ?
|
||||||
|
let name = '';
|
||||||
|
while (pos < xml.length && xml[pos] !== ' ' && xml[pos] !== '?') {
|
||||||
|
name += xml[pos++];
|
||||||
|
}
|
||||||
|
while (xml[pos] !== '>') pos++;
|
||||||
|
pos++;
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseComment() {
|
||||||
|
while (!(xml[pos] === '-' && xml[pos + 1] === '-' && xml[pos + 2] === '>')) pos++;
|
||||||
|
pos += 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
function skipWhitespace() {
|
||||||
|
while (pos < xml.length && /\s/.test(xml[pos])) pos++;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isNumeric(str: string) {
|
||||||
|
return !isNaN(Number(str)) && !isNaN(parseFloat(str)) && str.trim() !== '';
|
||||||
|
}
|
||||||
|
|
||||||
|
return parseNode();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts a JavaScript object into an XML string.
|
||||||
|
* @param {Object} obj - Object with `tag`, `attributes`, and `children` properties, or a string
|
||||||
|
* @param {string} indent - Current indentation level (used internally for formatting)
|
||||||
|
* @returns {string} The formatted XML string
|
||||||
|
*/
|
||||||
|
export function toXml(obj: any, indent = '') {
|
||||||
|
if(typeof obj === 'string') return escapeXml(obj);
|
||||||
|
const { tag, attributes = {}, children = [] } = obj;
|
||||||
|
let xml = `${indent}<${tag}`;
|
||||||
|
for (const [key, value] of Object.entries(attributes))
|
||||||
|
xml += ` ${key}="${escapeXml(<any>value)}"`;
|
||||||
|
if (children.length === 0) {
|
||||||
|
xml += ' />';
|
||||||
|
return xml;
|
||||||
|
}
|
||||||
|
xml += '>';
|
||||||
|
const hasComplexChildren = children.some((c: any) => typeof c === 'object');
|
||||||
|
for (const child of children) {
|
||||||
|
if (hasComplexChildren) xml += '\n';
|
||||||
|
xml += toXml(child, hasComplexChildren ? indent + ' ' : '');
|
||||||
|
}
|
||||||
|
if(hasComplexChildren) xml += `\n${indent}`;
|
||||||
|
xml += `</${tag}>`;
|
||||||
|
return xml;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Escapes or unescapes XML special characters.
|
||||||
|
* @param {string} str - The string to process
|
||||||
|
* @param {boolean} decode - If true, decodes XML entities; if false, encodes special characters
|
||||||
|
* @returns {string} The processed string
|
||||||
|
*/
|
||||||
|
export function escapeXml(str: string, decode = false) {
|
||||||
|
if(decode) {
|
||||||
|
return str
|
||||||
|
.replace(/</g, '<')
|
||||||
|
.replace(/>/g, '>')
|
||||||
|
.replace(/"/g, '"')
|
||||||
|
.replace(/'/g, "'")
|
||||||
|
.replace(/&/g, '&');
|
||||||
|
}
|
||||||
|
return str
|
||||||
|
.replace(/&/g, '&')
|
||||||
|
.replace(/</g, '<')
|
||||||
|
.replace(/>/g, '>')
|
||||||
|
.replace(/"/g, '"')
|
||||||
|
.replace(/'/g, ''');
|
||||||
|
}
|
||||||
210
tests/xml.spec.ts
Normal file
210
tests/xml.spec.ts
Normal file
@@ -0,0 +1,210 @@
|
|||||||
|
import { toXml, fromXml } from '../src';
|
||||||
|
|
||||||
|
describe('XML Parser', () => {
|
||||||
|
describe('fromXml', () => {
|
||||||
|
it('should parse simple tag', () => {
|
||||||
|
const xml = '<root></root>';
|
||||||
|
const result = fromXml(xml);
|
||||||
|
expect(result).toEqual({ root: '' });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should parse self-closing tag', () => {
|
||||||
|
const xml = '<item />';
|
||||||
|
const result = fromXml(xml);
|
||||||
|
expect(result).toEqual({ item: '' });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should parse tag with attributes (ignored in fast-xml-parser format)', () => {
|
||||||
|
const xml = '<user id="1" name="someone" />';
|
||||||
|
const result = fromXml(xml);
|
||||||
|
expect(result).toEqual({ user: '' });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should parse tag with text content', () => {
|
||||||
|
const xml = '<email>someone@example.com</email>';
|
||||||
|
const result = fromXml(xml);
|
||||||
|
expect(result).toEqual({ email: 'someone@example.com' });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should parse tag with numeric content', () => {
|
||||||
|
const xml = '<ttl>240</ttl>';
|
||||||
|
const result = fromXml(xml);
|
||||||
|
expect(result).toEqual({ ttl: 240 });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should parse nested tags', () => {
|
||||||
|
const xml = '<root><child>text</child></root>';
|
||||||
|
const result = fromXml(xml);
|
||||||
|
expect(result).toEqual({
|
||||||
|
root: {
|
||||||
|
child: 'text'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should parse multiple children with same tag as array', () => {
|
||||||
|
const xml = '<root><item>a</item><item>b</item><item>c</item></root>';
|
||||||
|
const result = fromXml(xml);
|
||||||
|
expect(result).toEqual({
|
||||||
|
root: {
|
||||||
|
item: ['a', 'b', 'c']
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should parse mixed children', () => {
|
||||||
|
const xml = '<root><a>1</a><b>2</b><c>3</c></root>';
|
||||||
|
const result = fromXml(xml);
|
||||||
|
expect(result.root).toEqual({ a: 1, b: 2, c: 3 });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should skip XML declaration and include as key', () => {
|
||||||
|
const xml = '<?xml version="1.0"?><root />';
|
||||||
|
const result = fromXml(xml);
|
||||||
|
expect(result).toHaveProperty('?xml');
|
||||||
|
expect(result).toHaveProperty('root');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should skip comments', () => {
|
||||||
|
const xml = '<root><!-- comment --><child>text</child></root>';
|
||||||
|
const result = fromXml(xml);
|
||||||
|
expect(result).toEqual({
|
||||||
|
root: {
|
||||||
|
child: 'text'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle escaped characters', () => {
|
||||||
|
const xml = '<text><hello> & "world"</text>';
|
||||||
|
const result = fromXml(xml);
|
||||||
|
expect(result.text).toBe('<hello> & "world"');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should parse complex nested structure', () => {
|
||||||
|
const xml = `
|
||||||
|
<root>
|
||||||
|
<user id="1" name="someone">
|
||||||
|
<email>someone@example.com</email>
|
||||||
|
<active />
|
||||||
|
</user>
|
||||||
|
</root>
|
||||||
|
`;
|
||||||
|
const result = fromXml(xml);
|
||||||
|
expect(result).toEqual({
|
||||||
|
root: {
|
||||||
|
user: {
|
||||||
|
email: 'someone@example.com',
|
||||||
|
active: ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should parse RSS-like structure with multiple items', () => {
|
||||||
|
const xml = `
|
||||||
|
<rss>
|
||||||
|
<channel>
|
||||||
|
<title>Test Feed</title>
|
||||||
|
<item>
|
||||||
|
<title>Item 1</title>
|
||||||
|
<link>http://example.com/1</link>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<title>Item 2</title>
|
||||||
|
<link>http://example.com/2</link>
|
||||||
|
</item>
|
||||||
|
</channel>
|
||||||
|
</rss>
|
||||||
|
`;
|
||||||
|
const result = fromXml(xml);
|
||||||
|
expect(result.rss.channel.title).toBe('Test Feed');
|
||||||
|
expect(Array.isArray(result.rss.channel.item)).toBe(true);
|
||||||
|
expect(result.rss.channel.item.length).toBe(2);
|
||||||
|
expect(result.rss.channel.item[0].title).toBe('Item 1');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('toXml', () => {
|
||||||
|
it('should encode simple tag', () => {
|
||||||
|
const obj = { tag: 'root', attributes: {}, children: [] };
|
||||||
|
expect(toXml(obj)).toBe('<root />');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should encode tag with attributes', () => {
|
||||||
|
const obj = { tag: 'user', attributes: { id: '1', name: 'someone' }, children: [] };
|
||||||
|
const result = toXml(obj);
|
||||||
|
expect(result).toContain('id="1"');
|
||||||
|
expect(result).toContain('name="someone"');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should encode tag with text content', () => {
|
||||||
|
const obj = { tag: 'email', attributes: {}, children: ['someone@example.com'] };
|
||||||
|
expect(toXml(obj)).toBe('<email>someone@example.com</email>');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should encode nested tags with indentation', () => {
|
||||||
|
const obj = {
|
||||||
|
tag: 'root',
|
||||||
|
attributes: {},
|
||||||
|
children: [
|
||||||
|
{ tag: 'child', attributes: {}, children: ['text'] }
|
||||||
|
]
|
||||||
|
};
|
||||||
|
const result = toXml(obj);
|
||||||
|
expect(result).toContain('<root>');
|
||||||
|
expect(result).toContain(' <child>');
|
||||||
|
expect(result).toContain('</root>');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should escape special characters', () => {
|
||||||
|
const obj = { tag: 'text', attributes: {}, children: ['<hello> & "world"'] };
|
||||||
|
const result = toXml(obj);
|
||||||
|
expect(result).toContain('<hello> & "world"');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should escape attributes', () => {
|
||||||
|
const obj = { tag: 'node', attributes: { attr: 'a & b' }, children: [] };
|
||||||
|
const result = toXml(obj);
|
||||||
|
expect(result).toContain('attr="a & b"');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle multiple children', () => {
|
||||||
|
const obj = {
|
||||||
|
tag: 'root',
|
||||||
|
attributes: {},
|
||||||
|
children: [
|
||||||
|
{ tag: 'a', attributes: {}, children: [] },
|
||||||
|
{ tag: 'b', attributes: {}, children: [] }
|
||||||
|
]
|
||||||
|
};
|
||||||
|
const result = toXml(obj);
|
||||||
|
expect(result).toContain('<a />');
|
||||||
|
expect(result).toContain('<b />');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should encode string directly', () => {
|
||||||
|
expect(toXml('hello')).toBe('hello');
|
||||||
|
expect(toXml('a & b')).toBe('a & b');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('round-trip', () => {
|
||||||
|
it('should parse toXml output back to fast-xml-parser format', () => {
|
||||||
|
const obj = {
|
||||||
|
tag: 'root',
|
||||||
|
attributes: { id: '1' },
|
||||||
|
children: [
|
||||||
|
{ tag: 'child', attributes: {}, children: ['text'] }
|
||||||
|
]
|
||||||
|
};
|
||||||
|
const xml = toXml(obj);
|
||||||
|
const parsed = fromXml(xml);
|
||||||
|
expect(parsed).toEqual({
|
||||||
|
root: {
|
||||||
|
child: 'text'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user