blob: 67adfb67b30a88f93ad9cd7845acd9d2a9201132 [file] [log] [blame]
/*
*
* 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.
*
*/
/*
* This module deals with shared configuration / dependency "stuff". That is:
* - XML configuration files such as config.xml, AndroidManifest.xml or WMAppManifest.xml.
* - plist files in iOS
* - pbxproj files in iOS
* Essentially, any type of shared resources that we need to handle with awareness
* of how potentially multiple plugins depend on a single shared resource, should be
* handled in this module.
*
* The implementation uses an object as a hash table, with "leaves" of the table tracking
* reference counts.
*/
/* jshint node:true, sub:true, unused:true, indent:4 */
var fs = require('fs'),
path = require('path'),
glob = require('glob'),
plist = require('plist-with-patches'),
bplist = require('bplist-parser'),
xcode = require('xcode'),
et = require('elementtree'),
_ = require('underscore'),
xml_helpers = require('./../util/xml-helpers'),
platforms = require('./../platforms'),
events = require('./../events'),
plist_helpers = require('./../util/plist-helpers');
// These frameworks are required by cordova-ios by default. We should never add/remove them.
var keep_these_frameworks = [
'MobileCoreServices.framework',
'CoreGraphics.framework',
'CoreLocation.framework',
'AssetsLibrary.framework'
];
exports.PlatformMunger = PlatformMunger;
/******************************************************************************
Adapters to keep the current refactoring effort to within this file
******************************************************************************/
exports.add_plugin_changes = function(platform, project_dir, plugins_dir, plugin_id, plugin_vars, is_top_level, should_increment, cache) {
var munger = new PlatformMunger(platform, project_dir, plugins_dir);
munger.add_plugin_changes(plugin_id, plugin_vars, is_top_level, should_increment, cache);
munger.save_all();
};
exports.remove_plugin_changes = function(platform, project_dir, plugins_dir, plugin_name, plugin_id, is_top_level, should_decrement) {
// TODO: should_decrement parameter is never used, remove it here and wherever called
var munger = new PlatformMunger(platform, project_dir, plugins_dir);
munger.remove_plugin_changes(plugin_name, plugin_id, is_top_level);
munger.save_all();
};
exports.process = function(plugins_dir, project_dir, platform) {
var munger = new PlatformMunger(platform, project_dir, plugins_dir);
munger.process();
munger.save_all();
};
exports.get_munge_change = function(munge, keys) {
return deep_find.apply(null, arguments);
}
/******************************************************************************/
exports.add_installed_plugin_to_prepare_queue = add_installed_plugin_to_prepare_queue;
function add_installed_plugin_to_prepare_queue(plugins_dir, plugin, platform, vars, is_top_level) {
checkPlatform(platform);
var config = exports.get_platform_json(plugins_dir, platform);
config.prepare_queue.installed.push({'plugin':plugin, 'vars':vars, 'topLevel':is_top_level});
exports.save_platform_json(config, plugins_dir, platform);
}
exports.add_uninstalled_plugin_to_prepare_queue = add_uninstalled_plugin_to_prepare_queue;
function add_uninstalled_plugin_to_prepare_queue(plugins_dir, plugin, platform, is_top_level) {
checkPlatform(platform);
var plugin_xml = xml_helpers.parseElementtreeSync(path.join(plugins_dir, plugin, 'plugin.xml'));
var config = exports.get_platform_json(plugins_dir, platform);
config.prepare_queue.uninstalled.push({'plugin':plugin, 'id':plugin_xml.getroot().attrib['id'], 'topLevel':is_top_level});
exports.save_platform_json(config, plugins_dir, platform);
}
/******************************************************************************
* PlatformMunger class
*
* Can deal with config file of a single project.
* Parsed config files are cached in a ConfigKeeper object.
******************************************************************************/
function PlatformMunger(platform, project_dir, plugins_dir) {
checkPlatform(platform);
this.platform = platform;
this.project_dir = project_dir;
this.plugins_dir = plugins_dir;
this.platform_handler = platforms[platform];
this.config_keeper = new ConfigKeeper();
}
// Write out all unsaved files.
PlatformMunger.prototype.save_all = PlatformMunger_save_all;
function PlatformMunger_save_all() {
this.config_keeper.save_all();
}
// Apply a munge object to a single config file.
// The remove parameter tells whether to add the change or remove it.
PlatformMunger.prototype.apply_file_munge = PlatformMunger_apply_file_munge;
function PlatformMunger_apply_file_munge(file, munge, remove) {
var self = this;
var xml_child;
if ( file === 'framework' && self.platform === 'ios' ) {
// ios pbxproj file
var pbxproj = self.config_keeper.get(self.project_dir, self.platform, 'framework');
for (var src in munge.parents) {
for (xml_child in munge.parents[src]) {
var xml = munge.parents[src][xml_child].xml;
// Only add the framework if it's not a cordova-ios core framework
if (keep_these_frameworks.indexOf(src) == -1) {
// xml_child in this case is whether the framework should use weak or not
if (remove) {
pbxproj.data.removeFramework(src);
} else {
pbxproj.data.addFramework(src, {weak: (xml === 'true')});
}
pbxproj.is_changed = true;
}
}
}
} else {
// all other types of files
for (var selector in munge.parents) {
for (xml_child in munge.parents[selector]) {
// this xml child is new, graft it (only if config file exists)
var config_file = self.config_keeper.get(self.project_dir, self.platform, file);
if (config_file.exists) {
if (remove) config_file.prune_child(selector, munge.parents[selector][xml_child]);
else config_file.graft_child(selector, munge.parents[selector][xml_child]);
}
}
}
}
}
PlatformMunger.prototype.remove_plugin_changes = remove_plugin_changes;
function remove_plugin_changes(plugin_name, plugin_id, is_top_level) {
var self = this;
var platform_config = exports.get_platform_json(self.plugins_dir, self.platform);
var plugin_dir = path.join(self.plugins_dir, plugin_name);
var plugin_vars = (is_top_level ? platform_config.installed_plugins[plugin_id] : platform_config.dependent_plugins[plugin_id]);
// get config munge, aka how did this plugin change various config files
var config_munge = self.generate_plugin_config_munge(plugin_dir, plugin_vars);
// global munge looks at all plugins' changes to config files
var global_munge = platform_config.config_munge;
var munge = decrement_munge(global_munge, config_munge);
for (var file in munge.files) {
if (file == 'plugins-plist' && self.platform == 'ios') {
// TODO: remove this check and <plugins-plist> sections in spec/plugins/../plugin.xml files.
events.emit(
'warn',
'WARNING: Plugin "' + plugin_id + '" uses <plugins-plist> element(s), ' +
'which are no longer supported. Support has been removed as of Cordova 3.4.'
);
continue;
}
self.apply_file_munge(file, munge.files[file], /* remove = */ true);
}
// Remove from installed_plugins
if (is_top_level) {
delete platform_config.installed_plugins[plugin_id];
} else {
delete platform_config.dependent_plugins[plugin_id];
}
// save
exports.save_platform_json(platform_config, self.plugins_dir, self.platform);
}
PlatformMunger.prototype.add_plugin_changes = add_plugin_changes;
function add_plugin_changes(plugin_id, plugin_vars, is_top_level, should_increment) {
var self = this;
var platform_config = exports.get_platform_json(self.plugins_dir, self.platform);
var plugin_dir = path.join(self.plugins_dir, plugin_id);
var plugin_config = self.config_keeper.get(plugin_dir, '', 'plugin.xml');
plugin_id = plugin_config.data.getroot().attrib.id;
// get config munge, aka how should this plugin change various config files
var config_munge = self.generate_plugin_config_munge(plugin_dir, plugin_vars);
// global munge looks at all plugins' changes to config files
// TODO: The should_increment param is only used by cordova-cli and is going away soon.
// If should_increment is set to false, avoid modifying the global_munge (use clone)
// and apply the entire config_munge because it's already a proper subset of the global_munge.
var munge, global_munge;
if (should_increment) {
global_munge = platform_config.config_munge;
munge = increment_munge(global_munge, config_munge);
} else {
global_munge = clone_munge(platform_config.config_munge);
munge = config_munge;
}
for (var file in munge.files) {
// TODO: remove this warning some time after 3.4 is out.
if (file == 'plugins-plist' && self.platform == 'ios') {
events.emit(
'warn',
'WARNING: Plugin "' + plugin_id + '" uses <plugins-plist> element(s), ' +
'which are no longer supported. Support has been removed as of Cordova 3.4.'
);
continue;
}
self.apply_file_munge(file, munge.files[file]);
}
// Move to installed_plugins if it is a top-level plugin
if (is_top_level) {
platform_config.installed_plugins[plugin_id] = plugin_vars || {};
} else {
platform_config.dependent_plugins[plugin_id] = plugin_vars || {};
}
// save
exports.save_platform_json(platform_config, self.plugins_dir, self.platform);
}
// Load the global munge from platform json and apply all of it.
// Used by cordova prepare to re-generate some config file from platform
// defaults and the global munge.
PlatformMunger.prototype.reapply_global_munge = reapply_global_munge ;
function reapply_global_munge () {
var self = this;
var platform_config = exports.get_platform_json(self.plugins_dir, self.platform);
var global_munge = platform_config.config_munge;
for (var file in global_munge.files) {
// TODO: remove this warning some time after 3.4 is out.
if (file == 'plugins-plist' && self.platform == 'ios') {
events.emit(
'warn',
'WARNING: One of your plugins uses <plugins-plist> element(s), ' +
'which are no longer supported. Support has been removed as of Cordova 3.4.'
);
continue;
}
self.apply_file_munge(file, global_munge.files[file]);
}
}
// generate_plugin_config_munge
// Generate the munge object from plugin.xml + vars
PlatformMunger.prototype.generate_plugin_config_munge = generate_plugin_config_munge;
function generate_plugin_config_munge(plugin_dir, vars) {
var self = this;
vars = vars || {};
// Add PACKAGE_NAME variable into vars
if (!vars['PACKAGE_NAME']) {
vars['PACKAGE_NAME'] = self.platform_handler.package_name(self.project_dir);
}
var munge = { files: {} };
var plugin_config = self.config_keeper.get(plugin_dir, '', 'plugin.xml');
var plugin_xml = plugin_config.data;
var platformTag = plugin_xml.find('platform[@name="' + self.platform + '"]');
var changes = [];
// add platform-agnostic config changes
changes = changes.concat(plugin_xml.findall('config-file'));
if (platformTag) {
// add platform-specific config changes if they exist
changes = changes.concat(platformTag.findall('config-file'));
// note down pbxproj framework munges in special section of munge obj
// CB-5238 this is only for systems frameworks
var frameworks = platformTag.findall('framework');
frameworks.forEach(function(f) {
var custom = f.attrib['custom'];
if(!custom) {
var file = f.attrib['src'];
var weak = ('true' == f.attrib['weak']).toString();
deep_add(munge, 'framework', file, { xml: weak, count: 1 });
}
});
}
changes.forEach(function(change) {
var target = change.attrib['target'];
var parent = change.attrib['parent'];
var after = change.attrib['after'];
var xmls = change.getchildren();
xmls.forEach(function(xml) {
// 1. stringify each xml
var stringified = (new et.ElementTree(xml)).write({xml_declaration:false});
// interp vars
if (vars) {
Object.keys(vars).forEach(function(key) {
var regExp = new RegExp("\\$" + key, "g");
stringified = stringified.replace(regExp, vars[key]);
});
}
// 2. add into munge
deep_add(munge, target, parent, { xml: stringified, count: 1, after: after });
});
});
return munge;
}
// Go over the prepare queue an apply the config munges for each plugin
// that has been (un)installed.
PlatformMunger.prototype.process = PlatformMunger_process;
function PlatformMunger_process() {
var self = this;
var platform_config = exports.get_platform_json(self.plugins_dir, self.platform);
// Uninstallation first
platform_config.prepare_queue.uninstalled.forEach(function(u) {
self.remove_plugin_changes(u.plugin, u.id, u.topLevel);
});
// Now handle installation
platform_config.prepare_queue.installed.forEach(function(u) {
self.add_plugin_changes(u.plugin, u.vars, u.topLevel, true);
});
platform_config = exports.get_platform_json(self.plugins_dir, self.platform);
// Empty out installed/ uninstalled queues.
platform_config.prepare_queue.uninstalled = [];
platform_config.prepare_queue.installed = [];
// save platform json
exports.save_platform_json(platform_config, self.plugins_dir, self.platform);
}
/**** END of PlatformMunger ****/
/******************************************************************************
* ConfigKeeper class
*
* Used to load and store config files to avoid re-parsing and writing them out
* multiple times.
*
* The config files are referred to by a fake path constructed as
* project_dir/platform/file
* where file is the name used for the file in config munges.
******************************************************************************/
function ConfigKeeper() {
this._cached = {};
}
ConfigKeeper.prototype.get = ConfigKeeper_get;
function ConfigKeeper_get(project_dir, platform, file) {
var self = this;
//This fixes a bug with older plugins - when specifying config xml instead of res/xml/config.xml
//https://issues.apache.org/jira/browse/CB-6414
if(file == 'config.xml' && platform == 'android'){
file = 'res/xml/config.xml';
}
var fake_path = path.join(project_dir, platform, file);
if (self._cached[fake_path]) {
return self._cached[fake_path];
}
// File was not cached, need to load.
var config_file = new ConfigFile(project_dir, platform, file);
self._cached[fake_path] = config_file;
return config_file;
}
ConfigKeeper.prototype.save_all = ConfigKeeper_save_all;
function ConfigKeeper_save_all() {
var self = this;
Object.keys(self._cached).forEach(function (fake_path) {
var config_file = self._cached[fake_path];
if (config_file.is_changed) config_file.save();
});
}
/**** END of ConfigKeeper ****/
// TODO: move save/get_platform_json to be part of ConfigKeeper or ConfigFile
// For now they are used in many places in plugman and cordova-cli and can
// save the file bypassing the ConfigKeeper's cache.
exports.get_platform_json = get_platform_json;
function get_platform_json(plugins_dir, platform) {
checkPlatform(platform);
var filepath = path.join(plugins_dir, platform + '.json');
if (fs.existsSync(filepath)) {
return fix_munge(JSON.parse(fs.readFileSync(filepath, 'utf-8')));
} else {
var config = {
prepare_queue:{installed:[], uninstalled:[]},
config_munge:{},
installed_plugins:{},
dependent_plugins:{}
};
return config;
}
}
exports.save_platform_json = save_platform_json;
function save_platform_json(config, plugins_dir, platform) {
checkPlatform(platform);
var filepath = path.join(plugins_dir, platform + '.json');
fs.writeFileSync(filepath, JSON.stringify(config, null, 4), 'utf-8');
}
// convert a munge from the old format ([file][parent][xml] = count) to the current one
function fix_munge(platform_config) {
var munge = platform_config.config_munge;
if (!munge.files) {
var new_munge = { files: {} };
for (var file in munge) {
for (var selector in munge[file]) {
for (var xml_child in munge[file][selector]) {
var val = parseInt(munge[file][selector][xml_child]);
for (var i = 0; i < val; i++) {
deep_add(new_munge, [file, selector, { xml: xml_child, count: val }]);
}
}
}
}
platform_config.config_munge = new_munge;
}
return platform_config;
}
/**** END of ConfigKeeper ****/
/******************************************************************************
* ConfigFile class
*
* Can load and keep various types of config files. Provides some functionality
* specific to some file types such as grafting XML children. In most cases it
* should be instantiated by ConfigKeeper.
*
* For plugin.xml files use as:
* plugin_config = self.config_keeper.get(plugin_dir, '', 'plugin.xml');
*
* TODO: Consider moving it out to a separate file and maybe partially with
* overrides in platform handlers.
******************************************************************************/
function ConfigFile(project_dir, platform, file_tag) {
this.project_dir = project_dir;
this.platform = platform;
this.file_tag = file_tag;
this.is_changed = false;
this.load();
}
// ConfigFile.load()
ConfigFile.prototype.load = ConfigFile_load;
function ConfigFile_load() {
var self = this;
// config file may be in a place not exactly specified in the target
var filepath = self.filepath = resolveConfigFilePath(self.project_dir, self.platform, self.file_tag);
if ( !filepath || !fs.existsSync(filepath) ) {
self.exists = false;
return;
}
self.exists = true;
var ext = path.extname(filepath);
// Windows8 uses an appxmanifest, and wp8 will likely use
// the same in a future release
if (ext == '.xml' || ext == '.appxmanifest') {
self.type = 'xml';
self.data = xml_helpers.parseElementtreeSync(filepath);
} else if (ext == '.pbxproj') {
self.type = 'pbxproj';
self.data = xcode.project(filepath);
self.data.parseSync();
} else {
// plist file
self.type = 'plist';
// TODO: isBinaryPlist() reads the file and then parse re-reads it again.
// We always write out text plist, not binary.
// Do we still need to support binary plist?
// If yes, use plist.parseStringSync() and read the file once.
self.plist_module = (isBinaryPlist(filepath) ? bplist : plist);
self.data = self.plist_module.parseFileSync(filepath);
}
}
// ConfigFile.save()
ConfigFile.prototype.save = ConfigFile_save;
function ConfigFile_save() {
var self = this;
if (self.type === 'xml') {
fs.writeFileSync(self.filepath, self.data.write({indent: 4}), 'utf-8');
} else if (self.type === 'pbxproj') {
fs.writeFileSync(self.filepath, self.data.writeSync());
} else {
// plist
var regExp = new RegExp("<string>[ \t\r\n]+?</string>", "g");
fs.writeFileSync(self.filepath, plist.build(self.data).replace(regExp, "<string></string>"));
}
self.is_changed = false;
}
// ConfigFile.graft_child()
ConfigFile.prototype.graft_child = ConfigFile_graft_child;
function ConfigFile_graft_child(selector, xml_child) {
var self = this;
var filepath = self.filepath;
var result;
if (self.type === 'xml') {
var xml_to_graft = [et.XML(xml_child.xml)];
result = xml_helpers.graftXML(self.data, xml_to_graft, selector, xml_child.after);
if ( !result) {
throw new Error('grafting xml at selector "' + selector + '" from "' + filepath + '" during config install went bad :(');
}
} else {
// plist file
result = plist_helpers.graftPLIST(self.data, xml_child.xml, selector);
if ( !result ) {
throw new Error('grafting to plist "' + filepath + '" during config install went bad :(');
}
}
self.is_changed = true;
}
// ConfigFile.prune_child()
ConfigFile.prototype.prune_child = ConfigFile_prune_child;
function ConfigFile_prune_child(selector, xml_child) {
var self = this;
var filepath = self.filepath;
var result;
if (self.type === 'xml') {
var xml_to_graft = [et.XML(xml_child.xml)];
result = xml_helpers.pruneXML(self.data, xml_to_graft, selector);
} else {
// plist file
result = plist_helpers.prunePLIST(self.data, xml_child.xml, selector);
}
if ( !result) {
var err_msg = 'Pruning at selector "' + selector + '" from "' + filepath + '" went bad.';
throw new Error(err_msg);
}
self.is_changed = true;
}
/**** END of ConfigFile ****/
/******************************************************************************
* Utility functions
******************************************************************************/
// Check if we know such platform
function checkPlatform(platform) {
if (!(platform in platforms)) throw new Error('platform "' + platform + '" not recognized.');
}
// determine if a plist file is binary
function isBinaryPlist(filename) {
// I wish there was a synchronous way to read only the first 6 bytes of a
// file. This is wasteful :/
var buf = '' + fs.readFileSync(filename, 'utf8');
// binary plists start with a magic header, "bplist"
return buf.substring(0, 6) === 'bplist';
}
// Find out the real name of an iOS project
// TODO: glob is slow, need a better way or caching, or avoid using more than once.
function getIOSProjectname(project_dir) {
var matches = glob.sync(path.join(project_dir, '*.xcodeproj'));
var iospath;
if (matches.length === 1) {
iospath = path.basename(matches[0],'.xcodeproj');
} else {
var msg;
if (matches.length === 0) {
msg = 'Does not appear to be an xcode project, no xcode project file in ' + project_dir;
}
else {
msg = 'There are multiple *.xcodeproj dirs in ' + project_dir;
}
throw new Error(msg);
}
return iospath;
}
// Some config-file target attributes are not qualified with a full leading directory, or contain wildcards.
// Resolve to a real path in this function.
// TODO: getIOSProjectname is slow because of glob, try to avoid calling it several times per project.
function resolveConfigFilePath(project_dir, platform, file) {
var filepath = path.join(project_dir, file);
var matches;
// .pbxproj file
if (file === 'framework') {
var proj_name = getIOSProjectname(project_dir);
filepath = path.join(project_dir, proj_name + '.xcodeproj', 'project.pbxproj');
return filepath;
}
if (file.indexOf('*') > -1) {
// handle wildcards in targets using glob.
matches = glob.sync(path.join(project_dir, '**', file));
if (matches.length) filepath = matches[0];
return filepath;
}
// special-case config.xml target that is just "config.xml". This should be resolved to the real location of the file.
// TODO: move the logic that contains the locations of config.xml from cordova CLI into plugman.
if (file == 'config.xml') {
if (platform == 'ubuntu') {
filepath = path.join(project_dir, 'config.xml');
} else if (platform == 'ios') {
var iospath = getIOSProjectname(project_dir);
filepath = path.join(project_dir,iospath, 'config.xml');
} else if (platform == 'android') {
filepath = path.join(project_dir, 'res', 'xml', 'config.xml');
} else {
matches = glob.sync(path.join(project_dir, '**', 'config.xml'));
if (matches.length) filepath = matches[0];
}
return filepath;
}
// None of the special cases matched, returning project_dir/file.
return filepath;
}
/******************************************************************************
* Munge object manipulations functions
******************************************************************************/
// add the count of [key1][key2]...[keyN] to obj
// return true if it didn't exist before
function deep_add(obj, keys /* or key1, key2 .... */ ) {
if ( !Array.isArray(keys) ) {
keys = Array.prototype.slice.call(arguments, 1);
}
return process_munge(obj, true/*createParents*/, function (parentArray, k) {
var found = _.find(parentArray, function(element) {
return element.xml == k.xml;
});
if (found) {
found.after = found.after || k.after;
found.count += k.count;
} else {
parentArray.push(k);
}
return !found;
}, keys);
}
// decrement the count of [key1][key2]...[keyN] from obj and remove if it reaches 0
// return true if it was removed or not found
function deep_remove(obj, keys /* or key1, key2 .... */ ) {
if ( !Array.isArray(keys) ) {
keys = Array.prototype.slice.call(arguments, 1);
}
var result = process_munge(obj, false/*createParents*/, function (parentArray, k) {
var index = -1;
var found = _.find(parentArray, function (element) {
index++;
return element.xml == k.xml;
});
if (found) {
found.count -= k.count;
if (found.count > 0) {
return false;
}
else {
parentArray.splice(index, 1);
}
}
return undefined;
}, keys);
return typeof result === "undefined" ? true : result;
}
// search for [key1][key2]...[keyN]
// return the object or undefined if not found
function deep_find(obj, keys /* or key1, key2 .... */ ) {
if ( !Array.isArray(keys) ) {
keys = Array.prototype.slice.call(arguments, 1);
}
return process_munge(obj, false/*createParents?*/, function (parentArray, k) {
return _.find(parentArray, function (element) {
return element.xml == (k.xml || k);
});
}, keys);
}
// Execute func passing it the parent array and the xmlChild key.
// When createParents is true, add the file and parent items they are missing
// When createParents is false, stop and return undefined if the file and/or parent items are missing
function process_munge(obj, createParents, func, keys /* or key1, key2 .... */ ) {
if ( !Array.isArray(keys) ) {
keys = Array.prototype.slice.call(arguments, 1);
}
var k = keys[0];
if (keys.length == 1) {
return func(obj, k);
} else if (keys.length == 2) {
if (!obj.parents[k] && !createParents) {
return undefined;
}
obj.parents[k] = obj.parents[k] || [];
return process_munge(obj.parents[k], createParents, func, keys.slice(1));
} else if (keys.length == 3){
if (!obj.files[k] && !createParents) {
return undefined;
}
obj.files[k] = obj.files[k] || { parents: {} };
return process_munge(obj.files[k], createParents, func, keys.slice(1));
} else {
throw new Error("Invalid key format. Must contain at most 3 elements (file, parent, xmlChild).");
}
}
// All values from munge are added to base as
// base[file][selector][child] += base[file][selector][child]
// Returns a munge object containing values that exist in munge
// but not in base.
function increment_munge(base, munge) {
var diff = { files: {} };
for (var file in munge.files) {
for (var selector in munge.files[file].parents) {
for (var xml_child in munge.files[file].parents[selector]) {
var val = munge.files[file].parents[selector][xml_child];
// if node not in base, add it to diff and base
// else increment it's value in base without adding to diff
var newlyAdded = deep_add(base, [file, selector, val]);
if (newlyAdded) {
deep_add(diff, file, selector, val);
}
}
}
}
return diff;
}
// Update the base munge object as
// base[file][selector][child] -= base[file][selector][child]
// nodes that reached zero value are removed from base and added to the returned munge
// object.
function decrement_munge(base, munge) {
var zeroed = { files: {} };
for (var file in munge.files) {
for (var selector in munge.files[file].parents) {
for (var xml_child in munge.files[file].parents[selector]) {
var val = munge.files[file].parents[selector][xml_child];
// if node not in base, add it to diff and base
// else increment it's value in base without adding to diff
var removed = deep_remove(base, [file, selector, val]);
if (removed) {
deep_add(zeroed, file, selector, val);
}
}
}
}
return zeroed;
}
// For better readability where used
function clone_munge(munge) {
return increment_munge({}, munge);
}