diff --git a/Dockerfile b/Dockerfile index 0a82b68..864331d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -10,4 +10,6 @@ RUN if [ ! -d "node_modules" ]; then npm i; fi && \ # Use Nginx to serve FROM nginx:1.23-alpine +RUN npm i fast-xml-parser + COPY --from=build /app/docs /usr/share/nginx/html diff --git a/src/xml.ts b/src/xml.ts index d415fdf..2f3ef02 100644 --- a/src/xml.ts +++ b/src/xml.ts @@ -3,6 +3,11 @@ * @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 style). + * @param {string} xml - The XML string to parse + * @returns {Object} An object matching fast-xml-parser output format + */ export function fromXml(xml: string) { xml = xml.trim(); let pos = 0; @@ -28,7 +33,7 @@ export function fromXml(xml: string) { if(xml[pos] === '/' && xml[pos + 1] === '>') { pos += 2; // skip /> - return { tag: tagName, attributes, children: [] }; + return { [tagName]: Object.keys(attributes).length > 0 ? { '@_': attributes } : '' }; } pos++; // skip > @@ -45,17 +50,54 @@ export function fromXml(xml: string) { const child = parseNode(); if(child) children.push(child); } - return { tag: tagName, attributes, children }; + + // Build fast-xml-parser style output + const result: any = {}; + + // Add attributes with @_ prefix + if(Object.keys(attributes).length > 0) { + for(const [key, value] of Object.entries(attributes)) { + result[`@_${key}`] = value; + } + } + + // Process children + if(children.length === 1 && typeof children[0] === 'string') { + // Single text node + if(Object.keys(attributes).length > 0) { + result['#text'] = children[0]; + return { [tagName]: result }; + } + return { [tagName]: children[0] }; + } + + // Group children by tag name + for(const child of children) { + if(typeof child === 'string') { + result['#text'] = child; + } else { + for(const [childTag, childValue] of Object.entries(child)) { + if(result[childTag]) { + if(!Array.isArray(result[childTag])) { + result[childTag] = [result[childTag]]; + } + result[childTag].push(childValue); + } else { + result[childTag] = childValue; + } + } + } + } + + return { [tagName]: Object.keys(result).length > 0 ? result : '' }; } - /** Parses and returns the tag name at the current position */ function parseTagName() { let name = ''; while (pos < xml.length && /[a-zA-Z0-9_:-]/.test(xml[pos])) name += xml[pos++]; return name; } - /** Parses and returns an object containing all attributes at the current position */ function parseAttributes() { const attrs: any = {}; while (pos < xml.length) { @@ -76,7 +118,6 @@ export function fromXml(xml: string) { return attrs; } - /** Parses and returns text content, or null if empty */ function parseText() { let text = ''; while (pos < xml.length && xml[pos] !== '<') text += xml[pos++]; @@ -84,19 +125,16 @@ export function fromXml(xml: string) { return text ? escapeXml(text, true) : null; } - /** Skips over XML declaration () */ function parseDeclaration() { while (xml[pos] !== '>') pos++; pos++; } - /** Skips over XML comments () */ function parseComment() { while (!(xml[pos] === '-' && xml[pos + 1] === '-' && xml[pos + 2] === '>')) pos++; pos += 3; } - /** Advances position past any whitespace characters */ function skipWhitespace() { while (pos < xml.length && /\s/.test(xml[pos])) pos++; } diff --git a/tests/xml.spec.ts b/tests/xml.spec.ts index e78e353..ef95851 100644 --- a/tests/xml.spec.ts +++ b/tests/xml.spec.ts @@ -5,22 +5,20 @@ describe('XML Parser', () => { it('should parse simple tag', () => { const xml = ''; const result = fromXml(xml); - expect(result).toEqual({ tag: 'root', attributes: {}, children: [] }); + expect(result).toEqual({ root: '' }); }); it('should parse self-closing tag', () => { const xml = ''; const result = fromXml(xml); - expect(result).toEqual({ tag: 'item', attributes: {}, children: [] }); + expect(result).toEqual({ item: '' }); }); it('should parse tag with attributes', () => { const xml = ''; const result = fromXml(xml); expect(result).toEqual({ - tag: 'user', - attributes: { id: '1', name: 'someone' }, - children: [] + user: { '@_id': '1', '@_name': 'someone' } }); }); @@ -28,9 +26,7 @@ describe('XML Parser', () => { const xml = 'someone@example.com'; const result = fromXml(xml); expect(result).toEqual({ - tag: 'email', - attributes: {}, - children: ['someone@example.com'] + email: 'someone@example.com' }); }); @@ -38,54 +34,75 @@ describe('XML Parser', () => { const xml = 'text'; const result = fromXml(xml); expect(result).toEqual({ - tag: 'root', - attributes: {}, - children: [ - { tag: 'child', attributes: {}, children: ['text'] } - ] + root: { + child: 'text' + } }); }); it('should parse multiple children', () => { const xml = ''; const result = fromXml(xml); - expect(result.children.length).toBe(3); - expect(result.children[0]).toEqual({ tag: 'a', attributes: {}, children: [] }); + expect(result.root.a).toBe(''); + expect(result.root.b).toBe(''); + expect(result.root.c).toBe(''); + }); + + it('should parse repeated tags as arrays', () => { + const xml = '123'; + const result = fromXml(xml); + expect(result).toEqual({ + root: { + item: ['1', '2', '3'] + } + }); }); it('should skip XML declaration', () => { const xml = ''; const result = fromXml(xml); - expect(result.tag).toBe('root'); + expect(result.root).toBe(''); }); it('should skip comments', () => { const xml = ''; const result = fromXml(xml); - expect(result.children.length).toBe(1); - expect(result.children[0].tag).toBe('child'); + expect(result).toEqual({ + root: { child: '' } + }); }); it('should handle escaped characters', () => { const xml = '<hello> & "world"'; const result = fromXml(xml); - expect(result.children[0]).toBe(' & "world"'); + expect(result.text).toBe(' & "world"'); + }); + + it('should parse tag with attributes and text', () => { + const xml = 'John'; + const result = fromXml(xml); + expect(result).toEqual({ + user: { + '@_id': '1', + '#text': 'John' + } + }); }); it('should parse complex nested structure', () => { const xml = ` - - - someone@example.com - - - - `; + + + someone@example.com + + + + `; const result = fromXml(xml); - expect(result.tag).toBe('root'); - expect(result.children[0].tag).toBe('user'); - expect(result.children[0].attributes.name).toBe('someone'); - expect(result.children[0].children.length).toBe(2); + expect(result.root.user['@_id']).toBe('1'); + expect(result.root.user['@_name']).toBe('someone'); + expect(result.root.user.email).toBe('someone@example.com'); + expect(result.root.user.active).toBe(''); }); });