xml-formatter
Version:
Converts a XML string into a human readable format (pretty print) while respecting the xml:space attribute
182 lines • 6.92 kB
JavaScript
import xmlParser from 'xml-parser-xo';
function newLine(state) {
if (!state.options.indentation && !state.options.lineSeparator)
return;
state.content += state.options.lineSeparator;
let i;
for (i = 0; i < state.level; i++) {
state.content += state.options.indentation;
}
}
function indent(state) {
state.content = state.content.replace(/ +$/, '');
let i;
for (i = 0; i < state.level; i++) {
state.content += state.options.indentation;
}
}
function appendContent(state, content) {
state.content += content;
}
function processNode(node, state, preserveSpace) {
if (typeof node.content === 'string') {
processContent(node.content, state, preserveSpace);
}
else if (node.type === 'Element') {
processElementNode(node, state, preserveSpace);
}
else if (node.type === 'ProcessingInstruction') {
processProcessingIntruction(node, state);
}
else {
throw new Error('Unknown node type: ' + node.type);
}
}
function processContent(content, state, preserveSpace) {
if (!preserveSpace) {
const trimmedContent = content.trim();
if (state.options.lineSeparator) {
content = trimmedContent;
}
else if (trimmedContent.length === 0) {
content = trimmedContent;
}
}
if (content.length > 0) {
if (!preserveSpace && state.content.length > 0) {
newLine(state);
}
appendContent(state, content);
}
}
function isPathMatchingIgnoredPaths(path, ignoredPaths) {
const fullPath = '/' + path.join('/');
const pathLastPart = path[path.length - 1];
return ignoredPaths.includes(pathLastPart) || ignoredPaths.includes(fullPath);
}
function processElementNode(node, state, preserveSpace) {
state.path.push(node.name);
if (!preserveSpace && state.content.length > 0) {
newLine(state);
}
appendContent(state, '<' + node.name);
processAttributes(state, node.attributes);
if (node.children === null || (state.options.forceSelfClosingEmptyTag && node.children.length === 0)) {
const selfClosingNodeClosingTag = state.options.whiteSpaceAtEndOfSelfclosingTag ? ' />' : '/>';
// self-closing node
appendContent(state, selfClosingNodeClosingTag);
}
else if (node.children.length === 0) {
// empty node
appendContent(state, '></' + node.name + '>');
}
else {
const nodeChildren = node.children;
appendContent(state, '>');
state.level++;
let nodePreserveSpace = node.attributes['xml:space'] === 'preserve' || preserveSpace;
let ignoredPath = false;
if (!nodePreserveSpace && state.options.ignoredPaths) {
ignoredPath = isPathMatchingIgnoredPaths(state.path, state.options.ignoredPaths);
nodePreserveSpace = ignoredPath;
}
if (!nodePreserveSpace && state.options.collapseContent) {
let containsTextNodes = false;
let containsTextNodesWithLineBreaks = false;
let containsNonTextNodes = false;
nodeChildren.forEach(function (child, index) {
if (child.type === 'Text') {
if (child.content.includes('\n')) {
containsTextNodesWithLineBreaks = true;
child.content = child.content.trim();
}
else if ((index === 0 || index === nodeChildren.length - 1) && !preserveSpace) {
if (child.content.trim().length === 0) {
// If the text node is at the start or end and is empty, it should be ignored when formatting
child.content = '';
}
}
// If there is some content or whitespaces have been removed and there is no other siblings
if (child.content.trim().length > 0 || nodeChildren.length === 1) {
containsTextNodes = true;
}
}
else if (child.type === 'CDATA') {
containsTextNodes = true;
}
else {
containsNonTextNodes = true;
}
});
if (containsTextNodes && (!containsNonTextNodes || !containsTextNodesWithLineBreaks)) {
nodePreserveSpace = true;
}
}
nodeChildren.forEach(function (child) {
processNode(child, state, preserveSpace || nodePreserveSpace);
});
state.level--;
if (!preserveSpace && !nodePreserveSpace) {
newLine(state);
}
if (ignoredPath) {
indent(state);
}
appendContent(state, '</' + node.name + '>');
}
state.path.pop();
}
function processAttributes(state, attributes) {
Object.keys(attributes).forEach(function (attr) {
const escaped = attributes[attr].replace(/"/g, '"');
appendContent(state, ' ' + attr + '="' + escaped + '"');
});
}
function processProcessingIntruction(node, state) {
if (state.content.length > 0) {
newLine(state);
}
appendContent(state, '<?' + node.name);
processAttributes(state, node.attributes);
appendContent(state, '?>');
}
/**
* Converts the given XML into human readable format.
*/
function formatXml(xml, options = {}) {
options.indentation = 'indentation' in options ? options.indentation : ' ';
options.collapseContent = options.collapseContent === true;
options.lineSeparator = 'lineSeparator' in options ? options.lineSeparator : '\r\n';
options.whiteSpaceAtEndOfSelfclosingTag = options.whiteSpaceAtEndOfSelfclosingTag === true;
options.throwOnFailure = options.throwOnFailure !== false;
try {
const parsedXml = xmlParser(xml, { filter: options.filter, strictMode: options.strictMode });
const state = { content: '', level: 0, options: options, path: [] };
if (parsedXml.declaration) {
processProcessingIntruction(parsedXml.declaration, state);
}
parsedXml.children.forEach(function (child) {
processNode(child, state, false);
});
if (!options.lineSeparator) {
return state.content;
}
return state.content
.replace(/\r\n/g, '\n')
.replace(/\n/g, options.lineSeparator);
}
catch (err) {
if (options.throwOnFailure) {
throw err;
}
return xml;
}
}
formatXml.minify = (xml, options = {}) => {
return formatXml(xml, Object.assign(Object.assign({}, options), { indentation: '', lineSeparator: '' }));
};
if (typeof module !== 'undefined' && typeof exports === 'object') {
module.exports = formatXml;
}
export default formatXml;
//# sourceMappingURL=index.js.map