UNPKG

xmlbuilder2

Version:

An XML builder for node.js

741 lines 29.9 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.XMLBuilderImpl = void 0; const interfaces_1 = require("../interfaces"); const util_1 = require("@oozcitak/util"); const writers_1 = require("../writers"); const interfaces_2 = require("@oozcitak/dom/lib/dom/interfaces"); const util_2 = require("@oozcitak/dom/lib/util"); const algorithm_1 = require("@oozcitak/dom/lib/algorithm"); const dom_1 = require("./dom"); const infra_1 = require("@oozcitak/infra"); const readers_1 = require("../readers"); /** * Represents a wrapper that extends XML nodes to implement easy to use and * chainable document builder methods. */ class XMLBuilderImpl { _domNode; /** * Initializes a new instance of `XMLBuilderNodeImpl`. * * @param domNode - the DOM node to wrap */ constructor(domNode) { this._domNode = domNode; } /** @inheritdoc */ get node() { return this._domNode; } /** @inheritdoc */ get options() { return this._options; } /** @inheritdoc */ set(options) { this._options = (0, util_1.applyDefaults)((0, util_1.applyDefaults)(this._options, options, true), // apply user settings interfaces_1.DefaultBuilderOptions); // provide defaults return this; } /** @inheritdoc */ ele(p1, p2, p3) { let namespace; let name; let attributes; if ((0, util_1.isObject)(p1)) { // ele(obj: ExpandObject) return new readers_1.ObjectReader(this._options).parse(this, p1); } else if ((0, util_1.isString)(p1) && p1 !== null && /^\s*</.test(p1)) { // parse XML document string return new readers_1.XMLReader(this._options).parse(this, p1); } else if ((0, util_1.isString)(p1) && p1 !== null && /^\s*[\{\[]/.test(p1)) { // parse JSON string return new readers_1.JSONReader(this._options).parse(this, p1); } else if ((0, util_1.isString)(p1) && p1 !== null && /^(\s*|(#.*)|(%.*))*---/.test(p1)) { // parse YAML string return new readers_1.YAMLReader(this._options).parse(this, p1); } if ((p1 === null || (0, util_1.isString)(p1)) && (0, util_1.isString)(p2)) { // ele(namespace: string, name: string, attributes?: AttributesObject) [namespace, name, attributes] = [p1, p2, p3]; } else if (p1 !== null) { // ele(name: string, attributes?: AttributesObject) [namespace, name, attributes] = [undefined, p1, (0, util_1.isObject)(p2) ? p2 : undefined]; } else { throw new Error("Element name cannot be null. " + this._debugInfo()); } if (attributes) { attributes = (0, util_1.getValue)(attributes); } [namespace, name] = this._extractNamespace((0, dom_1.sanitizeInput)(namespace, this._options.invalidCharReplacement), (0, dom_1.sanitizeInput)(name, this._options.invalidCharReplacement), true); // inherit namespace from parent if (namespace === undefined) { const [prefix] = (0, algorithm_1.namespace_extractQName)(name); namespace = this.node.lookupNamespaceURI(prefix); } // create a child element node const childNode = (namespace !== undefined && namespace !== null ? this._doc.createElementNS(namespace, name) : this._doc.createElement(name)); this.node.appendChild(childNode); const builder = new XMLBuilderImpl(childNode); // update doctype node if the new node is the document element node const oldDocType = this._doc.doctype; if (childNode === this._doc.documentElement && oldDocType !== null) { const docType = this._doc.implementation.createDocumentType(this._doc.documentElement.tagName, oldDocType.publicId, oldDocType.systemId); this._doc.replaceChild(docType, oldDocType); } // create attributes if (attributes && !(0, util_1.isEmpty)(attributes)) { builder.att(attributes); } return builder; } /** @inheritdoc */ remove() { const parent = this.up(); parent.node.removeChild(this.node); return parent; } /** @inheritdoc */ att(p1, p2, p3) { if ((0, util_1.isMap)(p1) || (0, util_1.isObject)(p1)) { // att(obj: AttributesObject) // expand if object (0, util_1.forEachObject)(p1, (attName, attValue) => this.att(attName, attValue), this); return this; } // get primitive values if (p1 !== undefined && p1 !== null) p1 = (0, util_1.getValue)(p1 + ""); if (p2 !== undefined && p2 !== null) p2 = (0, util_1.getValue)(p2 + ""); if (p3 !== undefined && p3 !== null) p3 = (0, util_1.getValue)(p3 + ""); let namespace; let name; let value; if ((p1 === null || (0, util_1.isString)(p1)) && (0, util_1.isString)(p2) && (p3 === null || (0, util_1.isString)(p3))) { // att(namespace: string, name: string, value: string) [namespace, name, value] = [p1, p2, p3]; } else if ((0, util_1.isString)(p1) && (p2 == null || (0, util_1.isString)(p2))) { // ele(name: string, value: string) [namespace, name, value] = [undefined, p1, p2]; } else { throw new Error("Attribute name and value not specified. " + this._debugInfo()); } if (this._options.keepNullAttributes && (value == null)) { // keep null attributes value = ""; } else if (value == null) { // skip null|undefined attributes return this; } if (!util_2.Guard.isElementNode(this.node)) { throw new Error("An attribute can only be assigned to an element node."); } let ele = this.node; [namespace, name] = this._extractNamespace(namespace, name, false); name = (0, dom_1.sanitizeInput)(name, this._options.invalidCharReplacement); namespace = (0, dom_1.sanitizeInput)(namespace, this._options.invalidCharReplacement); value = (0, dom_1.sanitizeInput)(value, this._options.invalidCharReplacement); const [prefix, localName] = (0, algorithm_1.namespace_extractQName)(name); const [elePrefix] = (0, algorithm_1.namespace_extractQName)(ele.prefix ? ele.prefix + ':' + ele.localName : ele.localName); // check if this is a namespace declaration attribute // assign a new element namespace if it wasn't previously assigned let eleNamespace = null; if (prefix === "xmlns") { namespace = infra_1.namespace.XMLNS; if (ele.namespaceURI === null && elePrefix === localName) { eleNamespace = value; } } else if (prefix === null && localName === "xmlns" && elePrefix === null) { namespace = infra_1.namespace.XMLNS; eleNamespace = value; } // re-create the element node if its namespace changed // we can't simply change the namespaceURI since its read-only if (eleNamespace !== null) { this._updateNamespace(eleNamespace); ele = this.node; } if (namespace !== undefined) { ele.setAttributeNS(namespace, name, value); } else { ele.setAttribute(name, value); } return this; } /** @inheritdoc */ removeAtt(p1, p2) { if (!util_2.Guard.isElementNode(this.node)) { throw new Error("An attribute can only be removed from an element node."); } // get primitive values p1 = (0, util_1.getValue)(p1); if (p2 !== undefined) { p2 = (0, util_1.getValue)(p2); } let namespace; let name; if (p1 !== null && p2 === undefined) { name = p1; } else if ((p1 === null || (0, util_1.isString)(p1)) && p2 !== undefined) { namespace = p1; name = p2; } else { throw new Error("Attribute namespace must be a string. " + this._debugInfo()); } if ((0, util_1.isArray)(name) || (0, util_1.isSet)(name)) { // removeAtt(names: string[]) // removeAtt(namespace: string, names: string[]) (0, util_1.forEachArray)(name, attName => namespace === undefined ? this.removeAtt(attName) : this.removeAtt(namespace, attName), this); } else if (namespace !== undefined) { // removeAtt(namespace: string, name: string) name = (0, dom_1.sanitizeInput)(name, this._options.invalidCharReplacement); namespace = (0, dom_1.sanitizeInput)(namespace, this._options.invalidCharReplacement); this.node.removeAttributeNS(namespace, name); } else { // removeAtt(name: string) name = (0, dom_1.sanitizeInput)(name, this._options.invalidCharReplacement); this.node.removeAttribute(name); } return this; } /** @inheritdoc */ txt(content) { if (content === null || content === undefined) { if (this._options.keepNullNodes) { // keep null nodes content = ""; } else { // skip null|undefined nodes return this; } } const child = this._doc.createTextNode((0, dom_1.sanitizeInput)(content, this._options.invalidCharReplacement)); this.node.appendChild(child); return this; } /** @inheritdoc */ com(content) { if (content === null || content === undefined) { if (this._options.keepNullNodes) { // keep null nodes content = ""; } else { // skip null|undefined nodes return this; } } const child = this._doc.createComment((0, dom_1.sanitizeInput)(content, this._options.invalidCharReplacement)); this.node.appendChild(child); return this; } /** @inheritdoc */ dat(content) { if (content === null || content === undefined) { if (this._options.keepNullNodes) { // keep null nodes content = ""; } else { // skip null|undefined nodes return this; } } const child = this._doc.createCDATASection((0, dom_1.sanitizeInput)(content, this._options.invalidCharReplacement)); this.node.appendChild(child); return this; } /** @inheritdoc */ ins(target, content = '') { if (content === null || content === undefined) { if (this._options.keepNullNodes) { // keep null nodes content = ""; } else { // skip null|undefined nodes return this; } } if ((0, util_1.isArray)(target) || (0, util_1.isSet)(target)) { (0, util_1.forEachArray)(target, item => { item += ""; const insIndex = item.indexOf(' '); const insTarget = (insIndex === -1 ? item : item.substr(0, insIndex)); const insValue = (insIndex === -1 ? '' : item.substr(insIndex + 1)); this.ins(insTarget, insValue); }, this); } else if ((0, util_1.isMap)(target) || (0, util_1.isObject)(target)) { (0, util_1.forEachObject)(target, (insTarget, insValue) => this.ins(insTarget, insValue), this); } else { const child = this._doc.createProcessingInstruction((0, dom_1.sanitizeInput)(target, this._options.invalidCharReplacement), (0, dom_1.sanitizeInput)(content, this._options.invalidCharReplacement)); this.node.appendChild(child); } return this; } /** @inheritdoc */ dec(options) { this._options.version = options.version || "1.0"; this._options.encoding = options.encoding; this._options.standalone = options.standalone; return this; } /** @inheritdoc */ dtd(options) { const name = (0, dom_1.sanitizeInput)((options && options.name) || (this._doc.documentElement ? this._doc.documentElement.tagName : "ROOT"), this._options.invalidCharReplacement); const pubID = (0, dom_1.sanitizeInput)((options && options.pubID) || "", this._options.invalidCharReplacement); const sysID = (0, dom_1.sanitizeInput)((options && options.sysID) || "", this._options.invalidCharReplacement); // name must match document element if (this._doc.documentElement !== null && name !== this._doc.documentElement.tagName) { throw new Error("DocType name does not match document element name."); } // create doctype node const docType = this._doc.implementation.createDocumentType(name, pubID, sysID); if (this._doc.doctype !== null) { // replace existing doctype this._doc.replaceChild(docType, this._doc.doctype); } else { // insert before document element node or append to end this._doc.insertBefore(docType, this._doc.documentElement); } return this; } /** @inheritdoc */ import(node) { const hostNode = this._domNode; const hostDoc = this._doc; const importedNode = node.node; const updateImportedNodeNs = (clone) => { // update namespace of imported node only when not specified if (!clone._namespace) { const [prefix] = (0, algorithm_1.namespace_extractQName)(clone.prefix ? clone.prefix + ':' + clone.localName : clone.localName); const namespace = hostNode.lookupNamespaceURI(prefix); new XMLBuilderImpl(clone)._updateNamespace(namespace); } }; if (util_2.Guard.isDocumentNode(importedNode)) { // import document node const elementNode = importedNode.documentElement; if (elementNode === null) { throw new Error("Imported document has no document element node. " + this._debugInfo()); } const clone = hostDoc.importNode(elementNode, true); hostNode.appendChild(clone); updateImportedNodeNs(clone); } else if (util_2.Guard.isDocumentFragmentNode(importedNode)) { // import child nodes for (const childNode of importedNode.childNodes) { const clone = hostDoc.importNode(childNode, true); hostNode.appendChild(clone); if (util_2.Guard.isElementNode(clone)) { updateImportedNodeNs(clone); } } } else { // import node const clone = hostDoc.importNode(importedNode, true); hostNode.appendChild(clone); if (util_2.Guard.isElementNode(clone)) { updateImportedNodeNs(clone); } } return this; } /** @inheritdoc */ doc() { if (this._doc._isFragment) { let node = this.node; while (node && node.nodeType !== interfaces_2.NodeType.DocumentFragment) { node = node.parentNode; } /* istanbul ignore next */ if (node === null) { throw new Error("Node has no parent node while searching for document fragment ancestor. " + this._debugInfo()); } return new XMLBuilderImpl(node); } else { return new XMLBuilderImpl(this._doc); } } /** @inheritdoc */ root() { const ele = this._doc.documentElement; if (!ele) { throw new Error("Document root element is null. " + this._debugInfo()); } return new XMLBuilderImpl(ele); } /** @inheritdoc */ up() { const parent = this._domNode.parentNode; if (!parent) { throw new Error("Parent node is null. " + this._debugInfo()); } return new XMLBuilderImpl(parent); } /** @inheritdoc */ prev() { const node = this._domNode.previousSibling; if (!node) { throw new Error("Previous sibling node is null. " + this._debugInfo()); } return new XMLBuilderImpl(node); } /** @inheritdoc */ next() { const node = this._domNode.nextSibling; if (!node) { throw new Error("Next sibling node is null. " + this._debugInfo()); } return new XMLBuilderImpl(node); } /** @inheritdoc */ first() { const node = this._domNode.firstChild; if (!node) { throw new Error("First child node is null. " + this._debugInfo()); } return new XMLBuilderImpl(node); } /** @inheritdoc */ last() { const node = this._domNode.lastChild; if (!node) { throw new Error("Last child node is null. " + this._debugInfo()); } return new XMLBuilderImpl(node); } /** @inheritdoc */ each(callback, self = false, recursive = false, thisArg) { let result = this._getFirstDescendantNode(this._domNode, self, recursive); while (result[0]) { const nextResult = this._getNextDescendantNode(this._domNode, result[0], recursive, result[1], result[2]); callback.call(thisArg, new XMLBuilderImpl(result[0]), result[1], result[2]); result = nextResult; } return this; } /** @inheritdoc */ map(callback, self = false, recursive = false, thisArg) { let result = []; this.each((node, index, level) => result.push(callback.call(thisArg, node, index, level)), self, recursive); return result; } /** @inheritdoc */ reduce(callback, initialValue, self = false, recursive = false, thisArg) { let value = initialValue; this.each((node, index, level) => value = callback.call(thisArg, value, node, index, level), self, recursive); return value; } /** @inheritdoc */ find(predicate, self = false, recursive = false, thisArg) { let result = this._getFirstDescendantNode(this._domNode, self, recursive); while (result[0]) { const builder = new XMLBuilderImpl(result[0]); if (predicate.call(thisArg, builder, result[1], result[2])) { return builder; } result = this._getNextDescendantNode(this._domNode, result[0], recursive, result[1], result[2]); } return undefined; } /** @inheritdoc */ filter(predicate, self = false, recursive = false, thisArg) { let result = []; this.each((node, index, level) => { if (predicate.call(thisArg, node, index, level)) { result.push(node); } }, self, recursive); return result; } /** @inheritdoc */ every(predicate, self = false, recursive = false, thisArg) { let result = this._getFirstDescendantNode(this._domNode, self, recursive); while (result[0]) { const builder = new XMLBuilderImpl(result[0]); if (!predicate.call(thisArg, builder, result[1], result[2])) { return false; } result = this._getNextDescendantNode(this._domNode, result[0], recursive, result[1], result[2]); } return true; } /** @inheritdoc */ some(predicate, self = false, recursive = false, thisArg) { let result = this._getFirstDescendantNode(this._domNode, self, recursive); while (result[0]) { const builder = new XMLBuilderImpl(result[0]); if (predicate.call(thisArg, builder, result[1], result[2])) { return true; } result = this._getNextDescendantNode(this._domNode, result[0], recursive, result[1], result[2]); } return false; } /** @inheritdoc */ toArray(self = false, recursive = false) { let result = []; this.each(node => result.push(node), self, recursive); return result; } /** @inheritdoc */ toString(writerOptions) { writerOptions = writerOptions || {}; if (writerOptions.format === undefined) { writerOptions.format = "xml"; } return this._serialize(writerOptions); } /** @inheritdoc */ toObject(writerOptions) { writerOptions = writerOptions || {}; if (writerOptions.format === undefined) { writerOptions.format = "object"; } return this._serialize(writerOptions); } /** @inheritdoc */ end(writerOptions) { writerOptions = writerOptions || {}; if (writerOptions.format === undefined) { writerOptions.format = "xml"; } return this.doc()._serialize(writerOptions); } /** * Gets the next descendant of the given node of the tree rooted at `root` * in depth-first pre-order. Returns a three-tuple with * [descendant, descendant_index, descendant_level]. * * @param root - root node of the tree * @param self - whether to visit the current node along with child nodes * @param recursive - whether to visit all descendant nodes in tree-order or * only the immediate child nodes */ _getFirstDescendantNode(root, self, recursive) { if (self) return [this._domNode, 0, 0]; else if (recursive) return this._getNextDescendantNode(root, root, recursive, 0, 0); else return [this._domNode.firstChild, 0, 1]; } /** * Gets the next descendant of the given node of the tree rooted at `root` * in depth-first pre-order. Returns a three-tuple with * [descendant, descendant_index, descendant_level]. * * @param root - root node of the tree * @param node - current node * @param recursive - whether to visit all descendant nodes in tree-order or * only the immediate child nodes * @param index - child node index * @param level - current depth of the XML tree */ _getNextDescendantNode(root, node, recursive, index, level) { if (recursive) { // traverse child nodes if (node.firstChild) return [node.firstChild, 0, level + 1]; if (node === root) return [null, -1, -1]; // traverse siblings if (node.nextSibling) return [node.nextSibling, index + 1, level]; // traverse parent's next sibling let parent = node.parentNode; while (parent && parent !== root) { if (parent.nextSibling) return [parent.nextSibling, (0, algorithm_1.tree_index)(parent.nextSibling), level - 1]; parent = parent.parentNode; level--; } } else { if (root === node) return [node.firstChild, 0, level + 1]; else return [node.nextSibling, index + 1, level]; } return [null, -1, -1]; } /** * Converts the node into its string or object representation. * * @param options - serialization options */ _serialize(writerOptions) { if (writerOptions.format === "xml") { const writer = new writers_1.XMLWriter(this._options, writerOptions); return writer.serialize(this.node); } else if (writerOptions.format === "map") { const writer = new writers_1.MapWriter(this._options, writerOptions); return writer.serialize(this.node); } else if (writerOptions.format === "object") { const writer = new writers_1.ObjectWriter(this._options, writerOptions); return writer.serialize(this.node); } else if (writerOptions.format === "json") { const writer = new writers_1.JSONWriter(this._options, writerOptions); return writer.serialize(this.node); } else if (writerOptions.format === "yaml") { const writer = new writers_1.YAMLWriter(this._options, writerOptions); return writer.serialize(this.node); } else { throw new Error("Invalid writer format: " + writerOptions.format + ". " + this._debugInfo()); } } /** * Extracts a namespace and name from the given string. * * @param namespace - namespace * @param name - a string containing both a name and namespace separated by an * `'@'` character * @param ele - `true` if this is an element namespace; otherwise `false` */ _extractNamespace(namespace, name, ele) { // extract from name const atIndex = name.indexOf("@"); if (atIndex > 0) { if (namespace === undefined) namespace = name.slice(atIndex + 1); name = name.slice(0, atIndex); } if (namespace === undefined) { // look-up default namespace namespace = (ele ? this._options.defaultNamespace.ele : this._options.defaultNamespace.att); } else if (namespace !== null && namespace[0] === "@") { // look-up namespace aliases const alias = namespace.slice(1); namespace = this._options.namespaceAlias[alias]; if (namespace === undefined) { throw new Error("Namespace alias `" + alias + "` is not defined. " + this._debugInfo()); } } return [namespace, name]; } /** * Updates the element's namespace. * * @param ns - new namespace */ _updateNamespace(ns) { const ele = this._domNode; if (util_2.Guard.isElementNode(ele) && ns !== null && ele.namespaceURI !== ns) { const [elePrefix, eleLocalName] = (0, algorithm_1.namespace_extractQName)(ele.prefix ? ele.prefix + ':' + ele.localName : ele.localName); // re-create the element node if its namespace changed // we can't simply change the namespaceURI since its read-only const newEle = (0, algorithm_1.create_element)(this._doc, eleLocalName, ns, elePrefix); for (const attr of ele.attributes) { const attrQName = attr.prefix ? attr.prefix + ':' + attr.localName : attr.localName; const [attrPrefix] = (0, algorithm_1.namespace_extractQName)(attrQName); let newAttrNS = attr.namespaceURI; if (newAttrNS === null && attrPrefix !== null) { newAttrNS = ele.lookupNamespaceURI(attrPrefix); } if (newAttrNS === null) { newEle.setAttribute(attrQName, attr.value); } else { newEle.setAttributeNS(newAttrNS, attrQName, attr.value); } } // replace the new node in parent node const parent = ele.parentNode; /* istanbul ignore next */ if (parent === null) { throw new Error("Parent node is null." + this._debugInfo()); } parent.replaceChild(newEle, ele); this._domNode = newEle; // check child nodes for (const childNode of ele.childNodes) { const newChildNode = childNode.cloneNode(true); newEle.appendChild(newChildNode); if (util_2.Guard.isElementNode(newChildNode) && !newChildNode._namespace) { const [newChildNodePrefix] = (0, algorithm_1.namespace_extractQName)(newChildNode.prefix ? newChildNode.prefix + ':' + newChildNode.localName : newChildNode.localName); const newChildNodeNS = newEle.lookupNamespaceURI(newChildNodePrefix); new XMLBuilderImpl(newChildNode)._updateNamespace(newChildNodeNS); } } } } /** * Returns the document owning this node. */ get _doc() { const node = this.node; if (util_2.Guard.isDocumentNode(node)) { return node; } else { const docNode = node.ownerDocument; /* istanbul ignore next */ if (!docNode) throw new Error("Owner document is null. " + this._debugInfo()); return docNode; } } /** * Returns debug information for this node. * * @param name - node name */ _debugInfo(name) { const node = this.node; const parentNode = node.parentNode; name = name || node.nodeName; const parentName = parentNode ? parentNode.nodeName : ''; if (!parentName) { return "node: <" + name + ">"; } else { return "node: <" + name + ">, parent: <" + parentName + ">"; } } /** * Gets or sets builder options. */ get _options() { const doc = this._doc; /* istanbul ignore next */ if (doc._xmlBuilderOptions === undefined) { throw new Error("Builder options is not set."); } return doc._xmlBuilderOptions; } set _options(value) { const doc = this._doc; doc._xmlBuilderOptions = value; } } exports.XMLBuilderImpl = XMLBuilderImpl; //# sourceMappingURL=XMLBuilderImpl.js.map