Fix XML Beautify indentation after xmlns self-closing element
vkbeautify splits `<b xmlns="foo" />` into two fragments during preprocessing. The `<b` fragment incorrectly incremented the indent depth, but the corresponding decrement never fired because `/>` was on the xmlns fragment, not a standalone self-closing token. Subsequent siblings were therefore indented one level too deep. Fix by moving the xmlns branch before the generic `/>` branch in the if-else chain and decrementing depth when the xmlns fragment ends with `/>`. The vkbeautify xml and createShiftArr functions are inlined into XMLBeautify.mjs to avoid patching node_modules directly. Fixes #2501 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
275b594f7c
commit
f533329f77
@ -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 `<tagName` fragment was classified as an opening element.
|
||||
*/
|
||||
function xmlBeautify(text, step) {
|
||||
const ar = text
|
||||
.replace(/>\s{0,}</g, "><")
|
||||
.replace(/</g, "~::~<")
|
||||
.replace(/\s*xmlns:/g, "~::~xmlns:")
|
||||
.replace(/\s*xmlns=/g, "~::~xmlns=")
|
||||
.split("~::~");
|
||||
|
||||
const len = ar.length;
|
||||
const shift = createShiftArr(step);
|
||||
let inComment = false;
|
||||
let deep = 0;
|
||||
let str = "";
|
||||
|
||||
for (let ix = 0; ix < len; ix++) {
|
||||
// start comment or <![CDATA[...]]> or <!DOCTYPE
|
||||
if (ar[ix].search(/<!/) > -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 <![CDATA[...]]>
|
||||
} else if (ar[ix].search(/-->/) > -1 || ar[ix].search(/\]>/) > -1) {
|
||||
str += ar[ix];
|
||||
inComment = false;
|
||||
// <elm></elm>
|
||||
} 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--;
|
||||
// <elm>
|
||||
} 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];
|
||||
// <elm>...</elm>
|
||||
} else if (ar[ix].search(/<\w/) > -1 && ar[ix].search(/<\//) > -1) {
|
||||
str = !inComment ? str + shift[deep] + ar[ix] : str + ar[ix];
|
||||
// </elm>
|
||||
} else if (ar[ix].search(/<\//) > -1) {
|
||||
str = !inComment ? str + shift[--deep] + ar[ix] : str + ar[ix];
|
||||
// <? xml ... ?>
|
||||
} 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. <b xmlns="foo" />) 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--;
|
||||
// <elm/>
|
||||
} 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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
59
tests/operations/tests/XMLBeautify.mjs
Normal file
59
tests/operations/tests/XMLBeautify.mjs
Normal file
@ -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: "<root><parent><child/></parent></root>",
|
||||
expectedOutput: "<root>\n\t<parent>\n\t\t<child/>\n\t</parent>\n</root>",
|
||||
recipeConfig: [{ op: "XML Beautify", args: ["\t"] }],
|
||||
},
|
||||
{
|
||||
name: "XML Beautify: self-closing element without xmlns",
|
||||
input: "<a><b/></a>",
|
||||
expectedOutput: "<a>\n\t<b/>\n</a>",
|
||||
recipeConfig: [{ op: "XML Beautify", args: ["\t"] }],
|
||||
},
|
||||
{
|
||||
name: "XML Beautify: self-closing element with xmlns (issue #2501)",
|
||||
input: "<a><b xmlns=\"foo\" /></a>",
|
||||
expectedOutput: "<a>\n\t<b\n\t\txmlns=\"foo\" />\n</a>",
|
||||
recipeConfig: [{ op: "XML Beautify", args: ["\t"] }],
|
||||
},
|
||||
{
|
||||
name: "XML Beautify: self-closing element with multiple xmlns attributes (issue #2501)",
|
||||
input: "<a><b xmlns:foo=\"a\" xmlns:bar=\"b\" /></a>",
|
||||
expectedOutput: "<a>\n\t<b\n\t\txmlns:foo=\"a\"\n\t\txmlns:bar=\"b\" />\n</a>",
|
||||
recipeConfig: [{ op: "XML Beautify", args: ["\t"] }],
|
||||
},
|
||||
{
|
||||
name: "XML Beautify: xmlns on opening (non-self-closing) tag",
|
||||
input: "<a><b xmlns=\"foo\">text</b></a>",
|
||||
expectedOutput: "<a>\n\t<b\n\t\txmlns=\"foo\">text\n\t</b>\n</a>",
|
||||
recipeConfig: [{ op: "XML Beautify", args: ["\t"] }],
|
||||
},
|
||||
{
|
||||
name: "XML Beautify: siblings after xmlns self-closing are correctly indented",
|
||||
input: "<root><a xmlns=\"foo\" /><b/><c/></root>",
|
||||
expectedOutput: "<root>\n\t<a\n\t\txmlns=\"foo\" />\n\t<b/>\n\t<c/>\n</root>",
|
||||
recipeConfig: [{ op: "XML Beautify", args: ["\t"] }],
|
||||
},
|
||||
{
|
||||
name: "XML Beautify: xml declaration preserved",
|
||||
input: "<?xml version=\"1.0\"?><root><child/></root>",
|
||||
expectedOutput: "<?xml version=\"1.0\"?>\n<root>\n\t<child/>\n</root>",
|
||||
recipeConfig: [{ op: "XML Beautify", args: ["\t"] }],
|
||||
},
|
||||
{
|
||||
name: "XML Beautify: 4-space indent",
|
||||
input: "<root><child/></root>",
|
||||
expectedOutput: "<root>\n <child/>\n</root>",
|
||||
recipeConfig: [{ op: "XML Beautify", args: [" "] }],
|
||||
},
|
||||
]);
|
||||
Loading…
x
Reference in New Issue
Block a user