Source: util/format_xml.es.js

/**
 * Copyright (c) 2000-present Liferay, Inc. All rights reserved.
 *
 * This library is free software; you can redistribute it and/or modify it under
 * the terms of the GNU Lesser General Public License as published by the Free
 * Software Foundation; either version 2.1 of the License, or (at your option)
 * any later version.
 *
 * This library is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
 * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
 * details.
 */

const NEW_LINE = '\r\n';

const REGEX_CDATA = /<!\[CDATA\[.*?\]\]>/gs;

const REGEX_DECLARATIVE_CLOSE = /-->|\]>/;

const REGEX_DECLARATIVE_OPEN = /<!/;

const REGEX_DIRECTIVE = /<\?/;

const REGEX_DOCTYPE = /!DOCTYPE/;

const REGEX_ELEMENT = /^<\w/;

const REGEX_ELEMENT_CLOSE = /^<\/\w/;

const REGEX_ELEMENT_NAMESPACED = /^<[\w:\-.,]+/;

const REGEX_ELEMENT_NAMESPACED_CLOSE = /^<\/[\w:\-.,]+/;

const REGEX_ELEMENT_OPEN = /<\w/;

const REGEX_NAMESPACE_XML = /xmlns(?::|=)/g;

const REGEX_NAMESPACE_XML_ATTR = /\s*(xmlns)(:|=)/g;

const REGEX_TAG_CLOSE = /<\//;

const REGEX_TAG_OPEN = /</g;

const REGEX_TAG_SINGLE_CLOSE = /\/>/;

const REGEX_WHITESPACE_BETWEEN_TAGS = />\s+</g;

const STR_BLANK = '';

const STR_TOKEN = '~::~';

const STR_TOKEN_CDATA = '<' + STR_TOKEN + 'CDATA' + STR_TOKEN + '>';

const REGEX_TOKEN_CDATA = new RegExp(STR_TOKEN_CDATA, 'g');

const TAG_INDENT = '\t';

const DEFAULT_OPTIONS = {
	newLine: NEW_LINE,
	tagIndent: TAG_INDENT,
};

/**
 * Returns a formatted XML
 * @param {!String} content String to format
 * @param {Object} options Optional parameter that can accept provided options
 * @return {!String} Formatted content
 */
export default function formatXML(content, options = {}) {
	const {newLine, tagIndent} = {
		...DEFAULT_OPTIONS,
		...options,
	};

	if (typeof content !== 'string') {
		throw new TypeError('Parameter content must be a string');
	}

	const cdata = [];

	content = content.trim();
	content = content.replace(REGEX_CDATA, (match) => {
		cdata.push(match);

		return STR_TOKEN_CDATA;
	});
	content = content.replace(REGEX_WHITESPACE_BETWEEN_TAGS, '><');
	content = content.replace(REGEX_TAG_OPEN, STR_TOKEN + '<');
	content = content.replace(REGEX_NAMESPACE_XML_ATTR, STR_TOKEN + '$1$2');
	content = content.replace(REGEX_TOKEN_CDATA, () => cdata.shift());

	let commentCounter = 0;
	let inComment = false;
	const items = content.split(STR_TOKEN);
	let level = 0;
	let result = '';

	items.forEach((item, index) => {
		if (REGEX_CDATA.test(item)) {
			result += indent(level, newLine, tagIndent) + item;
		}
		else if (REGEX_DECLARATIVE_OPEN.test(item)) {
			result += indent(level, newLine, tagIndent) + item;

			commentCounter++;

			inComment = true;

			if (
				REGEX_DECLARATIVE_CLOSE.test(item) ||
				REGEX_DOCTYPE.test(item)
			) {
				commentCounter--;

				inComment = commentCounter !== 0;
			}
		}
		else if (REGEX_DECLARATIVE_CLOSE.test(item)) {
			result += item;

			commentCounter--;

			inComment = commentCounter !== 0;
		}
		else if (
			REGEX_ELEMENT.exec(items[index - 1]) &&
			REGEX_ELEMENT_CLOSE.exec(item) &&
			REGEX_ELEMENT_NAMESPACED.exec(items[index - 1]).toString() ===
				REGEX_ELEMENT_NAMESPACED_CLOSE.exec(item)[0]
					.replace('/', STR_BLANK)
					.toString()
		) {
			result += item;

			if (!inComment) {
				--level;
			}
		}
		else if (
			REGEX_ELEMENT_OPEN.test(item) &&
			!REGEX_TAG_CLOSE.test(item) &&
			!REGEX_TAG_SINGLE_CLOSE.test(item)
		) {
			if (inComment) {
				result += item;
			}
			else {
				result += indent(level++, newLine, tagIndent) + item;
			}
		}
		else if (
			REGEX_ELEMENT_OPEN.test(item) &&
			REGEX_TAG_CLOSE.test(item)
		) {
			if (inComment) {
				result += item;
			}
			else {
				result += indent(level, newLine, tagIndent) + item;
			}
		}
		else if (REGEX_TAG_CLOSE.test(item)) {
			if (inComment) {
				result += item;
			}
			else {
				result += indent(--level, newLine, tagIndent) + item;
			}
		}
		else if (REGEX_TAG_SINGLE_CLOSE.test(item)) {
			if (inComment) {
				result += item;
			}
			else {
				result += indent(level, newLine, tagIndent) + item;
			}
		}
		else if (REGEX_DIRECTIVE.test(item)) {
			result += indent(level, newLine, tagIndent) + item;
		}
		else if (REGEX_NAMESPACE_XML) {
			result += indent(level, newLine, tagIndent) + item;
		}
		else {
			result += item;
		}

		if (new RegExp('^' + newLine).test(result)) {
			result = result.slice(newLine.length);
		}
	});

	return result;
}

/**
 * Returns a string for starting a new line at the specified indent level
 * @param {number} level The level of indentation
 * @return {String} Return a string for starting a new line at the specified indent level
 */
function indent(level, newLine, tagIndent) {
	return newLine + new Array(level + 1).join(tagIndent);
}