UNPKG

xml.js

Version:

XML.js provides helper functions for XML manipulation, including conversion of documents into [JXON](https://developer.mozilla.org/en-US/docs/JXON) objects. It is built on top of [xmldom](https://www.npmjs.org/package/xmldom) & [xpath](https://www.npmjs.o

314 lines (252 loc) 7.67 kB
var FileSystem = require("fs"); var Utility = require("ytility"); var DOM = require("xmldom"); var XML = { declaration: "<?xml version=\"1.1\" encoding=\"UTF-8\"?>", /** * @param {String} text XML text to parse. * @return {Document} */ parse: function parse(text) { var parser = new DOM.DOMParser(); return parser.parseFromString(text); }, /** * @param {String} file XML file to load. * @param {Boolean} [convert] Tells whether the document should be converted into JXON. * @return {Document} */ load: function load(file, convert) { var document = String(FileSystem.readFileSync(file)); document = XML.parse(document); return convert ? JXON.convert(document, true) : document; }, /** * @param {Node} node XML node to serialize. * @return {String} */ serialize: function serialize(node) { var serializer = new DOM.XMLSerializer(); return serializer.serializeToString(node); }, /** * @param {Node} node Node to evaluate the expression on. * @param {String} expression X-Path expression to evaluate. * @param {Object} [resolver] Simple URI resolver like {"prefix": "http://...", ...}. * Default namespace may be specified with the empty key: "". * @return {Array} X-Path evaluation results. */ query: function query(node, expression, resolver) { var XPath = require("xpath"); var evaluate = !!resolver && (typeof resolver == "object") ? XPath.useNamespaces(resolver) : XPath.select; return evaluate(expression, node); } }; var JXON = { /** * @param {Node} node Node to convert into a JXON object. * @return {Object} */ convert: function convert(node, compact) { var transformer = this.transformers[node.nodeType]; if(!transformer) { throw new Error("Unsupported node type: " + node.nodeType); } var object = transformer(node); return compact || !Utility.isDefined(compact) ? this.compact(object) : object; }, /** * @param {Object} object JXON object to compact, which should not be compacted already. * @return {Object} */ compact: function compact(object) { for(var type in object) break; (type[0] !== "#") && (type = "#ELEMENT"); return this.compactors[type](object); } }; Object.defineProperty(JXON, "transformers", { enumerable: false, value: (function() { var transformers = { "#DOCUMENT": function(node) { return transformers["#ELEMENT"](node.documentElement); }, "#ELEMENT": function(node) { var result = {}; var properties = result[node.nodeName] = {}; Utility.forEach(node.attributes, function(attribute) { var transformer = transformers["#ATTRIBUTE"]; Utility.forOwn(transformer(attribute), function(value, name) { if(name.indexOf("@xmlns") === 0) { var alias = name.split(":")[1]; if(!("#NAMESPACE" in properties)) { properties["#NAMESPACE"] = {}; } properties["#NAMESPACE"][alias || ""] = value; } else { properties[name] = value; } }); }); var children = properties["children"] = []; if(node.hasChildNodes()) { Utility.forEach(node.childNodes, function(child) { var transformer = transformers[child.nodeType]; if(transformer) { var childResult = transformer(child); if(!Utility.isEmpty(childResult)) { children.push(childResult); } } }); } return result; }, "#ATTRIBUTE": function(node) { var result = {}; result["@" + node.name] = node.value; return result; }, "#TEXT": function(node) { var result = {}; result["#TEXT"] = node.nodeValue; return result; }, "#CDATA": function(node) { var result = {}; result["#CDATA"] = node.nodeValue; return result; }, "#COMMENT": function(node) { var result = {}; result["#COMMENT"] = node.nodeValue; return result; } }; transformers[1] = transformers["#ELEMENT"]; transformers[2] = transformers["#ATTRIBUTE"]; transformers[3] = transformers["#TEXT"]; transformers[4] = transformers["#CDATA"]; transformers[8] = transformers["#COMMENT"]; transformers[9] = transformers["#DOCUMENT"]; return transformers; })() }); Object.defineProperty(JXON, "compactors", { enumerable: false, value: (function() { var compactors = { "#ELEMENT": function(object) { for(var tag in object) break; var properties = object[tag]; // Free the "children" key before attributes are re-written: var children = properties["children"]; delete properties["children"]; if(!Utility.isEmpty(children)) { properties["*"] = children; var index = 0; while(index < children.length) { var child = children[index]; for(var type in child) break; (type[0] !== "#") && (type = "#ELEMENT"); var result = compactors[type](child); if(!result) { children.splice(index, 1); } else { var previous = (index > 0) ? children[index - 1] : {}; if(type === "#TEXT") { // Combine #TEXT objects: if(typeof(previous) === "string") { children[index - 1] += "\n" + child; children.splice(index, 1); } else { // Only #TEXT objects are to be replaced by `result`: children.splice(index, 1, result); index++ } } else if((type === "#COMMENT") && (Utility.isObject(previous) && "#C" in previous)) { // Combine #COMMENT objects: previous["#C"] += "\n" + child["#C"]; children.splice(index, 1); } else { index++; } } } // Take out the first child when it is alone: if(children.length === 1) { child = properties["*"] = children[0]; // When it is a #TEXT child, extract its value: if(typeof(child) === "string") { properties["*"] = child; } } } // Rewrite attributes: var attributes = []; Utility.forOwn(properties, function(value, attribute) { if(attribute.charAt(0) === "@") { attributes.push(attribute); compactors["#ATTRIBUTE"](attribute, object); } else if(attribute === "#NAMESPACE") { delete properties["#NAMESPACE"]; var keys = Object.keys(value); if(keys.length) { properties["#N"] = value; if(keys.length === 1) { properties["#N"] = value[keys[0]]; } } } }); // Compact even more when the object has no attribute and its unique child is a #TEXT object: if(Utility.isEmpty(attributes) && Utility.isString(properties["*"])) { object[tag] = properties["*"]; } return object; }, "#ATTRIBUTE": function compact(attribute, object) { var tag; for(tag in object) break; var properties = object[tag]; properties[attribute.split("@")[1]] = properties[attribute]; delete properties[attribute]; return object; }, "#TEXT": function(object) { return object["#TEXT"].trim() || null; }, "#CDATA": function(object) { var text = object["#CDATA"].trim(); if(text) { object["#D"] = text; delete object["#CDATA"]; } return text ? object : null; }, "#COMMENT": function(object) { var text = object["#COMMENT"].trim(); if(text) { object["#C"] = text; delete object["#COMMENT"]; } return text ? object : null; } }; compactors[1] = compactors["#ELEMENT"]; compactors[2] = compactors["#ATTRIBUTE"]; compactors[3] = compactors["#TEXT"]; compactors[4] = compactors["#CDATA"]; compactors[8] = compactors["#COMMENT"]; return compactors; })() }); XML.JXON = JXON; module.exports = XML;