/ .. / / -> download
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <getopt.h>
#include <stdbool.h>
#include <ctype.h>
#include <libgen.h>
#include <regex.h>
#include <libxml/tree.h>
#include <libxml/xpath.h>
#include <libxslt/transform.h>
#include "s1kd_tools.h"
#include "xslt.h"
#include "elems.h"

#define PROG_NAME "s1kd-ref"
#define VERSION "3.8.0"

#define ERR_PREFIX PROG_NAME ": ERROR: "
#define WRN_PREFIX PROG_NAME ": WARNING: "
#define INF_PREFIX PROG_NAME ": INFO: "

#define EXIT_MISSING_FILE 1
#define EXIT_BAD_INPUT 2
#define EXIT_BAD_ISSUE 3
#define EXIT_BAD_XPATH 4

#define OPT_TITLE     (int) 0x001
#define OPT_ISSUE     (int) 0x002
#define OPT_LANG      (int) 0x004
#define OPT_DATE      (int) 0x008
#define OPT_SRCID     (int) 0x010
#define OPT_CIRID     (int) 0x020
#define OPT_INS       (int) 0x040
#define OPT_URL       (int) 0x080
#define OPT_CONTENT   (int) 0x100
#define OPT_NONSTRICT (int) 0x200

/* Regular expressions to match references. */

/* Common components */
#define ISSNO_REGEX "(_[0-9]{3}-[0-9]{2})?"
#define LANG_REGEX  "(_[A-Z]{2}-[A-Z]{2})?"

/* Optional prefix */
#define DME_REGEX "(DME-)?[0-9A-Z]+-[0-9A-Z]+-[0-9A-Z]{2,14}-[0-9A-Z]{1,4}-[0-9A-Z]{2,3}-[0-9A-Z]{2}-[0-9A-Z]{2,4}-[0-9A-Z]{3,5}-[0-9A-Z]{4}-[ABCDT](-[0-9A-Z]{4})?" ISSNO_REGEX LANG_REGEX
#define DMC_REGEX "(DMC-)?[0-9A-Z]{2,14}-[0-9A-Z]{1,4}-[0-9A-Z]{2,3}-[0-9A-Z]{2}-[0-9A-Z]{2,4}-[0-9A-Z]{3,5}-[0-9A-Z]{4}-[ABCDT](-[0-9A-Z]{4})?" ISSNO_REGEX LANG_REGEX
#define CSN_REGEX "(CSN-)?[0-9A-Z]{2,14}-[0-9A-Z]{1,4}-[0-9A-Z]{2,3}-[0-9A-Z]{2}-[0-9A-Z]{2,4}-[0-9A-Z]{3,5}-[0-9A-Z]{4}-[ABCDT]"
#define PME_REGEX "(PME-)?[0-9A-Z]+-[0-9A-Z]+-[0-9A-Z]{2,14}-[0-9A-Z]{5}-[0-9A-Z]{5}-[0-9]{2}" ISSNO_REGEX LANG_REGEX
#define PMC_REGEX "(PMC-)?[0-9A-Z]{2,14}-[0-9A-Z]{5}-[0-9A-Z]{5}-[0-9]{2}" ISSNO_REGEX LANG_REGEX
#define SME_REGEX "(SME-)?[0-9A-Z]+-[0-9A-Z]+-[0-9A-Z]{2,14}-[0-9A-Z]{5}-[0-9A-Z]{5}-[0-9]{2}" ISSNO_REGEX LANG_REGEX
#define SMC_REGEX "(SMC-)?[0-9A-Z]{2,14}-[0-9A-Z]{5}-[0-9A-Z]{5}-[0-9]{2}" ISSNO_REGEX LANG_REGEX
#define COM_REGEX "(COM-)?[0-9A-Z]{2,14}-[0-9A-Z]{5}-[0-9]{4}-[0-9]{5}-[QIR]" LANG_REGEX
#define DML_REGEX "(DML-)?[0-9A-Z]{2,14}-[0-9A-Z]{5}-[CPS]-[0-9]{4}-[0-9]{5}" ISSNO_REGEX

/* Mandatory prefix */
#define DME_REGEX_STRICT "DME-[0-9A-Z]+-[0-9A-Z]+-[0-9A-Z]{2,14}-[0-9A-Z]{1,4}-[0-9A-Z]{2,3}-[0-9A-Z]{2}-[0-9A-Z]{2,4}-[0-9A-Z]{3,5}-[0-9A-Z]{4}-[ABCDT](-[0-9A-Z]{4})?" ISSNO_REGEX LANG_REGEX
#define DMC_REGEX_STRICT "DMC-[0-9A-Z]{2,14}-[0-9A-Z]{1,4}-[0-9A-Z]{2,3}-[0-9A-Z]{2}-[0-9A-Z]{2,4}-[0-9A-Z]{3,5}-[0-9A-Z]{4}-[ABCDT](-[0-9A-Z]{4})?" ISSNO_REGEX LANG_REGEX
#define CSN_REGEX_STRICT "CSN-[0-9A-Z]{2,14}-[0-9A-Z]{1,4}-[0-9A-Z]{2,3}-[0-9A-Z]{2}-[0-9A-Z]{2,4}-[0-9A-Z]{3,5}-[0-9A-Z]{4}-[ABCDT]"
#define PME_REGEX_STRICT "PME-[0-9A-Z]+-[0-9A-Z]+-[0-9A-Z]{2,14}-[0-9A-Z]{5}-[0-9A-Z]{5}-[0-9]{2}" ISSNO_REGEX LANG_REGEX
#define PMC_REGEX_STRICT "PMC-[0-9A-Z]{2,14}-[0-9A-Z]{5}-[0-9A-Z]{5}-[0-9]{2}" ISSNO_REGEX LANG_REGEX
#define SME_REGEX_STRICT "SME-[0-9A-Z]+-[0-9A-Z]+-[0-9A-Z]{2,14}-[0-9A-Z]{5}-[0-9A-Z]{5}-[0-9]{2}" ISSNO_REGEX LANG_REGEX
#define SMC_REGEX_STRICT "SMC-[0-9A-Z]{2,14}-[0-9A-Z]{5}-[0-9A-Z]{5}-[0-9]{2}" ISSNO_REGEX LANG_REGEX
#define COM_REGEX_STRICT "COM-[0-9A-Z]{2,14}-[0-9A-Z]{5}-[0-9]{4}-[0-9]{5}-[QIR]" LANG_REGEX
#define DML_REGEX_STRICT "DML-[0-9A-Z]{2,14}-[0-9A-Z]{5}-[CPS]-[0-9]{4}-[0-9]{5}" ISSNO_REGEX
#define ICN_REGEX "(ICN-[A-Z0-9]{5}-[A-Z0-9]{5,10}-[0-9]{3}-[0-9]{2})|(ICN-[A-Z0-9]{2,14}-[A-Z0-9]{1,4}-[A-Z0-9]{6,9}-[A-Z0-9]{1}-[A-Z0-9]{5}-[A-Z0-9]{5}-[A-Z]{1}-[0-9]{2,3}-[0-9]{1,2})"

/* No prefix */
#define DME_REGEX_NOPRE "^[0-9A-Z]+-[0-9A-Z]+-[0-9A-Z]{2,14}-[0-9A-Z]{1,4}-[0-9A-Z]{2,3}-[0-9A-Z]{2}-[0-9A-Z]{2,4}-[0-9A-Z]{3,5}-[0-9A-Z]{4}-[ABCDT](-[0-9A-Z]{4})?" ISSNO_REGEX LANG_REGEX "$"
#define DMC_REGEX_NOPRE "^[0-9A-Z]{2,14}-[0-9A-Z]{1,4}-[0-9A-Z]{2,3}-[0-9A-Z]{2}-[0-9A-Z]{2,4}-[0-9A-Z]{3,5}-[0-9A-Z]{4}-[ABCDT](-[0-9A-Z]{4})?" ISSNO_REGEX LANG_REGEX "$"
#define PME_REGEX_NOPRE "^[0-9A-Z]+-[0-9A-Z]+-[0-9A-Z]{2,14}-[0-9A-Z]{5}-[0-9A-Z]{5}-[0-9]{2}" ISSNO_REGEX LANG_REGEX "$"
#define PMC_REGEX_NOPRE "^[0-9A-Z]{2,14}-[0-9A-Z]{5}-[0-9A-Z]{5}-[0-9]{2}" ISSNO_REGEX LANG_REGEX "$"
#define COM_REGEX_NOPRE "^[0-9A-Z]{2,14}-[0-9A-Z]{5}-[0-9]{4}-[0-9]{5}-[QIR]" LANG_REGEX "$"
#define DML_REGEX_NOPRE "^[0-9A-Z]{2,14}-[0-9A-Z]{5}-[CPS]-[0-9]{4}-[0-9]{5}" ISSNO_REGEX "$"

/* Issue of the S1000D specification to create references for. */
enum issue { ISS_20, ISS_21, ISS_22, ISS_23, ISS_30, ISS_40, ISS_41, ISS_42, ISS_50 };

#define DEFAULT_S1000D_ISSUE ISS_50

/* Verbosity of the program's output. */
static enum verbosity { QUIET, NORMAL, VERBOSE, DEBUG } verbosity = NORMAL;

/* Function for creating a new reference node. */
typedef xmlNodePtr (*newref_t)(const char *, const char *, int);

static xmlNode *find_child(xmlNode *parent, char *name)
{
	xmlNode *cur;

	if (!parent) return NULL;

	for (cur = parent->children; cur; cur = cur->next) {
		if (strcmp((char *) cur->name, name) == 0) {
			return cur;
		}
	}

	return NULL;
}

static xmlNodePtr first_xpath_node(xmlDocPtr doc, xmlNodePtr node, xmlChar *xpath)
{
	xmlXPathContextPtr ctx;
	xmlXPathObjectPtr obj;
	xmlNodePtr first;

	ctx = xmlXPathNewContext(doc ? doc : node->doc);
	ctx->node = node;
	obj = xmlXPathEvalExpression(xpath, ctx);

	first = xmlXPathNodeSetIsEmpty(obj->nodesetval) ? NULL : obj->nodesetval->nodeTab[0];

	xmlXPathFreeObject(obj);
	xmlXPathFreeContext(ctx);

	return first;
}

static xmlChar *first_xpath_value(xmlDocPtr doc, xmlNodePtr node, xmlChar *xpath)
{
	return xmlNodeGetContent(first_xpath_node(doc, node, xpath));
}

static xmlNodePtr find_or_create_refs(xmlDocPtr doc)
{
	xmlNodePtr refs;

	refs = first_xpath_node(doc, NULL, BAD_CAST "//content//refs");

	if (!refs) {
		xmlNodePtr content, child;
		content = first_xpath_node(doc, NULL, BAD_CAST "//content");
		child = xmlFirstElementChild(content);
		refs = xmlNewNode(NULL, BAD_CAST "refs");
		if (child) {
			refs = xmlAddPrevSibling(child, refs);
		} else {
			refs = xmlAddChild(content ,refs);
		}
	}

	return refs;
}

static void dump_node(xmlNodePtr node, const char *dst)
{
	xmlBufferPtr buf;
	buf = xmlBufferCreate();
	xmlNodeDump(buf, NULL, node, 0, 0);
	if (strcmp(dst, "-") == 0) {
		puts((char *) buf->content);
	} else {
		FILE *f;
		f = fopen(dst, "w");
		fputs((char *) buf->content, f);
		fclose(f);
	}
	xmlBufferFree(buf);
}

static xmlNodePtr new_issue_info(char *s)
{
	char n[4], w[3];
	xmlNodePtr issue_info;

	if (sscanf(s, "_%3[^-]-%2s", n, w) != 2) {
		return NULL;
	}

	issue_info = xmlNewNode(NULL, BAD_CAST "issueInfo");
	xmlSetProp(issue_info, BAD_CAST "issueNumber", BAD_CAST n);
	xmlSetProp(issue_info, BAD_CAST "inWork", BAD_CAST w);

	return issue_info;
}

static xmlNodePtr new_language(char *s)
{
	char l[4], c[3];
	xmlNodePtr language;

	if (sscanf(s, "_%3[^-]-%2s", l, c) != 2) {
		return NULL;
	}

	lowercase(l);

	language = xmlNewNode(NULL, BAD_CAST "language");
	xmlSetProp(language, BAD_CAST "languageIsoCode", BAD_CAST l);
	xmlSetProp(language, BAD_CAST "countryIsoCode", BAD_CAST c);

	return language;
}

static void set_xlink(xmlNodePtr node, const char *href)
{
	xmlNsPtr xlink;
	xlink = xmlNewNs(node, BAD_CAST "http://www.w3.org/1999/xlink", BAD_CAST "xlink");
	xmlSetNsProp(node, xlink, BAD_CAST "href", BAD_CAST href);
}

#define SME_FMT "SME-%255[^-]-%255[^-]-%14[^-]-%5s-%5s-%2s"
#define SMC_FMT "SMC-%14[^-]-%5s-%5s-%2s"

static xmlNodePtr new_smc_ref(const char *ref, const char *fname, int opts)
{
	char extension_producer[256] = "";
	char extension_code[256]     = "";
	char model_ident_code[15]    = "";
	char smc_issuer[6]            = "";
	char smc_number[6]            = "";
	char smc_volume[3]            = "";
	xmlNode *smc_ref;
	xmlNode *smc_ref_ident;
	xmlNode *smc_code;
	bool is_extended;
	int n;

	is_extended = strncmp(ref, "SME-", 4) == 0;

	if (is_extended) {
		n = sscanf(ref, SME_FMT,
			extension_producer,
			extension_code,
			model_ident_code,
			smc_issuer,
			smc_number,
			smc_volume);
		if (n != 6) {
			if (verbosity > QUIET) {
				fprintf(stderr, ERR_PREFIX "SCORM content package extended code invalid: %s\n", ref);
			}
			exit(EXIT_BAD_INPUT);
		}
	} else {
		n = sscanf(ref, SMC_FMT,
			model_ident_code,
			smc_issuer,
			smc_number,
			smc_volume);
		if (n != 4) {
			if (verbosity > QUIET) {
				fprintf(stderr, ERR_PREFIX "SCORM content package code invalid: %s\n", ref);
			}
			exit(EXIT_BAD_INPUT);
		}
	}

	smc_ref = xmlNewNode(NULL, BAD_CAST "scormContentPackageRef");
	smc_ref_ident = xmlNewChild(smc_ref, NULL, BAD_CAST "scormContentPackageRefIdent", NULL);

	if (is_extended) {
		xmlNode *ident_extension;
		ident_extension = xmlNewChild(smc_ref_ident, NULL, BAD_CAST "identExtension", NULL);
		xmlSetProp(ident_extension, BAD_CAST "extensionProducer", BAD_CAST extension_producer);
		xmlSetProp(ident_extension, BAD_CAST "extensionCode", BAD_CAST extension_code);
	}

	smc_code = xmlNewChild(smc_ref_ident, NULL, BAD_CAST "scormContentPackageCode", NULL);

	xmlSetProp(smc_code, BAD_CAST "modelIdentCode", BAD_CAST model_ident_code);
	xmlSetProp(smc_code, BAD_CAST "scormContentPackageIssuer", BAD_CAST smc_issuer);
	xmlSetProp(smc_code, BAD_CAST "scormContentPackageNumber", BAD_CAST smc_number);
	xmlSetProp(smc_code, BAD_CAST "scormContentPackageVolume", BAD_CAST smc_volume);

	if (opts) {
		xmlDocPtr doc;
		xmlNodePtr ref_smc_address = NULL;
		xmlNodePtr ref_smc_ident = NULL;
		xmlNodePtr ref_smc_address_items = NULL;
		xmlNodePtr ref_smc_title = NULL;
		xmlNodePtr ref_smc_issue_date = NULL;
		xmlNodePtr issue_info = NULL;
		xmlNodePtr language = NULL;
		char *s;

		if ((doc = read_xml_doc(fname))) {
			ref_smc_address = first_xpath_node(doc, NULL, BAD_CAST "//scormContentPackageAddress");
			ref_smc_ident = find_child(ref_smc_address, "scormContentPackageIdent");
			ref_smc_address_items = find_child(ref_smc_address, "scormContentPackageAddressItems");
			ref_smc_title = find_child(ref_smc_address_items, "scormContentPackageTitle");
			ref_smc_issue_date = find_child(ref_smc_address_items, "issueDate");
		}

		s = strchr(ref, '_');

		if (optset(opts, OPT_ISSUE)) {
			if (doc) {
				issue_info = xmlCopyNode(find_child(ref_smc_ident, "issueInfo"), 1);
			} else if (s && isdigit((unsigned char) s[1])) {
				issue_info = new_issue_info(s);
			} else {
				if (verbosity > QUIET) {
					fprintf(stderr, WRN_PREFIX "Could not read issue info from SCORM content package: %s\n", ref);
				}
				issue_info = NULL;
			}

			xmlAddChild(smc_ref_ident, issue_info);
		}

		if (optset(opts, OPT_LANG)) {
			if (doc) {
				language = xmlCopyNode(find_child(ref_smc_ident, "language"), 1);
			} else if (s && (s = strchr(s + 1, '_'))) {
				language = new_language(s);
			} else {
				if (verbosity > QUIET) {
					fprintf(stderr, WRN_PREFIX "Could not read language from SCORM content package: %s\n", ref);
				}
				language = NULL;
			}

			xmlAddChild(smc_ref_ident, language);
		}

		if (optset(opts, OPT_TITLE) || optset(opts, OPT_DATE)) {
			xmlNodePtr smc_ref_address_items = NULL, smc_title, issue_date;

			if (doc) {
				smc_ref_address_items = xmlNewChild(smc_ref, NULL, BAD_CAST "scormContentPackageRefAddressItems", NULL);
				smc_title = ref_smc_title;
				issue_date = ref_smc_issue_date;
			} else {
				smc_title = NULL;
				issue_date = NULL;
			}

			if (optset(opts, OPT_TITLE)) {
				if (smc_title) {
					xmlAddChild(smc_ref_address_items, xmlCopyNode(smc_title, 1));
				} else {
					if (verbosity > QUIET) {
						fprintf(stderr, WRN_PREFIX "Could not read title from SCORM content package: %s\n", ref);
					}
				}
			}
			if (optset(opts, OPT_DATE)) {
				if (issue_date) {
					xmlAddChild(smc_ref_address_items, xmlCopyNode(issue_date, 1));
				} else {
					if (verbosity > QUIET) {
						fprintf(stderr, WRN_PREFIX "Could not read date from SCORM content package: %s\n", ref);
					}
				}
			}
		}

		xmlFreeDoc(doc);

		if (optset(opts, OPT_SRCID)) {
			xmlNodePtr smc, issno, lang, src;

			smc   = xmlCopyNode(smc_code, 1);
			issno = xmlCopyNode(issue_info, 1);
			lang  = xmlCopyNode(language, 1);

			src = xmlNewNode(NULL, BAD_CAST "sourceScormContentPackageIdent");
			xmlAddChild(src, smc);
			xmlAddChild(src, lang);
			xmlAddChild(src, issno);

			xmlFreeNode(smc_ref);
			smc_ref = src;
		}

		if (optset(opts, OPT_URL)) {
			set_xlink(smc_ref, fname);
		}
	}

	return smc_ref;
}

#define PME_FMT "PME-%255[^-]-%255[^-]-%14[^-]-%5s-%5s-%2s"
#define PMC_FMT "PMC-%14[^-]-%5s-%5s-%2s"

static xmlNodePtr new_pm_ref(const char *ref, const char *fname, int opts)
{
	char extension_producer[256] = "";
	char extension_code[256]     = "";
	char model_ident_code[15]    = "";
	char pm_issuer[6]            = "";
	char pm_number[6]            = "";
	char pm_volume[3]            = "";
	xmlNode *pm_ref;
	xmlNode *pm_ref_ident;
	xmlNode *pm_code;
	bool is_extended;
	int n;

	is_extended = strncmp(ref, "PME-", 4) == 0;

	if (is_extended) {
		n = sscanf(ref, PME_FMT,
			extension_producer,
			extension_code,
			model_ident_code,
			pm_issuer,
			pm_number,
			pm_volume);
		if (n != 6) {
			if (verbosity > QUIET) {
				fprintf(stderr, ERR_PREFIX "Publication module extended code invalid: %s\n", ref);
			}
			exit(EXIT_BAD_INPUT);
		}
	} else {
		n = sscanf(ref, PMC_FMT,
			model_ident_code,
			pm_issuer,
			pm_number,
			pm_volume);
		if (n != 4) {
			if (verbosity > QUIET) {
				fprintf(stderr, ERR_PREFIX "Publication module code invalid: %s\n", ref);
			}
			exit(EXIT_BAD_INPUT);
		}
	}

	pm_ref = xmlNewNode(NULL, BAD_CAST "pmRef");
	pm_ref_ident = xmlNewChild(pm_ref, NULL, BAD_CAST "pmRefIdent", NULL);

	if (is_extended) {
		xmlNode *ident_extension;
		ident_extension = xmlNewChild(pm_ref_ident, NULL, BAD_CAST "identExtension", NULL);
		xmlSetProp(ident_extension, BAD_CAST "extensionProducer", BAD_CAST extension_producer);
		xmlSetProp(ident_extension, BAD_CAST "extensionCode", BAD_CAST extension_code);
	}

	pm_code = xmlNewChild(pm_ref_ident, NULL, BAD_CAST "pmCode", NULL);

	xmlSetProp(pm_code, BAD_CAST "modelIdentCode", BAD_CAST model_ident_code);
	xmlSetProp(pm_code, BAD_CAST "pmIssuer", BAD_CAST pm_issuer);
	xmlSetProp(pm_code, BAD_CAST "pmNumber", BAD_CAST pm_number);
	xmlSetProp(pm_code, BAD_CAST "pmVolume", BAD_CAST pm_volume);

	if (opts) {
		xmlDocPtr doc;
		xmlNodePtr ref_pm_address = NULL;
		xmlNodePtr ref_pm_ident = NULL;
		xmlNodePtr ref_pm_address_items = NULL;
		xmlNodePtr ref_pm_title = NULL;
		xmlNodePtr ref_pm_issue_date = NULL;
		xmlNodePtr issue_info = NULL;
		xmlNodePtr language = NULL;
		char *s;

		if ((doc = read_xml_doc(fname))) {
			ref_pm_address = first_xpath_node(doc, NULL, BAD_CAST "//pmAddress|//pmaddres");

			if (xmlStrcmp(ref_pm_address->name, BAD_CAST "pmaddres") == 0) {
				ref_pm_ident = ref_pm_address;
				ref_pm_address_items = ref_pm_address;
				ref_pm_title = find_child(ref_pm_address_items, "pmtitle");
				ref_pm_issue_date = find_child(ref_pm_address_items, "issdate");
			} else {
				ref_pm_ident = find_child(ref_pm_address, "pmIdent");
				ref_pm_address_items = find_child(ref_pm_address, "pmAddressItems");
				ref_pm_title = find_child(ref_pm_address_items, "pmTitle");
				ref_pm_issue_date = find_child(ref_pm_address_items, "issueDate");
			}
		}

		s = strchr(ref, '_');

		if (optset(opts, OPT_ISSUE)) {
			if (doc) {
				xmlNodePtr node;
				xmlChar *issno, *inwork;

				node   = first_xpath_node(doc, ref_pm_ident, BAD_CAST "issueInfo|issno");
				issno  = first_xpath_value(doc, node, BAD_CAST "@issueNumber|@issno");
				inwork = first_xpath_value(doc, node, BAD_CAST "@inWork|@inwork");
				if (!inwork) inwork = xmlStrdup(BAD_CAST "00");

				issue_info = xmlNewNode(NULL, BAD_CAST "issueInfo");
				xmlSetProp(issue_info, BAD_CAST "issueNumber", issno);
				xmlSetProp(issue_info, BAD_CAST "inWork", inwork);

				xmlFree(issno);
				xmlFree(inwork);
			} else if (s && isdigit((unsigned char) s[1])) {
				issue_info = new_issue_info(s);
			} else {
				if (verbosity > QUIET) {
					fprintf(stderr, WRN_PREFIX "Could not read issue info from publication module: %s\n", ref);
				}
				issue_info = NULL;
			}

			xmlAddChild(pm_ref_ident, issue_info);
		}

		if (optset(opts, OPT_LANG)) {
			if (doc) {
				xmlNodePtr node;
				xmlChar *l, *c;

				node = find_child(ref_pm_ident, "language");
				l    = first_xpath_value(doc, node, BAD_CAST "@languageIsoCode|@language");
				c    = first_xpath_value(doc, node, BAD_CAST "@countryIsoCode|@country");

				language = xmlNewNode(NULL, BAD_CAST "language");
				xmlSetProp(language, BAD_CAST "languageIsoCode", l);
				xmlSetProp(language, BAD_CAST "countryIsoCode", c);

				xmlFree(l);
				xmlFree(c);
			} else if (s && (s = strchr(s + 1, '_'))) {
				language = new_language(s);
			} else {
				if (verbosity > QUIET) {
					fprintf(stderr, WRN_PREFIX "Could not read language from publication module: %s\n", ref);
				}
				language = NULL;
			}

			xmlAddChild(pm_ref_ident, language);
		}

		if (optset(opts, OPT_TITLE) || optset(opts, OPT_DATE)) {
			xmlNodePtr pm_ref_address_items = NULL, pm_title, issue_date;

			if (doc) {
				pm_ref_address_items = xmlNewChild(pm_ref, NULL, BAD_CAST "pmRefAddressItems", NULL);
				pm_title = ref_pm_title;
				issue_date = ref_pm_issue_date;
			} else {
				pm_title = NULL;
				issue_date = NULL;
			}

			if (optset(opts, OPT_TITLE)) {
				if (pm_title) {
					pm_title = xmlAddChild(pm_ref_address_items, xmlCopyNode(pm_title, 1));
					xmlNodeSetName(pm_title, BAD_CAST "pmTitle");
				} else {
					if (verbosity > QUIET) {
						fprintf(stderr, WRN_PREFIX "Could not read title from publication module: %s\n", ref);
					}
				}
			}
			if (optset(opts, OPT_DATE)) {
				if (issue_date) {
					issue_date = xmlAddChild(pm_ref_address_items, xmlCopyNode(issue_date, 1));
					xmlNodeSetName(issue_date, BAD_CAST "issueDate");
				} else {
					if (verbosity > QUIET) {
						fprintf(stderr, WRN_PREFIX "Could not read date from publication module: %s\n", ref);
					}
				}
			}
		}

		xmlFreeDoc(doc);

		if (optset(opts, OPT_SRCID)) {
			xmlNodePtr pmc, issno, lang, src;

			pmc   = xmlCopyNode(pm_code, 1);
			issno = xmlCopyNode(issue_info, 1);
			lang  = xmlCopyNode(language, 1);

			src = xmlNewNode(NULL, BAD_CAST "sourcePmIdent");
			xmlAddChild(src, pmc);
			xmlAddChild(src, lang);
			xmlAddChild(src, issno);

			xmlFreeNode(pm_ref);
			pm_ref = src;
		}

		if (optset(opts, OPT_URL)) {
			set_xlink(pm_ref, fname);
		}
	}

	return pm_ref;
}

#define DME_FMT "DME-%255[^-]-%255[^-]-%14[^-]-%4[^-]-%3[^-]-%1s%1s-%4[^-]-%2s%3[^-]-%3s%1s-%1s-%3s%1s"
#define DMC_FMT "DMC-%14[^-]-%4[^-]-%3[^-]-%1s%1s-%4[^-]-%2s%3[^-]-%3s%1s-%1s-%3s%1s"

static xmlNodePtr new_dm_ref(const char *ref, const char *fname, int opts)
{
	char extension_producer[256] = "";
	char extension_code[256]     = "";
	char model_ident_code[15]    = "";
	char system_diff_code[5]     = "";
	char system_code[4]          = "";
	char assy_code[5]            = "";
	char item_location_code[2]   = "";
	char learn_code[4]           = "";
	char learn_event_code[2]     = "";
	char sub_system_code[2]      = "";
	char sub_sub_system_code[2]  = "";
	char disassy_code[3]         = "";
	char disassy_code_variant[4] = "";
	char info_code[4]            = "";
	char info_code_variant[2]    = "";
	xmlNode *dm_ref;
	xmlNode *dm_ref_ident;
	xmlNode *dm_code;
	bool is_extended;
	bool has_learn;
	int n;

	is_extended = strncmp(ref, "DME-", 4) == 0;

	if (is_extended) {
		n = sscanf(ref, DME_FMT,
			extension_producer,
			extension_code,
			model_ident_code,
			system_diff_code,
			system_code,
			sub_system_code,
			sub_sub_system_code,
			assy_code,
			disassy_code,
			disassy_code_variant,
			info_code,
			info_code_variant,
			item_location_code,
			learn_code,
			learn_event_code);
		if (n != 15 && n != 13) {
			if (verbosity > QUIET) {
				fprintf(stderr, ERR_PREFIX "Data module extended code invalid: %s\n", ref);
			}
			exit(EXIT_BAD_INPUT);
		}
		has_learn = n == 15;
	} else {
		n = sscanf(ref, DMC_FMT,
			model_ident_code,
			system_diff_code,
			system_code,
			sub_system_code,
			sub_sub_system_code,
			assy_code,
			disassy_code,
			disassy_code_variant,
			info_code,
			info_code_variant,
			item_location_code,
			learn_code,
			learn_event_code);
		if (n != 13 && n != 11) {
			if (verbosity > QUIET) {
				fprintf(stderr, ERR_PREFIX "Data module code invalid: %s\n", ref);
			}
			exit(EXIT_BAD_INPUT);
		}
		has_learn = n == 13;
	}

	dm_ref = xmlNewNode(NULL, BAD_CAST "dmRef");
	dm_ref_ident = xmlNewChild(dm_ref, NULL, BAD_CAST "dmRefIdent", NULL);

	if (is_extended) {
		xmlNode *ident_extension;
		ident_extension = xmlNewChild(dm_ref_ident, NULL, BAD_CAST "identExtension", NULL);
		xmlSetProp(ident_extension, BAD_CAST "extensionProducer", BAD_CAST extension_producer);
		xmlSetProp(ident_extension, BAD_CAST "extensionCode", BAD_CAST extension_code);
	}

	dm_code = xmlNewChild(dm_ref_ident, NULL, BAD_CAST "dmCode", NULL);

	xmlSetProp(dm_code, BAD_CAST "modelIdentCode", BAD_CAST model_ident_code);
	xmlSetProp(dm_code, BAD_CAST "systemDiffCode", BAD_CAST system_diff_code);
	xmlSetProp(dm_code, BAD_CAST "systemCode", BAD_CAST system_code);
	xmlSetProp(dm_code, BAD_CAST "subSystemCode", BAD_CAST sub_system_code);
	xmlSetProp(dm_code, BAD_CAST "subSubSystemCode", BAD_CAST sub_sub_system_code);
	xmlSetProp(dm_code, BAD_CAST "assyCode", BAD_CAST assy_code);
	xmlSetProp(dm_code, BAD_CAST "disassyCode", BAD_CAST disassy_code);
	xmlSetProp(dm_code, BAD_CAST "disassyCodeVariant", BAD_CAST disassy_code_variant);
	xmlSetProp(dm_code, BAD_CAST "infoCode", BAD_CAST info_code);
	xmlSetProp(dm_code, BAD_CAST "infoCodeVariant", BAD_CAST info_code_variant);
	xmlSetProp(dm_code, BAD_CAST "itemLocationCode", BAD_CAST item_location_code);
	if (has_learn) {
		xmlSetProp(dm_code, BAD_CAST "learnCode", BAD_CAST learn_code);
		xmlSetProp(dm_code, BAD_CAST "learnEventCode", BAD_CAST learn_event_code);
	}

	if (opts) {
		xmlDocPtr doc;
		xmlNodePtr ref_dm_address = NULL;
		xmlNodePtr ref_dm_ident = NULL;
		xmlNodePtr ref_dm_address_items = NULL;
		xmlNodePtr ref_dm_title = NULL;
		xmlNodePtr ref_dm_issue_date = NULL;
		xmlNodePtr issue_info = NULL;
		xmlNodePtr language = NULL;
		char *s;

		if ((doc = read_xml_doc(fname))) {
			ref_dm_address = first_xpath_node(doc, NULL, BAD_CAST "//dmAddress|//dmaddres");

			if (xmlStrcmp(ref_dm_address->name, BAD_CAST "dmaddres") == 0) {
				ref_dm_ident = ref_dm_address;
				ref_dm_address_items = ref_dm_address;
				ref_dm_title = find_child(ref_dm_address_items, "dmtitle");
				ref_dm_issue_date = find_child(ref_dm_address_items, "issdate");
			} else {
				ref_dm_ident = find_child(ref_dm_address, "dmIdent");
				ref_dm_address_items = find_child(ref_dm_address, "dmAddressItems");
				ref_dm_title = find_child(ref_dm_address_items, "dmTitle");
				ref_dm_issue_date = find_child(ref_dm_address_items, "issueDate");
			}
		}

		s = strchr(ref, '_');

		if (optset(opts, OPT_ISSUE)) {
			if (doc) {
				xmlNodePtr node;
				xmlChar *issno, *inwork;

				node   = first_xpath_node(doc, ref_dm_ident, BAD_CAST "issueInfo|issno");
				issno  = first_xpath_value(doc, node, BAD_CAST "@issueNumber|@issno");
				inwork = first_xpath_value(doc, node, BAD_CAST "@inWork|@inwork");
				if (!inwork) inwork = xmlStrdup(BAD_CAST "00");

				issue_info = xmlNewNode(NULL, BAD_CAST "issueInfo");
				xmlSetProp(issue_info, BAD_CAST "issueNumber", issno);
				xmlSetProp(issue_info, BAD_CAST "inWork", inwork);

				xmlFree(issno);
				xmlFree(inwork);
			} else if (s && isdigit((unsigned char) s[1])) {
				issue_info = new_issue_info(s);
			} else {
				if (verbosity > QUIET) {
					fprintf(stderr, WRN_PREFIX "Could not read issue info from data module: %s\n", ref);
				}
				issue_info = NULL;
			}

			xmlAddChild(dm_ref_ident, issue_info);
		}

		if (optset(opts, OPT_LANG)) {
			if (doc) {
				xmlNodePtr node;
				xmlChar *l, *c;

				node = find_child(ref_dm_ident, "language");
				l    = first_xpath_value(doc, node, BAD_CAST "@languageIsoCode|@language");
				c    = first_xpath_value(doc, node, BAD_CAST "@countryIsoCode|@country");

				language = xmlNewNode(NULL, BAD_CAST "language");
				xmlSetProp(language, BAD_CAST "languageIsoCode", l);
				xmlSetProp(language, BAD_CAST "countryIsoCode", c);

				xmlFree(l);
				xmlFree(c);
			} else if (s && (s = strchr(s + 1, '_'))) {
				language = new_language(s);
			} else {
				if (verbosity > QUIET) {
					fprintf(stderr, WRN_PREFIX "Could not read language from data module: %s\n", ref);
				}
				language = NULL;
			}

			xmlAddChild(dm_ref_ident, language);
		}

		if (optset(opts, OPT_TITLE) || optset(opts, OPT_DATE)) {
			xmlNodePtr dm_ref_address_items = NULL, dm_title = NULL, issue_date = NULL;

			if (doc) {
				dm_ref_address_items = xmlNewChild(dm_ref, NULL, BAD_CAST "dmRefAddressItems", NULL);

				if (optset(opts, OPT_TITLE)) {
					xmlChar *tech, *info, *infv;

					tech = first_xpath_value(doc, ref_dm_title, BAD_CAST "techName|techname");
					info = first_xpath_value(doc, ref_dm_title, BAD_CAST "infoName|infoname");
					infv = first_xpath_value(doc, ref_dm_title, BAD_CAST "infoNameVariant");

					dm_title = xmlNewNode(NULL, BAD_CAST "dmTitle");
					xmlNewTextChild(dm_title, NULL, BAD_CAST "techName", tech);
					if (info) xmlNewTextChild(dm_title, NULL, BAD_CAST "infoName", info);
					if (infv) xmlNewTextChild(dm_title, NULL, BAD_CAST "infoNameVariant", infv);

					xmlFree(tech);
					xmlFree(info);
					xmlFree(infv);
				}

				if (optset(opts, OPT_DATE)) {
					issue_date = xmlCopyNode(ref_dm_issue_date, 1);
					xmlNodeSetName(issue_date, BAD_CAST "issueDate");
				}

			}

			if (optset(opts, OPT_TITLE)) {
				if (dm_title) {
					xmlAddChild(dm_ref_address_items, dm_title);
				} else {
					if (verbosity > QUIET) {
						fprintf(stderr, WRN_PREFIX "Could not read title from data module: %s\n", ref);
					}
				}
			}
			if (optset(opts, OPT_DATE)) {
				if (issue_date) {
					xmlAddChild(dm_ref_address_items, issue_date);
				} else {
					if (verbosity > QUIET) {
						fprintf(stderr, WRN_PREFIX "Could not read issue date from data module: %s\n", ref);
					}
				}
			}
		}

		xmlFreeDoc(doc);

		if (optset(opts, OPT_SRCID)) {
			xmlNodePtr dmc, issno, lang, src;

			dmc   = xmlCopyNode(dm_code, 1);
			issno = xmlCopyNode(issue_info, 1);
			lang  = xmlCopyNode(language, 1);

			src = xmlNewNode(NULL, BAD_CAST (optset(opts, OPT_CIRID) ? "repositorySourceDmIdent" : "sourceDmIdent"));
			xmlAddChild(src, dmc);
			xmlAddChild(src, lang);
			xmlAddChild(src, issno);

			xmlFreeNode(dm_ref);
			dm_ref = src;
		}

		if (optset(opts, OPT_URL)) {
			set_xlink(dm_ref, fname);
		}
	}

	return dm_ref;
}

#define COM_FMT "COM-%14[^-]-%5s-%4s-%5s-%1s"

static xmlNodePtr new_com_ref(const char *ref, const char *fname, int opts)
{
	char model_ident_code[15]  = "";
	char sender_ident[6]       = "";
	char year_of_data_issue[5] = "";
	char seq_number[6]         = "";
	char comment_type[2]       = "";

	int n;

	xmlNodePtr comment_ref, comment_ref_ident, comment_code;

	n = sscanf(ref, COM_FMT,
		model_ident_code,
		sender_ident,
		year_of_data_issue,
		seq_number,
		comment_type);
	if (n != 5) {
		if (verbosity > QUIET) {
			fprintf(stderr, ERR_PREFIX "Comment code invalid: %s\n", ref);
		}
		exit(EXIT_BAD_INPUT);
	}

	lowercase(comment_type);

	comment_ref = xmlNewNode(NULL, BAD_CAST "commentRef");
	comment_ref_ident = xmlNewChild(comment_ref, NULL, BAD_CAST "commentRefIdent", NULL);
	comment_code = xmlNewChild(comment_ref_ident, NULL, BAD_CAST "commentCode", NULL);

	xmlSetProp(comment_code, BAD_CAST "modelIdentCode", BAD_CAST model_ident_code);
	xmlSetProp(comment_code, BAD_CAST "senderIdent", BAD_CAST sender_ident);
	xmlSetProp(comment_code, BAD_CAST "yearOfDataIssue", BAD_CAST year_of_data_issue);
	xmlSetProp(comment_code, BAD_CAST "seqNumber", BAD_CAST seq_number);
	xmlSetProp(comment_code, BAD_CAST "commentType", BAD_CAST comment_type);

	if (opts) {
		xmlDocPtr doc;
		xmlNodePtr ref_comment_address;
		xmlNodePtr ref_comment_ident;
		char *s;

		if ((doc = read_xml_doc(fname))) {
			ref_comment_address = first_xpath_node(doc, NULL, BAD_CAST "//commentAddress");
			ref_comment_ident = find_child(ref_comment_address, "commentIdent");
		}

		s = strchr(ref, '_');

		if (optset(opts, OPT_LANG)) {
			xmlNodePtr language;

			if (doc) {
				language = xmlCopyNode(find_child(ref_comment_ident, "language"), 1);
			} else if (s && (s = strchr(s + 1, '_'))) {
				language = new_language(s);
			} else {
				if (verbosity > QUIET) {
					fprintf(stderr, WRN_PREFIX "Could not read language from comment: %s\n", ref);
				}
				language = NULL;
			}

			xmlAddChild(comment_ref_ident, language);
		}

		xmlFreeDoc(doc);

		if (optset(opts, OPT_URL)) {
			set_xlink(comment_ref, fname);
		}
	}

	return comment_ref;
}

#define DML_FMT "DML-%14[^-]-%5s-%1s-%4s-%5s"

static xmlNodePtr new_dml_ref(const char *ref, const char *fname, int opts)
{
	char model_ident_code[15]  = "";
	char sender_ident[6]       = "";
	char dml_type[2]           = "";
	char year_of_data_issue[5] = "";
	char seq_number[6]         = "";

	int n;

	xmlNodePtr dml_ref, dml_ref_ident, dml_code;

	n = sscanf(ref, DML_FMT,
		model_ident_code,
		sender_ident,
		dml_type,
		year_of_data_issue,
		seq_number);
	if (n != 5) {
		if (verbosity > QUIET) {
			fprintf(stderr, ERR_PREFIX "DML code invalid: %s\n", ref);
		}
		exit(EXIT_BAD_INPUT);
	}

	lowercase(dml_type);

	dml_ref = xmlNewNode(NULL, BAD_CAST "dmlRef");
	dml_ref_ident = xmlNewChild(dml_ref, NULL, BAD_CAST "dmlRefIdent", NULL);
	dml_code = xmlNewChild(dml_ref_ident, NULL, BAD_CAST "dmlCode", NULL);

	xmlSetProp(dml_code, BAD_CAST "modelIdentCode", BAD_CAST model_ident_code);
	xmlSetProp(dml_code, BAD_CAST "senderIdent", BAD_CAST sender_ident);
	xmlSetProp(dml_code, BAD_CAST "dmlType", BAD_CAST dml_type);
	xmlSetProp(dml_code, BAD_CAST "yearOfDataIssue", BAD_CAST year_of_data_issue);
	xmlSetProp(dml_code, BAD_CAST "seqNumber", BAD_CAST seq_number);

	if (opts) {
		xmlDocPtr doc;
		xmlNodePtr ref_dml_address;
		xmlNodePtr ref_dml_ident;
		char *s;

		if ((doc = read_xml_doc(fname))) {
			ref_dml_address = first_xpath_node(doc, NULL, BAD_CAST "//dmlAddress");
			ref_dml_ident = find_child(ref_dml_address, "dmlIdent");
		}

		s = strchr(ref, '_');

		if (optset(opts, OPT_ISSUE)) {
			xmlNodePtr issue_info;

			if (doc) {
				issue_info = xmlCopyNode(find_child(ref_dml_ident, "issueInfo"), 1);
			} else if (s && isdigit((unsigned char) s[1])) {
				issue_info = new_issue_info(s);
			} else {
				if (verbosity > QUIET) {
					fprintf(stderr, WRN_PREFIX "Could not read issue info from DML: %s\n", ref);
				}
				issue_info = NULL;
			}

			xmlAddChild(dml_ref_ident, issue_info);
		}

		xmlFreeDoc(doc);

		if (optset(opts, OPT_URL)) {
			set_xlink(dml_ref, fname);
		}
	}

	return dml_ref;
}

static xmlNodePtr new_icn_ref(const char *ref, const char *fname, int opts)
{
	xmlNodePtr info_entity_ref;

	info_entity_ref = xmlNewNode(NULL, BAD_CAST "infoEntityRef");

	xmlSetProp(info_entity_ref, BAD_CAST "infoEntityRefIdent", BAD_CAST ref);

	return info_entity_ref;
}

#define CSN_FMT "CSN-%14[^-]-%4[^-]-%3[^-]-%1s%1s-%4[^-]-%2s%3[^-]-%3s%1s-%1s"

static xmlNodePtr new_csn_ref(const char *ref, const char *fname, int opts)
{
	char model_ident_code[15]    = "";
	char system_diff_code[5]     = "";
	char system_code[4]          = "";
	char assy_code[5]            = "";
	char item_location_code[2]   = "";
	char sub_system_code[2]      = "";
	char sub_sub_system_code[2]  = "";
	char figure_number[3]         = "";
	char figure_number_variant[4] = "";
	char item[4]            = "";
	char item_variant[2]    = "";
	xmlNode *csn_ref;
	int n;

	n = sscanf(ref, CSN_FMT,
		model_ident_code,
		system_diff_code,
		system_code,
		sub_system_code,
		sub_sub_system_code,
		assy_code,
		figure_number,
		figure_number_variant,
		item,
		item_variant,
		item_location_code);
	if (n != 11) {
		if (verbosity > QUIET) {
			fprintf(stderr, ERR_PREFIX "CSN invalid: %s\n", ref);
		}
		exit(EXIT_BAD_INPUT);
	}

	csn_ref = xmlNewNode(NULL, BAD_CAST "catalogSeqNumberRef");

	xmlSetProp(csn_ref, BAD_CAST "modelIdentCode", BAD_CAST model_ident_code);
	xmlSetProp(csn_ref, BAD_CAST "systemDiffCode", BAD_CAST system_diff_code);
	xmlSetProp(csn_ref, BAD_CAST "systemCode", BAD_CAST system_code);
	xmlSetProp(csn_ref, BAD_CAST "subSystemCode", BAD_CAST sub_system_code);
	xmlSetProp(csn_ref, BAD_CAST "subSubSystemCode", BAD_CAST sub_sub_system_code);
	xmlSetProp(csn_ref, BAD_CAST "assyCode", BAD_CAST assy_code);
	xmlSetProp(csn_ref, BAD_CAST "figureNumber", BAD_CAST figure_number);
	if (strcmp(figure_number_variant, "*") != 0) {
		xmlSetProp(csn_ref, BAD_CAST "figureNumberVariant", BAD_CAST figure_number_variant);
	}
	xmlSetProp(csn_ref, BAD_CAST "item", BAD_CAST item);
	if (strcmp(item_variant, "*") != 0) {
		xmlSetProp(csn_ref, BAD_CAST "itemVariant", BAD_CAST item_variant);
	}
	xmlSetProp(csn_ref, BAD_CAST "itemLocationCode", BAD_CAST item_location_code);

	if (optset(opts, OPT_URL)) {
		set_xlink(csn_ref, fname);
	}

	return csn_ref;
}

static bool is_smc_ref(const char *ref)
{
	return strncmp(ref, "SMC-", 4) == 0 || strncmp(ref, "SME-", 4) == 0;
}

static bool is_pm_ref(const char *ref)
{
	return strncmp(ref, "PMC-", 4) == 0 || strncmp(ref, "PME-", 4) == 0;
}

static bool is_dm_ref(const char *ref)
{
	return strncmp(ref, "DMC-", 4) == 0 || strncmp(ref, "DME-", 4) == 0;
}

static bool is_com_ref(const char *ref)
{
	return strncmp(ref, "COM-", 4) == 0;
}

static bool is_dml_ref(const char *ref)
{
	return strncmp(ref, "DML-", 4) == 0;
}

static bool is_icn_ref(const char *ref)
{
	return strncmp(ref, "ICN-", 4) == 0;
}

static bool is_csn_ref(const char *ref)
{
	return strncmp(ref, "CSN-", 4) == 0;
}

static void add_ref(const char *src, const char *dst, xmlNodePtr ref, int opts)
{
	xmlDocPtr doc;
	xmlNodePtr refs;

	if (!(doc = read_xml_doc(src))) {
		if (verbosity > QUIET) {
			fprintf(stderr, ERR_PREFIX "Could not read source data module: %s\n", src);
		}
		exit(EXIT_MISSING_FILE);
	}

	if (optset(opts, OPT_SRCID)) {
		xmlNodePtr src, node;

		src  = first_xpath_node(doc, NULL, BAD_CAST "//dmStatus/sourceDmIdent|//pmStatus/sourcePmIdent|//status/srcdmaddres");
		node = first_xpath_node(doc, NULL, BAD_CAST "(//dmStatus/repositorySourceDmIdent|//dmStatus/security|//pmStatus/security|//status/security)[1]");
		if (node) {
			if (src) {
				xmlUnlinkNode(src);
				xmlFreeNode(src);
			}
			xmlAddPrevSibling(node, xmlCopyNode(ref, 1));
		}
	} else {
		refs = find_or_create_refs(doc);
		xmlAddChild(refs, xmlCopyNode(ref, 1));
	}

	save_xml_doc(doc, dst);

	xmlFreeDoc(doc);
}

/* Apply a built-in XSLT transform to a doc in place. */
static void transform_doc(xmlDocPtr doc, unsigned char *xsl, unsigned int len)
{
	xmlDocPtr styledoc, src, res;
	xsltStylesheetPtr style;
	xmlNodePtr old;

	src = xmlCopyDoc(doc, 1);

	styledoc = read_xml_mem((const char *) xsl, len);
	style = xsltParseStylesheetDoc(styledoc);

	res = xsltApplyStylesheet(style, src, NULL);

	old = xmlDocSetRootElement(doc, xmlCopyNode(xmlDocGetRootElement(res), 1));
	xmlFreeNode(old);

	xmlFreeDoc(src);
	xmlFreeDoc(res);
	xsltFreeStylesheet(style);
}

static xmlNodePtr find_ext_pub(xmlDocPtr extpubs, const char *ref)
{
	xmlChar xpath[512];
	xmlNodePtr node;

	/* Attempt to match an exact code (e.g., "ABC") */
	xmlStrPrintf(xpath, 512, "//externalPubRef[externalPubRefIdent/externalPubCode='%s']", ref);
	node = first_xpath_node(extpubs, NULL, xpath);

	/* Attempt to match a file name (e.g., "ABC.PDF") */
	if (!node) {
		xmlStrPrintf(xpath, 512, "//externalPubRef[starts-with('%s', externalPubRefIdent/externalPubCode)]", ref);
		node = first_xpath_node(extpubs, NULL, xpath);
	}

	return xmlCopyNode(node, 1);
}

static xmlNodePtr new_ext_pub(const char *ref, const char *fname, int opts)
{
	xmlNodePtr epr, epr_ident;

	epr = xmlNewNode(NULL, BAD_CAST "externalPubRef");
	epr_ident = xmlNewChild(epr, NULL, BAD_CAST "externalPubRefIdent", NULL);

	if (optset(opts, OPT_TITLE)) {
		xmlNewTextChild(epr_ident, NULL, BAD_CAST "externalPubTitle", BAD_CAST ref);
	} else {
		xmlNewTextChild(epr_ident, NULL, BAD_CAST "externalPubCode", BAD_CAST ref);
	}

	if (optset(opts, OPT_URL)) {
		set_xlink(epr, fname);
	}

	return epr;
}

static xmlNodePtr find_ref_type(const char *fname, int opts)
{
	xmlDocPtr doc, styledoc, res;
	xsltStylesheetPtr style;
	xmlNodePtr node = NULL;

	if (!(doc = read_xml_doc(fname))) {
		return NULL;
	}

	styledoc = read_xml_mem((const char *) ref_xsl, ref_xsl_len);
	style = xsltParseStylesheetDoc(styledoc);

	res = xsltApplyStylesheet(style, doc, NULL);

	if (res->children) {
		const char *ref;
		xmlNodePtr (*f)(const char *, const char *, int) = NULL;

		ref = (char *) res->children->content;

		if (is_dm_ref(ref)) {
			f = new_dm_ref;
		} else if (is_pm_ref(ref)) {
			f = new_pm_ref;
		} else if (is_smc_ref(ref)) {
			f = new_smc_ref;
		} else if (is_com_ref(ref)) {
			f = new_com_ref;
		} else if (is_dml_ref(ref)) {
			f = new_dml_ref;
		} else if (is_icn_ref(ref)) {
			f = new_icn_ref;
		}

		if (f) {
			node = f(ref, fname, opts);
		}
	}

	xmlFreeDoc(res);
	xsltFreeStylesheet(style);

	xmlFreeDoc(doc);

	return node;
}

/* Determine whether a string is matched by a regular expression. */
static bool matches_regex(const char *s, const char *regex)
{
	regex_t re;
	bool match;
	regcomp(&re, regex, REG_EXTENDED);
	match = regexec(&re, s, 0, NULL, 0) == 0;
	regfree(&re);
	return match;
}

/* Attempt to automatically add the prefix to a ref. */
static char *add_prefix(const char *ref)
{
	int n = strlen(ref) + 5;
	char *s = malloc(n);

	/* Notes:
	 *   Check against extended variants (DME, PME, SME) before
	 *   non-extended variants (DMC, PMC, SMC).
	 *
	 *   There is no need to check for CSN, SME or SMC, as these are
	 *   indistinguishable from DMC, PME and PMC without a prefix or an
	 *   XML context.
	 */
	if (matches_regex(ref, DME_REGEX_NOPRE)) {
		snprintf(s, n, "DME-%s", ref);
	} else if (matches_regex(ref, DMC_REGEX_NOPRE)) {
		snprintf(s, n, "DMC-%s", ref);
	} else if (matches_regex(ref, PME_REGEX_NOPRE)) {
		snprintf(s, n, "PME-%s", ref);
	} else if (matches_regex(ref, PMC_REGEX_NOPRE)) {
		snprintf(s, n, "PMC-%s", ref);
	} else if (matches_regex(ref, COM_REGEX_NOPRE)) {
		snprintf(s, n, "COM-%s", ref);
	} else if (matches_regex(ref, DML_REGEX_NOPRE)) {
		snprintf(s, n, "DML-%s", ref);
	} else {
		snprintf(s, n, "%s", ref);
	}

	return s;
}

static void print_ref(const char *src, const char *dst, const char *ref,
	const char *fname, int opts, bool overwrite, enum issue iss,
	xmlDocPtr extpubs)
{
	xmlNodePtr node;
	xmlNodePtr (*f)(const char *, const char *, int);
	char *fullref;

	/* If -p is given, try automatically adding the prefix. */
	if (optset(opts, OPT_NONSTRICT)) {
		fullref = add_prefix(ref);
	/* Otherwise, just copy the ref as-is. */
	} else {
		fullref = strdup(ref);
	}

	if (is_dm_ref(fullref)) {
		f = new_dm_ref;
	} else if (is_pm_ref(fullref)) {
		f = new_pm_ref;
	} else if (is_smc_ref(fullref)) {
		f = new_smc_ref;
	} else if (is_com_ref(fullref)) {
		f = new_com_ref;
	} else if (is_dml_ref(fullref)) {
		f = new_dml_ref;
	} else if (is_icn_ref(fullref)) {
		f = new_icn_ref;
	} else if (is_csn_ref(fullref)) {
		f = new_csn_ref;
	} else if (extpubs && (node = find_ext_pub(extpubs, fullref))) {
		f = NULL;
	} else if ((node = find_ref_type(fname, opts))) {
		f = NULL;
	} else {
		f = new_ext_pub;
	}

	if (f) {
		node = f(fullref, fname, opts);
	}

	if (iss < DEFAULT_S1000D_ISSUE) {
		unsigned char *xsl;
		unsigned int len;
		xmlDocPtr doc;

		switch (iss) {
			case ISS_20:
				xsl = ___common_to20_xsl;
				len = ___common_to20_xsl_len;
				break;
			case ISS_21:
				xsl = ___common_to21_xsl;
				len = ___common_to21_xsl_len;
				break;
			case ISS_22:
				xsl = ___common_to22_xsl;
				len = ___common_to22_xsl_len;
				break;
			case ISS_23:
				xsl = ___common_to23_xsl;
				len = ___common_to23_xsl_len;
				break;
			case ISS_30:
				xsl = ___common_to30_xsl;
				len = ___common_to30_xsl_len;
				break;
			case ISS_40:
				xsl = ___common_to40_xsl;
				len = ___common_to40_xsl_len;
				break;
			case ISS_41:
				xsl = ___common_to41_xsl;
				len = ___common_to41_xsl_len;
				break;
			case ISS_42:
				xsl = ___common_to42_xsl;
				len = ___common_to42_xsl_len;
				break;
			default:
				xsl = NULL;
				len = 0;
				break;
		}

		doc = xmlNewDoc(BAD_CAST "1.0");
		xmlDocSetRootElement(doc, node);

		transform_doc(doc, xsl, len);

		node = xmlCopyNode(xmlDocGetRootElement(doc), 1);

		xmlFreeDoc(doc);
	}

	if (optset(opts, OPT_INS)) {
		if (verbosity >= VERBOSE) {
			fprintf(stderr, INF_PREFIX "Adding reference %s to %s...\n", fullref, src);
		}

		if (overwrite) {
			add_ref(src, src, node, opts);
		} else {
			add_ref(src, dst, node, opts);
		}
	} else {
		dump_node(node, dst);
	}

	xmlFreeNode(node);
	free(fullref);
}

static char *trim(char *str)
{
	char *end;

	while (isspace((unsigned char) *str)) str++;

	if (*str == 0) return str;

	end = str + strlen(str) - 1;
	while (end > str && isspace((unsigned char) *end)) end--;

	*(end + 1) = 0;

	return str;
}

static enum issue spec_issue(const char *s)
{
	if (strcmp(s, "2.0") == 0) {
		return ISS_20;
	} else if (strcmp(s, "2.1") == 0) {
		return ISS_21;
	} else if (strcmp(s, "2.2") == 0) {
		return ISS_22;
	} else if (strcmp(s, "2.3") == 0) {
		return ISS_23;
	} else if (strcmp(s, "3.0") == 0) {
		return ISS_30;
	} else if (strcmp(s, "4.0") == 0) {
		return ISS_40;
	} else if (strcmp(s, "4.1") == 0) {
		return ISS_41;
	} else if (strcmp(s, "4.2") == 0) {
		return ISS_42;
	} else if (strcmp(s, "5.0") == 0) {
		return ISS_50;
	}

	if (verbosity > QUIET) {
		fprintf(stderr, ERR_PREFIX "Unsupported issue: %s\n", s);
	}
	exit(EXIT_BAD_ISSUE);
}

/* Skip a reference if it has a conflicting prefix. */
static bool skip_confl_ref(xmlNodePtr *node, xmlChar **content, regoff_t so, regoff_t eo, const char *pre)
{
	xmlChar *p = (*content) + so - 4;

	if (p > (*content) && (xmlStrncmp(p, BAD_CAST pre, 4) == 0)) {
		xmlChar *s1, *s2;

		s1 = xmlStrndup((*content), eo);
		s2 = xmlStrdup((*content) + eo);

		xmlFree(*content);
		xmlNodeSetContent(*node, s1);
		xmlFree(s1);

		*node = xmlAddNextSibling(*node, xmlNewText(s2));
		*content = s2;

		return true;
	}

	return false;
}

/* Replace a textual reference with XML. */
static void transform_ref(xmlNodePtr *node, const char *path, xmlChar **content, regoff_t so, regoff_t eo, const char *prefix, newref_t f, int opts)
{
	xmlChar *r, *s1, *s2;
	xmlNodePtr ref;

	if (verbosity >= DEBUG) {
		const char *type;

		if (f == new_dm_ref) {
			type = "DM";
		} else if (f == new_pm_ref) {
			type = "PM";
		} else if (f == new_csn_ref) {
			type = "CSN";
		} else if (f == new_icn_ref) {
			type = "ICN";
		} else if (f == new_dml_ref) {
			type = "DML";
		} else if (f == new_smc_ref) {
			type = "SMC";
		} else if (f == new_ext_pub) {
			type = "external pub";
		} else {
			type = "unknown";
		}

		fprintf(stderr, INF_PREFIX "%s: Found %s ref %.*s\n", path, type, (int) (eo - so), (*content) + so);
	}

	/* If prefixes are not required, some types of references have
	 * overlapping formats. This will look backwards to determine if a
	 * reference has a conflicting prefix.
	 *
	 * FIXME: Does not account for extended variants (DME, PME, SME). */
	if (optset(opts, OPT_NONSTRICT)) {
		if (f ==  new_dm_ref && skip_confl_ref(node, content, so, eo, "CSN-")) return;
		if (f == new_csn_ref && skip_confl_ref(node, content, so, eo, "DMC-")) return;
		if (f ==  new_pm_ref && skip_confl_ref(node, content, so, eo, "SMC-")) return;
		if (f == new_smc_ref && skip_confl_ref(node, content, so, eo, "PMC-")) return;
	}

	if (prefix && xmlStrncmp((*content) + so, BAD_CAST prefix, 4) != 0) {
		r = xmlStrdup(BAD_CAST prefix);
	} else {
		r = xmlStrdup(BAD_CAST "");
	}
	r = xmlStrncat(r, (*content) + so, eo - so);

	s1 = xmlStrndup((*content), so);
	s2 = xmlStrdup((*content) + eo);

	xmlFree(*content);

	xmlNodeSetContent(*node, s1);
	xmlFree(s1);

	ref = xmlAddNextSibling(*node, f((char *) r, NULL, opts));

	xmlFree(r);

	*node = xmlAddNextSibling(ref, xmlNewText(s2));

	*content = s2;
}

/* Transform all textual references in a particular node. */
static void transform_refs_in_node(xmlNodePtr node, const char *path, const char *regex, const char *prefix, newref_t f, const int opts)
{
	xmlChar *content;
	regex_t re;
	regmatch_t pmatch[1];

	content = xmlNodeGetContent(node);

	regcomp(&re, regex, REG_EXTENDED);

	while (regexec(&re, (char *) content, 1, pmatch, 0) == 0) {
		transform_ref(&node, path, &content, pmatch[0].rm_so, pmatch[0].rm_eo, prefix, f, opts);
	}

	regfree(&re);
	xmlFree(content);
}

/* Transform all textual references in text nodes in an XML document. */
static void transform_refs_in_doc(const xmlDocPtr doc, const char *path, const xmlChar *xpath, const char *regex, const char *prefix, newref_t f, const int opts)
{
	xmlXPathContextPtr ctx;
	xmlXPathObjectPtr obj;

	ctx = xmlXPathNewContext(doc);

	/* If the -c option is given, only transform refs in the content section. */
	if (optset(opts, OPT_CONTENT)) {
		xmlXPathSetContextNode(first_xpath_node(doc, NULL, BAD_CAST "//content"), ctx);
	} else {
		xmlXPathSetContextNode(xmlDocGetRootElement(doc), ctx);
	}

	/* Use the user-specified XPath. */
	if (xpath) {
		if (!(obj = xmlXPathEvalExpression(xpath, ctx))) {
			if (verbosity > QUIET) {
				fprintf(stderr, ERR_PREFIX "Invalid XPath expression: %s\n", (char *) xpath);
			}
			exit(EXIT_BAD_XPATH);
		}
	/* Use the appropriate built-in XPath based on ref type. */
	} else {
		unsigned char *els;
		unsigned int len;
		xmlChar *s;
		int n;

		if (f == new_dm_ref) {
			els = elems_dmc_txt;
			len = elems_dmc_txt_len;
		} else if (f == new_pm_ref) {
			els = elems_pmc_txt;
			len = elems_pmc_txt_len;
		} else if (f == new_csn_ref) {
			els = elems_csn_txt;
			len = elems_csn_txt_len;
		} else if (f == new_dml_ref) {
			els = elems_dml_txt;
			len = elems_dml_txt_len;
		} else if (f == new_icn_ref) {
			els = elems_icn_txt;
			len = elems_icn_txt_len;
		} else if (f == new_smc_ref) {
			els = elems_smc_txt;
			len = elems_smc_txt_len;
		} else if (f == new_ext_pub) {
			els = elems_epr_txt;
			len = elems_epr_txt_len;
		} else {
			els = BAD_CAST "descendant-or-self::*/text()";
			len = 11;
		}

		n = len + 1;

		s = malloc(n * sizeof(xmlChar));
		xmlStrPrintf(s, n, "%.*s", len, els);

		if (!(obj = xmlXPathEvalExpression(s, ctx))) {
			if (verbosity > QUIET) {
				fprintf(stderr, ERR_PREFIX "Invalid XPath expression: %s\n", (char *) s);
			}
			exit(EXIT_BAD_XPATH);
		}

		xmlFree(s);
	}

	if (!xmlXPathNodeSetIsEmpty(obj->nodesetval)) {
		int i;

		for (i = 0; i < obj->nodesetval->nodeNr; ++i) {
			transform_refs_in_node(obj->nodesetval->nodeTab[i], path, regex, prefix, f, opts);
		}
	}

	xmlXPathFreeObject(obj);
	xmlXPathFreeContext(ctx);
}

/* Build a regex pattern to match a string. */
static char *regex_esc(const char *s)
{
	int i, j;
	char *esc;

	/* At most, the resulting pattern will be twice the length of the original string. */
	esc = malloc(strlen(s) * 2 + 1);

	for (i = 0, j = 0; s[i]; ++i, ++j) {
		switch (s[i]) {
			/* These special characters must be escaped. */
			case '.':
			case '[':
			case '{':
			case '}':
			case '(':
			case ')':
			case '\\':
			case '*':
			case '+':
			case '?':
			case '|':
			case '^':
			case '$':
				esc[j++] = '\\';
			/* All other characters match themselves. */
			default:
				esc[j] = s[i];
				break;
		}
	}

	/* Ensure the pattern is null-terminated. */
	esc[j] = '\0';

	return esc;
}

/* Transform all external pub refs in an XML document. */
static void transform_extpub_refs_in_doc(const xmlDocPtr doc, const char *path, const xmlChar *xpath, const xmlDocPtr extpubs, int opts)
{
	xmlXPathContextPtr ctx;
	xmlXPathObjectPtr obj;

	ctx = xmlXPathNewContext(extpubs);
	obj = xmlXPathEvalExpression(BAD_CAST "//externalPubCode", ctx);

	if (!xmlXPathNodeSetIsEmpty(obj->nodesetval)) {
		int i;

		for (i = 0; i < obj->nodesetval->nodeNr; ++i) {
			char *code, *code_esc;

			code = (char *) xmlNodeGetContent(obj->nodesetval->nodeTab[i]);
			code_esc = regex_esc(code);
			xmlFree(code);

			transform_refs_in_doc(doc, path, xpath, (char *) code_esc, NULL, new_ext_pub, opts);

			free(code_esc);
		}
	}

	xmlXPathFreeObject(obj);
	xmlXPathFreeContext(ctx);
}

/* Transform all textual references in a file. */
static void transform_refs_in_file(const char *path, const char *transform, const xmlChar *xpath, const xmlDocPtr extpubs, bool overwrite, const int opts)
{
	xmlDocPtr doc;
	int i;
	bool nonstrict = optset(opts, OPT_NONSTRICT);

	if (!(doc = read_xml_doc(path))) {
		if (verbosity > QUIET) {
			fprintf(stderr, ERR_PREFIX "Could not read object: %s\n", path);
		}
		exit(EXIT_MISSING_FILE);
	}

	if (verbosity >= VERBOSE) {
		fprintf(stderr, INF_PREFIX "Transforming textual references in %s...\n", path);
	}

	for (i = 0; transform[i]; ++i) {
		switch (transform[i]) {
			case 'C':
				transform_refs_in_doc(doc, path, xpath,
					nonstrict ? COM_REGEX : COM_REGEX_STRICT,
					"COM-", new_com_ref, opts);
				break;
			case 'D':
				transform_refs_in_doc(doc, path, xpath,
					nonstrict ? DME_REGEX : DME_REGEX_STRICT,
					"DME-", new_dm_ref, opts);
				transform_refs_in_doc(doc, path, xpath,
					nonstrict ? DMC_REGEX : DMC_REGEX_STRICT,
					"DMC-", new_dm_ref, opts);
				break;
			case 'E':
				if (extpubs) {
					transform_extpub_refs_in_doc(doc, path, xpath, extpubs, opts);
				}
				break;
			case 'G':
				transform_refs_in_doc(doc, path, xpath, ICN_REGEX, NULL, new_icn_ref, opts);
				break;
			case 'L':
				transform_refs_in_doc(doc, path, xpath,
					nonstrict ? DML_REGEX : DML_REGEX_STRICT,
					"DML-", new_dml_ref, opts);
				break;
			case 'P':
				transform_refs_in_doc(doc, path, xpath,
					nonstrict ? PME_REGEX : PME_REGEX_STRICT,
					"PME-", new_pm_ref, opts);
				transform_refs_in_doc(doc, path, xpath,
					nonstrict ? PMC_REGEX : PMC_REGEX_STRICT,
					"PMC-", new_pm_ref, opts);
				break;
			case 'S':
				transform_refs_in_doc(doc, path, xpath,
					nonstrict ? SME_REGEX : SME_REGEX_STRICT,
					"SME-", new_smc_ref, opts);
				transform_refs_in_doc(doc, path, xpath,
					nonstrict ? SMC_REGEX : SMC_REGEX_STRICT,
					"SMC-", new_smc_ref, opts);
				break;
			case 'Y':
				transform_refs_in_doc(doc, path, xpath,
					nonstrict ? CSN_REGEX : CSN_REGEX_STRICT,
					"CSN-", new_csn_ref, opts);
				break;
			default:
				if (verbosity > QUIET) {
					fprintf(stderr, WRN_PREFIX "Unknown reference type: %c\n", transform[i]);
				}
				break;
		}
	}

	if (overwrite) {
		save_xml_doc(doc, path);
	} else {
		save_xml_doc(doc, "-");
	}

	xmlFreeDoc(doc);
}

/* Transform all textual references in all files in a list. */
static void transform_refs_in_list(const char *path, const char *transform, const xmlChar *xpath, const xmlDocPtr extpubs, bool overwrite, const int opts)
{
	FILE *f;
	char line[PATH_MAX];

	if (path) {
		if (!(f = fopen(path, "r"))) {
			if (verbosity > QUIET) {
				fprintf(stderr, ERR_PREFIX "Could not read list: %s\n", path);
			}
			return;
		}
	} else {
		f = stdin;
	}

	while (fgets(line, PATH_MAX, f)) {
		strtok(line, "\t\r\n");
		transform_refs_in_file(line, transform, xpath, extpubs, overwrite, opts);
	}

	if (path) {
		fclose(f);
	}
}

/* Show usage message. */
static void show_help(void)
{
	puts("Usage: " PROG_NAME " [-cdfgiLlqRrStuvh?] [-$ <issue>] [-s <src>] [-T <opts>] [-o <dst>] [-x <xpath>] [-3 <file>] [<code>|<file> ...]");
	puts("");
	puts("Options:");
	puts("  -$, --issue <issue>        Output XML for the specified issue of S1000D.");
	puts("  -c, --content              Only transform textual references in the content section.");
	puts("  -d, --include-date         Include issue date (target must be file).");
	puts("  -f, --overwrite            Overwrite source data module instead of writing to stdout.");
	puts("  -h, -?, --help             Show this help message.");
	puts("  -i, --include-issue        Include issue info.");
	puts("  -L, --list                 Treat input as a list of CSDB objects.");
	puts("  -l, --include-lang         Include language.");
	puts("  -o, --out <dst>            Output to <dst> instead of stdout.");
	puts("  -g, --guess-prefix         Accept references without a prefix.");
	puts("  -q, --quiet                Quiet mode. Do not print errors.");
	puts("  -R, --repository-id        Generate a <repositorySourceDmIdent>.");
	puts("  -r, --add                  Add reference to data module's <refs> table.");
	puts("  -S, --source-id            Generate a <sourceDmIdent> or <sourcePmIdent>.");
	puts("  -s, --source <src>         Source data module to add references to.");
	puts("  -T, --transform <opts>     Transform textual references to XML in objects.");
	puts("  -t, --include-title        Include title (target must be file)");
	puts("  -u, --include-url          Include xlink:href to the full URL/filename.");
	puts("  -v, --verbose              Verbose output.");
	puts("  -x, --xpath <xpath>        Transform textual references using <xpath>.");
	puts("  -3, --externalpubs <file>  Use a custom .externalpubs file.");
	puts("  --version                  Show version information.");
	puts("  <code>                     The code of the reference (must include prefix DMC/PMC/etc.).");
	puts("  <file>                     A file to reference, or transform references in (-T).");
	LIBXML2_PARSE_LONGOPT_HELP
}

/* Show version information. */
static void show_version(void)
{
	printf("%s (s1kd-tools) %s\n", PROG_NAME, VERSION);
	printf("Using libxml %s and libxslt %s\n", xmlParserVersion, xsltEngineVersion);
}

int main(int argc, char **argv)
{
	char scratch[PATH_MAX];
	int i;
	int opts = 0;
	char src[PATH_MAX] = "-";
	char dst[PATH_MAX] = "-";
	bool overwrite = false;
	enum issue iss = DEFAULT_S1000D_ISSUE;
	char extpubs_fname[PATH_MAX] = "";
	xmlDocPtr extpubs = NULL;
	char *transform = NULL;
	xmlChar *transform_xpath = NULL;
	bool is_list = false;

	const char *sopts = "3:cfgiLlo:qRrSs:T:tvd$:ux:h?";
	struct option lopts[] = {
		{"version"      , no_argument      , 0, 0},
		{"help"         , no_argument      , 0, 'h'},
		{"externalpubs" , required_argument, 0, '3'},
		{"content"      , no_argument      , 0, 'c'},
		{"overwrite"    , no_argument      , 0, 'f'},
		{"guess-prefix" , no_argument      , 0, 'g'},
		{"include-issue", no_argument      , 0, 'i'},
		{"include-lang" , no_argument      , 0, 'l'},
		{"out"          , required_argument, 0, 'o'},
		{"quiet"        , no_argument      , 0, 'q'},
		{"add"          , no_argument      , 0, 'r'},
		{"repository-id", no_argument      , 0, 'R'},
		{"source-id"    , no_argument      , 0, 'S'},
		{"source"       , required_argument, 0, 's'},
		{"transform"    , required_argument, 0, 'T'},
		{"include-title", no_argument      , 0, 't'},
		{"verbose"      , no_argument      , 0, 'v'},
		{"include-date" , no_argument      , 0, 'd'},
		{"issue"        , required_argument, 0, '$'},
		{"include-url"  , no_argument      , 0, 'u'},
		{"xpath"        , required_argument, 0, 'x'},
		LIBXML2_PARSE_LONGOPT_DEFS
		{0, 0, 0, 0}
	};
	int loptind = 0;

	while ((i = getopt_long(argc, argv, sopts, lopts, &loptind)) != -1) {
		switch (i) {
			case 0:
				if (strcmp(lopts[loptind].name, "version") == 0) {
					show_version();
					goto cleanup;
				}
				LIBXML2_PARSE_LONGOPT_HANDLE(lopts, loptind, optarg)
				break;
			case '3':
				strncpy(extpubs_fname, optarg, PATH_MAX - 1);
				break;
			case 'c':
				opts |= OPT_CONTENT;
				break;
			case 'f':
				overwrite = true;
				break;
			case 'g':
				opts |= OPT_NONSTRICT;
				break;
			case 'i':
				opts |= OPT_ISSUE;
				break;
			case 'L':
				is_list = true;
				break;
			case 'l':
				opts |= OPT_LANG;
				break;
			case 'o':
				strcpy(dst, optarg);
				break;
			case 'q':
				--verbosity;
				break;
			case 'r':
				opts |= OPT_INS;
				break;
			case 'R':
				opts |= OPT_CIRID;
			case 'S':
				opts |= OPT_SRCID;
				opts |= OPT_ISSUE;
				opts |= OPT_LANG;
				break;
			case 's':
				strcpy(src, optarg);
				break;
			case 'T':
				  free(transform);
				  if (strcmp(optarg, "all") == 0) {
					  transform = strdup("CDEGLPSY");
				  } else {
					  transform = strdup(optarg);
				  }
				  break;
			case 't':
				  opts |= OPT_TITLE;
				  break;
			case 'v':
				  ++verbosity;
				  break;
			case 'd':
				  opts |= OPT_DATE;
				  break;
			case '$':
				  iss = spec_issue(optarg);
				  break;
			case 'u':
				  opts |= OPT_URL;
				  break;
			case 'x':
				  free(transform_xpath);
				  transform_xpath = xmlCharStrdup(optarg);
				  break;
			case '?':
			case 'h': show_help(); goto cleanup;
		}
	}

	/* Load .externalpubs config file. */
	if (strcmp(extpubs_fname, "") != 0 || find_config(extpubs_fname, DEFAULT_EXTPUBS_FNAME)) {
		extpubs = read_xml_doc(extpubs_fname);
	}

	if (optind < argc) {
		for (i = optind; i < argc; ++i) {
			if (transform) {
				if (is_list) {
					transform_refs_in_list(argv[i], transform, transform_xpath, extpubs, overwrite, opts);
				} else {
					transform_refs_in_file(argv[i], transform, transform_xpath, extpubs, overwrite, opts);
				}
			} else {
				char *base;

				if (strncmp(argv[i], "URN:S1000D:", 11) == 0) {
					base = argv[i] + 11;
				} else {
					strcpy(scratch, argv[i]);
					base = basename(scratch);
				}

				print_ref(src, dst, base, argv[i], opts, overwrite, iss, extpubs);
			}
		}
	} else if (transform) {
		if (is_list) {
			transform_refs_in_list(NULL, transform, transform_xpath, extpubs, overwrite, opts);
		} else {
			transform_refs_in_file("-", transform, transform_xpath, extpubs, overwrite, opts);
		}
	} else {
		while (fgets(scratch, PATH_MAX, stdin)) {
			print_ref(src, dst, trim(scratch), NULL, opts, overwrite, iss, extpubs);
		}
	}

cleanup:
	xmlFreeDoc(extpubs);
	free(transform);
	xmlFree(transform_xpath);
	xsltCleanupGlobals();
	xmlCleanupParser();

	return 0;
}


/ gopher://khzae.net/0/s1000d/s1kd-tools/src/tools/s1kd-ref/s1kd-ref.c
Styles: Light Dark Classic