Skip to content

Commit

Permalink
add tests for xml formatter
Browse files Browse the repository at this point in the history
  • Loading branch information
bmesuere committed Aug 1, 2024
1 parent 0f915f5 commit 8044011
Show file tree
Hide file tree
Showing 5 changed files with 285 additions and 8 deletions.
255 changes: 255 additions & 0 deletions lib/formatters/to_xml.ts
Original file line number Diff line number Diff line change
@@ -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": "
",
" ": " ",
"&": "&",
"<": "&lt;",
">": "&gt;",
'"': "&quot;"
};

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 = "<?" + value + "?>";
} else if (key === "!") {
// comment, CDATA section
value = "<!" + value + ">";
} else {
value = escapeTextNode(value);
if (key) {
// text element without attributes
value = "<" + key + ">" + value + "</" + key + ">";
}
}

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 += '</' + key + '>';
}
}

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);
}
2 changes: 1 addition & 1 deletion lib/formatters/xml_formatter.ts
Original file line number Diff line number Diff line change
@@ -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 {

Expand Down
3 changes: 1 addition & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
28 changes: 28 additions & 0 deletions tests/formatters/xml_formatter.test.ts
Original file line number Diff line number Diff line change
@@ -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("<results>");
});

test('test footer', () => {
expect(formatter.footer()).toBe("</results>\n");
});

test('test convert', () => {
const object = [TestObject.testObject()];
const xml = `<result>${TestObject.asXml()}</result>`;

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);
});
5 changes: 0 additions & 5 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down

0 comments on commit 8044011

Please sign in to comment.