Fandom Developers Wiki
Advertisement

Note: After publishing, you may have to bypass your browser's cache to see the changes.

  • Firefox / Safari: Hold Shift while clicking Reload, or press either Ctrl-F5 or Ctrl-R (⌘-R on a Mac)
  • Google Chrome: Press Ctrl-Shift-R (⌘-Shift-R on a Mac)
  • Internet Explorer / Edge: Hold Ctrl while clicking Refresh, or press Ctrl-F5
  • Opera: Press Ctrl-F5.
/**
 * CodeQuickLinks/code.js
 * @file Adds modules containing quick links to personal and wiki code files
 * @author Eizen <dev.fandom.com/wiki/User_talk:Eizen>
 * @license CC-BY-SA 3.0
 * @external "mediawiki.util"
 * @external "I18n-js"
 */

/**
 * <pre>
 * <em>Table of contents</em>        <em>Summary</em>
 * - Pseudo-enums                    Storage for CodeQuickLinks utility consts
 * - Utility methods                 Helper methods for validation, etc.
 * - Assembly methods                Builder functions assembling on-page HTML
 * - Main methods                    Main functionality/app logic methods
 * - Setup methods                   Initialization methods for loading, setup
 * </pre>
 */

/* jshint -W030, undef: true, unused: true, eqnull: true, laxbreak: true */

;(function (module, window, $, mw) {
  "use strict";

  // Prevent double loads and respect prior double load check formatting
  if (
    !window || !$ || !mw || module.isLoaded || window.isCodeQuickLinksLoaded ||
    $("body").hasClass("mainpage")
  ) {
    return;
  }
  module.isLoaded = true;

  /****************************************************************************/
  /*                              Pseudo-enums                                */
  /****************************************************************************/

  // Namespace protected properties
  Object.defineProperties(this, {

    /**
     * @description This pseudo-enum used to initialize the script stores data
     * related to the external dependencies and core modules required by the
     * script. It consists of two properties. The former, a constant
     * <code>object</code> called "ARTICLES," originally contained key/value
     * pairs wherein the key was the specific name of the <code>mw.hook</code>
     * and the value was the script's location for use by
     * <code>importArticles.articles</code>. However, this system was eventually
     * replaced in favor of an array of <code>object</code>s containing
     * properties for hook, <code>window.dev</code> alias, and script for more
     * efficient, readable loading of dependencies. The latter array, a constant
     * array named <code>MODULES</code>, contains a listing of the core modules
     * required for use by <code>mw.loader.using</code>.
     * <br />
     * <br />
     * The key for the <code>ARTICLES</code> array entries is as follows:
     * <pre>
     * - DEV/WINDOW: The name and location of the <code>window</code> property
     * - HOOK: The name of the <code>mw.hook</code> event
     * - ARTICLE: The location of the script or stylesheet on the Dev wiki
     * - TYPE: Either "script" for JS scripts or "style" for CSS stylesheets
     * - MODULE: Name of the temporary ResourceLoader module used to async load
     * </pre>
     *
     * @readonly
     * @enum {object}
     */
    Dependencies: {
      enumerable: true,
      writable: false,
      configurable: false,
      value: Object.freeze({
        ARTICLES: Object.freeze([
          Object.freeze({
            DEV: "i18n",
            HOOK: "dev.i18n",
            ARTICLE: "u:dev:MediaWiki:I18n-js/code.js",
            TYPE: "script",
          }),
          Object.freeze({
            DEV: null,
            HOOK: null,
            ARTICLE: "u:dev:MediaWiki:CodeQuickLinks.css",
            TYPE: "style",
          }),
        ]),
        MODULES: Object.freeze([
          "mediawiki.util",
        ]),
      }),
    },

    /**
     * @description This pseudo-enum of the <code>main</code> namespace object
     * is used to store all CSS selectors in a single place in the event that
     * one or more need to be changed. The formatting of the object literal key
     * naming is type (id or class), location (placement, modal, content,
     * preview), and either the name for ids or the type of element (div, span,
     * etc.). This system was adopted, like many aspects of this script, from
     * the author's MassEdit script.
     *
     * @readonly
     * @enum {object}
     */
    Selectors: {
      enumerable: true,
      writable: false,
      configurable: false,
      value: Object.freeze({

        // ID selectors
        ID_MODULE_WRAPPER: "cql-module",
        ID_LISTING_USERFILES: "cql-listing-user",
        ID_LISTING_SITEFILES: "cql-listing-site",

        // General purpose id/class selectors
        ID_WIKIA_RAIL: "WikiaRail",
        CLASS_RAIL_MODULE: "rail-module",

        // Module selectors
        CLASS_MODULE_HEADER: "cql-module-header",
        CLASS_MODULE_CONTENT: "cql-module-content",

        // Column selectors
        CLASS_LISTING_CONTAINER: "cql-listing",
        CLASS_LISTING_HEADER: "cql-listing-header",
        CLASS_LISTING_CONTENT: "cql-listing-content",
        CLASS_LISTING_LIST: "cql-listing-list",
        CLASS_LISTING_ENTRY: "cql-listing-entry",
        CLASS_LISTING_LINK: "cql-listing-link",
      }),
    },

    /**
     * @description This pseudo-enum is used to store various data related to
     * the construction of the default link objects displayed in the
     * CodeQuickLinks rail module in most cases. It contains an array-populated
     * <code>object</code>, <code>NAMES</code>, and a pair of arrays, namely
     * <code>SUFFIXES</code> and <code>DIVISIONS</code>. <code>NAMES</code>
     * holds the names of common MediaWiki/MyPage files commonly encountered on
     * most wikis, while <code>SUFFIXES</code> holds the accepted file suffixes
     * for CSS and JavaScript files. <code>DIVISIONS</code> houses the names of
     * properties that are used in <code>this.buildDefaultFiles</code> to
     * organize the assembled files object.
     *
     * @readonly
     * @enum {object}
     */
    Files: {
      enumerable: true,
      writable: false,
      configurable: false,
      value: Object.freeze({
        NAMES: Object.freeze({
          STANDARD: Object.freeze([
            "Chat",
            "Common",
            "Wikia"
          ]),
          CUSTOM: Object.freeze([
            "Global",
            "ImportJS",
            "JSPages"
          ]),
        }),
        SUFFIXES: Object.freeze([
          ".css",
          ".js"
        ]),
        DIVISIONS: Object.freeze([
          "siteFiles",
          "userFiles",
        ])
      })
    },

    /**
     * @description This pseudo-enum is used to store the <code>string</code>
     * names of the various <code>WikipediaGlobal</code> (wg) variables required
     * for use in the script. These are fetched within the body of the
     * <code>this.preload</code> function via a <code>mw.config.get</code>
     * invocation and stored in a namespace property named <code>globals</code>
     * for subsequent usage. This approach replaces the deprecated approach
     * previously used in the script of assuming the relevant wg variables exist
     * as properties of the <code>window</code> object, an assumption that is
     * discouraged in more recent versions of MediaWiki.
     *
     * @readonly
     * @enum {object}
     */
    Globals: {
      enumerable: true,
      writable: false,
      configurable: false,
      value: Object.freeze([
        "wgArticlePath",
        "wgFormattedNamespaces",
        "wgLoadScript",
        "wgVersion",
      ]),
    },

    /**
     * @description The <code>Config</code> pseudo-enum is used primarily by
     * <code>this.validateConfig</code> to ensure that the user's input config
     * (if applicable) is well-formed and properly defined prior to its usage
     * by the script. If the user has chosen not to include certain properties
     * in the config object, the default values established in this enum are
     * applied instead as default values. The enum contains a pair of data
     * <code>object</code> establishing both the formal name of the property as
     * it exists in the config object and its associated default value.
     *
     * @readonly
     * @enum {object}
     */
    Config: {
      enumerable: true,
      writable: false,
      configurable: false,
      value: Object.freeze({
        REPLACE: Object.freeze({
          NAME: "replaceAllDefaultLinks",
          DEFAULT: false,
        }),
        LINKS: Object.freeze({
          NAME: "linkSet",
          DEFAULT: {},
        }),
      }),
    },

    /**
     * @description This catchall pseudo-enum of the <code>init</code< namespace
     * object is used to house assorted values of various data types that don't
     * fit well into other pseudo-enums. It contains the I18n-js language cache
     * version <code>number</code>, a <code>string</code> constant denoting the
     * name of the script, and another <code>string</code> for the name of the
     * <code>mw.hook</code> event.
     *
     * @readonly
     * @enum {string|boolean|number}
     */
    Utility: {
      enumerable: true,
      writable: false,
      configurable: false,
      value: Object.freeze({
        SCRIPT: "CodeQuickLinks",
        HOOK_NAME: "dev.cql",
        CACHE_VERSION: 1,
      }),
    },
  });

  /****************************************************************************/
  /*                           Utility methods                                */
  /****************************************************************************/

  /**
   * @description As the name implies, this helper function capitalizes the
   * first character of the input string and returns the altered, adjusted
   * string. it is generally used in the dynamic construction of i18n messages
   * in various assembly methods.
   *
   * @param {string} paramTarget - <code>string</code> to be capitalized
   * @returns {string} - Capitalized <code>string</code>
   */
  this.capitalize = function (paramTarget) {
    return paramTarget.charAt(0).toUpperCase() + paramTarget.slice(1);
  };

  /**
   * @description This helper method is used to check whether the target object
   * is one of several types of <code>object</code>. It is most often used to
   * determine if the target is an <code>array</code> or a straight-up
   * <code>object</code>.
   *
   * @param {string} paramType - Either "Object" or "Array"
   * @param {string} paramTarget - Target to check
   * @returns {boolean} - Flag denoting the nature of the target
   */
  this.isThisAn = function (paramType, paramTarget) {
    return Object.prototype.toString.call(paramTarget) === "[object " +
      this.capitalize.call(this, paramType.toLowerCase()) + "]";
  };

  /**
   * @description This helper function is used to automatically generate an
   * appropriate contrived ResourceLoader module name for use in loading scripts
   * via <code>mw.loader.implement</code> on UCP wikis. The use of this function
   * replaces the previous approach that saw the inclusion of hardcoded module
   * names as properties of the relevant dependency <code>object</code>s stored
   * in <code>this.Dependencies.ARTICLES</code>. When passed an argument
   * formatted as <code>u:dev:MediaWiki:Test/code.js</code>, the function will
   * extract the subdomain name ("dev") and join it to the name of the script
   * ("Test") with the article type ("script") as <code>script.dev.Test</code>.
   *
   * @param {string} paramType - Either "script" or "style"
   * @param {string} paramPage - Article formatted as "u:dev:MediaWiki:Test.js"
   * @returns {string} - A ResourceLoader module name formatted as "dev.Test"
   */
  this.generateModuleName = function (paramType, paramPage) {
    return $.merge([paramType], paramPage.split(/[\/.]+/)[0].split(":").filter(
      function (paramItem) {
        return !paramItem.match(/^u$|^mediawiki$/gi);
      }
    )).join(".");
  };

  /**
   * @description This helper function, based on MassEdit's assorted validation
   * methods, is used to ensure that the user's inputted config has properties
   * of the proper data type, i.e. <code>boolean</code> for the
   * <code>replaceAllDefaultLinks</code> flag and <code>object</code> for
   * <code>linkSet</code>. If no property exists or if the wrong data type is
   * detected, the default value specified in <code>this.Config</code> is
   * applied instead.
   *
   * @param {object} paramConfig - User config <code>object</code> to validate
   * @returns {object} config - Frozen well-formed config <code>object</code>
   */
  this.validateConfig = function (paramConfig) {

    // Declarations
    var element, entry, fields, config;

    // Definitions
    config = {};
    fields = this.Config;

    // Set to default if user input doesn't exist or if wrong data type
    for (element in fields) {
      if (!fields.hasOwnProperty(element)) {
        continue;
      }
      entry = fields[element];

      // Define with default if no property or if input is of wrong data type
      config[entry.NAME] = (!paramConfig.hasOwnProperty(entry.NAME) ||
        typeof paramConfig[entry.NAME] !== typeof entry.DEFAULT)
          ? entry.DEFAULT
          : paramConfig[entry.NAME];
    }

    return Object.freeze(config);
  };

  /****************************************************************************/
  /*                          Assembly methods                                */
  /****************************************************************************/

  /**
   * @description This function is a simple recursive <code>string</code> HTML
   * generator that makes use of <code>mw.html</code>'s assembly methods to
   * construct wellformed HTML strings from a set of nested input arrays. This
   * allows for a more readable means of producing proper HTML than the default
   * <code>jQuery</code> approach or the hardcoded HTML <code>string</code>
   * approach employed in earlier iterations of this script. Through the use of
   * nested arrays, this function permits the laying out of parent/child DOM
   * nodes in array form in a fashion similar to actual HTML, enhancing both
   * readability and usability.
   * <br />
   * <br />
   * Furthermore, as the <code>assembleElement</code> function returns a
   * <code>string</code>, nested invocations of the method within parameter
   * arrays is permitted, as evidenced in certain, more specialized assembly
   * methods elsewhere in the script.
   * <br />
   * <br />
   * An example of wellformed input is shown below:
   * <br />
   * <pre>
   * this.assembleElement(
   *   ["div", {id: "foo-id", class: "foo-class"},
   *     ["button", {id: "bar-id", class: "bar-class"},
   *       "Button text",
   *     ],
   *     ["li", {class: "overflow"},
   *       ["a", {href: "#"},
   *         "Link text",
   *       ],
   *     ],
   *   ],
   * );
   * </pre>
   *
   * @param {Array<string>} paramArray - Wellformed array representing DOM nodes
   * @returns {string} - Assembled <code>string</code> HTML
   */
  this.assembleElement = function (paramArray) {

    // Declarations
    var type, attributes, counter, content;

    // Make sure input argument is a well-formatted array
    if (!this.isThisAn("Array", paramArray)) {
      return this.assembleElement.call(this,
        Array.prototype.slice.call(arguments));
    }

    // Definitions
    counter = 0;
    content = "";
    type = paramArray[counter++];

    // mw.html.element requires an object for the second param
    attributes = (this.isThisAn("Object", paramArray[counter]))
      ? paramArray[counter++]
      : {};

    while (counter < paramArray.length) {

      // Check if recursive assembly is required for another inner DOM element
      content += (this.isThisAn("Array", paramArray[counter]))
        ? this.assembleElement(paramArray[counter++])
        : paramArray[counter++];
    }

    return mw.html.element(type, attributes, new mw.html.Raw(content));
  };

  /**
   * @description This self-contained assembly function is used to assemble the
   * rail module from the links <code>object</code> passed as a parameter. This
   * object should possess a pair of arrays, namely <code>userFiles</code>
   * and <code>siteFiles</code>, that contain objects with properties
   * <code>name</code> and <code>href</code> representing each link to be added
   * to the module in one of the two columns. Originally, this method made use
   * of three helper functions for the construction of links, columns, and the
   * rail module itself, though these were eventually merged into a single
   * method for readability's sake. This approach, using MassEdit's recursive
   * <code>assembleElement</code> method, replaces the previous approach, which
   * saw the script fix affix the rail module to the rail before populating its
   * contents with columns and links.
   *
   * @param {object} paramLinks - <code>object</code> containing link href/names
   * @returns {string} - Assembled <code>string</code> HTML
   */
  this.buildRailModule = function (paramLinks) {
    return this.assembleElement(
      ["section", {
        "class": this.Selectors.CLASS_RAIL_MODULE,
        "id": this.Selectors.ID_MODULE_WRAPPER,
      },
        ["h2", {"class": this.Selectors.CLASS_MODULE_HEADER,},
          this.i18n.msg("title").plain()
        ],
        ["div", {"class": this.Selectors.CLASS_MODULE_CONTENT,},
          Object.keys(paramLinks).map(function (paramColumn) {
            return this.assembleElement(
              ["div", {
                "class": this.Selectors.CLASS_LISTING_CONTAINER,
                "id": this.Selectors["ID_LISTING_" + paramColumn.toUpperCase()],
               },
                ["h4", {"class": this.Selectors.CLASS_LISTING_HEADER,},
                  this.i18n.msg(
                    (paramColumn === "userFiles") ? "personal" : "local"
                  ).plain()
                ],
                ["div", {"class": this.Selectors.CLASS_LISTING_CONTENT,},
                  ["ul", {"class": this.Selectors.CLASS_LISTING_LIST,},
                    paramLinks[paramColumn].map(function (paramLink) {
                      return this.assembleElement(
                        ["li", {"class": this.Selectors.CLASS_LISTING_ENTRY,},
                          ["a", {
                            "class": this.Selectors.CLASS_LISTING_LINK,
                            "href": paramLink.href,
                            "title": paramLink.name,
                          },
                            paramLink.name,
                          ],
                        ]
                      );
                    }.bind(this)).join(""),
                  ],
                ],
              ]
            );
          }.bind(this)).join(""),
        ],
      ]
    );
  };

  /****************************************************************************/
  /*                              Main methods                                */
  /****************************************************************************/

  /**
   * @description A method more or less just copied and reformatted from the
   * previous incarnation of CodeQuickLinks, this function serves as the primary
   * means by which default link <code>object</code>s possessing the properties
   * <code>name</code> and <code>href</code> are assembled from the
   * <code>string</code> names listed in <code>this.Files.NAMES</code>. The
   * method is not very efficient or optimized and will require a refactor and
   * simplification in the future once a better way is developed by the author.
   * <br />
   * <br />
   * The method takes the names specified in <code>this.Files.NAMES</code> and
   * constructs link objects that are subsequently converted to actual links for
   * display in the rail module by <code>this.buildRailModule</code>. For non-
   * rule-breaking links, personal "Special:MyPage" and MediaWiki namespace
   * versions of each link are constructed for both CSS and JS pages, while
   * rulebreakers like <code>MediaWiki:ImportJS</code> that lack personal
   * equivalents or suffixes are handled separately. All entries are sorted
   * prior to their return from the method.
   *
   * @returns {object} assembledFiles - Built from <code>this.Files.NAMES</code>
   */
  this.buildDefaultFiles = function () {

    // Declarations
    var assembledFiles, fileNames, prefix, suffixes, prefixes, communityServer,
      divisions;

    // Definitions
    assembledFiles = {};
    communityServer = "https://community.fandom.com";

    // Define prefixes using wgFormattedNamespaces
    $.each(prefixes = {mw: 8, sp: -1}, function (paramName, paramId) {
      prefixes[paramName] = this.globals.wgFormattedNamespaces[paramId] + ":";
    }.bind(this));
    prefixes.my = prefixes.sp + "MyPage/";

    // Aliases
    fileNames = this.Files.NAMES;
    suffixes = this.Files.SUFFIXES;
    divisions = this.Files.DIVISIONS;

    // Populate files object with container arrays
    divisions.forEach(function (paramColumn) {
      assembledFiles[paramColumn] = [];
    });

    // Assemble non-rulebreaking file names
    fileNames.STANDARD.forEach(function (paramFile) {
      divisions.forEach(function (paramColumn) {
        prefix = prefixes[(paramColumn === "siteFiles") ? "mw" : "my"];

        suffixes.forEach(function (paramSuffix) {
          assembledFiles[paramColumn].push({
            name: paramFile + paramSuffix,
            href: mw.util.getUrl(prefix + paramFile.toLowerCase() + paramSuffix)
          });
        });
      });
    }.bind(this));

    // Handle rule-breakers
    fileNames.CUSTOM.forEach(function (paramFile) {
      if (paramFile === "Global") {
        suffixes.forEach(function (paramSuffix) {
          assembledFiles.userFiles.push({
            name: paramFile + paramSuffix,
            href: communityServer + this.globals.wgArticlePath.replace("$1",
              prefixes.my + paramFile.toLowerCase() + paramSuffix)
          });
        }.bind(this));
      } else {
        prefix = prefixes[(paramFile === "ImportJS") ? "mw" : "sp"];

        assembledFiles.siteFiles.push({
          name: paramFile,
          href: mw.util.getUrl(prefix + paramFile)
        });
      }
    }.bind(this));

    // Sort entries alphabetically
    $.each(assembledFiles, function (paramColumn, paramFiles) {
      paramFiles.sort(function (paramA, paramB) {
        return paramA.name.localeCompare(paramB.name);
      });
    });

    return assembledFiles;
  };

  /**
   * @description The <code>main</code> method is called by
   * <code>this.init</code> once the initialization process is complete and the
   * <code>mw.hook</code> event triggered. The main method is used to assemble
   * all the link objects denoting list elements existing in the rail module's
   * columns, using the user's input if it exists. Once the link config objects
   * have been assembled, the method assembles a rail module via
   * <code>this.buildRailModule</code> and prepends it to the top of the Wikia
   * rail.
   *
   * @returns {void}
   */
  this.main = function () {

    // Declarations
    var userLinks, assembledFiles, railModule;

    // Definitions/aliases
    userLinks = this.config.linkSet;

    if (this.config.replaceAllDefaultLinks && userLinks) {
      assembledFiles = userLinks;
    } else {
      // Assemble default page names
      assembledFiles = this.buildDefaultFiles();

      if (Object.keys(userLinks).length) {
        $.each(userLinks, function (paramColumn, paramLinks) {
          $.merge(assembledFiles[paramColumn], paramLinks);
        });
      }
    }

    railModule = this.buildRailModule(assembledFiles);
    $("#" + this.Selectors.ID_WIKIA_RAIL).prepend(railModule);
  };

  /****************************************************************************/
  /*                             Setup methods                                */
  /****************************************************************************/

  /**
   * @description The <code>this.init</code> initialization method is called
   * once all external dependencies and ResourceLoader modules have been loaded.
   * The method is responsible for validating the user's config (if applicable),
   * setting the i18n language for the script, and defining a new protected
   * <code>module</code> property <code>exports</code> containing exposed public
   * methods for post-load invocation. At present, the only method publicly
   * accessible is <code>observeScript</code>, which allows the user to view the
   * layout of the script and the namespace object's various properties via a
   * <code>console.dir</code> invocation. The method returns after firing the
   * CodeQuickLinks <code>mw.hook</code> event and attaching
   * <code>this.main</code> as a listener callback.
   *
   * @param {undefined|function} paramRequire - Either function or undefined
   * @param {object} paramLang - I18n-js data content
   * @returns {void}
   */
  this.init = function (paramRequire, paramLang) {

    // Validate user-input config elements
    this.config = this.validateConfig(window.customCodeQuickLinks || {});

    // Add i18n data as local property
    (this.i18n = paramLang).useContentLang();

    // Expose public methods for external debugging
    Object.defineProperty(module, "exports", {
      enumerable: true,
      writable: false,
      configurable: false,
      value: Object.freeze({
        observeScript: window.console.dir.bind(this, this),
      })
    });

    // Dispatch hook with window.dev.cql once initialization is complete
    mw.hook(this.Utility.HOOK_NAME).fire(module).add(this.main.bind(this));
  };

  /**
   * @description Originally a pair of functions called <code>init.load</code>
   * and <code>init.preload</code>, this function is used to load all required
   * external dependencies from Dev and attach <code>mw.hook</code> listeners.
   * Once all scripts have been loaded and their events fired, the I18n-js
   * method <code>loadMessages</code> is invoked, the <code>$.Deferred</code>
   * promise resolved, and the resultant i18n data passed for subsequent usage
   * in <code>init.main</code>.
   * <br />
   * <br />
   * As an improvement to the previous manner of loading scripts, this function
   * first checks to see if the relevant <code>window.dev</code> property of
   * each script already exists, thus signaling that the script has already been
   * loaded elsewhere. In such cases, this function will skip that import and
   * move on to the next rather than blindly reimport the script again as it
   * did in the previous version.
   * <br />
   * <br />
   * As of the 1st of July update, an extendable framework for the loading of
   * ResourceLoader modules and Dev external dependencies (scripts and
   * stylesheets alike) on both UCP wikis and legacy 1.19 wikis has been put
   * into place, pending UCPification of the aforementioned Dev scripts or the
   * importation of legacy features to the UCP codebase. To handle the lack of
   * async callbacks in <code>mw.loader.load</code>, this framework invokes
   * <code>mw.loader.implement</code> to create temporary, local RL modules that
   * can then be asynchronously loaded via <code>mw.loader.using</code> and
   * handled by a dedicated callback.
   *
   * @param {object} paramDeferred - <code>$.Deferred</code> instance
   * @returns {void}
   */
  this.load = function (paramDeferred) {

    // Declarations
    var debug, articles, counter, numArticles, $loadNext, current, isLoaded,
      article, server, params, resource, moduleName;

    // Definitions
    debug = false;
    counter = 0;
    articles = this.Dependencies.ARTICLES;
    numArticles = articles.length;
    $loadNext = new $.Deferred();

    /**
     * @description The passed <code>$.Deferred</code> argument instance called
     * <code>paramDeferred</code> is variously notified during the loading of
     * dependencies by the <code>$loadNext</code> promise whenever a dependency
     * has been successfully imported by <code>window.importArticles</code> or
     * <code>mw.loader.using</code>. The <code>progress</code> handler checks if
     * all dependencies have been successfully loaded for use before loading the
     * latest version of cached <code>i18n</code> messages and resolving itself
     * to pass program execution on to <code>init.main</code>.
     */
    paramDeferred.notify().progress(function () {
      if (counter === numArticles) {
        // Resolve helper $.Deferred instance
        $loadNext.resolve();
        if (debug) {
          window.console.log("$loadNext", $loadNext.state());
        }

        // Load latest version of cached i18n messages
        window.dev.i18n.loadMessages(this.Utility.SCRIPT, {
          cacheVersion: this.Utility.CACHE_VERSION,
        }).then(paramDeferred.resolve).fail(paramDeferred.reject);
      } else {
        if (debug) {
          window.console.log((counter + 1) + "/" + numArticles);
        }

        // Load next
        $loadNext.notify(counter++);
      }
    }.bind(this));

    /**
    * @description The <code>$loadNext</code> helper <code>$.Deferred</code>
    * instance is used to load each dependency using methods appropriate to the
    * version of MediaWiki detected on the wiki. While the standard
    * <code>importArticle</code> method is used for legacy 1.19 wikis, a local
    * ResourceLoader module is defined via <code>mw.loader.implement</code> and
    * loaded via <code>mw.loader.using</code> to sidestep the fact that the
    * <code>mw.loader.load</code> method traditionally used to load dependencies
    * has no callback or promise. Once all imports are loaded, the handler
    * applies a callback to any extant <code>mw.hook</code> events and notifies
    * the main <code>paramDeferred.progress</code> handler to check if all
    * dependencies have been loaded.
    */
    $loadNext.progress(function (paramCounter) {

      // Selected dependency to load next
      current = articles[paramCounter];

      // If window has property related to dependency indicating load status
      isLoaded =
        (current.DEV && window.dev.hasOwnProperty(current.DEV)) ||
        (current.WINDOW && window.hasOwnProperty(current.WINDOW));

      // Add hook if loaded; dependencies w/o hooks must always be loaded
      if (isLoaded && current.HOOK) {
        if (debug) {
          window.console.log("isLoaded", current.ARTICLE);
        }
        return mw.hook(current.HOOK).add(paramDeferred.notify);
      }

      // Use standard importArticle approach if legacy wiki
      if (!this.flags.isUCP) {
        article = window.importArticle({
          type: current.TYPE,
          article: current.ARTICLE,
        });

        // Log for local debugging (problem spot)
        if (debug) {
          window.console.log("importArticle", article);
        }

        // Styles won't have hooks; notify status with load event if styles
        return (current.HOOK)
          ? mw.hook(current.HOOK).add(paramDeferred.notify)
          : $(article).on("load", paramDeferred.notify);
      }

      // Build url with REST params
      server = "https://dev.fandom.com";
      params = "?" + $.param({
        mode: "articles",
        only: current.TYPE + "s",
        articles: current.ARTICLE,
      });
      resource = server + this.globals.wgLoadScript + params;
      moduleName = this.generateModuleName(current.TYPE, current.ARTICLE);

      // Ensure wellformed module name
      if (debug) {
        window.console.log(moduleName);
      }

      // Define temp local modules to sidestep mw.loader.load's lack of callback
      try {
        mw.loader.implement.apply(null, $.merge([moduleName],
          (current.TYPE === "script")
            ? [[resource]]
            : [null, {"url": {"all": [resource]}}]
        ));
      } catch (paramError) {
        if (debug) {
          window.console.error(paramError);
        }
      }

      // Load script/stylesheet once temporary module has been defined
      mw.loader.using(moduleName)
        .then((current.HOOK)
          ? mw.hook(current.HOOK).add(paramDeferred.notify)
          : paramDeferred.notify)
        .fail(paramDeferred.reject);
    }.bind(this));
  };

  /**
   * @description This particular loading function is used simply to calculate
   * and inject some pre-load <code>init</code> object properties prior to the
   * loading of required external dependencies or ResourceLoader modules. As the
   * loading process depends on this function's set informational properies, the
   * function is called prior to the initial <code>init.load</code> invocation
   * at the start of the script's execution and returns a reference to the
   * <code>init</code> object (presumably) for use in subsequent method chaining
   * purposes.
   *
   * @returns {object} init - Reference to <code>init</code> object for chaining
   */
  this.preload = function () {

    // Fetch, define, and cache globals for use in init and MassEdit instance
    this.globals = Object.freeze(mw.config.get(this.Globals));

    // Object for informational booleans (extended in MassEdit init method)
    this.flags = {
      isUCP: window.parseFloat(this.globals.wgVersion) > 1.19,
    };

    // Return reference for method chaining purposes
    return this;
  };

  $.when(
    mw.loader.using((this.preload.call(this)).Dependencies.MODULES),
    new $.Deferred(this.load.bind(this)).promise())
  .then(this.init.bind(this))
  .fail(window.console.error);

}.call(Object.create(null), (this.dev = this.dev || {}).cql =
  this.dev.cql || {}, this, this.jQuery, this.mediaWiki));
Advertisement