Generalize library loader

Until recently, the only side-loaded resource was the HLJS library for
computing syntax highlighting. For this task, the gr-syntax-lib-loader
provided an interface to load the library whether or not PG is being
served from a CDN.

With this change, the component is refactored to allow loading resources
other than the syntax library. A method is added for loading the
"dark-theme" document independently of whether a CDN is configured.

Also, some documentation comments are added to the existing methods.

Change-Id: I9891539cd4cf76ac0fe430ff3988e3a9dfbb0ca3
diff --git a/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer.html b/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer.html
index cd9f9dc..017cd5d 100644
--- a/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer.html
+++ b/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer.html
@@ -15,11 +15,11 @@
 limitations under the License.
 -->
 <link rel="import" href="../../../bower_components/polymer/polymer.html">
-<link rel="import" href="../gr-syntax-lib-loader/gr-syntax-lib-loader.html">
+<link rel="import" href="../../shared/gr-lib-loader/gr-lib-loader.html">
 
 <dom-module id="gr-syntax-layer">
   <template>
-    <gr-syntax-lib-loader id="libLoader"></gr-syntax-lib-loader>
+    <gr-lib-loader id="libLoader"></gr-lib-loader>
   </template>
   <script src="../gr-diff/gr-diff-line.js"></script>
   <script src="../gr-diff-highlight/gr-annotation.js"></script>
diff --git a/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer.js b/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer.js
index f8db343..15a8a0a 100644
--- a/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer.js
+++ b/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer.js
@@ -442,7 +442,7 @@
     },
 
     _loadHLJS() {
-      return this.$.libLoader.get().then(hljs => {
+      return this.$.libLoader.getHLJS().then(hljs => {
         this._hljs = hljs;
       });
     },
diff --git a/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer_test.html b/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer_test.html
index 74fc3bf..f2458fc 100644
--- a/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer_test.html
@@ -185,7 +185,7 @@
 
       const mockHLJS = getMockHLJS();
       const highlightSpy = sinon.spy(mockHLJS, 'highlight');
-      sandbox.stub(element.$.libLoader, 'get',
+      sandbox.stub(element.$.libLoader, 'getHLJS',
           () => { return Promise.resolve(mockHLJS); });
       const processNextSpy = sandbox.spy(element, '_processNextLine');
       const processPromise = element.process();
diff --git a/polygerrit-ui/app/elements/diff/gr-syntax-lib-loader/gr-syntax-lib-loader.js b/polygerrit-ui/app/elements/diff/gr-syntax-lib-loader/gr-syntax-lib-loader.js
deleted file mode 100644
index 6ec7ab2..0000000
--- a/polygerrit-ui/app/elements/diff/gr-syntax-lib-loader/gr-syntax-lib-loader.js
+++ /dev/null
@@ -1,113 +0,0 @@
-/**
- * @license
- * Copyright (C) 2016 The Android Open Source Project
- *
- * 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.
- */
-(function() {
-  'use strict';
-
-  const HLJS_PATH = 'bower_components/highlightjs/highlight.min.js';
-  const LIB_ROOT_PATTERN = /(.+\/)elements\/gr-app\.html/;
-
-  Polymer({
-    is: 'gr-syntax-lib-loader',
-
-    properties: {
-      _state: {
-        type: Object,
-
-        // NOTE: intended singleton.
-        value: {
-          configured: false,
-          loading: false,
-          callbacks: [],
-        },
-      },
-    },
-
-    get() {
-      return new Promise((resolve, reject) => {
-        // If the lib is totally loaded, resolve immediately.
-        if (this._getHighlightLib()) {
-          resolve(this._getHighlightLib());
-          return;
-        }
-
-        // If the library is not currently being loaded, then start loading it.
-        if (!this._state.loading) {
-          this._state.loading = true;
-          this._loadHLJS().then(this._onLibLoaded.bind(this)).catch(reject);
-        }
-
-        this._state.callbacks.push(resolve);
-      });
-    },
-
-    _onLibLoaded() {
-      const lib = this._getHighlightLib();
-      this._state.loading = false;
-      for (const cb of this._state.callbacks) {
-        cb(lib);
-      }
-      this._state.callbacks = [];
-    },
-
-    _getHighlightLib() {
-      const lib = window.hljs;
-      if (lib && !this._state.configured) {
-        this._state.configured = true;
-
-        lib.configure({classPrefix: 'gr-diff gr-syntax gr-syntax-'});
-      }
-      return lib;
-    },
-
-    _getLibRoot() {
-      if (this._cachedLibRoot) { return this._cachedLibRoot; }
-
-      const appLink = document.head
-        .querySelector('link[rel=import][href$="gr-app.html"]');
-
-      if (!appLink) { return null; }
-
-      return this._cachedLibRoot = appLink
-          .href
-          .match(LIB_ROOT_PATTERN)[1];
-    },
-    _cachedLibRoot: null,
-
-    _loadHLJS() {
-      return new Promise((resolve, reject) => {
-        const script = document.createElement('script');
-        const src = this._getHLJSUrl();
-
-        if (!src) {
-          reject(new Error('Unable to load blank HLJS url.'));
-          return;
-        }
-
-        script.src = src;
-        script.onload = resolve;
-        script.onerror = reject;
-        Polymer.dom(document.head).appendChild(script);
-      });
-    },
-
-    _getHLJSUrl() {
-      const root = this._getLibRoot();
-      if (!root) { return null; }
-      return root + HLJS_PATH;
-    },
-  });
-})();
diff --git a/polygerrit-ui/app/elements/gr-app.html b/polygerrit-ui/app/elements/gr-app.html
index de62646..7661d8e 100644
--- a/polygerrit-ui/app/elements/gr-app.html
+++ b/polygerrit-ui/app/elements/gr-app.html
@@ -56,6 +56,7 @@
 <link rel="import" href="./settings/gr-registration-dialog/gr-registration-dialog.html">
 <link rel="import" href="./settings/gr-settings-view/gr-settings-view.html">
 <link rel="import" href="./shared/gr-fixed-panel/gr-fixed-panel.html">
+<link rel="import" href="./shared/gr-lib-loader/gr-lib-loader.html">
 <link rel="import" href="./shared/gr-rest-api-interface/gr-rest-api-interface.html">
 
 <script src="../scripts/util.js"></script>
@@ -229,6 +230,7 @@
     <gr-plugin-host id="plugins"
         config="[[_serverConfig]]">
     </gr-plugin-host>
+    <gr-lib-loader id="libLoader"></gr-lib-loader>
     <gr-external-style id="externalStyle" name="app-theme"></gr-external-style>
   </template>
   <script src="gr-app.js" crossorigin="anonymous"></script>
diff --git a/polygerrit-ui/app/elements/gr-app.js b/polygerrit-ui/app/elements/gr-app.js
index 921415f..af9c27a 100644
--- a/polygerrit-ui/app/elements/gr-app.js
+++ b/polygerrit-ui/app/elements/gr-app.js
@@ -128,7 +128,7 @@
       });
 
       if (window.localStorage.getItem('dark-theme')) {
-        this.importHref('../styles/themes/dark-theme.html');
+        this.$.libLoader.loadDarkTheme();
       }
 
       // Note: this is evaluated here to ensure that it only happens after the
diff --git a/polygerrit-ui/app/elements/diff/gr-syntax-lib-loader/gr-syntax-lib-loader.html b/polygerrit-ui/app/elements/shared/gr-lib-loader/gr-lib-loader.html
similarity index 88%
rename from polygerrit-ui/app/elements/diff/gr-syntax-lib-loader/gr-syntax-lib-loader.html
rename to polygerrit-ui/app/elements/shared/gr-lib-loader/gr-lib-loader.html
index f5b71be..f70aff4 100644
--- a/polygerrit-ui/app/elements/diff/gr-syntax-lib-loader/gr-syntax-lib-loader.html
+++ b/polygerrit-ui/app/elements/shared/gr-lib-loader/gr-lib-loader.html
@@ -16,6 +16,6 @@
 -->
 <link rel="import" href="../../../bower_components/polymer/polymer.html">
 
-<dom-module id="gr-syntax-lib-loader">
-  <script src="gr-syntax-lib-loader.js"></script>
+<dom-module id="gr-lib-loader">
+  <script src="gr-lib-loader.js"></script>
 </dom-module>
diff --git a/polygerrit-ui/app/elements/shared/gr-lib-loader/gr-lib-loader.js b/polygerrit-ui/app/elements/shared/gr-lib-loader/gr-lib-loader.js
new file mode 100644
index 0000000..fc26b51
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-lib-loader/gr-lib-loader.js
@@ -0,0 +1,152 @@
+/**
+ * @license
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * 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.
+ */
+(function() {
+  'use strict';
+
+  const HLJS_PATH = 'bower_components/highlightjs/highlight.min.js';
+  const DARK_THEME_PATH = 'styles/themes/dark-theme.html';
+  const LIB_ROOT_PATTERN = /(.+\/)elements\/gr-app\.html/;
+
+  Polymer({
+    is: 'gr-lib-loader',
+
+    properties: {
+      _hljsState: {
+        type: Object,
+
+        // NOTE: intended singleton.
+        value: {
+          configured: false,
+          loading: false,
+          callbacks: [],
+        },
+      },
+    },
+
+    /**
+     * Get the HLJS library. Returns a promise that resolves with a reference to
+     * the library after it's been loaded. The promise resolves immediately if
+     * it's already been loaded.
+     * @return {!Promise<Object>}
+     */
+    getHLJS() {
+      return new Promise((resolve, reject) => {
+        // If the lib is totally loaded, resolve immediately.
+        if (this._getHighlightLib()) {
+          resolve(this._getHighlightLib());
+          return;
+        }
+
+        // If the library is not currently being loaded, then start loading it.
+        if (!this._hljsState.loading) {
+          this._hljsState.loading = true;
+          this._loadScript(this._getHLJSUrl())
+              .then(this._onHLJSLibLoaded.bind(this)).catch(reject);
+        }
+
+        this._hljsState.callbacks.push(resolve);
+      });
+    },
+
+    /**
+     * Load and apply the dark theme document.
+     */
+    loadDarkTheme() {
+      this.importHref(this._getLibRoot() + DARK_THEME_PATH);
+    },
+
+    /**
+     * Execute callbacks awaiting the HLJS lib load.
+     */
+    _onHLJSLibLoaded() {
+      const lib = this._getHighlightLib();
+      this._hljsState.loading = false;
+      for (const cb of this._hljsState.callbacks) {
+        cb(lib);
+      }
+      this._hljsState.callbacks = [];
+    },
+
+    /**
+     * Get the HLJS library, assuming it has been loaded. Configure the library
+     * if it hasn't already been configured.
+     * @return {!Object}
+     */
+    _getHighlightLib() {
+      const lib = window.hljs;
+      if (lib && !this._hljsState.configured) {
+        this._hljsState.configured = true;
+
+        lib.configure({classPrefix: 'gr-diff gr-syntax gr-syntax-'});
+      }
+      return lib;
+    },
+
+    /**
+     * Get the resource path used to load the application. If the application
+     * was loaded through a CDN, then this will be the path to CDN resources.
+     * @return {string}
+     */
+    _getLibRoot() {
+      if (this._cachedLibRoot) { return this._cachedLibRoot; }
+
+      const appLink = document.head
+        .querySelector('link[rel=import][href$="gr-app.html"]');
+
+      if (!appLink) { throw new Error('Could not find application link'); }
+
+      this._cachedLibRoot = appLink
+          .href
+          .match(LIB_ROOT_PATTERN)[1];
+
+      if (!this._cachedLibRoot) {
+        throw new Error('Could not extract lib root');
+      }
+
+      return this._cachedLibRoot;
+    },
+    _cachedLibRoot: null,
+
+    /**
+     * Load and execute a JS file from the lib root.
+     * @param {string} src The path to the JS file without the lib root.
+     * @return {Promise} a promise that resolves when the script's onload
+     *     executes.
+     */
+    _loadScript(src) {
+      return new Promise((resolve, reject) => {
+        const script = document.createElement('script');
+
+        if (!src) {
+          reject(new Error('Unable to load blank script url.'));
+          return;
+        }
+
+        script.src = src;
+        script.onload = resolve;
+        script.onerror = reject;
+        Polymer.dom(document.head).appendChild(script);
+      });
+    },
+
+    _getHLJSUrl() {
+      const root = this._getLibRoot();
+      if (!root) { return null; }
+      return root + HLJS_PATH;
+    },
+  });
+})();
diff --git a/polygerrit-ui/app/elements/diff/gr-syntax-lib-loader/gr-syntax-lib-loader_test.html b/polygerrit-ui/app/elements/shared/gr-lib-loader/gr-lib-loader_test.html
similarity index 77%
rename from polygerrit-ui/app/elements/diff/gr-syntax-lib-loader/gr-syntax-lib-loader_test.html
rename to polygerrit-ui/app/elements/shared/gr-lib-loader/gr-lib-loader_test.html
index a260a97..cf9a41c 100644
--- a/polygerrit-ui/app/elements/diff/gr-syntax-lib-loader/gr-syntax-lib-loader_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-lib-loader/gr-lib-loader_test.html
@@ -17,64 +17,67 @@
 -->
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
-<title>gr-syntax-lib-loader</title>
+<title>gr-lib-loader</title>
 
 <script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
 <script src="../../../bower_components/web-component-tester/browser.js"></script>
 <link rel="import" href="../../../test/common-test-setup.html"/>
-<link rel="import" href="gr-syntax-lib-loader.html">
+<link rel="import" href="gr-lib-loader.html">
 
 <script>void(0);</script>
 
 <test-fixture id="basic">
   <template>
-    <gr-syntax-lib-loader></gr-syntax-lib-loader>
+    <gr-lib-loader></gr-lib-loader>
   </template>
 </test-fixture>
 
 <script>
-  suite('gr-syntax-lib-loader tests', () => {
+  suite('gr-lib-loader tests', () => {
+    let sandbox;
     let element;
     let resolveLoad;
     let loadStub;
 
     setup(() => {
+      sandbox = sinon.sandbox.create();
       element = fixture('basic');
 
-      loadStub = sinon.stub(element, '_loadHLJS', () =>
+      loadStub = sandbox.stub(element, '_loadScript', () =>
         new Promise(resolve => resolveLoad = resolve)
       );
 
       // Assert preconditions:
-      assert.isFalse(element._state.loading);
+      assert.isFalse(element._hljsState.loading);
     });
 
     teardown(() => {
       if (window.hljs) {
         delete window.hljs;
       }
-      loadStub.restore();
+      sandbox.restore();
 
       // Because the element state is a singleton, clean it up.
-      element._state.configured = false;
-      element._state.loading = false;
-      element._state.callbacks = [];
+      element._hljsState.configured = false;
+      element._hljsState.loading = false;
+      element._hljsState.callbacks = [];
     });
 
     test('only load once', done => {
+      sandbox.stub(element, '_getHLJSUrl').returns('');
       const firstCallHandler = sinon.stub();
-      element.get().then(firstCallHandler);
+      element.getHLJS().then(firstCallHandler);
 
       // It should now be in the loading state.
       assert.isTrue(loadStub.called);
-      assert.isTrue(element._state.loading);
+      assert.isTrue(element._hljsState.loading);
       assert.isFalse(firstCallHandler.called);
 
       const secondCallHandler = sinon.stub();
-      element.get().then(secondCallHandler);
+      element.getHLJS().then(secondCallHandler);
 
       // No change in state.
-      assert.isTrue(element._state.loading);
+      assert.isTrue(element._hljsState.loading);
       assert.isFalse(firstCallHandler.called);
       assert.isFalse(secondCallHandler.called);
 
@@ -82,7 +85,7 @@
       resolveLoad();
       flush(() => {
         // The state should be loaded and both handlers called.
-        assert.isFalse(element._state.loading);
+        assert.isFalse(element._hljsState.loading);
         assert.isTrue(firstCallHandler.called);
         assert.isTrue(secondCallHandler.called);
         done();
@@ -105,7 +108,7 @@
 
       test('returns hljs', done => {
         const firstCallHandler = sinon.stub();
-        element.get().then(firstCallHandler);
+        element.getHLJS().then(firstCallHandler);
         flush(() => {
           assert.isTrue(firstCallHandler.called);
           assert.isTrue(firstCallHandler.calledWith(hljsStub));
@@ -114,7 +117,7 @@
       });
 
       test('configures hljs', done => {
-        element.get().then(() => {
+        element.getHLJS().then(() => {
           assert.isTrue(window.hljs.configure.calledOnce);
           done();
         });
@@ -123,15 +126,10 @@
 
     suite('_getHLJSUrl', () => {
       suite('checking _getLibRoot', () => {
-        let libRootStub;
         let root;
 
         setup(() => {
-          libRootStub = sinon.stub(element, '_getLibRoot', () => root);
-        });
-
-        teardown(() => {
-          libRootStub.restore();
+          sandbox.stub(element, '_getLibRoot', () => root);
         });
 
         test('with no root', () => {
diff --git a/polygerrit-ui/app/test/index.html b/polygerrit-ui/app/test/index.html
index 6a562fc..5a5dbcd 100644
--- a/polygerrit-ui/app/test/index.html
+++ b/polygerrit-ui/app/test/index.html
@@ -112,7 +112,6 @@
     'diff/gr-ranged-comment-layer/gr-ranged-comment-layer_test.html',
     'diff/gr-selection-action-box/gr-selection-action-box_test.html',
     'diff/gr-syntax-layer/gr-syntax-layer_test.html',
-    'diff/gr-syntax-lib-loader/gr-syntax-lib-loader_test.html',
     'edit/gr-default-editor/gr-default-editor_test.html',
     'edit/gr-edit-controls/gr-edit-controls_test.html',
     'edit/gr-edit-file-controls/gr-edit-file-controls_test.html',
@@ -165,6 +164,7 @@
     'shared/gr-js-api-interface/gr-plugin-endpoints_test.html',
     'shared/gr-js-api-interface/gr-plugin-rest-api_test.html',
     'shared/gr-fixed-panel/gr-fixed-panel_test.html',
+    'shared/gr-lib-loader/gr-lib-loader_test.html',
     'shared/gr-limited-text/gr-limited-text_test.html',
     'shared/gr-linked-chip/gr-linked-chip_test.html',
     'shared/gr-linked-text/gr-linked-text_test.html',