Add support for experiments

This change refactors the options logic and adds support for
experiments: a new type of options which are not shown in the options
page (their usage will be similar to Chrome flags).

Experiments can be set from the
chrome-extension://{extension_id}/options/experiments.html page.

This code refactoring simplifies the options definition. Each option now
has a default value, and a context: the place where the option is set
(options, experiments, internal, deprecated).

Change-Id: I358ae07c832acae6b4536788c4dbe12a0e4730bf
diff --git a/src/common/common.js b/src/common/common.js
index e4af064..205cdc9 100644
--- a/src/common/common.js
+++ b/src/common/common.js
@@ -1,28 +1,108 @@
-const defaultOptions = {
-  'list': true,
-  'thread': true,
-  'threadall': false,
-  'fixedtoolbar': false,
-  'redirect': false,
-  'history': false,
-  'loaddrafts': false,
-  'batchduplicate': false,
-  'escalatethreads': false,
-  'movethreads': false,
-  'increasecontrast': false,
-  'stickysidebarheaders': false,
-  'profileindicator': false,
-  'profileindicatoralt': false,
-  'profileindicatoralt_months': 12,
-  'ccdarktheme': false,
-  'ccdarktheme_mode': 'switch',
-  'ccdarktheme_switch_enabled': true,
-  'ccforcehidedrawer': false,
-  'ccdragndropfix': false,
-  'batchlock': false,
-  'smei_sortdirection': false,
-  'enhancedannouncementsdot': false,
-  'repositionexpandthread': false,
+const optionsPrototype = {
+  // Available options:
+  'list': {
+    defaultValue: true,
+    context: 'options',
+  },
+  'thread': {
+    defaultValue: true,
+    context: 'options',
+  },
+  'threadall': {
+    defaultValue: false,
+    context: 'options',
+  },
+  'fixedtoolbar': {
+    defaultValue: false,
+    context: 'options',
+  },
+  'redirect': {
+    defaultValue: false,
+    context: 'options',
+  },
+  'history': {
+    defaultValue: false,
+    context: 'options',
+  },
+  'loaddrafts': {
+    defaultValue: false,
+    context: 'options',
+  },
+  'increasecontrast': {
+    defaultValue: false,
+    context: 'options',
+  },
+  'stickysidebarheaders': {
+    defaultValue: false,
+    context: 'options',
+  },
+  'profileindicator': {
+    defaultValue: false,
+    context: 'options',
+  },
+  'profileindicatoralt': {
+    defaultValue: false,
+    context: 'options',
+  },
+  'profileindicatoralt_months': {
+    defaultValue: 12,
+    context: 'options',
+  },
+  'ccdarktheme': {
+    defaultValue: false,
+    context: 'options',
+  },
+  'ccdarktheme_mode': {
+    defaultValue: 'switch',
+    context: 'options',
+  },
+  'ccforcehidedrawer': {
+    defaultValue: false,
+    context: 'options',
+  },
+  'ccdragndropfix': {
+    defaultValue: false,
+    context: 'options',
+  },
+  'batchlock': {
+    defaultValue: false,
+    context: 'options',
+  },
+  'smei_sortdirection': {
+    defaultValue: false,
+    context: 'options',
+  },
+  'enhancedannouncementsdot': {
+    defaultValue: false,
+    context: 'options',
+  },
+  'repositionexpandthread': {
+    defaultValue: false,
+    context: 'options',
+  },
+
+  // Experiments:
+
+
+  // Internal options:
+  'ccdarktheme_switch_enabled': {
+    defaultValue: true,
+    context: 'internal',
+  },
+
+  // Deprecated options:
+  'escalatethreads': {
+    defaultValue: false,
+    context: 'deprecated',
+  },
+  'movethreads': {
+    defaultValue: false,
+    context: 'deprecated',
+  },
+  'batchduplicate': {
+    defaultValue: false,
+    context: 'deprecated',
+  },
 };
 
 const specialOptions = [
@@ -32,34 +112,28 @@
   'ccdragndropfix',
 ];
 
-const deprecatedOptions = [
-  'escalatethreads',
-  'movethreads',
-  'batchduplicate',
-];
-
 function isEmpty(obj) {
   return Object.keys(obj).length === 0;
 }
 
-function cleanUpOptions(options) {
-  console.log('[cleanUpOptions] Previous options', options);
+// Adds missing options with their default value. If |dryRun| is set to false,
+// they are also saved to the sync storage area.
+function cleanUpOptions(options, dryRun = false) {
+  console.log('[cleanUpOptions] Previous options', JSON.stringify(options));
 
-  if (typeof options !== 'object' || options === null) {
-    options = defaultOptions;
-  } else {
-    var ok = true;
-    for (const [opt, value] of Object.entries(defaultOptions)) {
-      if (!(opt in options)) {
-        ok = false;
-        options[opt] = value;
-      }
+  if (typeof options !== 'object' || options === null) options = {};
+
+  var ok = true;
+  for (const [opt, optMeta] of Object.entries(optionsPrototype)) {
+    if (!(opt in options)) {
+      ok = false;
+      options[opt] = optMeta['defaultValue'];
     }
   }
 
-  console.log('[cleanUpOptions] New options', options);
+  console.log('[cleanUpOptions] New options', JSON.stringify(options));
 
-  if (!ok) {
+  if (!ok && !dryRun) {
     chrome.storage.sync.set(options);
   }