| /* |
| * 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. |
| * |
| */ |
| |
| // @ts-check |
| |
| /** |
| * @typedef {Object} MungeElement |
| * @property {String} xml |
| * @property {Number} count |
| * @property {import('elementtree').Attributes} [oldAttrib] |
| * |
| * @property {'merge' | 'overwrite' | 'remove'} [mode] edit-config only |
| * |
| * @property {String} [id] 'config.xml' or the id of the plugin from whose |
| * plugin.xml this was taken; edit-config only |
| * @property {String} [after] 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 |
| * order of children has to be As, Bs, Cs then `after` will be equal to "C;B;A". |
| * config-file only |
| */ |
| |
| /** |
| * @typedef {Object} FileMunge |
| * @property {Object.<string, MungeElement[]>} parents |
| */ |
| |
| /** |
| * @typedef {Object} Munge |
| * @property {Object.<string, FileMunge>} files |
| */ |
| |
| /** |
| * Adds element.count to obj[file][selector][element] |
| * |
| * @return {Boolean} true iff it didn't exist before |
| */ |
| exports.deep_add = (...args) => { |
| const { element, siblings } = processArgs(...args, { create: true }); |
| const matchingSibling = siblings.find(sibling => sibling.xml === element.xml); |
| |
| if (matchingSibling) { |
| matchingSibling.after = matchingSibling.after || element.after; |
| matchingSibling.count += element.count; |
| } else { |
| siblings.push(element); |
| } |
| |
| return !matchingSibling; |
| }; |
| |
| /** |
| * Subtracts element.count from obj[file][selector][element] |
| * |
| * @return {Boolean} true iff element was removed or not found |
| */ |
| exports.deep_remove = (...args) => { |
| const { element, siblings } = processArgs(...args); |
| const index = siblings.findIndex(sibling => sibling.xml === element.xml); |
| |
| if (index < 0) return true; |
| |
| const matchingSibling = siblings[index]; |
| |
| if (matchingSibling.oldAttrib) { |
| element.oldAttrib = Object.assign({}, matchingSibling.oldAttrib); |
| } |
| matchingSibling.count -= element.count; |
| |
| if (matchingSibling.count > 0) return false; |
| |
| siblings.splice(index, 1); |
| return true; |
| }; |
| |
| /** |
| * Find element with given key in obj |
| * |
| * @return {MungeElement} the sought-after object or undefined if not found |
| */ |
| exports.deep_find = (...args) => { |
| const { element, siblings } = processArgs(...args); |
| |
| const elementXml = (element.xml || element); |
| return siblings.find(sibling => sibling.xml === elementXml); |
| }; |
| |
| function processArgs (obj, fileName, selector, element, opts) { |
| if (Array.isArray(fileName)) { |
| opts = selector; |
| [fileName, selector, element] = fileName; |
| } |
| |
| const siblings = getElements(obj, [fileName, selector], opts); |
| return { element, siblings }; |
| } |
| |
| /** |
| * Get the element array for given keys |
| * |
| * If a key entry is missing, create it if opts.create is true else return [] |
| * |
| * @param {Munge} obj |
| * @param {String[]} keys [fileName, selector] |
| * @param {{create: Boolean}} [opts] |
| * @return {MungeElement[]} |
| */ |
| function getElements ({ files }, [fileName, selector], opts = { create: false }) { |
| if (!files[fileName] && !opts.create) return []; |
| |
| const { parents: fileChanges } = (files[fileName] = files[fileName] || { parents: {} }); |
| if (!fileChanges[selector] && !opts.create) return []; |
| |
| return (fileChanges[selector] = fileChanges[selector] || []); |
| } |
| |
| /** |
| * All values from munge are added to base as |
| * base[file][selector][child] += munge[file][selector][child] |
| * |
| * @param {Munge} base |
| * @param {Munge} munge |
| * @return {Munge} A munge object containing values that exist in munge but not |
| * in base. |
| */ |
| exports.increment_munge = (base, munge) => { |
| return mungeItems(base, munge, exports.deep_add); |
| }; |
| |
| /** |
| * Update the base munge object as |
| * base[file][selector][child] -= munge[file][selector][child] |
| * |
| * @param {Munge} base |
| * @param {Munge} munge |
| * @return {Munge} nodes that reached zero value are removed from base and added |
| * to the returned munge object |
| */ |
| exports.decrement_munge = (base, munge) => { |
| return mungeItems(base, munge, exports.deep_remove); |
| }; |
| |
| /** |
| * For every key [file, selector, element] in munge run mungeOperation on base. |
| * |
| * @param {Munge} base |
| * @param {Munge} munge |
| * @param {typeof exports.deep_add} mungeOperation - TODO how can I constrain |
| * that to an enum of functions |
| * @return {Munge} - the union of all changes for which mungeOperation returned |
| * true |
| */ |
| function mungeItems (base, { files }, mungeOperation) { |
| const diff = { files: {} }; |
| |
| for (const file in files) { |
| for (const selector in files[file].parents) { |
| for (const element of files[file].parents[selector]) { |
| // if node not in base, add it to diff and base |
| // else increment it's value in base without adding to diff |
| |
| const hasChanges = mungeOperation(base, [file, selector, element]); |
| if (hasChanges) exports.deep_add(diff, [file, selector, element]); |
| } |
| } |
| } |
| |
| return diff; |
| } |
| |
| /** |
| * Clones given munge |
| * |
| * @param {Munge} munge |
| * @return {Munge} clone of munge |
| */ |
| exports.clone_munge = munge => exports.increment_munge({ files: {} }, munge); |