feat: add stylesheet script type

This script type will help in adding stylesheets dynamically based on
whether an option is enabled or not.

Change-Id: I946dadb2674bb0f98112adba3ac204c1e04f765e
diff --git a/src/common/StylesheetManager.ts b/src/common/StylesheetManager.ts
new file mode 100644
index 0000000..6867dd4
--- /dev/null
+++ b/src/common/StylesheetManager.ts
@@ -0,0 +1,33 @@
+import { StylesheetAttributes, injectStylesheet } from './contentScriptsUtils';
+
+export default class StylesheetManager {
+  private injectedElement: HTMLElement;
+
+  constructor(
+    /**
+     * Relative path to the stylesheet from the extension root.
+     */
+    public stylesheet: string,
+
+    /**
+     * Attributes to include in the injected <link> element.
+     */
+    public attributes: StylesheetAttributes = {},
+  ) {}
+
+  isInjected() {
+    return this.injectedElement !== undefined;
+  }
+
+  inject() {
+    this.injectedElement = injectStylesheet(
+      chrome.runtime.getURL(this.stylesheet),
+      this.attributes,
+    );
+  }
+
+  remove() {
+    this.injectedElement.remove();
+    this.injectedElement = undefined;
+  }
+}
diff --git a/src/common/architecture/scripts/stylesheet/StylesheetScript.ts b/src/common/architecture/scripts/stylesheet/StylesheetScript.ts
new file mode 100644
index 0000000..991afa5
--- /dev/null
+++ b/src/common/architecture/scripts/stylesheet/StylesheetScript.ts
@@ -0,0 +1,62 @@
+import StylesheetManager from '../../../StylesheetManager';
+import { StylesheetAttributes } from '../../../contentScriptsUtils';
+import OptionsProvider from '../../../options/OptionsProvider';
+import DependenciesProviderSingleton, {
+  OptionsProviderDependency,
+} from '../../dependenciesProvider/DependenciesProvider';
+import Script, { ScriptEnvironment, ScriptRunPhase } from '../Script';
+
+/**
+ * Script which injects a stylesheet depending on a set condition. It
+ * dynamically reevaluates the condition when the options configuration changes.
+ */
+export default abstract class StylesheetScript extends Script {
+  readonly environment = ScriptEnvironment.ContentScript;
+  readonly runPhase = ScriptRunPhase.Start;
+
+  /**
+   * Relative path to the stylesheet from the extension root.
+   */
+  abstract readonly stylesheet: string;
+  /**
+   * Attributes to include in the injected <link> element.
+   */
+  readonly attributes: StylesheetAttributes = {};
+
+  protected optionsProvider: OptionsProvider;
+  private stylesheetManager: StylesheetManager;
+
+  constructor() {
+    super();
+    const dependenciesProvider = DependenciesProviderSingleton.getInstance();
+    this.optionsProvider = dependenciesProvider.getDependency(
+      OptionsProviderDependency,
+    );
+  }
+
+  /**
+   * Condition which decides whether the stylesheet should be injected or not.
+   *
+   * @returns {boolean} Whether the stylesheet should be injected.
+   */
+  abstract shouldBeInjected(): Promise<boolean>;
+
+  execute() {
+    this.stylesheetManager = new StylesheetManager(
+      this.stylesheet,
+      this.attributes,
+    );
+    this.optionsProvider.addListener(this.evaluateInjection.bind(this));
+    this.evaluateInjection();
+  }
+
+  async evaluateInjection() {
+    const shouldBeInjected = await this.shouldBeInjected();
+    if (!this.stylesheetManager.isInjected() && shouldBeInjected) {
+      this.stylesheetManager.inject();
+    }
+    if (this.stylesheetManager.isInjected() && !shouldBeInjected) {
+      this.stylesheetManager.remove();
+    }
+  }
+}