/ .. / / -> download
#include <stdio.h>
#include <stdbool.h>
#include <string.h>
#include <unistd.h>
#include <getopt.h>
#include <libxml/tree.h>
#include <libxml/xmlsave.h>
#include "xml-utils.h"

#define PROG_NAME "xml-format"
#define VERSION "2.6.0"

/* Formatter options */
#define FORMAT_OVERWRITE	0x01
#define FORMAT_REMWSONLY	0x02
#define FORMAT_OMIT_DECL	0x04
#define FORMAT_COMPACT		0x08
#define FORMAT_INDENT		0x10

/* Determine whether the node is a textual node. Textual nodes are never
 * subject to formatting or indenting.
 */
static bool is_text(const xmlNodePtr node)
{
	return
		node->type == XML_TEXT_NODE ||
		node->type == XML_CDATA_SECTION_NODE;
}

/* The blank text node children in an element are considered removable only if
 * ALL the text node children of that element are blank (no mixed content), or
 * there are no text node children at all.
 *
 * If there is only a single blank text node child, it is considered removable
 * only if FORMAT_REMWSONLY is set.
 */
static bool blanks_are_removable(xmlNodePtr node, int opts)
{
	xmlNodePtr cur;
	int i;

	if (xmlNodeGetSpacePreserve(node) == 1) {
		return false;
	}

	for (cur = node->children, i = 0; cur; cur = cur->next, ++i) {
		if (is_text(cur) && !xmlIsBlankNode(cur)) {
			return false;
		}
	}

	return i > 1 || (node->children && !is_text(node->children)) || optset(opts, FORMAT_REMWSONLY);
}

/* Remove blank children. */
static void remove_blanks(xmlNodePtr node)
{
	xmlNodePtr cur;

	cur = node->children;

	while (cur) {
		xmlNodePtr next;
		next = cur->next;

		if (xmlIsBlankNode(cur)) {
			xmlUnlinkNode(cur);
			xmlFreeNode(cur);
		}

		cur = next;
	}
}

/* Add indentation to nodes within non-blank nodes. */
static void indent(xmlNodePtr node)
{
	xmlNodePtr cur;
	int n = 0, i;

	for (cur = node->parent; cur; cur = cur->parent) {
		++n;
	}

	if (!node->prev) {
		xmlAddPrevSibling(node, xmlNewText(BAD_CAST "\n"));
		for (i = 1; i < n; ++i) {
			xmlAddPrevSibling(node, xmlNewText(BAD_CAST xmlTreeIndentString));
		}
	}

	for (i = node->next ? 1 : 2; i < n; ++i) {
		xmlAddNextSibling(node, xmlNewText(BAD_CAST xmlTreeIndentString));
	}
	xmlAddNextSibling(node, xmlNewText(BAD_CAST "\n"));
}

/* Format XML nodes. */
static void format(xmlNodePtr node, int opts)
{
	bool remblanks;

	if ((remblanks = blanks_are_removable(node, opts))) {
		remove_blanks(node);
	}

	if (remblanks || optset(opts, FORMAT_INDENT)) {
		xmlNodePtr cur = node->children;

		while (cur) {
			xmlNodePtr next = cur->next;

			format(cur, opts);

			if (remblanks && optset(opts, FORMAT_INDENT) && !optset(opts, FORMAT_COMPACT)) {
				indent(cur);
			}

			cur = next;
		}
	}
}

/* Format an XML file. */
static void format_file(const char *path, const char *out, int opts)
{
	xmlDocPtr doc;
	xmlSaveCtxtPtr save;
	int saveopts = 0;

	if (!(doc = read_xml_doc(path))) {
		return;
	}

	format(xmlDocGetRootElement(doc), opts);

	if (!optset(opts, FORMAT_COMPACT)) {
		saveopts |= XML_SAVE_FORMAT;
	}
	if (optset(opts, FORMAT_OMIT_DECL)) {
		saveopts |= XML_SAVE_NO_DECL;
	}

	if (out) {
		save = xmlSaveToFilename(out, NULL, saveopts);
	} else if (optset(opts, FORMAT_OVERWRITE)) {
		save = xmlSaveToFilename(path, NULL, saveopts);
	} else {
		save = xmlSaveToFilename("-", NULL, saveopts);
	}

	xmlSaveDoc(save, doc);
	xmlSaveClose(save);

	xmlFreeDoc(doc);
}

/* Show usage message. */
static void show_help(void)
{
	puts("Usage: " PROG_NAME " [-cfIOwh?] [-i <str>] [-o <path>] [<file>...]");
	puts("");
	puts("Options:");
	puts("  -c, --compact       Compact output.");
	puts("  -f, --overwrite     Overwrite input XML files.");
	puts("  -h, -?, --help      Show usage message.");
	puts("  -I, --indent-all    Indent nodes within non-blank nodes.");
	puts("  -i, --indent <str>  Set the indentation string.");
	puts("  -O, --omit-decl     Omit XML declaration.");
	puts("  -o, --out <path>    Output to <path> instead of stdout.");
	puts("  -w, --empty         Treat elements containing only whitespace as empty.");
	puts(" --version            Show version information.");
	puts("  <file>              XML file(s) to format. Otherwise, read from stdin.");
	LIBXML2_PARSE_LONGOPT_HELP
}

/* Show version information. */
static void show_version(void)
{
	printf("%s (xml-utils) %s\n", PROG_NAME, VERSION);
	printf("Using libxml %s\n", xmlParserVersion);
}

int main(int argc, char **argv)
{
	int i;
	const char *sopts = "cfIi:Oo:wh?";
	struct option lopts[] = {
		{"version"  , no_argument      , 0, 0},
		{"help"      , no_argument      , 0, 'h'},
		{"compact"   , no_argument      , 0, 'c'},
		{"overwrite" , no_argument      , 0, 'f'},
		{"indent-all", no_argument      , 0, 'I'},
		{"indent"    , required_argument, 0, 'i'},
		{"omit-decl" , no_argument      , 0, 'O'},
		{"out"       , required_argument, 0, 'o'},
		{"empty"     , no_argument      , 0, 'w'},
		LIBXML2_PARSE_LONGOPT_DEFS
		{0, 0, 0, 0}
	};
	int loptind = 0;

	int opts = 0;
	char *indent = NULL;
	char *out = NULL;

	while ((i = getopt_long(argc, argv, sopts, lopts, &loptind)) != -1) {
		switch (i) {
			case 0:
				if (strcmp(lopts[loptind].name, "version") == 0) {
					show_version();
					return 0;
				}
				LIBXML2_PARSE_LONGOPT_HANDLE(lopts, loptind, optarg)
				break;
			case 'c':
				opts |= FORMAT_COMPACT;
				break;
			case 'f':
				opts |= FORMAT_OVERWRITE;
				break;
			case 'I':
				opts |= FORMAT_INDENT;
				break;
			case 'i':
				indent = strdup(optarg);
				break;
			case 'O':
				opts |= FORMAT_OMIT_DECL;
				break;
			case 'o':
				out = strdup(optarg);
				break;
			case 'w':
				opts |= FORMAT_REMWSONLY;
				break;
			case 'h':
			case '?':
				show_help();
				return 0;
		}
	}

	if (indent) {
		xmlTreeIndentString = indent;
	}

	if (optind < argc) {
		for (i = optind; i < argc; ++i) {
			format_file(argv[i], out, opts);
		}
	} else {
		format_file("-", out, opts);
	}

	free(indent);
	free(out);

	xmlCleanupParser();

	return 0;
}


/ gopher://khzae.net/0/s1kd/xml/xml-utils/src/utils/xml-format/xml-format.c
Styles: Light Dark Classic