| /** |
| Licensed to the Apache Software Foundation (ASF) under one |
| or more contributor license agreements. See the NOTICE file |
| distributed with this work for additional information |
| regarding copyright ownership. The ASF licenses this file |
| to you 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. |
| */ |
| |
| /** |
| * contains XML utility functions, some of which are specific to elementtree |
| */ |
| |
| var fs = require('fs-extra'); |
| var path = require('path'); |
| var _ = require('underscore'); |
| var et = require('elementtree'); |
| var stripBom = require('strip-bom'); |
| |
| var ROOT = /^\/([^/]*)/; |
| var 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) + '/>'); |
| var 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 = findChild(node, parent); |
| if (matchingKid !== undefined) { |
| // 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; |
| }, |
| |
| pruneXMLRemove: 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 = stripBom(fs.readFileSync(filename, 'utf-8')); |
| 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) { |
| const matches = parent.findall(node.tag); |
| return matches.find(m => module.exports.equalNodes(node, m)); |
| } |
| |
| function uniqueChild (node, parent) { |
| return !findChild(node, parent); |
| } |
| |
| // 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.includes(src.tag)) 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; |
| var destChild = new et.Element(srcTag); |
| var foundChild; |
| var query = srcTag + ''; |
| var shouldMerge = true; |
| |
| if (BLACKLIST.includes(srcTag)) return; |
| |
| if (SINGLETONS.includes(srcTag)) { |
| 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) { |
| 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+/, '') : ''; |
| var 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; |
| } |