|
|
- const ARRAY = "array";
- const BOOLEAN = "boolean";
- const DATE = "date";
- const NULL = "null";
- const NUMBER = "number";
- const OBJECT = "object";
- const SPECIAL_OBJECT = "special-object";
- const STRING = "string";
-
- const PRIVATE_VARS = ["_selfCloseTag", "_attrs"];
- const PRIVATE_VARS_REGEXP = new RegExp(PRIVATE_VARS.join("|"), "g");
-
- /**
- * Determines the indent string based on current tree depth.
- */
- const getIndentStr = (indent = "", depth = 0) => indent.repeat(depth);
-
- /**
- * Sugar function supplementing JS's quirky typeof operator, plus some extra help to detect
- * "special objects" expected by jstoxml.
- * Example:
- * getType(new Date());
- * -> 'date'
- */
- const getType = (val) =>
- (Array.isArray(val) && ARRAY) ||
- (typeof val === OBJECT && val !== null && val._name && SPECIAL_OBJECT) ||
- (val instanceof Date && DATE) ||
- (val === null && NULL) ||
- typeof val;
-
- /**
- * Replaces matching values in a string with a new value.
- * Example:
- * filterStr('foo&bar', { '&': '&' });
- * -> 'foo&bar'
- */
- const filterStr = (inputStr = "", filter = {}) => {
- // Passthrough/no-op for nonstrings (e.g. number, boolean).
- if (typeof inputStr !== "string") {
- return inputStr;
- }
-
- const regexp = new RegExp(
- `(${Object.keys(filter).join("|")})(?!(\\w|#)*;)`,
- "g"
- );
-
- return String(inputStr).replace(
- regexp,
- (str, entity) => filter[entity] || ""
- );
- };
-
- /**
- * Maps an object or array of arribute keyval pairs to a string.
- * Examples:
- * { foo: 'bar', baz: 'g' } -> 'foo="bar" baz="g"'
- * [ { ⚡: true }, { foo: 'bar' } ] -> '⚡ foo="bar"'
- */
- const getAttributeKeyVals = (attributes = {}, filter) => {
- let keyVals = [];
- if (Array.isArray(attributes)) {
- // Array containing complex objects and potentially duplicate attributes.
- keyVals = attributes.map((attr) => {
- const key = Object.keys(attr)[0];
- const val = attr[key];
-
- const filteredVal = filter ? filterStr(val, filter) : val;
- const valStr = filteredVal === true ? "" : `="${filteredVal}"`;
- return `${key}${valStr}`;
- });
- } else {
- const keys = Object.keys(attributes);
- keyVals = keys.map((key) => {
- // Simple object - keyval pairs.
-
- // For boolean true, simply output the key.
- const filteredVal = filter
- ? filterStr(attributes[key], filter)
- : attributes[key];
- const valStr = attributes[key] === true ? "" : `="${filteredVal}"`;
-
- return `${key}${valStr}`;
- });
- }
-
- return keyVals;
- };
-
- /**
- * Converts an attributes object/array to a string of keyval pairs.
- * Example:
- * formatAttributes({ a: 1, b: 2 })
- * -> 'a="1" b="2"'
- */
- const formatAttributes = (attributes = {}, filter) => {
- const keyVals = getAttributeKeyVals(attributes, filter);
- if (keyVals.length === 0) return "";
-
- const keysValsJoined = keyVals.join(" ");
- return ` ${keysValsJoined}`;
- };
-
- /**
- * Converts an object to a jstoxml array.
- * Example:
- * objToArray({ foo: 'bar', baz: 2 });
- * ->
- * [
- * {
- * _name: 'foo',
- * _content: 'bar'
- * },
- * {
- * _name: 'baz',
- * _content: 2
- * }
- * ]
- */
- const objToArray = (obj = {}) =>
- Object.keys(obj).map((key) => {
- return {
- _name: key,
- _content: obj[key],
- };
- });
-
- /**
- * Determines if a value is a primitive JavaScript value (not including Symbol).
- * Example:
- * isPrimitive(4);
- * -> true
- */
- const PRIMITIVE_TYPES = [STRING, NUMBER, BOOLEAN];
- const isPrimitive = (val) => PRIMITIVE_TYPES.includes(getType(val));
-
- /**
- * Determines if a value is a simple primitive type that can fit onto one line. Needed for
- * determining any needed indenting and line breaks.
- * Example:
- * isSimpleType(new Date());
- * -> true
- */
- const SIMPLE_TYPES = [...PRIMITIVE_TYPES, DATE, SPECIAL_OBJECT];
- const isSimpleType = (val) => SIMPLE_TYPES.includes(getType(val));
- /**
- * Determines if an XML string is a simple primitive, or contains nested data.
- * Example:
- * isSimpleXML('<foo />');
- * -> false
- */
- const isSimpleXML = (xmlStr) => !xmlStr.match("<");
-
- /**
- * Assembles an XML header as defined by the config.
- */
- const DEFAULT_XML_HEADER = '<?xml version="1.0" encoding="UTF-8"?>';
- const getHeaderString = ({ header, indent, isOutputStart /*, depth */ }) => {
- const shouldOutputHeader = header && isOutputStart;
- if (!shouldOutputHeader) return "";
-
- const shouldUseDefaultHeader = typeof header === BOOLEAN;
- // return `${shouldUseDefaultHeader ? DEFAULT_XML_HEADER : header}${indent ? "\n" : ""
- // }`;
- return shouldUseDefaultHeader ? DEFAULT_XML_HEADER : header;
- };
-
- /**
- * Recursively traverses an object tree and converts the output to an XML string.
- * Example:
- * toXML({ foo: 'bar' });
- * -> <foo>bar</foo>
- */
- const defaultEntityFilter = {
- "<": "<",
- ">": ">",
- "&": "&",
- };
- export const toXML = (obj = {}, config = {}) => {
- const {
- // Tree depth
- depth = 0,
- indent,
- _isFirstItem,
- // _isLastItem,
- _isOutputStart = true,
- header,
- attributesFilter: rawAttributesFilter = {},
- filter: rawFilter = {},
- } = config;
-
- const shouldTurnOffAttributesFilter = typeof rawAttributesFilter === 'boolean' && !rawAttributesFilter;
- const attributesFilter = shouldTurnOffAttributesFilter ? {} : {
- ...defaultEntityFilter,
- ...{ '"': """ },
- ...rawAttributesFilter,
- };
-
- const shouldTurnOffFilter = typeof rawFilter === 'boolean' && !rawFilter;
- const filter = shouldTurnOffFilter ? {} : { ...defaultEntityFilter, ...rawFilter };
-
- // Determine indent string based on depth.
- const indentStr = getIndentStr(indent, depth);
-
- // For branching based on value type.
- const valType = getType(obj);
-
- const headerStr = getHeaderString({ header, indent, depth, isOutputStart: _isOutputStart });
-
- const isOutputStart = _isOutputStart && !headerStr && _isFirstItem && depth === 0;
-
- let outputStr = "";
- switch (valType) {
- case "special-object": {
- // Processes a specially-formatted object used by jstoxml.
-
- const { _name, _content } = obj;
-
- // Output text content without a tag wrapper.
- if (_content === null) {
- outputStr = _name;
- break;
- }
-
- // Handles arrays of primitive values. (#33)
- const isArrayOfPrimitives =
- Array.isArray(_content) && _content.every(isPrimitive);
- if (isArrayOfPrimitives) {
- const primitives = _content
- .map((a) => {
- return toXML(
- {
- _name,
- _content: a,
- },
- {
- ...config,
- depth,
- _isOutputStart: false
- }
- );
- });
- return primitives.join('');
- }
-
- // Don't output private vars (such as _attrs).
- if (_name.match(PRIVATE_VARS_REGEXP)) break;
-
- // Process the nested new value and create new config.
- const newVal = toXML(_content, { ...config, depth: depth + 1, _isOutputStart: isOutputStart });
- const newValType = getType(newVal);
- const isNewValSimple = isSimpleXML(newVal);
-
- // Pre-tag output (indent and line breaks).
- const preIndentStr = (indent && !isOutputStart) ? "\n" : "";
- const preTag = `${preIndentStr}${indentStr}`;
-
- // Special handling for comments, preserving preceding line breaks/indents.
- if (_name === '_comment') {
- outputStr += `${preTag}<!-- ${_content} -->`;
- break;
- }
-
- // Tag output.
- const valIsEmpty = newValType === "undefined" || newVal === "";
- const shouldSelfClose =
- typeof obj._selfCloseTag === BOOLEAN
- ? valIsEmpty && obj._selfCloseTag
- : valIsEmpty;
- const selfCloseStr = shouldSelfClose ? "/" : "";
- const attributesString = formatAttributes(obj._attrs, attributesFilter);
- const tag = `<${_name}${attributesString}${selfCloseStr}>`;
-
- // Post-tag output (closing tag, indent, line breaks).
- const preTagCloseStr = indent && !isNewValSimple ? `\n${indentStr}` : "";
- const postTag = !shouldSelfClose
- ? `${newVal}${preTagCloseStr}</${_name}>`
- : "";
- outputStr += `${preTag}${tag}${postTag}`;
- break;
- }
-
- case "object": {
- // Iterates over keyval pairs in an object, converting each item to a special-object.
-
- const keys = Object.keys(obj);
- const outputArr = keys.map((key, index) => {
- const newConfig = {
- ...config,
- _isFirstItem: index === 0,
- _isLastItem: index + 1 === keys.length,
- _isOutputStart: isOutputStart
- };
-
- const outputObj = { _name: key };
-
- if (getType(obj[key]) === "object") {
- // Sub-object contains an object.
-
- // Move private vars up as needed. Needed to support certain types of objects
- // E.g. { foo: { _attrs: { a: 1 } } } -> <foo a="1"/>
- PRIVATE_VARS.forEach((privateVar) => {
- const val = obj[key][privateVar];
- if (typeof val !== "undefined") {
- outputObj[privateVar] = val;
- delete obj[key][privateVar];
- }
- });
-
- const hasContent = typeof obj[key]._content !== "undefined";
- if (hasContent) {
- // _content has sibling keys, so pass as an array (edge case).
- // E.g. { foo: 'bar', _content: { baz: 2 } } -> <foo>bar</foo><baz>2</baz>
- if (Object.keys(obj[key]).length > 1) {
- const newContentObj = Object.assign({}, obj[key]);
- delete newContentObj._content;
-
- outputObj._content = [
- ...objToArray(newContentObj),
- obj[key]._content,
- ];
- }
- }
- }
-
- // Fallthrough: just pass the key as the content for the new special-object.
- if (typeof outputObj._content === "undefined")
- outputObj._content = obj[key];
-
- const xml = toXML(outputObj, newConfig, key);
-
- return xml;
- }, config);
-
- outputStr = outputArr.join('');
- break;
- }
-
- case "function": {
- // Executes a user-defined function and returns output.
-
- const fnResult = obj(config);
-
- outputStr = toXML(fnResult, config);
- break;
- }
-
- case "array": {
- // Iterates and converts each value in an array.
- const outputArr = obj.map((singleVal, index) => {
- const newConfig = {
- ...config,
- _isFirstItem: index === 0,
- _isLastItem: index + 1 === obj.length,
- _isOutputStart: isOutputStart
- };
- return toXML(singleVal, newConfig);
- });
-
- outputStr = outputArr.join('');
-
- break;
- }
-
- // number, string, boolean, date, null, etc
- default: {
- outputStr = filterStr(obj, filter);
- break;
- }
- }
-
- return `${headerStr}${outputStr}`;
- };
-
- export default {
- toXML,
- };
|