UNPKG

judpack-common

Version:
367 lines (304 loc) 11.9 kB
/* * * Copyright 2013 Anis Kadri * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. * */ /* jshint sub:true, laxcomma:true */ /** * contains XML utility functions, some of which are specific to elementtree */ var fs = require('fs') , path = require('path') , _ = require('underscore') , et = require('elementtree') ; var ROOT = /^\/([^\/]*)/, ABSOLUTE = /^\/([^\/]*)\/(.*)/; module.exports = { // compare two et.XML nodes, see if they match // compares tagName, text, attributes and children (recursively) equalNodes: function(one, two) { if (one.tag != two.tag) { return false; } else if (one.text.trim() != two.text.trim()) { return false; } else if (one._children.length != two._children.length) { return false; } if (!attribMatch(one, two)) return false; for (var i = 0; i < one._children.length; i++) { if (!module.exports.equalNodes(one._children[i], two._children[i])) { return false; } } return true; }, // adds node to doc at selector, creating parent if it doesn't exist graftXML: function(doc, nodes, selector, after) { var parent = module.exports.resolveParent(doc, selector); if (!parent) { //Try to create the parent recursively if necessary try { var parentToCreate = et.XML('<' + path.basename(selector) + '>'), parentSelector = path.dirname(selector); this.graftXML(doc, [parentToCreate], parentSelector); } catch (e) { return false; } parent = module.exports.resolveParent(doc, selector); if (!parent) return false; } nodes.forEach(function (node) { // check if child is unique first if (uniqueChild(node, parent)) { var children = parent.getchildren(); var insertIdx = after ? findInsertIdx(children, after) : children.length; //TODO: replace with parent.insert after the bug in ElementTree is fixed parent.getchildren().splice(insertIdx, 0, node); } }); return true; }, // adds new attributes to doc at selector // Will only merge if attribute has not been modified already or --force is used graftXMLMerge: function(doc, nodes, selector, xml) { var target = module.exports.resolveParent(doc, selector); if (!target) return false; // saves the attributes of the original xml before making changes xml.oldAttrib = _.extend({}, target.attrib); nodes.forEach(function (node) { var attributes = node.attrib; for (var attribute in attributes) { target.attrib[attribute] = node.attrib[attribute]; } }); return true; }, // overwrite all attributes to doc at selector with new attributes // Will only overwrite if attribute has not been modified already or --force is used graftXMLOverwrite: function(doc, nodes, selector, xml) { var target = module.exports.resolveParent(doc, selector); if (!target) return false; // saves the attributes of the original xml before making changes xml.oldAttrib = _.extend({}, target.attrib); // remove old attributes from target var targetAttributes = target.attrib; for (var targetAttribute in targetAttributes) { delete targetAttributes[targetAttribute]; } // add new attributes to target nodes.forEach(function (node) { var attributes = node.attrib; for (var attribute in attributes) { target.attrib[attribute] = node.attrib[attribute]; } }); return true; }, // removes node from doc at selector pruneXML: function(doc, nodes, selector) { var parent = module.exports.resolveParent(doc, selector); if (!parent) return false; nodes.forEach(function (node) { var matchingKid = null; if ((matchingKid = findChild(node, parent)) !== null) { // stupid elementtree takes an index argument it doesn't use // and does not conform to the python lib parent.remove(matchingKid); } }); return true; }, // restores attributes from doc at selector pruneXMLRestore: function(doc, selector, xml) { var target = module.exports.resolveParent(doc, selector); if (!target) return false; if (xml.oldAttrib) { target.attrib = _.extend({}, xml.oldAttrib); } return true; }, prunXMLRemove: function(doc, selector, nodes) { var target = module.exports.resolveParent(doc, selector); if (!target) return false; nodes.forEach(function (node) { var attributes = node.attrib; for (var attribute in attributes) { if (target.attrib[attribute]) { delete target.attrib[attribute]; } } }); return true; }, parseElementtreeSync: function (filename) { var contents = fs.readFileSync(filename, 'utf-8'); if(contents) { //Windows is the BOM. Skip the Byte Order Mark. contents = contents.substring(contents.indexOf('<')); } return new et.ElementTree(et.XML(contents)); }, resolveParent: function (doc, selector) { var parent, tagName, subSelector; // handle absolute selector (which elementtree doesn't like) if (ROOT.test(selector)) { tagName = selector.match(ROOT)[1]; // test for wildcard "any-tag" root selector if (tagName == '*' || tagName === doc._root.tag) { parent = doc._root; // could be an absolute path, but not selecting the root if (ABSOLUTE.test(selector)) { subSelector = selector.match(ABSOLUTE)[2]; parent = parent.find(subSelector); } } else { return false; } } else { parent = doc.find(selector); } return parent; } }; function findChild(node, parent) { var matchingKids = parent.findall(node.tag) , i, j; for (i = 0, j = matchingKids.length ; i < j ; i++) { if (module.exports.equalNodes(node, matchingKids[i])) { return matchingKids[i]; } } return null; } function uniqueChild(node, parent) { var matchingKids = parent.findall(node.tag) , i = 0; if (matchingKids.length === 0) { return true; } else { for (i; i < matchingKids.length; i++) { if (module.exports.equalNodes(node, matchingKids[i])) { return false; } } return true; } } // Find the index at which to insert an entry. After is a ;-separated priority list // of tags after which the insertion should be made. E.g. If we need to // insert an element C, and the rule is that the order of children has to be // As, Bs, Cs. After will be equal to "C;B;A". function findInsertIdx(children, after) { var childrenTags = children.map(function(child) { return child.tag; }); var afters = after.split(';'); var afterIndexes = afters.map(function(current) { return childrenTags.lastIndexOf(current); }); var foundIndex = _.find(afterIndexes, function(index) { return index != -1; }); //add to the beginning if no matching nodes are found return typeof foundIndex === 'undefined' ? 0 : foundIndex+1; } var BLACKLIST = ['platform', 'feature','plugin','engine']; var SINGLETONS = ['content', 'author', 'name']; function mergeXml(src, dest, platform, clobber) { // Do nothing for blacklisted tags. if (BLACKLIST.indexOf(src.tag) != -1) return; //Handle attributes Object.getOwnPropertyNames(src.attrib).forEach(function (attribute) { if (clobber || !dest.attrib[attribute]) { dest.attrib[attribute] = src.attrib[attribute]; } }); //Handle text if (src.text && (clobber || !dest.text)) { dest.text = src.text; } //Handle children src.getchildren().forEach(mergeChild); //Handle platform if (platform) { src.findall('platform[@name="' + platform + '"]').forEach(function (platformElement) { platformElement.getchildren().forEach(mergeChild); }); } //Handle duplicate preference tags (by name attribute) removeDuplicatePreferences(dest); function mergeChild (srcChild) { var srcTag = srcChild.tag, destChild = new et.Element(srcTag), foundChild, query = srcTag + '', shouldMerge = true; if (BLACKLIST.indexOf(srcTag) !== -1) return; if (SINGLETONS.indexOf(srcTag) !== -1) { foundChild = dest.find(query); if (foundChild) { destChild = foundChild; dest.remove(destChild); } } else { //Check for an exact match and if you find one don't add var mergeCandidates = dest.findall(query) .filter(function (foundChild) { return foundChild && textMatch(srcChild, foundChild) && attribMatch(srcChild, foundChild); }); if (mergeCandidates.length > 0) { destChild = mergeCandidates[0]; dest.remove(destChild); shouldMerge = false; } } mergeXml(srcChild, destChild, platform, clobber && shouldMerge); dest.append(destChild); } function removeDuplicatePreferences(xml) { // reduce preference tags to a hashtable to remove dupes var prefHash = xml.findall('preference[@name][@value]').reduce(function(previousValue, currentValue) { previousValue[ currentValue.attrib.name ] = currentValue.attrib.value; return previousValue; }, {}); // remove all preferences xml.findall('preference[@name][@value]').forEach(function(pref) { xml.remove(pref); }); // write new preferences Object.keys(prefHash).forEach(function(key, index) { var element = et.SubElement(xml, 'preference'); element.set('name', key); element.set('value', this[key]); }, prefHash); } } // Expose for testing. module.exports.mergeXml = mergeXml; function textMatch(elm1, elm2) { var text1 = elm1.text ? elm1.text.replace(/\s+/, '') : '', text2 = elm2.text ? elm2.text.replace(/\s+/, '') : ''; return (text1 === '' || text1 === text2); } function attribMatch(one, two) { var oneAttribKeys = Object.keys(one.attrib); var twoAttribKeys = Object.keys(two.attrib); if (oneAttribKeys.length != twoAttribKeys.length) { return false; } for (var i = 0; i < oneAttribKeys.length; i++) { var attribName = oneAttribKeys[i]; if (one.attrib[attribName] != two.attrib[attribName]) { return false; } } return true; }