UNPKG

@hendt/xml2json

Version:

Pure javascript XML to JSON converter with zero dependencies.

184 lines (158 loc) 6.41 kB
type Options = { aloneValueName?: string } const defaultOptions: Options = { aloneValueName: '_@attribute' }; /** * Main function. Clears the given xml and then starts the recursion * @param xmlStr * @param options the options */ export default function xml2json(xmlStr: string, options = defaultOptions) { const opt = {...defaultOptions, ...options} as Required<Options>; xmlStr = cleanXML(xmlStr, opt.aloneValueName); return xml2jsonRecurse(xmlStr, opt); } /** * Recursive function that creates a JSON object with a given XML string. * * @param xmlStr * @param options the options */ function xml2jsonRecurse(xmlStr: string, options: Required<Options>) { const obj: any = {}; let startTagMatch; while ((startTagMatch = xmlStr.match(/<[^\/][^>]*>/))) { let openingTag = startTagMatch[0]; let tagName = openingTag.substring(1, openingTag.length - 1); let indexClosingTag = xmlStr.indexOf(openingTag.replace('<', '</')); // account for case where additional information in the opening tag let closingTagMatch; if (indexClosingTag == -1 && (closingTagMatch = openingTag.match(/[^<][\S+$]*/))) { tagName = closingTagMatch[0]; indexClosingTag = xmlStr.indexOf('</' + tagName); if (indexClosingTag == -1) { indexClosingTag = xmlStr.indexOf('<\\/' + tagName); } } let inner_substring = xmlStr.substring(openingTag.length, indexClosingTag); const tempVal = inner_substring.match(/<[^\/][^>]*>/) ? xml2json(inner_substring, options) : inner_substring; // account for array or obj if (obj[tagName] === undefined) { obj[tagName] = tempVal; } else if (Array.isArray(obj[tagName])) { obj[tagName].push(tempVal); } else { obj[tagName] = [obj[tagName], tempVal]; } xmlStr = xmlStr.substring(openingTag.length * 2 + 1 + inner_substring.length); } return obj; } /** * Removes some characters that would break the recursive function. * * @param xmlStr * @param aloneValueName * @returns {string} */ function cleanXML(xmlStr: string, aloneValueName: string) { xmlStr = xmlStr.replace(/<!--[\s\S]*?-->/g, ''); // remove commented lines xmlStr = xmlStr.replace(/[\n\t\r]/g, ''); // replace special characters xmlStr = xmlStr.replace(/>[ \t]+</g, '><'); // replace leading spaces and tabs betweet elements xmlStr = xmlStr.replace(/<\?[^>]*\?>/g, ''); // delete docType tags xmlStr = replaceSelfClosingTags(xmlStr); // replace self closing tags xmlStr = replaceAloneValues(xmlStr, aloneValueName); // replace the alone tags values xmlStr = replaceAttributes(xmlStr); // replace attributes return xmlStr; } /** * Replaces all the self closing tags with attributes with another tag containing its attribute as a property. * The function works if the tag contains multiple attributes. * Example : '<tagName attrName="attrValue" />' becomes * '<tagName><attrName>attrValue</attrName></tagName>' * @param xmlStr * @returns {*} */ function replaceSelfClosingTags(xmlStr: string) { const selfClosingTags = xmlStr.match(/<[^/][^>]*\/>/g); if (!selfClosingTags) { return xmlStr } for (let i = 0; i < selfClosingTags.length; i++) { const oldTag = selfClosingTags[i]; const match = oldTag.match(/[^<][\S+$]*/); if (match) { const tagName = match[0]; const closingTag = "</" + tagName + ">"; const newTag = extractAttributeValue(tagName, oldTag) + closingTag; xmlStr = xmlStr.replace(oldTag, newTag); } } return xmlStr; } /** * Replaces all the tags with attributes and a value with a new tag. * * Example : '<tagName attrName="attrValue">tagValue</tagName>' becomes * '<tagName><attrName>attrValue</attrName><_@attribute>tagValue</_@attribute></tagName>' * * @param xmlStr * @param aloneValueName * @returns {string} */ function replaceAloneValues(xmlStr: string, aloneValueName: string) { const tagsWithAttributesAndValue = xmlStr.match(/<[^\/][^>][^<]+\s+.[^<]+[=][^<]+>([^<]+)/g); if (!tagsWithAttributesAndValue) { return xmlStr; } for (let i = 0; i < tagsWithAttributesAndValue.length; i++) { const oldTag = tagsWithAttributesAndValue[i]; const oldTagName = oldTag.substring(0, oldTag.indexOf(">") + 1); const oldTagValue = oldTag.substring(oldTag.indexOf(">") + 1); const newTag = oldTagName + "<" + aloneValueName + ">" + oldTagValue + "</" + aloneValueName + ">"; xmlStr = xmlStr.replace(oldTag, newTag); } return xmlStr; } function extractAttributeValue(tagName: string, oldTag: string) { let newTag = "<" + tagName + ">"; const attrs = oldTag.match(/(\S+)\s?=\s?((?:"[^"]+")|(?:'[^']+'))/g); if (!attrs) { return newTag } for (let j = 0; j < attrs.length; j++) { const attr = attrs[j]; const attrName = attr.substring(0, attr.indexOf('=')).trim(); const quote = attr[attr.length - 1]; const attrValue = attr.substring(attr.indexOf(quote) + 1, attr.lastIndexOf(quote)); newTag += "<" + attrName + ">" + attrValue + "</" + attrName + ">"; } return newTag; } /** * Replaces all the tags with attributes with another tag containing its attribute as a property. * The function works if the tag contains multiple attributes. * * Example : '<tagName attrName="attrValue"></tagName>' becomes '<tagName><attrName>attrValue</attrName></tagName>' * * @param xmlStr * @returns {*} */ function replaceAttributes(xmlStr: string) { const tagsWithAttributes = xmlStr.match(/<[^\/><]\S+\s+[^<]+[=][^<]+>/g); if (!tagsWithAttributes) { return xmlStr } for (let i = 0; i < tagsWithAttributes.length; i++) { const oldTag = tagsWithAttributes[i]; const match = oldTag.match(/[^<]\S*/); if (match) { const tagName = match[0]; const newTag = extractAttributeValue(tagName, oldTag); xmlStr = xmlStr.replace(oldTag, newTag); } } return xmlStr; }