From 35c14d01c2f1405ac649c9d9864118a270dd88c0 Mon Sep 17 00:00:00 2001 From: ztimson Date: Mon, 24 Aug 2026 18:37:29 -0400 Subject: [PATCH] Added better attribute support to fromXML --- package.json | 2 +- src/xml.ts | 26 +++++++++++++------------- tests/xml.spec.ts | 9 ++++++--- 3 files changed, 20 insertions(+), 17 deletions(-) diff --git a/package.json b/package.json index 7e4d2f4..3c36333 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@ztimson/utils", - "version": "0.30.6", + "version": "0.30.7", "description": "Utility library", "author": "Zak Timson", "license": "MIT", diff --git a/src/xml.ts b/src/xml.ts index cc8e613..19ce0af 100644 --- a/src/xml.ts +++ b/src/xml.ts @@ -1,12 +1,12 @@ /** - * Parses an XML string into a structured JavaScript object. + * Parses an XML string into a plain JavaScript object. + * Each tag becomes a key. Attributes and child tags are merged as + * sibling properties on that tag's object. Duplicate tag/attribute + * names are collapsed into arrays. Text-only tags resolve to their + * (optionally numeric) value; if a tag has both text and attributes/ + * children, the text is kept under a `_text` key. * @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 + * @returns {Object} The parsed object tree */ export function fromXml(xml: string) { xml = xml.trim(); @@ -33,11 +33,11 @@ export function fromXml(xml: string) { if(xml[pos] === '/' && xml[pos + 1] === '>') { pos += 2; // skip /> - return { [tagName]: '' }; + return { [tagName]: Object.keys(attributes).length ? attributes : '' }; } pos++; // skip > - const children: any[] = []; + const children: any[] = Object.entries(attributes).map(([k, v]) => ({ [k]: v })); let textContent = ''; while(pos < xml.length) { @@ -49,7 +49,6 @@ export function fromXml(xml: string) { pos++; // skip > break; } - const startPos = pos; const child = parseNode(); if(typeof child === 'string') { textContent += child; @@ -64,17 +63,18 @@ export function fromXml(xml: string) { return { [tagName]: value }; } - // If only text with no children + // If nothing at all if(children.length === 0) { return { [tagName]: '' }; } - // Merge children into object + // Merge attributes/children into object const result: any = {}; + if(textContent) result._text = isNumeric(textContent) ? Number(textContent) : textContent; for(const child of children) { for(const [key, value] of Object.entries(child)) { if(result[key]) { - // Convert to array if duplicate tags + // Convert to array if duplicate tags/attrs if(!Array.isArray(result[key])) { result[key] = [result[key]]; } diff --git a/tests/xml.spec.ts b/tests/xml.spec.ts index ed91882..6cb7dd7 100644 --- a/tests/xml.spec.ts +++ b/tests/xml.spec.ts @@ -14,10 +14,10 @@ describe('XML Parser', () => { expect(result).toEqual({ item: '' }); }); - it('should parse tag with attributes (ignored in fast-xml-parser format)', () => { + it('should parse tag with attributes as merged properties', () => { const xml = ''; const result = fromXml(xml); - expect(result).toEqual({ user: '' }); + expect(result).toEqual({ user: { id: '1', name: 'someone' } }); }); it('should parse tag with text content', () => { @@ -94,6 +94,8 @@ describe('XML Parser', () => { expect(result).toEqual({ root: { user: { + id: '1', + name: 'someone', email: 'someone@example.com', active: '' } @@ -190,7 +192,7 @@ describe('XML Parser', () => { }); describe('round-trip', () => { - it('should parse toXml output back to fast-xml-parser format', () => { + it('should parse toXml output back with attributes merged as properties', () => { const obj = { tag: 'root', attributes: { id: '1' }, @@ -202,6 +204,7 @@ describe('XML Parser', () => { const parsed = fromXml(xml); expect(parsed).toEqual({ root: { + id: '1', child: 'text' } });