UNPKG

12.2 kBJavaScriptView Raw
1/**
2 Licensed to the Apache Software Foundation (ASF) under one
3 or more contributor license agreements. See the NOTICE file
4 distributed with this work for additional information
5 regarding copyright ownership. The ASF licenses this file
6 to you under the Apache License, Version 2.0 (the
7 "License"); you may not use this file except in compliance
8 with the License. You may obtain a copy of the License at
9
10 http://www.apache.org/licenses/LICENSE-2.0
11
12 Unless required by applicable law or agreed to in writing,
13 software distributed under the License is distributed on an
14 "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15 KIND, either express or implied. See the License for the
16 specific language governing permissions and limitations
17 under the License.
18*/
19
20/**
21 * contains XML utility functions, some of which are specific to elementtree
22 */
23
24var fs = require('fs');
25var path = require('path');
26var _ = require('underscore');
27var et = require('elementtree');
28
29/* eslint-disable no-useless-escape */
30var ROOT = /^\/([^\/]*)/;
31var ABSOLUTE = /^\/([^\/]*)\/(.*)/;
32/* eslint-enable no-useless-escape */
33
34module.exports = {
35 // compare two et.XML nodes, see if they match
36 // compares tagName, text, attributes and children (recursively)
37 equalNodes: function (one, two) {
38 if (one.tag !== two.tag) {
39 return false;
40 } else if (one.text.trim() !== two.text.trim()) {
41 return false;
42 } else if (one._children.length !== two._children.length) {
43 return false;
44 }
45
46 if (!attribMatch(one, two)) return false;
47
48 for (var i = 0; i < one._children.length; i++) {
49 if (!module.exports.equalNodes(one._children[i], two._children[i])) {
50 return false;
51 }
52 }
53
54 return true;
55 },
56
57 // adds node to doc at selector, creating parent if it doesn't exist
58 graftXML: function (doc, nodes, selector, after) {
59 var parent = module.exports.resolveParent(doc, selector);
60 if (!parent) {
61 // Try to create the parent recursively if necessary
62 try {
63 var parentToCreate = et.XML('<' + path.basename(selector) + '>');
64 var parentSelector = path.dirname(selector);
65
66 this.graftXML(doc, [parentToCreate], parentSelector);
67 } catch (e) {
68 return false;
69 }
70 parent = module.exports.resolveParent(doc, selector);
71 if (!parent) return false;
72 }
73
74 nodes.forEach(function (node) {
75 // check if child is unique first
76 if (uniqueChild(node, parent)) {
77 var children = parent.getchildren();
78 var insertIdx = after ? findInsertIdx(children, after) : children.length;
79
80 // TODO: replace with parent.insert after the bug in ElementTree is fixed
81 parent.getchildren().splice(insertIdx, 0, node);
82 }
83 });
84
85 return true;
86 },
87
88 // adds new attributes to doc at selector
89 // Will only merge if attribute has not been modified already or --force is used
90 graftXMLMerge: function (doc, nodes, selector, xml) {
91 var target = module.exports.resolveParent(doc, selector);
92 if (!target) return false;
93
94 // saves the attributes of the original xml before making changes
95 xml.oldAttrib = _.extend({}, target.attrib);
96
97 nodes.forEach(function (node) {
98 var attributes = node.attrib;
99 for (var attribute in attributes) {
100 target.attrib[attribute] = node.attrib[attribute];
101 }
102 });
103
104 return true;
105 },
106
107 // overwrite all attributes to doc at selector with new attributes
108 // Will only overwrite if attribute has not been modified already or --force is used
109 graftXMLOverwrite: function (doc, nodes, selector, xml) {
110 var target = module.exports.resolveParent(doc, selector);
111 if (!target) return false;
112
113 // saves the attributes of the original xml before making changes
114 xml.oldAttrib = _.extend({}, target.attrib);
115
116 // remove old attributes from target
117 var targetAttributes = target.attrib;
118 for (var targetAttribute in targetAttributes) {
119 delete targetAttributes[targetAttribute];
120 }
121
122 // add new attributes to target
123 nodes.forEach(function (node) {
124 var attributes = node.attrib;
125 for (var attribute in attributes) {
126 target.attrib[attribute] = node.attrib[attribute];
127 }
128 });
129
130 return true;
131 },
132
133 // removes node from doc at selector
134 pruneXML: function (doc, nodes, selector) {
135 var parent = module.exports.resolveParent(doc, selector);
136 if (!parent) return false;
137
138 nodes.forEach(function (node) {
139 var matchingKid = null;
140 if ((matchingKid = findChild(node, parent)) !== null) {
141 // stupid elementtree takes an index argument it doesn't use
142 // and does not conform to the python lib
143 parent.remove(matchingKid);
144 }
145 });
146
147 return true;
148 },
149
150 // restores attributes from doc at selector
151 pruneXMLRestore: function (doc, selector, xml) {
152 var target = module.exports.resolveParent(doc, selector);
153 if (!target) return false;
154
155 if (xml.oldAttrib) {
156 target.attrib = _.extend({}, xml.oldAttrib);
157 }
158
159 return true;
160 },
161
162 pruneXMLRemove: function (doc, selector, nodes) {
163 var target = module.exports.resolveParent(doc, selector);
164 if (!target) return false;
165
166 nodes.forEach(function (node) {
167 var attributes = node.attrib;
168 for (var attribute in attributes) {
169 if (target.attrib[attribute]) {
170 delete target.attrib[attribute];
171 }
172 }
173 });
174
175 return true;
176
177 },
178
179 parseElementtreeSync: function (filename) {
180 var contents = fs.readFileSync(filename, 'utf-8');
181 if (contents) {
182 // Windows is the BOM. Skip the Byte Order Mark.
183 contents = contents.substring(contents.indexOf('<'));
184 }
185 return new et.ElementTree(et.XML(contents));
186 },
187
188 resolveParent: function (doc, selector) {
189 var parent, tagName, subSelector;
190
191 // handle absolute selector (which elementtree doesn't like)
192 if (ROOT.test(selector)) {
193 tagName = selector.match(ROOT)[1];
194 // test for wildcard "any-tag" root selector
195 if (tagName === '*' || tagName === doc._root.tag) {
196 parent = doc._root;
197
198 // could be an absolute path, but not selecting the root
199 if (ABSOLUTE.test(selector)) {
200 subSelector = selector.match(ABSOLUTE)[2];
201 parent = parent.find(subSelector);
202 }
203 } else {
204 return false;
205 }
206 } else {
207 parent = doc.find(selector);
208 }
209 return parent;
210 }
211};
212
213function findChild (node, parent) {
214 var matchingKids = parent.findall(node.tag);
215 var i;
216 var j;
217
218 for (i = 0, j = matchingKids.length; i < j; i++) {
219 if (module.exports.equalNodes(node, matchingKids[i])) {
220 return matchingKids[i];
221 }
222 }
223 return null;
224}
225
226function uniqueChild (node, parent) {
227 var matchingKids = parent.findall(node.tag);
228 var i = 0;
229
230 if (matchingKids.length === 0) {
231 return true;
232 } else {
233 for (i; i < matchingKids.length; i++) {
234 if (module.exports.equalNodes(node, matchingKids[i])) {
235 return false;
236 }
237 }
238 return true;
239 }
240}
241
242// Find the index at which to insert an entry. After is a ;-separated priority list
243// of tags after which the insertion should be made. E.g. If we need to
244// insert an element C, and the rule is that the order of children has to be
245// As, Bs, Cs. After will be equal to "C;B;A".
246function findInsertIdx (children, after) {
247 var childrenTags = children.map(function (child) { return child.tag; });
248 var afters = after.split(';');
249 var afterIndexes = afters.map(function (current) { return childrenTags.lastIndexOf(current); });
250 var foundIndex = _.find(afterIndexes, function (index) { return index !== -1; });
251
252 // add to the beginning if no matching nodes are found
253 return typeof foundIndex === 'undefined' ? 0 : foundIndex + 1;
254}
255
256var BLACKLIST = ['platform', 'feature', 'plugin', 'engine'];
257var SINGLETONS = ['content', 'author', 'name'];
258function mergeXml (src, dest, platform, clobber) {
259 // Do nothing for blacklisted tags.
260 if (BLACKLIST.indexOf(src.tag) !== -1) return;
261
262 // Handle attributes
263 Object.getOwnPropertyNames(src.attrib).forEach(function (attribute) {
264 if (clobber || !dest.attrib[attribute]) {
265 dest.attrib[attribute] = src.attrib[attribute];
266 }
267 });
268 // Handle text
269 if (src.text && (clobber || !dest.text)) {
270 dest.text = src.text;
271 }
272 // Handle children
273 src.getchildren().forEach(mergeChild);
274
275 // Handle platform
276 if (platform) {
277 src.findall('platform[@name="' + platform + '"]').forEach(function (platformElement) {
278 platformElement.getchildren().forEach(mergeChild);
279 });
280 }
281
282 // Handle duplicate preference tags (by name attribute)
283 removeDuplicatePreferences(dest);
284
285 function mergeChild (srcChild) {
286 var srcTag = srcChild.tag;
287 var destChild = new et.Element(srcTag);
288 var foundChild;
289 var query = srcTag + '';
290 var shouldMerge = true;
291
292 if (BLACKLIST.indexOf(srcTag) !== -1) return;
293
294 if (SINGLETONS.indexOf(srcTag) !== -1) {
295 foundChild = dest.find(query);
296 if (foundChild) {
297 destChild = foundChild;
298 dest.remove(destChild);
299 }
300 } else {
301 // Check for an exact match and if you find one don't add
302 var mergeCandidates = dest.findall(query)
303 .filter(function (foundChild) {
304 return foundChild && textMatch(srcChild, foundChild) && attribMatch(srcChild, foundChild);
305 });
306
307 if (mergeCandidates.length > 0) {
308 destChild = mergeCandidates[0];
309 dest.remove(destChild);
310 shouldMerge = false;
311 }
312 }
313
314 mergeXml(srcChild, destChild, platform, clobber && shouldMerge);
315 dest.append(destChild);
316 }
317
318 function removeDuplicatePreferences (xml) {
319 // reduce preference tags to a hashtable to remove dupes
320 var prefHash = xml.findall('preference[@name][@value]').reduce(function (previousValue, currentValue) {
321 previousValue[ currentValue.attrib.name ] = currentValue.attrib.value;
322 return previousValue;
323 }, {});
324
325 // remove all preferences
326 xml.findall('preference[@name][@value]').forEach(function (pref) {
327 xml.remove(pref);
328 });
329
330 // write new preferences
331 Object.keys(prefHash).forEach(function (key, index) {
332 var element = et.SubElement(xml, 'preference');
333 element.set('name', key);
334 element.set('value', this[key]);
335 }, prefHash);
336 }
337}
338
339// Expose for testing.
340module.exports.mergeXml = mergeXml;
341
342function textMatch (elm1, elm2) {
343 var text1 = elm1.text ? elm1.text.replace(/\s+/, '') : '';
344 var text2 = elm2.text ? elm2.text.replace(/\s+/, '') : '';
345 return (text1 === '' || text1 === text2);
346}
347
348function attribMatch (one, two) {
349 var oneAttribKeys = Object.keys(one.attrib);
350 var twoAttribKeys = Object.keys(two.attrib);
351
352 if (oneAttribKeys.length !== twoAttribKeys.length) {
353 return false;
354 }
355
356 for (var i = 0; i < oneAttribKeys.length; i++) {
357 var attribName = oneAttribKeys[i];
358
359 if (one.attrib[attribName] !== two.attrib[attribName]) {
360 return false;
361 }
362 }
363
364 return true;
365}