added fast-xml-parser for testing
Some checks failed
Build / Publish Docs (push) Failing after 16s
Build / Build NPM Project (push) Failing after 54s
Build / Tag Version (push) Has been skipped

This commit is contained in:
2026-04-04 18:18:24 -04:00
parent c778f3d280
commit 15ac52b6a0
3 changed files with 95 additions and 38 deletions

View File

@@ -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

View File

@@ -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 (<?xml ... ?>) */
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++;
}

View File

@@ -5,22 +5,20 @@ describe('XML Parser', () => {
it('should parse simple tag', () => {
const xml = '<root></root>';
const result = fromXml(xml);
expect(result).toEqual({ tag: 'root', attributes: {}, children: [] });
expect(result).toEqual({ root: '' });
});
it('should parse self-closing tag', () => {
const xml = '<item />';
const result = fromXml(xml);
expect(result).toEqual({ tag: 'item', attributes: {}, children: [] });
expect(result).toEqual({ item: '' });
});
it('should parse tag with attributes', () => {
const xml = '<user id="1" name="someone" />';
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 = '<email>someone@example.com</email>';
const result = fromXml(xml);
expect(result).toEqual({
tag: 'email',
attributes: {},
children: ['someone@example.com']
email: 'someone@example.com'
});
});
@@ -38,38 +34,59 @@ describe('XML Parser', () => {
const xml = '<root><child>text</child></root>';
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 = '<root><a /><b /><c /></root>';
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 = '<root><item>1</item><item>2</item><item>3</item></root>';
const result = fromXml(xml);
expect(result).toEqual({
root: {
item: ['1', '2', '3']
}
});
});
it('should skip XML declaration', () => {
const xml = '<?xml version="1.0"?><root />';
const result = fromXml(xml);
expect(result.tag).toBe('root');
expect(result.root).toBe('');
});
it('should skip comments', () => {
const xml = '<root><!-- comment --><child /></root>';
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 = '<text>&lt;hello&gt; &amp; &quot;world&quot;</text>';
const result = fromXml(xml);
expect(result.children[0]).toBe('<hello> & "world"');
expect(result.text).toBe('<hello> & "world"');
});
it('should parse tag with attributes and text', () => {
const xml = '<user id="1">John</user>';
const result = fromXml(xml);
expect(result).toEqual({
user: {
'@_id': '1',
'#text': 'John'
}
});
});
it('should parse complex nested structure', () => {
@@ -82,10 +99,10 @@ describe('XML Parser', () => {
</root>
`;
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('');
});
});