diff --git a/src/core/operations/XMLBeautify.mjs b/src/core/operations/XMLBeautify.mjs index 2cabb9b2..db2c6b64 100644 --- a/src/core/operations/XMLBeautify.mjs +++ b/src/core/operations/XMLBeautify.mjs @@ -4,9 +4,110 @@ * @license Apache-2.0 */ -import vkbeautify from "vkbeautify"; import Operation from "../Operation.mjs"; +/** + * Adapted from vkBeautify (c) 2012 Vadim Kiryukhin, MIT/GPL dual licence. + */ +function createShiftArr(step) { + let space = " "; + + if (isNaN(parseInt(step, 10))) { + space = step; + } else { + switch (step) { + case 1: space = " "; break; + case 2: space = " "; break; + case 3: space = " "; break; + case 4: space = " "; break; + case 5: space = " "; break; + case 6: space = " "; break; + case 7: space = " "; break; + case 8: space = " "; break; + case 9: space = " "; break; + case 10: space = " "; break; + case 11: space = " "; break; + case 12: space = " "; break; + } + } + + const shift = ["\n"]; + for (let ix = 0; ix < 100; ix++) { + shift.push(shift[ix] + space); + } + return shift; +} + +/** + * Adapted from vkBeautify (c) 2012 Vadim Kiryukhin, MIT/GPL dual licence. + * + * Fix for issue #2501: the xmlns check is moved before the generic self-close + * check so that a fragment like `xmlns="foo" />` is caught here rather than + * by the `/>` branch. When such a fragment ends with `/>` the element is + * self-closing, so we decrement deep to undo the increment that fired when + * the preceding `\s{0,}<") + .replace(/ or -1) { + str += shift[deep] + ar[ix]; + inComment = true; + if (ar[ix].search(/-->/) > -1 || ar[ix].search(/\]>/) > -1 || ar[ix].search(/!DOCTYPE/) > -1) { + inComment = false; + } + // end comment or + } else if (ar[ix].search(/-->/) > -1 || ar[ix].search(/\]>/) > -1) { + str += ar[ix]; + inComment = false; + // + } else if ( + /^<\w/.exec(ar[ix - 1]) && /^<\/\w/.exec(ar[ix]) && + /^<[\w:\-.,]+/.exec(ar[ix - 1])?.[0] === /^<\/[\w:\-.,]+/.exec(ar[ix])?.[0].replace("/", "") + ) { + str += ar[ix]; + if (!inComment) deep--; + // + } else if (ar[ix].search(/<\w/) > -1 && ar[ix].search(/<\//) === -1 && ar[ix].search(/\/>/) === -1) { + str = !inComment ? str + shift[deep++] + ar[ix] : str + ar[ix]; + // ... + } else if (ar[ix].search(/<\w/) > -1 && ar[ix].search(/<\//) > -1) { + str = !inComment ? str + shift[deep] + ar[ix] : str + ar[ix]; + // + } else if (ar[ix].search(/<\//) > -1) { + str = !inComment ? str + shift[--deep] + ar[ix] : str + ar[ix]; + // + } else if (ar[ix].search(/<\?/) > -1) { + str += shift[deep] + ar[ix]; + // xmlns: or xmlns= — must be checked before the generic /> branch so that a self-closing + // element with a namespace attribute (e.g. ) is handled here. + } else if (ar[ix].search(/xmlns:/) > -1 || ar[ix].search(/xmlns=/) > -1) { + str = !inComment ? str + shift[deep] + ar[ix] : str + ar[ix]; + if (ar[ix].search(/\/>/) > -1) deep--; + // + } else if (ar[ix].search(/\/>/) > -1) { + str = !inComment ? str + shift[deep] + ar[ix] : str + ar[ix]; + } else { + str += ar[ix]; + } + } + + return str[0] === "\n" ? str.slice(1) : str; +} + /** * XML Beautify operation */ @@ -39,7 +140,7 @@ class XMLBeautify extends Operation { */ run(input, args) { const indentStr = args[0]; - return vkbeautify.xml(input, indentStr); + return xmlBeautify(input, indentStr); } } diff --git a/tests/operations/tests/XMLBeautify.mjs b/tests/operations/tests/XMLBeautify.mjs new file mode 100644 index 00000000..31624ba4 --- /dev/null +++ b/tests/operations/tests/XMLBeautify.mjs @@ -0,0 +1,59 @@ +/** + * XML Beautify tests. + * + * @author Allan Leary + * @copyright Crown Copyright 2026 + * @license Apache-2.0 + */ +import TestRegister from "../../lib/TestRegister.mjs"; + +TestRegister.addTests([ + { + name: "XML Beautify: basic nested elements", + input: "", + expectedOutput: "\n\t\n\t\t\n\t\n", + recipeConfig: [{ op: "XML Beautify", args: ["\t"] }], + }, + { + name: "XML Beautify: self-closing element without xmlns", + input: "", + expectedOutput: "\n\t\n", + recipeConfig: [{ op: "XML Beautify", args: ["\t"] }], + }, + { + name: "XML Beautify: self-closing element with xmlns (issue #2501)", + input: "", + expectedOutput: "\n\t\n", + recipeConfig: [{ op: "XML Beautify", args: ["\t"] }], + }, + { + name: "XML Beautify: self-closing element with multiple xmlns attributes (issue #2501)", + input: "", + expectedOutput: "\n\t\n", + recipeConfig: [{ op: "XML Beautify", args: ["\t"] }], + }, + { + name: "XML Beautify: xmlns on opening (non-self-closing) tag", + input: "text", + expectedOutput: "\n\ttext\n\t\n", + recipeConfig: [{ op: "XML Beautify", args: ["\t"] }], + }, + { + name: "XML Beautify: siblings after xmlns self-closing are correctly indented", + input: "", + expectedOutput: "\n\t\n\t\n\t\n", + recipeConfig: [{ op: "XML Beautify", args: ["\t"] }], + }, + { + name: "XML Beautify: xml declaration preserved", + input: "", + expectedOutput: "\n\n\t\n", + recipeConfig: [{ op: "XML Beautify", args: ["\t"] }], + }, + { + name: "XML Beautify: 4-space indent", + input: "", + expectedOutput: "\n \n", + recipeConfig: [{ op: "XML Beautify", args: [" "] }], + }, +]);