Build options page programatically

Until now we manually created the options page layout, but we want to
add custom elements before each option (for the kill switch
functionality), and this is very cumbersome to do it manually. Thus,
this CL builds the options page layout programatically to be able to
easilly inject a custom component before each option.

Change-Id: Ib110679971fa70c9933be911c4750b7fafa1d40e
diff --git a/docs/developers/add_feature.md b/docs/developers/add_feature.md
index 12ec328..d8e93bb 100644
--- a/docs/developers/add_feature.md
+++ b/docs/developers/add_feature.md
@@ -14,14 +14,10 @@
     users have to explicitly enable the option after they receive the extension
     update. Otherwise, it might cause confusion, because users wouldn't know if
     the feature was added by the extension or Google.
-3. Now, modify the `//src/static/options/options.html` file by appending the
-following HTML code in the corresponding section:
-    ```
-    <div class="option"><input type="checkbox" id="{{codename}}"> <label for="{{codename}}" data-i18n="{{codename}}"></label> <span class="experimental-label" data-i18n="experimental_label"></span></div>
-    ```
-    where you should substitute `{{codename}}` with the codename you've chosen.
-The _experimental_ label is optional and should only be used with features which
-are unreliable (or could be at some point in the future) due to their nature.
+3. Now, modify the `//src/static/options/optionsPage.json5` file by adding an
+entry to the corresponding section. The _experimental_ property is optional and
+should only be used with features which are unreliable (or could be at some
+point in the future) due to their nature.
 4. Finally, add the option string at `//src/static/_locales/en/manifest.json`,
 by adding the string under the `options_{{codename}}` key.
 
@@ -65,9 +61,9 @@
     showing/saving it in the options page, or so it is handled outside of the
     options page.
 3. If you want to handle the option from the options page, follow these steps:
-    1. Modify the `//src/static/options/options.html` file to add the
-    appropriate code which implements the option (usually in the same `.option`
-    div as the feature switch).
+    1. Modify the `//src/static/options/optionsPage.json5` file to add the
+    appropriate code which implements the option under the `customHTML`
+    property.
         - Don't include text, only the HTML structure. If you add a `data-i18n`
         attribute to an HTML element, its contents will be replaced with the
         corresponding i18n string (for instance,
diff --git a/src/optionsCommon.js b/src/options/optionsCommon.js
similarity index 79%
rename from src/optionsCommon.js
rename to src/options/optionsCommon.js
index 27db635..c552da2 100644
--- a/src/optionsCommon.js
+++ b/src/options/optionsCommon.js
@@ -1,5 +1,6 @@
-import {getExtVersion, isFirefox, isReleaseVersion} from './common/extUtils.js';
-import {cleanUpOptions, optionsPrototype, specialOptions} from './common/optionsUtils.js';
+import {getExtVersion, isFirefox, isReleaseVersion} from '../common/extUtils.js';
+import {cleanUpOptions, optionsPrototype, specialOptions} from '../common/optionsUtils.js';
+import optionsPage from './optionsPage.json5';
 
 var savedSuccessfullyTimeout = null;
 
@@ -94,8 +95,6 @@
 }
 
 window.addEventListener('load', function() {
-  i18n();
-
   if (window.CONTEXT == 'options') {
     if (!isReleaseVersion()) {
       var experimentsLink = document.querySelector('.experiments-link');
@@ -105,6 +104,52 @@
       }));
     }
 
+    // Add options to page
+    let optionsContainer = document.getElementById('options-container');
+    for (let section of optionsPage.sections) {
+      if (section?.name) {
+        let sectionHeader = document.createElement('h4');
+        sectionHeader.setAttribute('data-i18n', section.name);
+        optionsContainer.append(sectionHeader);
+      }
+
+      if (section?.options) {
+        for (let option of section.options) {
+          if (option?.customHTML) {
+            optionsContainer.insertAdjacentHTML('beforeend', option.customHTML);
+            continue;
+          }
+
+          let optionEl = document.createElement('div');
+          optionEl.classList.add('option');
+
+          let checkbox = document.createElement('input');
+          checkbox.setAttribute('type', 'checkbox');
+          checkbox.id = option.codename;
+
+          let label = document.createElement('label');
+          label.setAttribute('for', checkbox.id);
+          label.setAttribute('data-i18n', option.codename);
+
+          optionEl.append(checkbox, ' ', label);
+
+          if (option?.experimental) {
+            let experimental = document.createElement('span');
+            experimental.classList.add('experimental-label');
+            experimental.setAttribute('data-i18n', 'experimental_label');
+
+            optionEl.append(experimental);
+          }
+
+          optionsContainer.append(optionEl);
+        }
+      }
+
+      if (section?.footerHTML) {
+        optionsContainer.insertAdjacentHTML('beforeend', section.footerHTML);
+      }
+    }
+
     var featuresLink = document.querySelector('.features-link');
     featuresLink.href = getDocURL('features.md');
 
@@ -113,6 +158,8 @@
     profileIndicatorLink.href = getDocURL('op_indicator.md');
   }
 
+  i18n();
+
   chrome.storage.sync.get(null, function(items) {
     items = cleanUpOptions(items, false);
 
diff --git a/src/options/optionsPage.json5 b/src/options/optionsPage.json5
new file mode 100644
index 0000000..694c7dc
--- /dev/null
+++ b/src/options/optionsPage.json5
@@ -0,0 +1,40 @@
+// This file defines the sections of the options page, in which section each
+// option appears in, whether the "experimental" label should appear next to the
+// option label, and whether each option should use custom HTML.
+{
+  sections: [
+    {
+      name: null,
+      options: [
+        {codename: 'list'},
+        {codename: 'thread'},
+        {codename: 'threadall'},
+      ],
+    },
+    {
+      name: 'enhancements',
+      options: [
+        {codename: 'fixedtoolbar'},
+        {codename: 'redirect'},
+        {codename: 'history'},
+        {codename: 'loaddrafts', experimental: true},
+        {codename: 'increasecontrast'},
+        {codename: 'stickysidebarheaders'},
+        {codename: 'ccdarktheme'},
+        {codename: 'ccforcehidedrawer'},
+        {codename: 'ccdragndropfix', customHTML: '<div id="dragndrop-wrapper" class="option" hidden><input type="checkbox" id="ccdragndropfix"> <label for="ccdragndropfix" data-i18n="ccdragndropfix"></label></div>'},
+        {codename: 'batchlock'},
+        {codename: 'enhancedannouncementsdot'},
+        {codename: 'repositionexpandthread', experimental: true},
+      ],
+    },
+    {
+      name: 'profileindicator_header',
+      options: [
+        {codename: 'profileindicator'},
+        {codename: 'profileindicatoralt'},
+      ],
+      footerHTML: '<div class="option"><a id="profileIndicatorMoreInfo" target="_blank" rel="noreferrer noopener" data-i18n="profileindicator_moreinfo"></a></div>',
+    },
+  ],
+}
diff --git a/src/static/options/options.html b/src/static/options/options.html
index 91a8205..8dde2a4 100644
--- a/src/static/options/options.html
+++ b/src/static/options/options.html
@@ -40,26 +40,7 @@
       </div>
       <form>
         <div class="kill-switch-text" id="kill-switch-warning" hidden data-i18n="killswitchwarning"></div>
-        <div class="option"><input type="checkbox" id="list"> <label for="list" data-i18n="list"></label></div>
-        <div class="option"><input type="checkbox" id="thread"> <label for="thread" data-i18n="thread"></label></div>
-        <div class="option"><input type="checkbox" id="threadall"> <label for="threadall" data-i18n="threadall"></label></div>
-        <h4 data-i18n="enhancements"></h4>
-        <div class="option"><input type="checkbox" id="fixedtoolbar"> <label for="fixedtoolbar" data-i18n="fixedtoolbar"></label></div>
-        <div class="option"><input type="checkbox" id="redirect"> <label for="redirect" data-i18n="redirect"></label></div>
-        <div class="option"><input type="checkbox" id="history"> <label for="history" data-i18n="history"></label></div>
-        <div class="option"><input type="checkbox" id="loaddrafts"> <label for="loaddrafts" data-i18n="loaddrafts"></label> <span class="experimental-label" data-i18n="experimental_label"></span></div>
-        <div class="option"><input type="checkbox" id="increasecontrast"> <label for="increasecontrast" data-i18n="increasecontrast"></label></div>
-        <div class="option"><input type="checkbox" id="stickysidebarheaders"> <label for="stickysidebarheaders" data-i18n="stickysidebarheaders"></label></div>
-        <div class="option"><input type="checkbox" id="ccdarktheme"> <label for="ccdarktheme" data-i18n="ccdarktheme"></label></div>
-        <div class="option"><input type="checkbox" id="ccforcehidedrawer"> <label for="ccforcehidedrawer" data-i18n="ccforcehidedrawer"></label></div>
-        <div id="dragndrop-wrapper" class="option" hidden><input type="checkbox" id="ccdragndropfix"> <label for="ccdragndropfix" data-i18n="ccdragndropfix"></label></div>
-        <div class="option"><input type="checkbox" id="batchlock"> <label for="batchlock" data-i18n="batchlock"></label> <span class="experimental-label" data-i18n="experimental_label"></span></div>
-        <div class="option"><input type="checkbox" id="enhancedannouncementsdot"> <label for="enhancedannouncementsdot" data-i18n="enhancedannouncementsdot"></label></div>
-        <div class="option"><input type="checkbox" id="repositionexpandthread"> <label for="repositionexpandthread" data-i18n="repositionexpandthread"></label> <span class="experimental-label" data-i18n="experimental_label"></span></div>
-        <h4 data-i18n="profileindicator_header"></h4>
-        <div class="option"><input type="checkbox" id="profileindicator"> <label for="profileindicator" data-i18n="profileindicator"></label> <span class="experimental-label" data-i18n="experimental_label"></span></div>
-        <div class="option"><input type="checkbox" id="profileindicatoralt"> <label for="profileindicatoralt" data-i18n="profileindicatoralt"></label> <span class="experimental-label" data-i18n="experimental_label"></span></div>
-        <div class="option"><a id="profileIndicatorMoreInfo" target="_blank" rel="noreferrer noopener" data-i18n="profileindicator_moreinfo"></a></div>
+        <div id="options-container"></div>
         <div class="actions"><button id="save" data-i18n="save"></button></div>
       </form>
       <div id="save-indicator"></div>
diff --git a/webpack.config.js b/webpack.config.js
index 7d71978..d407ba1 100644
--- a/webpack.config.js
+++ b/webpack.config.js
@@ -45,7 +45,7 @@
     xhrInterceptorInject: './src/injections/xhrInterceptor.js',
 
     // Options page
-    optionsCommon: './src/optionsCommon.js',
+    optionsCommon: './src/options/optionsCommon.js',
   };
 
   // Background script (or service worker for MV3)