diff --git a/lib/formatters/to_xml.ts b/lib/formatters/to_xml.ts new file mode 100644 index 00000000..bcebc152 --- /dev/null +++ b/lib/formatters/to_xml.ts @@ -0,0 +1,255 @@ +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-nocheck + +// This file was taken from https://github.com/kawanet/to-xml and modified to have a specific output for arrays. + +/** + * The toXML() method converts a JavaScript value to an XML string. + * + * @function toXML + * @param value {Object} The value to convert to an XML string. + * @param [replacer] {Function} A function that alters the behavior + * of the stringification process. + * @param [space] {Number|String} A String or Number object that's + * used to insert white space into the output XML string for + * readability purposes. If this is a Number, it indicates the number + * of space characters to use as white space. + * If this is a String, the string is used as white space. + * @returns {String} + */ + +const TYPES = { + "boolean": fromString, + "number": fromString, + "object": fromObject, + "string": fromString +}; + +const ESCAPE = { + "\t": " ", + "\n": " ", + "\r": " ", + " ": " ", + "&": "&", + "<": "<", + ">": ">", + '"': """ +}; + +const ATTRIBUTE_KEY = "@"; +const CHILD_NODE_KEY = "#"; +const LF = "\n"; + +const isArray = Array.isArray || _isArray; + +const REPLACE = String.prototype.replace; + +function _toXML(value, replacer, space) { + const job = createJob(replacer, space); + fromAny(job, "", value); + return job.r; +} + +function createJob(replacer, space) { + const job = { + f: replacer, // replacer function + // s: "", // indent string + // i: 0, // indent string length + l: "", // current indent string + r: "" // result string + }; + + if (space) { + let str = ""; + + if (space > 0) { + for (let i = space; i; i--) { + str += " "; + } + } else { + str += space; // stringify + } + job.s = str; + + // indent string length + job.i = str.length; + } + + return job; +} + +function fromAny(job, key, value) { + // child node synonym + if (key === CHILD_NODE_KEY) key = ""; + + if (_isArray(value)) return fromArray(job, key, value); + + const replacer = job.f; + if (replacer) value = replacer(key, value); + + const f = TYPES[typeof value]; + if (f) f(job, key, value); +} + +function fromString(job, key, value) { + if (key === "?") { + // XML declaration + value = ""; + } else if (key === "!") { + // comment, CDATA section + value = ""; + } else { + value = escapeTextNode(value); + if (key) { + // text element without attributes + value = "<" + key + ">" + value + ""; + } + } + + if (key && job.i && job.r) { + job.r += LF + job.l; // indent + } + + job.r += value; +} + +function fromArray(job, key, value) { + if (key !== "item") { + fromObject(job, key, { item: value }); + } else { + Array.prototype.forEach.call(value, function (value) { + fromAny(job, key, value); + }); + } +} + +function fromObject(job, key, value) { + // empty tag + const hasTag = !!key; + const closeTag = (value === null); + if (closeTag) { + if (!hasTag) return; + value = {}; + } + + const keys = Object.keys(value); + const keyLength = keys.length; + const attrs = keys.filter(isAttribute); + const attrLength = attrs.length; + const hasIndent = job.i; + const curIndent = job.l; + let willIndent = hasTag && hasIndent; + let didIndent; + + // open tag + if (hasTag) { + if (hasIndent && job.r) { + job.r += LF + curIndent; + } + + job.r += '<' + key; + + // attributes + attrs.forEach(function (name) { + writeAttributes(job, name.substr(1), value[name]); + }); + + // empty element + const isEmpty = closeTag || (attrLength && keyLength === attrLength); + if (isEmpty) { + const firstChar = key[0]; + if (firstChar !== "!" && firstChar !== "?") { + job.r += "/"; + } + } + + job.r += '>'; + + if (isEmpty) return; + } + + keys.forEach(function (name) { + // skip attribute + if (isAttribute(name)) return; + + // indent when it has child node but not fragment + if (willIndent && ((name && name !== CHILD_NODE_KEY) || isArray(value[name]))) { + job.l += job.s; // increase indent level + willIndent = 0; + didIndent = 1; + } + + // child node or text node + fromAny(job, name, value[name]); + }); + + if (didIndent) { + // decrease indent level + job.l = job.l.substr(job.i); + + job.r += LF + job.l; + } + + // close tag + if (hasTag) { + job.r += ''; + } +} + +function writeAttributes(job, key, val) { + if (isArray(val)) { + val.forEach(function (child) { + writeAttributes(job, key, child); + }); + } else if (!key && "object" === typeof val) { + Object.keys(val).forEach(function (name) { + writeAttributes(job, name, val[name]); + }); + } else { + writeAttribute(job, key, val); + } +} + +function writeAttribute(job, key, val) { + const replacer = job.f; + if (replacer) val = replacer(ATTRIBUTE_KEY + key, val); + if ("undefined" === typeof val) return; + + // empty attribute name + if (!key) { + job.r += ' ' + val; + return; + } + + // attribute name + job.r += ' ' + key; + + // property attribute + if (val === null) return; + + job.r += '="' + escapeAttribute(val) + '"'; +} + +function isAttribute(name) { + return name && name[0] === ATTRIBUTE_KEY; +} + +function escapeTextNode(str) { + return REPLACE.call(str, /(^\s|[&<>]|\s$)/g, escapeRef); +} + +function escapeAttribute(str) { + return REPLACE.call(str, /([&"])/g, escapeRef); +} + +function escapeRef(str) { + return ESCAPE[str] || str; +} + +function _isArray(array) { + return array instanceof Array; +} + +export function toXML(value: Object, replacer?: Function, space?: Number | String): String { + return _toXML(value, replacer, space); +} diff --git a/lib/formatters/xml_formatter.ts b/lib/formatters/xml_formatter.ts index ebbd5a35..e30c54e5 100644 --- a/lib/formatters/xml_formatter.ts +++ b/lib/formatters/xml_formatter.ts @@ -1,5 +1,5 @@ import { Formatter } from "./formatter.js"; -import { toXML } from "to-xml"; +import { toXML } from "./to_xml.js"; export class XMLFormatter extends Formatter { diff --git a/package.json b/package.json index 24a343a8..fc1ff4c4 100644 --- a/package.json +++ b/package.json @@ -25,8 +25,7 @@ }, "dependencies": { "commander": "^12.1.0", - "csv-stringify": "^6.5.0", - "to-xml": "^0.1.11" + "csv-stringify": "^6.5.0" }, "devDependencies": { "@eslint/js": "^9.5.0", diff --git a/tests/formatters/xml_formatter.test.ts b/tests/formatters/xml_formatter.test.ts new file mode 100644 index 00000000..c327d034 --- /dev/null +++ b/tests/formatters/xml_formatter.test.ts @@ -0,0 +1,28 @@ +import { FormatterFactory } from "../../lib/formatters/formatter_factory"; +import { TestObject } from "./test_object"; + +const formatter = FormatterFactory.getFormatter("xml"); + +test('test header', () => { + const object = [TestObject.testObject(), TestObject.testObject()]; + expect(formatter.header(object)).toBe(""); +}); + +test('test footer', () => { + expect(formatter.footer()).toBe("\n"); +}); + +test('test convert', () => { + const object = [TestObject.testObject()]; + const xml = `${TestObject.asXml()}`; + + expect(formatter.convert(object, true)).toBe(xml); + expect(formatter.convert(object, false)).toBe(xml); +}); + +test('test format with fasta', () => { + //const fasta = [['>test', '5']]; + //const object = [TestObject.testObject()]; + //const json = '{"fasta_header":">test","integer":5,"string":"string","list":["a",2,false]}'; + //expect(formatter.format(object, fasta, true)).toBe(json); +}); diff --git a/yarn.lock b/yarn.lock index c6c157f9..90d703b4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2853,11 +2853,6 @@ to-regex-range@^5.0.1: dependencies: is-number "^7.0.0" -to-xml@^0.1.11: - version "0.1.11" - resolved "https://registry.yarnpkg.com/to-xml/-/to-xml-0.1.11.tgz#fae4dafe89889e5013c86e4ea65933dca97d985b" - integrity sha512-deRSQy7vONsawpyrPdhuV0Lh0yXtKQEhAnvfSv3JMahCq3PF0MZJB/BdV1jJANVbuMdnx96zhqD/X0zMMXx0Pw== - ts-api-utils@^1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/ts-api-utils/-/ts-api-utils-1.3.0.tgz#4b490e27129f1e8e686b45cc4ab63714dc60eea1"