123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342 |
- /**
- 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');
-
- /* eslint-disable no-useless-escape */
- var ROOT = /^\/([^\/]*)/;
- var ABSOLUTE = /^\/([^\/]*)\/(.*)/;
- /* eslint-enable no-useless-escape */
-
- 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;
- }
|