rapid-xml-to-json
Version:
Fast, dependency-free XML-parser for parsing XML structures into JSON. Optimised for speed and low memory usage.
607 lines (539 loc) • 16.3 kB
text/typescript
import { xmlToJson } from "./index";
// TODO: Benchmark tests
describe('Test of parse', () => {
test('parses a single attribute', () => {
const xml = `<root attr="value">Content</root>`;
const expected = {
root: {
__parent: expect.any(Object),
$attrs: { attr: "value" },
"#text": "Content",
},
};
expect(xmlToJson(xml)).toEqual(expected);
});
test('parses attributes in a self-closing tag', () => {
const xml = `<root attr="value"/>`;
const expected = {
root: {
__parent: expect.any(Object),
$attrs: { attr: "value" },
},
};
let actual = xmlToJson(xml)
expect(actual).toEqual(expected);
});
test('parses multiple attributes', () => {
const xml = `<root attr1="value1" attr2="value2">Content</root>`;
const expected = {
root: {
__parent: expect.any(Object),
$attrs: { attr1: "value1", attr2: "value2" },
"#text": "Content",
},
};
expect(xmlToJson(xml)).toEqual(expected);
});
test('parses attributes in nested elements', () => {
const xml = `<root><child attr="value">Content</child></root>`;
const expected = {
root: {
__parent: expect.any(Object),
child: {
__parent: expect.any(Object),
$attrs: { attr: "value" },
"#text": "Content",
},
},
};
let actual = xmlToJson(xml)
expect(actual).toEqual(expected);
});
it('Correctly parses an XML structure', () => {
const xml = `<root><a></a><b></b></root>`
const json = xmlToJson(xml);
expect(json.root).toHaveProperty('a');
expect(json.root).toHaveProperty('b');
});
it('Correctly parses CDATA', () => {
const aData = '<Data from CDATA>'
const xml = `<root><a><![CDATA[${aData}]]></a><b>Test</b>`
const json = xmlToJson(xml);
expect(json.root.a).toEqual({__parent: expect.any(Object), '#text': "<Data from CDATA>"});
expect(json.root.b).toEqual({__parent: expect.any(Object), '#text': "Test"});
});
it('Correctly ignores comments, DOCTYPE, stylesheets', () => {
const xml = `<root><a><!--Test comment--><c></c></a><b></b></root>`
const json = xmlToJson(xml);
expect(json.root).toHaveProperty('a');
expect(json.root).toHaveProperty('b');
expect(json.root.a).toHaveProperty('c');
});
it('Correctly parses lists of elements', () => {
const xml = `<root><a></a><a></a><a/><b></b></root>`
const json = xmlToJson(xml);
expect(json.root).toHaveProperty('b');
expect(json.root.a.length).toBe(3);
});
test('parses deeply nested structures', () => {
const xml = `
<root>
<level1>
<level2>
<level3>
<level4>Deep content</level4>
</level3>
</level2>
</level1>
</root>
`;
const expected = {
root: {
__parent: expect.any(Object),
level1: {
__parent: expect.any(Object),
level2: {
__parent: expect.any(Object),
level3: {
__parent: expect.any(Object),
level4: {__parent: expect.any(Object),'#text': 'Deep content'},
},
},
},
},
};
const actual = xmlToJson(xml)
expect(actual).toEqual(expected);
});
test('handles CDATA-heavy XML', () => {
const xml = `
<root>
<data><![CDATA[Some CDATA content]]></data>
<data><![CDATA[<tag>Inside CDATA</tag>]]></data>
<data><![CDATA[]]></data>
</root>
`;
const expected = {
root: {
__parent: expect.any(Object),
data: [
{__parent: expect.any(Object), '#text':'Some CDATA content'},
{__parent: expect.any(Object), '#text':'<tag>Inside CDATA</tag>'},
{__parent: expect.any(Object), '#text':''},
],
},
};
expect(xmlToJson(xml)).toEqual(expected);
});
test('parses XML with variations in whitespace formatting', () => {
const xml = `
<root>
<item> Content with leading and trailing spaces </item>
<item>
Content with
line breaks
</item>
<item> </item>
</root>
`;
const expected = {
root: {
__parent: expect.any(Object),
item: [
{__parent: expect.any(Object), '#text':' Content with leading and trailing spaces '},
{__parent: expect.any(Object), '#text':`
Content with
line breaks
`},
{__parent: expect.any(Object)}
],
},
};
expect(xmlToJson(xml)).toEqual(expected);
});
test('handles empty tags and self-closing tags', () => {
const xml = `
<root>
<empty></empty>
<selfClosing />
<nestedSelfClosing>
<child />
</nestedSelfClosing>
</root>
`;
const expected = {
root: {
__parent: expect.any(Object),
empty: {__parent: expect.any(Object)},
selfClosing: {__parent: expect.any(Object)},
nestedSelfClosing: {
__parent: expect.any(Object),
child: {__parent: expect.any(Object)},
},
},
};
const actual = xmlToJson(xml)
expect(actual).toEqual(expected);
});
test('skips comments, processing instructions, and DOCTYPE declarations', () => {
const xml = `
<root>
<!-- This is a comment -->
<item>Valid content</item>
</root>
`;
const expected = {
root: {
__parent: expect.any(Object),
item: {__parent: expect.any(Object), '#text': 'Valid content'},
},
};
expect(xmlToJson(xml)).toEqual(expected);
});
test('handles mixed CDATA and regular content in a single document', () => {
const xml = `
<root>
<data><![CDATA[CDATA content]]></data>
<data>Regular content</data>
<data><![CDATA[More CDATA]]></data>
</root>
`;
const expected = {
root: {
__parent: expect.any(Object),
data: [
{__parent: expect.any(Object), '#text': 'CDATA content'},
{__parent: expect.any(Object), '#text': 'Regular content'},
{__parent: expect.any(Object), '#text': 'More CDATA'},
],
},
};
expect(xmlToJson(xml)).toEqual(expected);
});
test('handles multiple sibling elements with the same name', () => {
const xml = `
<root>
<item>First</item>
<item>Second</item>
<item>Third</item>
</root>
`;
const expected = {
root: {
__parent: expect.any(Object),
item: [{__parent: expect.any(Object), '#text':'First'}, {__parent: expect.any(Object), '#text': 'Second'}, {__parent: expect.any(Object), '#text':'Third'}],
},
};
expect(xmlToJson(xml)).toEqual(expected);
});
test('handles deeply nested structures with mixed empty and populated elements', () => {
const xml = `
<root>
<level1>
<level2>
<level3>
<empty />
<populated>Content</populated>
</level3>
</level2>
</level1>
</root>
`;
const expected = {
root: {
__parent: expect.any(Object),
level1: {
__parent: expect.any(Object),
level2: {
__parent: expect.any(Object),
level3: {
__parent: expect.any(Object),
empty: {__parent: expect.any(Object)},
populated: {__parent: expect.any(Object), '#text': 'Content'},
},
},
},
},
};
expect(xmlToJson(xml)).toEqual(expected);
});
test('handles XML with no content inside tags', () => {
const xml = `
<root>
<empty1></empty1>
<empty2 />
</root>
`;
const expected = {
root: {
__parent: expect.any(Object),
empty1: {__parent: expect.any(Object)},
empty2: {__parent: expect.any(Object)},
},
};
const actual = xmlToJson(xml)
expect(actual).toEqual(expected);
});
test('parses CDATA with special characters', () => {
const xml = `
<root>
<data><![CDATA[<special>&characters</special>]]></data>
</root>
`;
const expected = {
root: {
__parent: expect.any(Object),
data: {__parent: expect.any(Object), '#text': '<special>&characters</special>'},
},
};
expect(xmlToJson(xml)).toEqual(expected);
});
test('handles documents with leading and trailing whitespace', () => {
const xml = `
<root>
<item>Content</item>
</root>
`;
const expected = {
root: {
__parent: expect.any(Object),
item: {__parent: expect.any(Object), '#text': 'Content'},
},
};
const actual = xmlToJson(xml)
expect(actual).toEqual(expected);
});
test('handles empty root element', () => {
const xml = `
<root></root>
`;
const expected = {
root: {__parent: expect.any(Object)},
};
expect(xmlToJson(xml)).toEqual(expected);
});
test('handles large documents', () => {
const xml = `
<root>
${'<item>Content</item>'.repeat(10000)}
</root>
`;
const expected = {
root: {
__parent: expect.any(Object),
item: Array(10000).fill({__parent: expect.any(Object), '#text':'Content'}),
},
};
expect(xmlToJson(xml)).toEqual(expected);
});
test('handles multiple levels of CDATA', () => {
const xml = `
<root>
<data><![CDATA[Level 1]]><![CDATA[ and Level 2]]></data>
</root>
`;
const expected = {
root: {
__parent: expect.any(Object),
data: {__parent: expect.any(Object), '#text': 'Level 1 and Level 2'},
},
};
const actual = xmlToJson(xml);
expect(actual).toEqual(expected);
});
test('handles mixed test and CDATA', () => {
const xml = `
<root>
<data>Test <![CDATA[Level 1]]>here<![CDATA[ and Level 2]]>there</data>
</root>
`;
const expected = {
root: {
__parent: expect.any(Object),
data: {__parent: expect.any(Object), '#text': 'Test Level 1here and Level 2there'},
},
};
const actual = xmlToJson(xml);
expect(actual).toEqual(expected);
});
test('ignores extraneous characters outside root element', () => {
const xml = `
<!-- Comment -->
<root>
<item>Valid content</item>
</root>
<!-- Another comment -->
`;
const expected = {
root: {
__parent: expect.any(Object),
item: {__parent: expect.any(Object), '#text': 'Valid content'},
},
};
expect(xmlToJson(xml)).toEqual(expected);
});
test('handles XML with unconventional spacing and line breaks', () => {
const xml = `
<root>
<item
>Content</item
>
<item>Another</item >
</root>
`;
const expected = {
root: {
__parent: expect.any(Object),
item: [{__parent: expect.any(Object), '#text': 'Content'}, {__parent: expect.any(Object), '#text':'Another'}],
},
};
expect(xmlToJson(xml)).toEqual(expected);
});
test('ignores invalid comments and CDATA-like content outside valid constructs', () => {
const xml = `
<root>
<item>Valid content</item>
<!-- This is not <![CDATA[valid]] -->
<!-- This is also not valid CDATA ]]> -->
</root>
`;
const expected = {
root: {
__parent: expect.any(Object),
item: {__parent: expect.any(Object), '#text': 'Valid content'},
},
};
const actual = xmlToJson(xml)
expect(actual).toEqual(expected);
});
test('parses attributes with special characters', () => {
const xml = `<root attr="value with spaces" another-attr="value&chars!">Content</root>`;
const expected = {
root: {
__parent: expect.any(Object),
$attrs: {
attr: "value with spaces",
"another-attr": "value&chars!",
},
"#text": "Content",
},
};
expect(xmlToJson(xml)).toEqual(expected);
});
test('parses attributes with empty values', () => {
const xml = `<root attr1="" attr2="non-empty">Content</root>`;
const expected = {
root: {
__parent: expect.any(Object),
$attrs: {
attr1: "",
attr2: "non-empty",
},
"#text": "Content",
},
};
const actual = xmlToJson(xml)
expect(actual).toEqual(expected);
});
test('parses element with only attributes and no text content', () => {
const xml = `<root attr1="value1" attr2="value2"></root>`;
const expected = {
root: {
__parent: expect.any(Object),
$attrs: {
attr1: "value1",
attr2: "value2",
},
},
};
expect(xmlToJson(xml)).toEqual(expected);
});
test('parses attributes with mixed whitespace formatting', () => {
const xml = `<root attr1="value1" attr2=" value2 ">Content</root>`;
const expected = {
root: {
__parent: expect.any(Object),
$attrs: {
attr1: "value1",
attr2: " value2 ", // Preserves the spaces
},
"#text": "Content",
},
};
expect(xmlToJson(xml)).toEqual(expected);
});
test('parses attributes with quotes inside the value', () => {
const xml = `<root attr="value with \\"quotes\\" inside">Content</root>`;
const expected = {
root: {
__parent: expect.any(Object),
$attrs: {
attr: 'value with \\"quotes\\" inside', // Preserves internal quotes
},
"#text": "Content",
},
};
const actual = xmlToJson(xml)
expect(actual).toEqual(expected);
});
test('parses attributes with multiple equal signs in the value', () => {
const xml = `<root attr="value=with=equals">Content</root>`;
const expected = {
root: {
__parent: expect.any(Object),
$attrs: {
attr: "value=with=equals",
},
"#text": "Content",
},
};
expect(xmlToJson(xml)).toEqual(expected);
});
test('Handles array of self-closing elements', () => {
const xml = `<Stemmer>
<Parti Id="5891" Bogstav="A" Navn="A. Socialdemokratiet" StemmerAntal="86226" StemmerPct="19"/>
<Parti Id="5893" Bogstav="B" Navn="B. Radikale Venstre" StemmerAntal="34031" StemmerPct="7.5"/>
<Parti Id="5895" Bogstav="C" Navn="C. Det Konservative Folkeparti" StemmerAntal="22797" StemmerPct="5"/>
</Stemmer>`
const expected = {
Stemmer: {
__parent: expect.any(Object),
Parti: [
{
__parent: expect.any(Object),
$attrs: {
Id: "5891",
Bogstav: "A",
Navn: "A. Socialdemokratiet",
StemmerAntal: "86226",
StemmerPct: "19",
},
},
{
__parent: expect.any(Object),
$attrs: {
Id: "5893",
Bogstav: "B",
Navn: "B. Radikale Venstre",
StemmerAntal: "34031",
StemmerPct: "7.5",
},
},
{
__parent: expect.any(Object),
$attrs: {
Id: "5895",
Bogstav: "C",
Navn: "C. Det Konservative Folkeparti",
StemmerAntal: "22797",
StemmerPct: "5",
},
},
],
}
}
expect(xmlToJson(xml)).toEqual(expected);
})
});