/* eslint-disable complexity */ const repeat = require("./string-repeat"); function getIndent(indent) { return repeat(" ", indent); } function attributeSorter(ln) { const aRegex = /<[A-Za-z0-9:]+ (.*?)([/ ]*)>/; let rest; if (aRegex.test(ln)) { rest = ln.replace(aRegex, "$1"); } const attrRegex = / *([a-zA-Z0-9:]+)="([^"]+)"/g; let match = attrRegex.exec(rest); const attributes = []; while (match != null) { // matched text: match[0] // match start: match.index // capturing group n: match[n] attributes.push({ key: match[1], value: match[2] }); match = attrRegex.exec(rest); } attributes.sort(function (a1, a2) { if (a1.key === a2.key) { return 0; } return a1.key > a2.key ? 1 : -1; }); const stringifiedAttrs = attributes .map(function (attribute) { return `${attribute.key}="${attribute.value}"`; }) .join(" "); if (rest != null) { ln = ln.replace(rest, stringifiedAttrs).replace(/ +>/, ">"); } return ln; } function xmlprettify(xml) { let result = "", skip = 0, indent = 0; const parsed = miniparser(xml); parsed.forEach(function ({ type, value }, i) { if (skip > 0) { skip--; return; } const nextType = i < parsed.length - 1 ? parsed[i + 1].type : ""; const nnextType = i < parsed.length - 2 ? parsed[i + 2].type : ""; if (type === "declaration") { result += value + "\n"; } if ( type === "opening" && nextType === "content" && nnextType === "closing" ) { result += getIndent(indent) + value + parsed[i + 1].value + parsed[i + 2].value + "\n"; skip = 2; return; } if (type === "opening") { result += getIndent(indent) + value + "\n"; indent++; } if (type === "closing") { indent--; if (indent < 0) { throw new Error(`Malformed xml : ${xml}`); } result += getIndent(indent) + value + "\n"; } if (type === "single") { result += getIndent(indent) + value + "\n"; } if (type === "content" && !/^[ \n\r\t]+$/.test(value)) { result += getIndent(indent) + value.trim() + "\n"; } }); if (indent !== 0) { throw new Error(`Malformed xml : ${xml}`); } return result; } function miniparser(xml) { let cursor = 0; let state = "outside"; let currentType = ""; let content = ""; const renderedArray = []; while (cursor < xml.length) { if (state === "outside") { const opening = xml.indexOf("<", cursor); if (opening !== -1) { if (opening !== cursor) { content = xml.substr(cursor, opening - cursor); content = content.replace(/>/g, ">"); renderedArray.push({ type: "content", value: content }); } state = "inside"; cursor = opening; } else { const content = xml.substr(cursor); renderedArray.push({ type: "content", value: content }); return renderedArray; } } if (state === "inside") { const closing = xml.indexOf(">", cursor); if (closing !== -1) { let tag = xml.substr(cursor, closing - cursor + 1); const isSingle = Boolean(tag.match(/^<.+\/>/)); // is this line a single tag? ex.
const isClosing = Boolean(tag.match(/^<\/.+>/)); // is this a closing tag? ex. const isXMLDeclaration = Boolean(tag.match(/^<\?xml/)); // is this a closing tag? ex. state = "outside"; cursor = closing + 1; if (isXMLDeclaration) { const encodingRegex = /encoding="([^"]+)"/; if (encodingRegex.test(tag)) { tag = tag.replace(encodingRegex, function (x, p0) { return `encoding="${p0.toUpperCase()}"`; }); } currentType = "declaration"; } else if (isSingle) { // drop whitespace at the end tag = tag.replace(/\s*\/\s*>$/g, "/>"); tag = attributeSorter(tag); currentType = "single"; } else if (isClosing) { // drop whitespace at the end tag = tag.replace(/\s+>$/g, ">"); currentType = "closing"; } else { // drop whitespace at the end tag = tag.replace(/\s+>$/g, ">"); tag = attributeSorter(tag); currentType = "opening"; } renderedArray.push({ type: currentType, value: tag }); } else { const content = xml.substr(cursor); renderedArray.push({ type: "content", value: content }); return renderedArray; } } } return renderedArray; } module.exports = xmlprettify;