Javadoc Search Frame

Javadoc incremental search for packages and classes

このスクリプトの質問や評価の投稿はこちら通報はこちらへお寄せください。
// ==UserScript==
// @name          Javadoc Search Frame
// @namespace     http://userscripts.org/users/46156
// @description   Javadoc incremental search for packages and classes
// @homepage      https://github.com/StevenGBrown/javadoc-search-frame
// @author        Steven G. Brown
// @copyright     2008, Steven G. Brown (https://github.com/StevenGBrown/javadoc-search-frame)
// @copyright     2006, KOSEKI Kengo (http://www.teria.com/~koseki/tools/gm/javadoc_isearch/index.html)
// @license       MIT License; http://www.opensource.org/licenses/mit-license.php
// @version       1.7.0
// @include       */allclasses-frame.html
// @include       */allclasses-frame.html#JavadocSearchFrameOptions
// @include       */package-frame.html
// @include       */package-frame.html#JavadocSearchFrameOptions
// @grant         GM.getValue
// @grant         GM.setValue
// @grant         GM_getValue
// @grant         GM_setValue
// ==/UserScript==

/**
 * Entry point of this script; called when the script has loaded.
 */
function main() {
  if (document.location.hash === '#JavadocSearchFrameOptions') {
    OptionsPageGenerator.generate();
    return;
  }

  // Version of this script. This value is set by the build script.
  var version = '1.7.0';

  var startupLogMessage =
    '\nJavadoc Search Frame ' +
    version +
    ' (User Script)\n' +
    'https://github.com/StevenGBrown/javadoc-search-frame\n' +
    navigator.userAgent +
    '\n';

  if (window.console) {
    console.log(startupLogMessage);
  }

  Storage.get(Option.HIDE_PACKAGE_FRAME, function (hidePackageFrame) {
    if (hidePackageFrame) {
      Frames.hideAllPackagesFrame(parent.document);
    }
    init(hidePackageFrame);
  });
}

// Call the main method once the rest of the script has executed.
window.setTimeout(main, 0);

// remainder of file added by the build script

var messages = {
  "extName": {
    "message": "Javadoc Search Frame",
    "description": "Extension name."
  },
  "extDescription": {
    "message": "Javadoc incremental search for packages and classes.",
    "description": "Extension description."
  },
  "optionsAnchor": {
    "message": "Options",
    "description": "Label used for the options page anchor."
  },
  "helpAnchor": {
    "message": "Help",
    "description": "Label used for the help anchor."
  },
  "optionsTitle": {
    "message": "Options: Javadoc Search Frame",
    "description": "Title of the options page."
  },
  "mergeFramesOptionTitle": {
    "message": "Page layout",
    "description": "Title of the options page section used to configure the merge frames option."
  },
  "mergeFramesOptionOn": {
    "message": "Merge the package list and class list",
    "description": "Label displayed next to the button to turn on the merge frames option."
  },
  "mergeFramesOptionOff": {
    "message": "Use separate frames for packages and classes",
    "description": "Label displayed next to the button to turn off the merge frames option."
  },
  "menuOptionTitle": {
    "message": "Menu displayed by pressing the @ key",
    "description": "Title of the options page section used to configure the menus."
  },
  "classOrMethodMenuOptionDescription": {
    "message": "When the top search result is a class or method:",
    "description": "Label for the class/method menu configuration."
  },
  "packageMenuOptionDescription": {
    "message": "When the top search result is a package:",
    "description": "Label for the package menu configuration."
  },
  "restoreDefault": {
    "message": "Restore Default",
    "description": "Label displayed on the buttons that reset a menu option to the default value."
  },
  "saveChanges": {
    "message": "Save Changes",
    "description": "Label displayed on the buttons that save a new value for an option."
  },
  "optionsReadOnly": {
    "message": "This browser does not support changing the options.",
    "description": "Error message displayed when the options cannot be configured."
  }
}

/*
 * ----------------------------------------------------------------------------
 * Messages
 * ----------------------------------------------------------------------------
 */

/**
 * @class Provides localised strings.
 */
Messages = {};

/**
 * Retrieve a localised string.
 * @param {string} key The key used to lookup the localised string.
 * @return {string} The localised string.
 */
Messages.get = function (key) {
  return messages[key].message;
};

/*
 * ----------------------------------------------------------------------------
 * Storage
 * ----------------------------------------------------------------------------
 */

/**
 * @class Provides storage functionality.
 */
Storage = {};

/**
 * @return {boolean} Whether storage is supported by this browser.
 */
Storage.isSupported = function () {
  if (typeof GM !== 'undefined') {
    return GM.getValue && GM.setValue;
  }
  return (
    typeof GM_getValue !== 'undefined' && typeof GM_setValue !== 'undefined'
  );
};

/**
 * Retrieve the current value of an option.
 * @param {Option} option the Option to retrieve.
 * @param {function(*)} callback Callback function that is provided with the
 *     value of this option. If the option cannot be retrieved, or has not yet
 *     been configured, then the default value will be returned.
 */
Storage.get = function (option, callback) {
  Storage._getValue(option.key, function (value) {
    if (value === undefined || value === null) {
      callback(option.defaultValue);
    } else {
      if (option.type === Boolean) {
        value = '' + value;
        value = option.defaultValue ? value !== 'false' : value === 'true';
      }
      Storage._getValue(option.key + '_version', function (version) {
        value = option.upgrade(value, version || '1.4.6');
        callback(value);
      });
    }
  });
};

Storage._getValue = function (key, callback) {
  if (typeof GM !== 'undefined' && GM.getValue) {
    GM.getValue(key).then(callback);
  } else if (typeof GM_getValue !== 'undefined') {
    callback(GM_getValue(key));
  } else {
    callback(undefined);
  }
};

/**
 * Set an option to a new value.
 * @param {Option} option The option to configure.
 * @param {*} value The new value.
 */
Storage.set = function (option, value) {
  Storage._setValue(option.key, value);
  var version = '1.7.0'; // The version number is set by the build script.
  Storage._setValue(option.key + '_version', version);
};

Storage._setValue = function (key, value) {
  if (typeof GM !== 'undefined' && GM.setValue) {
    GM.setValue(key, value);
  } else {
    GM_setValue(key, value);
  }
};

/*
 * ----------------------------------------------------------------------------
 * Option
 * ----------------------------------------------------------------------------
 */

/**
 * Option which can be configured to change the behaviour of the script.
 * @param {{key: string, defaultValue: string, type,
 *        upgrade: function(string, string)}} properties The option properties.
 * @constructor
 */
Option = function (properties) {
  this.key = properties.key;
  this.defaultValue = properties.defaultValue;
  this.type = properties.type;
  this.upgrade = properties.upgrade;
};

/**#@+
 * Option recognised by this script.
 */

/**
 * @type {Option}
 */
Option.HIDE_PACKAGE_FRAME = new Option({
  key: 'hide_package_frame',
  defaultValue: true,
  type: Boolean,
  upgrade: function (value, lastSavedVersion) {
    return value;
  },
});

/**
 * @type {Option}
 */
Option.PACKAGE_MENU = new Option({
  key: 'package_menu',
  defaultValue:
    '@1:search(krugle) -> http://opensearch.krugle.org/document/search/' +
    '#language=java&query=%20path%3A##PACKAGE_NAME##\n' +
    '@2:search(Docjar) -> http://www.docjar.com/s.jsp?q=##PACKAGE_NAME##',
  type: String,
  upgrade: function (value, lastSavedVersion) {
    if (lastSavedVersion === '1.4.6' && value.indexOf('->') === -1) {
      return this.defaultValue;
    }
    return Option._upgradeMenuOption(
      value,
      lastSavedVersion,
      '%20path%3A##PACKAGE_NAME##'
    );
  },
});

/**
 * @type {Option}
 */
Option.CLASS_MENU = new Option({
  key: 'class_menu',
  defaultValue:
    '@1:search(krugle) -> http://opensearch.krugle.org/document/search/' +
    '#language=java&query=##CLASS_NAME##%20##MEMBER_NAME##\n' +
    '@2:search(Docjar) -> http://www.docjar.com/s.jsp?q=##CLASS_NAME##\n' +
    '@3:source(Docjar) -> http://www.docjar.com/html/api/' +
    '##PACKAGE_PATH##/##CLASS_NAME##.java.html\n' +
    '@4:search(grepcode) -> http://grepcode.com/' +
    'search/?query=##PACKAGE_NAME##.##CLASS_NAME##.##MEMBER_NAME##',
  type: String,
  upgrade: function (value, lastSavedVersion) {
    if (lastSavedVersion === '1.4.6' && value.indexOf('->') === -1) {
      return this.defaultValue;
    }
    value = Option._upgradeMenuOption(
      value,
      lastSavedVersion,
      '##CLASS_NAME##%20##MEMBER_NAME##'
    );
    if (lastSavedVersion === '1.4.6' && value.indexOf('grepcode') === -1) {
      for (var i = 1; i < 10; i++) {
        if (value.indexOf('@' + i + ':') === -1) {
          value +=
            '\n@' +
            i +
            ':search(grepcode) -> http://grepcode.com/' +
            'search/?query=' +
            '##PACKAGE_NAME##.##CLASS_NAME##.##MEMBER_NAME##';
          break;
        }
      }
    }
    return value;
  },
});

/**#@-
 */

/**
 * Upgrade a configured menu option. This function performs the changes which
 * are used to upgrade both the class menu and package menu.
 * @param {string} value The current value of the option.
 * @param {string} lastSavedVersion The last version of the script to save the
 *                 option.
 * @param {string} krugleQuery The query for searching on krugle.
 * @return {string} The new value.
 */
Option._upgradeMenuOption = function (value, lastSavedVersion, krugleQuery) {
  if (lastSavedVersion === '1.4.6') {
    value = value.replace(':search(koders)', ':search(Ohloh)');
    value = value.replace('//www.koders.com/', '//code.ohloh.net/');
  }
  if (lastSavedVersion === '1.4.6' || lastSavedVersion === '1.5') {
    value = value.replace(':search(Ohloh)', ':search(Open HUB)');
    value = value.replace('//code.ohloh.net/', '//code.openhub.net/');
  }
  if (
    lastSavedVersion === '1.4.6' ||
    lastSavedVersion === '1.5' ||
    lastSavedVersion.indexOf('1.5.') === 0
  ) {
    value = value.replace(':search(Open HUB)', ':search(krugle)');
    value = value.replace(
      /http:\/\/code.openhub.net\/.*/,
      'http://opensearch.krugle.org/document/search/#language=java&query=' +
        krugleQuery
    );
  }
  return value;
};

/*
 * ----------------------------------------------------------------------------
 * Frames
 * ----------------------------------------------------------------------------
 */

/**
 * @class Provides functions to interact with other frames.
 */
Frames = {};

/**
 * Hide the packages frame. If the packages frame does not exist, calling this
 * function will have no effect.
 *
 * @param {Document} [parentDocument] The document containing the Javadoc frames or iframes.
 */
Frames.hideAllPackagesFrame = function (parentDocument) {
  var framesets = (parentDocument || document).getElementsByTagName('frameset');
  if (framesets.length > 1) {
    // Javadoc created with Java 8 or earlier
    var frameset = framesets[1];
    var framesetChildren = frameset.children;
    if (
      framesetChildren.length &&
      framesetChildren[0].name === 'packageListFrame'
    ) {
      frameset.setAttribute('rows', '0,*');
      frameset.setAttribute('border', '0');
      frameset.setAttribute('frameborder', '0');
      frameset.setAttribute('framespacing', '0');
    }
  } else {
    // Javadoc created with Java 9
    var divs = parentDocument.getElementsByTagName('div');
    for (var i = 0; i < divs.length; i++) {
      var div = divs[i];
      if (div.className === 'leftTop') {
        div.style.display = 'none';
      }
      if (div.className === 'leftBottom') {
        div.style.height = '100%';
      }
    }
  }
};

/**
 * Open the given URL in the summary frame. If the summary frame is not
 * displayed, the URL will be opened in a new tab or window.
 * @param {string} url The URL to open.
 */
Frames.openLinkInSummaryFrameOrNewTab = function (url) {
  if (window.top === window) {
    Frames.openLinkInNewTab(url);
  } else {
    window.open(url, 'classFrame');
  }
};

/**
 * Open the given URL in a new tab.
 * @param {string} url The URL to open.
 */
Frames.openLinkInNewTab = function (url) {
  window.open(url);
};

/*
 * ----------------------------------------------------------------------------
 * OptionsPage
 * ----------------------------------------------------------------------------
 */

/**
 * @class Options page.
 */
OptionsPage = {};

/**
 * Open the options page.
 */
OptionsPage.open = function () {
  window.open(location + '#JavadocSearchFrameOptions');
};

/*
 * ----------------------------------------------------------------------------
 * HttpRequest
 * ----------------------------------------------------------------------------
 */

/**
 * Asynchronously loads resources from external URLs.
 * @constructor
 */
HttpRequest = function () {
  this.xmlHttpRequest = null;
  this.url = null;
  this.loadedResource = null;
  this.bytesDownloaded = 0;
  this.errorMessage = null;
  this.progressCallback = null;
};

/**
 * Loads the resource at the given URL. If the resource at the given URL is
 * already being loaded, calling this function will have no effect.
 * @param {string} url The URL.
 * @param {function()} progressCallback Function that is called when whenever
 *     progress has been made towards loading the resource.
 */
HttpRequest.prototype.load = function (url, progressCallback) {
  if (this.url === url) {
    // Already loading the resource at this URL.
    return;
  }
  this.abort();
  this.url = url;
  this.progressCallback = progressCallback;
  var thisObj = this;
  try {
    var xmlHttpRequest = new XMLHttpRequest();
    xmlHttpRequest.onprogress = function (e) {
      thisObj._onprogress(e);
    };
    xmlHttpRequest.open('GET', url);
    xmlHttpRequest.onload = function (e) {
      thisObj._onload(e);
    };
    xmlHttpRequest.onerror = function (e) {
      thisObj._onerror(e);
    };
    xmlHttpRequest.overrideMimeType('text/plain; charset=x-user-defined');
    xmlHttpRequest.send(null);
  } catch (ex) {
    thisObj._onexception(ex);
  }
  this.xmlHttpRequest = xmlHttpRequest;
};

/**
 * @return {boolean} Whether the loading is complete.
 */
HttpRequest.prototype.isComplete = function () {
  return this.loadedResource !== null;
};

/**
 * @return {string} A status message on the progress made towards loading the
 *     resource.
 */
HttpRequest.prototype.getStatusMessage = function () {
  if (this.bytesDownloaded === -1) {
    return this.errorMessage;
  }
  if (this.bytesDownloaded > 1048576) {
    return 'loading... (' + Math.floor(this.bytesDownloaded / 1048576) + ' MB)';
  }
  if (this.bytesDownloaded > 1024) {
    return 'loading... (' + Math.floor(this.bytesDownloaded / 1024) + ' kB)';
  }
  if (this.bytesDownloaded > 0) {
    return 'loading... (' + this.bytesDownloaded + ' bytes)';
  }
  return 'loading...';
};

/**
 * @return {string} The loaded resource, or null if the loading is not
 *     complete.
 */
HttpRequest.prototype.getResource = function () {
  return this.loadedResource;
};

/**
 * Abort the current anchor load operation.
 */
HttpRequest.prototype.abort = function () {
  if (this.xmlHttpRequest) {
    this.xmlHttpRequest.abort();
  }
  this.xmlHttpRequest = null;
  this.url = null;
  this.loadedResource = null;
  this.bytesDownloaded = 0;
  this.errorMessage = null;
  this.progressCallback = null;
};

/**
 * @param {Event} e The progress event.
 */
HttpRequest.prototype._onprogress = function (e) {
  this.bytesDownloaded = e.position;
  this.errorMessage = null;
  this.progressCallback();
};

/**
 * @param {Event} e The load event.
 */
HttpRequest.prototype._onload = function (e) {
  this.loadedResource = this.xmlHttpRequest.responseText;
  this.progressCallback();
};

/**
 * @param {Event} e The error event.
 */
HttpRequest.prototype._onerror = function (e) {
  this.bytesDownloaded = -1;
  this.errorMessage = 'ERROR';
  this.progressCallback();
};

/**
 * @param {*} ex The exception.
 */
HttpRequest.prototype._onexception = function (ex) {
  this.bytesDownloaded = -1;
  this.errorMessage = ex;
  this.progressCallback();
};

/*
 * ----------------------------------------------------------------------------
 * OptionsPageGenerator
 * ----------------------------------------------------------------------------
 */

/**
 * @class Options page generator.
 */
OptionsPageGenerator = {};

/**
 * Generate the options page by replacing the current document.
 */
OptionsPageGenerator.generate = function () {
  document.title = Messages.get('optionsTitle');

  while (document.body.firstChild) {
    document.body.removeChild(document.body.firstChild);
  }

  var contents = OptionsPageGenerator._createContents(document);
  contents.forEach(function (pageElement) {
    document.body.appendChild(pageElement);
  });
};

/**
 * Create the contents of the options page.
 * @param {Document} pageDocument The options page document.
 * @return {Array.<Element>} The contents of the options page.
 */
OptionsPageGenerator._createContents = function (pageDocument) {
  var contents = [];
  var optionsTitleElement = OptionsPageGenerator._text(
    pageDocument,
    'h1',
    Messages.get('optionsTitle')
  );
  optionsTitleElement.setAttribute(
    'style',
    'font-family: sans-serif; ' +
      'font-weight:normal; font-size:1.5em; margin-bottom:0.2em'
  );
  contents.push(optionsTitleElement);
  contents.push(pageDocument.createElement('hr'));
  contents.push(pageDocument.createElement('p'));
  if (!Storage.isSupported()) {
    contents.push(
      OptionsPageGenerator._createOptionsCannotBeConfiguredErrorMessage(
        pageDocument
      )
    );
    contents.push(pageDocument.createElement('p'));
  }
  contents.push(
    OptionsPageGenerator._title(
      pageDocument,
      Messages.get('mergeFramesOptionTitle')
    )
  );
  contents.push(
    OptionsPageGenerator._booleanOption(
      pageDocument,
      Option.HIDE_PACKAGE_FRAME,
      'hidePackageFrame',
      Messages.get('mergeFramesOptionOn'),
      Messages.get('mergeFramesOptionOff')
    )
  );
  contents.push(pageDocument.createElement('p'));
  contents.push(
    OptionsPageGenerator._title(pageDocument, Messages.get('menuOptionTitle'))
  );
  contents.push(
    OptionsPageGenerator._menuOption(
      pageDocument,
      Messages.get('classOrMethodMenuOptionDescription'),
      Option.CLASS_MENU
    )
  );
  contents.push(pageDocument.createElement('p'));
  contents.push(
    OptionsPageGenerator._menuOption(
      pageDocument,
      Messages.get('packageMenuOptionDescription'),
      Option.PACKAGE_MENU
    )
  );
  return contents;
};

/**
 * @param {Document} pageDocument The options page document.
 * @param {string} elementType The type of element to create.
 * @param {string} text The text content of the element.
 * @return {Element} The text element.
 */
OptionsPageGenerator._text = function (pageDocument, elementType, text) {
  var labelElement = pageDocument.createElement(elementType);
  labelElement.textContent = text;
  labelElement.setAttribute('style', 'font-family: sans-serif');
  return labelElement;
};

/**
 * @param {Document} pageDocument The options page document.
 * @param {string} title The title text.
 * @return {Element} An element that displays the title for an option.
 */
OptionsPageGenerator._title = function (pageDocument, title) {
  var titleElement = OptionsPageGenerator._text(pageDocument, 'h2', title);
  titleElement.setAttribute(
    'style',
    'font-family: sans-serif; ' +
      'font-weight:normal; font-size:1.3em; margin-top:1em'
  );
  return titleElement;
};

/**
 * @param {Document} pageDocument The options page document.
 * @return {Element} An error message element.
 */
OptionsPageGenerator._createOptionsCannotBeConfiguredErrorMessage = function (
  pageDocument
) {
  var errorMessageElement = pageDocument.createElement('p');
  errorMessageElement.innerHTML = Messages.get('optionsReadOnly');
  errorMessageElement.style.color = 'red';
  return errorMessageElement;
};

/**
 * @param {Document} pageDocument The options page document.
 * @param {Option} option A boolean option.
 * @param {string} name The option name, used to name the form elements.
 * @param {string} trueText The message to display when the option is true.
 * @param {string} falseText The message to display when the option is false.
 * @return {Element} An element that allows the option to be configured.
 */
OptionsPageGenerator._booleanOption = function (
  pageDocument,
  option,
  name,
  trueText,
  falseText
) {
  var trueRadioButtonElement = OptionsPageGenerator._radioButton(
    pageDocument,
    option,
    name,
    true
  );
  var trueLabelElement = OptionsPageGenerator._text(
    pageDocument,
    'label',
    '   ' + trueText
  );
  trueLabelElement.insertBefore(
    trueRadioButtonElement,
    trueLabelElement.firstChild
  );

  var falseRadioButtonElement = OptionsPageGenerator._radioButton(
    pageDocument,
    option,
    name,
    false
  );
  var falseLabelElement = OptionsPageGenerator._text(
    pageDocument,
    'label',
    '   ' + falseText
  );
  falseLabelElement.insertBefore(
    falseRadioButtonElement,
    falseLabelElement.firstChild
  );

  Storage.get(option, function (value) {
    var radioButtonToCheck = value
      ? trueRadioButtonElement
      : falseRadioButtonElement;
    radioButtonToCheck.setAttribute('checked', true);

    var clickEventListener = function () {
      Storage.set(option, trueRadioButtonElement.checked);
    };

    trueRadioButtonElement.addEventListener('click', clickEventListener, false);
    falseRadioButtonElement.addEventListener(
      'click',
      clickEventListener,
      false
    );
  });

  var blockElement = pageDocument.createElement('div');
  blockElement.setAttribute('style', 'margin-left:20px');
  if (option.defaultValue) {
    blockElement.appendChild(trueLabelElement);
    blockElement.appendChild(pageDocument.createElement('p'));
    blockElement.appendChild(falseLabelElement);
  } else {
    blockElement.appendChild(falseLabelElement);
    blockElement.appendChild(pageDocument.createElement('p'));
    blockElement.appendChild(trueLabelElement);
  }
  return blockElement;
};

/**
 * @param {Document} pageDocument The options page document.
 * @param {Option} option A boolean option.
 * @param {string} name The name to display on the radio button.
 * @param {boolean} checked Whether to check the radio button.
 * @return {Element} A radio button element used to display the boolean option.
 */
OptionsPageGenerator._radioButton = function (
  pageDocument,
  option,
  name,
  checked
) {
  var radioButtonElement = pageDocument.createElement('input');
  radioButtonElement.setAttribute('type', 'radio');
  radioButtonElement.setAttribute('name', name);
  radioButtonElement.setAttribute('value', checked);
  if (!Storage.isSupported()) {
    radioButtonElement.disabled = true;
  }
  return radioButtonElement;
};

/**
 * @param {Document} pageDocument The options page document.
 * @param {string} description A description of the option.
 * @param {Option} option A menu option.
 * @return {Element} An element that allows the option to be configured.
 */
OptionsPageGenerator._menuOption = function (
  pageDocument,
  description,
  option
) {
  var restoreDefaultButtonElement = pageDocument.createElement('input');
  restoreDefaultButtonElement.setAttribute('type', 'button');
  restoreDefaultButtonElement.setAttribute(
    'value',
    Messages.get('restoreDefault')
  );
  restoreDefaultButtonElement.disabled = true;

  var saveButtonElement = pageDocument.createElement('input');
  saveButtonElement.setAttribute('type', 'button');
  saveButtonElement.setAttribute('value', Messages.get('saveChanges'));
  saveButtonElement.disabled = true;

  var textAreaElement = pageDocument.createElement('textarea');
  textAreaElement.setAttribute('rows', 5);
  textAreaElement.setAttribute('cols', 100);
  textAreaElement.setAttribute('wrap', 'off');
  textAreaElement.disabled = true;

  Storage.get(option, function (value) {
    textAreaElement.value = value;

    if (Storage.isSupported()) {
      var savedValue = value;

      var updateEnabled = function () {
        var hasDefault = textAreaElement.value === option.defaultValue;
        restoreDefaultButtonElement.disabled = hasDefault;
        var hasSaved = textAreaElement.value === savedValue;
        saveButtonElement.disabled = hasSaved;
      };

      var save = function () {
        Storage.set(option, textAreaElement.value);
        savedValue = textAreaElement.value;
        updateEnabled();
      };

      restoreDefaultButtonElement.addEventListener(
        'click',
        function () {
          textAreaElement.value = option.defaultValue;
          save();
        },
        false
      );

      saveButtonElement.addEventListener('click', save, false);

      textAreaElement.addEventListener('input', updateEnabled, false);
      updateEnabled();

      textAreaElement.disabled = false;
    }
  });

  var blockElement = pageDocument.createElement('div');
  blockElement.setAttribute('style', 'margin-left:20px');
  blockElement.appendChild(
    OptionsPageGenerator._text(pageDocument, 'span', description)
  );
  blockElement.appendChild(pageDocument.createElement('p'));
  blockElement.appendChild(restoreDefaultButtonElement);
  blockElement.appendChild(saveButtonElement);
  blockElement.appendChild(pageDocument.createElement('br'));
  blockElement.appendChild(textAreaElement);
  return blockElement;
};

/*
 * ----------------------------------------------------------------------------
 * Global variables
 * ----------------------------------------------------------------------------
 */

/**
 * Array of all package and class links.
 * @type {Array.<PackageLink|ClassLink>}
 */
var ALL_PACKAGE_AND_CLASS_LINKS = [];

/**
 * The window URL (cached).
 * @type {string}
 */
var LOCATION_HREF = location.href;

/*
 * ----------------------------------------------------------------------------
 * LinkType
 * ----------------------------------------------------------------------------
 */

/**
 * Package, class, class member and keyword link types.
 * @param {string} singularName The singular name of the link type.
 * @param {string} pluralName The plural name of the link type.
 * @constructor
 */
LinkType = function (singularName, pluralName) {
  this.singularName = singularName;
  this.pluralName = pluralName;
};

/**
 * @return {string} The singular name of this type.
 */
LinkType.prototype.getSingularName = function () {
  return this.singularName;
};

/**
 * @return {string} The plural name of this type.
 */
LinkType.prototype.getPluralName = function () {
  return this.pluralName;
};

/**
 * @return {string} A string representation of this type.
 */
LinkType.prototype.toString = function () {
  return this.singularName;
};

/**
 * Package link type.
 */
LinkType.PACKAGE = new LinkType('Package', 'Packages');

/**
 * Interface link type.
 */
LinkType.INTERFACE = new LinkType('Interface', 'Interfaces');

/**
 * Class link type.
 */
LinkType.CLASS = new LinkType('Class', 'Classes');

/**
 * Enum link type.
 */
LinkType.ENUM = new LinkType('Enum', 'Enums');

/**
 * Exception link type.
 */
LinkType.EXCEPTION = new LinkType('Exception', 'Exceptions');

/**
 * Error link type.
 */
LinkType.ERROR = new LinkType('Error', 'Errors');

/**
 * Annotation link type.
 */
LinkType.ANNOTATION = new LinkType('Annotation', 'Annotation Types');

/**
 * Script link type (GroovyDoc only).
 */
LinkType.SCRIPT = new LinkType('Script', 'Scripts');

/**
 * Class member link type.
 */
LinkType.CLASS_MEMBER = new LinkType('Method or Field', 'Methods and Fields');

/**
 * Keyword link type.
 */
LinkType.KEYWORD = new LinkType('Keyword', 'Keywords');

/**
 * Get the link type with the given singular name.
 * @param {string} singularName The singular name.
 * @return {LinkType} The link type.
 */
LinkType.getByName = function (singularName) {
  return LinkType[singularName.toUpperCase()];
};

/*
 * ----------------------------------------------------------------------------
 * PackageLink, ClassLink, MemberLink and KeywordLink
 * ----------------------------------------------------------------------------
 */

/**
 * Extract a URL from the given link.
 * @param {PackageLink|ClassLink|MemberLink|KeywordLink} link The link.
 * @return {string} The URL.
 */
function extractUrl(link) {
  var html = link.getHtml();
  // Assume that the HTML starts with <A HREF="..."
  var firstQuoteIndex = html.indexOf('"');
  var secondQuoteIndex = html.indexOf('"', firstQuoteIndex + 1);
  return html.substring(firstQuoteIndex + 1, secondQuoteIndex);
}

/**
 * Convert the given relative URL to an absolute URL.
 * @param {string} relativeUrl The relative URL.
 * @param {string=} opt_documentUrl The document's current URL, given by
 *     location.href (optional).
 * @return {string} The absolute URL.
 */
function toAbsoluteUrl(relativeUrl, opt_documentUrl) {
  var colonIndex = relativeUrl.indexOf(':');
  if (colonIndex != -1 && colonIndex < relativeUrl.indexOf('/')) {
    // Already an absolute URL.
    return relativeUrl;
  }
  if (!opt_documentUrl) {
    opt_documentUrl = LOCATION_HREF;
  }
  var documentUrlPath = opt_documentUrl.substring(
    0,
    opt_documentUrl.lastIndexOf('/') + 1
  );
  var relativeUrlPath = relativeUrl.substring(
    0,
    relativeUrl.lastIndexOf('/') + 1
  );
  if (endsWith(documentUrlPath, relativeUrlPath)) {
    documentUrlPath = documentUrlPath.substring(
      0,
      documentUrlPath.length - relativeUrlPath.length
    );
  }
  return documentUrlPath + relativeUrl;
}

/**
 * Link to a package. These links are of type {LinkType.PACKAGE}.
 * @param {string} packageName The package name.
 * @constructor
 */
PackageLink = function (packageName) {
  this.packageName = packageName;
  this.html =
    '<A HREF="' +
    packageName.replace(/\./g, '/') +
    '/package-summary.html" target="classFrame">' +
    packageName +
    '</A>';
};

/**
 * Determine whether this link matches the given regular expression.
 * @param {RegExp} regex The regular expression.
 * @return {boolean} Whether this link is a match.
 */
PackageLink.prototype.matches = function (regex) {
  return regex.test(this.packageName);
};

/**
 * @return {string} This link in HTML format.
 */
PackageLink.prototype.getHtml = function () {
  return this.html;
};

/**
 * @return {LinkType} The type of this link.
 */
PackageLink.prototype.getType = function () {
  return LinkType.PACKAGE;
};

/**
 * @return {string} The name of this package.
 */
PackageLink.prototype.getPackageName = function () {
  return this.packageName;
};

/**
 * @return {string} The URL of this link.
 */
PackageLink.prototype.getUrl = function () {
  return toAbsoluteUrl(extractUrl(this));
};

/**
 * Equals function.
 * @param {*} obj The object with which to compare.
 * @return {boolean} Whether this link is equal to the given object.
 */
PackageLink.prototype.equals = function (obj) {
  return obj instanceof PackageLink && this.packageName === obj.packageName;
};

/**
 * @return {string} A string representation of this link.
 */
PackageLink.prototype.toString = function () {
  return 'PackageLink: ' + this.packageName;
};

/**
 * Link to a class. These links are of type {LinkType.INTERFACE},
 * {LinkType.CLASS}, {LinkType.ENUM}, {LinkType.EXCEPTION}, {LinkType.ERROR},
 * {LinkType.ANNOTATION} or {LinkType.SCRIPT}.
 * @param {LinkType} type The type of this link.
 * @param {string} packageName The package name.
 * @param {string} className The class name.
 * @constructor
 */
ClassLink = function (type, packageName, className) {
  this.type = type;
  this.className = className;
  this.canonicalName = packageName + '.' + className;

  this.innerClassNames = [];
  var name = className;
  while (true) {
    var index = name.indexOf('.');
    if (index === -1) {
      break;
    }
    name = name.substring(index + 1);
    this.innerClassNames.push(name);
  }

  var url = toAbsoluteUrl(
    packageName.replace(/\./g, '/') + '/' + className + '.html'
  );
  var typeInHtml = type;
  if (type === LinkType.EXCEPTION || type === LinkType.ERROR) {
    typeInHtml = LinkType.CLASS;
  }
  var openingTag = '';
  var closingTag = '';
  if (type === LinkType.INTERFACE) {
    openingTag = '<I>';
    closingTag = '</I>';
  }
  this.html =
    '<A HREF="' +
    url +
    '" title="' +
    typeInHtml.getSingularName().toLowerCase() +
    ' in ' +
    packageName +
    '" target="classFrame">' +
    openingTag +
    className +
    closingTag +
    '</A>&nbsp;[&nbsp;' +
    packageName +
    '&nbsp;]';
};

/**
 * Determine whether this link matches the given regular expression.
 * @param {RegExp} regex The regular expression.
 * @return {boolean} Whether this link is a match.
 */
ClassLink.prototype.matches = function (regex) {
  return (
    regex.test(this.className) ||
    regex.test(this.canonicalName) ||
    this.innerClassNames.some(function (innerClassName) {
      return regex.test(innerClassName);
    })
  );
};

/**
 * @return {string} This link in HTML format.
 */
ClassLink.prototype.getHtml = function () {
  return this.html;
};

/**
 * @return {LinkType} The type of this link.
 */
ClassLink.prototype.getType = function () {
  return this.type;
};

/**
 * @return {string} The simple name of this class.
 */
ClassLink.prototype.getClassName = function () {
  return this.className;
};

/**
 * @return {string} The name of the package that contains this class.
 */
ClassLink.prototype.getPackageName = function () {
  return this.canonicalName.substring(
    0,
    this.canonicalName.length - this.className.length - 1
  );
};

/**
 * @return {string} The canonical name of this class.
 */
ClassLink.prototype.getCanonicalName = function () {
  return this.canonicalName;
};

/**
 * @return {string} The URL of this link.
 */
ClassLink.prototype.getUrl = function () {
  return toAbsoluteUrl(extractUrl(this));
};

/**
 * Equals function.
 * @param {*} obj The object with which to compare.
 * @return {boolean} Whether this link is equal to the given object.
 */
ClassLink.prototype.equals = function (obj) {
  return (
    obj instanceof ClassLink &&
    this.type === obj.type &&
    this.className === obj.className &&
    this.canonicalName === obj.canonicalName
  );
};

/**
 * @return {string} A string representation of this link.
 */
ClassLink.prototype.toString = function () {
  return 'ClassLink(' + this.type + '): ' + this.canonicalName;
};

/**
 * Link to a method or field of a class.
 * @param {string} baseUrl The base URL of this link.
 * @param {string} anchorName The method or field anchor name.
 * @param {string} displayName The method or field display name.
 * @constructor
 */
MemberLink = function (baseUrl, anchorName, displayName) {
  this.anchorName = anchorName;
  this.displayName = displayName;
  this.html =
    '<A HREF="' +
    baseUrl +
    '#' +
    anchorName +
    '" target="classFrame" class="anchorLink">' +
    displayName.replace(/ /g, '&nbsp;') +
    '</A><BR/>';
};

/**
 * Determine whether this link matches the given regular expression.
 * @param {RegExp} regex The regular expression.
 * @return {boolean} Whether this link is a match.
 */
MemberLink.prototype.matches = function (regex) {
  return regex.test(this.displayName);
};

/**
 * @return {string} This link in HTML format.
 */
MemberLink.prototype.getHtml = function () {
  return this.html;
};

/**
 * @return {LinkType} The type of this link.
 */
MemberLink.prototype.getType = function () {
  return LinkType.CLASS_MEMBER;
};

/**
 * @return {string} The URL of this link.
 */
MemberLink.prototype.getUrl = function () {
  return extractUrl(this);
};

/**
 * @return {string} The name of this class member.
 */
MemberLink.prototype.getName = function () {
  if (this.displayName.indexOf('(') !== -1) {
    return this.displayName.substring(0, this.displayName.indexOf('('));
  } else {
    return this.displayName;
  }
};

/**
 * Equals function.
 * @param {*} obj The object with which to compare.
 * @return {boolean} Whether this link is equal to the given object.
 */
MemberLink.prototype.equals = function (obj) {
  return obj instanceof MemberLink && this.html === obj.html;
};

/**
 * @return {string} A string representation of this link.
 */
MemberLink.prototype.toString = function () {
  return 'MemberLink: ' + this.displayName + ' -> #' + this.anchorName;
};

/**
 * Keyword link found on a package or class page.
 * @param {string} baseUrl The base URL of this link.
 * @param {string} anchorName The keyword anchor name.
 * @param {string} displayName The keyword display name.
 * @constructor
 */
KeywordLink = function (baseUrl, anchorName, displayName) {
  this.anchorName = anchorName;
  this.displayName = displayName;
  this.html =
    '<A HREF="' +
    baseUrl +
    '#' +
    anchorName +
    '" target="classFrame" class="anchorLink" style="color:#666">' +
    displayName.replace(/ /g, '&nbsp;') +
    '</A><BR/>';
};

/**
 * Determine whether this link matches the given regular expression.
 * @param {RegExp} regex The regular expression.
 * @return {boolean} Whether this link is a match.
 */
KeywordLink.prototype.matches = function (regex) {
  return regex.test(this.displayName);
};

/**
 * @return {string} This link in HTML format.
 */
KeywordLink.prototype.getHtml = function () {
  return this.html;
};

/**
 * @return {LinkType} The type of this link.
 */
KeywordLink.prototype.getType = function () {
  return LinkType.KEYWORD;
};

/**
 * @return {string} The URL of this link.
 */
KeywordLink.prototype.getUrl = function () {
  return extractUrl(this);
};

/**
 * Equals function.
 * @param {*} obj The object with which to compare.
 * @return {boolean} Whether this link is equal to the given object.
 */
KeywordLink.prototype.equals = function (obj) {
  return obj instanceof KeywordLink && this.html === obj.html;
};

/**
 * @return {string} A string representation of this link.
 */
KeywordLink.prototype.toString = function () {
  return 'KeywordLink: ' + this.displayName + ' -> #' + this.anchorName;
};

/*
 * ----------------------------------------------------------------------------
 * View
 * ----------------------------------------------------------------------------
 */

/**
 * @class View Provides access to the UI elements of the frame containing the
 *     search field.
 */
View = {
  searchField: null,
  contentNodeParent: null,
  contentNode: null,
};

/**
 * Access key that will focus on the search field when activated ('s').
 * This access key can be activated by pressing either Alt+s or Alt+Shift+s,
 * depending on the internet browser.
 * @type {string}
 */
View.searchAccessKey = 's';

/**
 * Access key that will clear the search field when activated ('a').
 * This access key can be activated by pressing either Alt+a or Alt+Shift+a,
 * depending on the internet browser.
 * @type {string}
 */
View.eraseAccessKey = 'a';

/**
 * Initialise the search field frame.
 * @param {EventHandlers} eventHandlers The event handlers.
 */
View.initialise = function (eventHandlers) {
  View._create(eventHandlers);
};

/**
 * Set the HTML contents of the area below the search field.
 * @param {string} contents The HTML contents.
 */
View.setContentsHtml = function (contents) {
  var newNode = View.contentNode.cloneNode(false);
  newNode.innerHTML = contents;
  View.contentNodeParent.replaceChild(newNode, View.contentNode);
  View.contentNode = newNode;
};

/**
 * Set the value displayed in the search field.
 * @param {string} value The value to display.
 */
View.setSearchFieldValue = function (value) {
  if (View.searchField.value !== value) {
    View.searchField.value = value;
  }
};

/**
 * @return {string} The current value displayed in the search field.
 */
View.getSearchFieldValue = function () {
  return View.searchField.value;
};

/**
 * Give focus to the search field.
 */
View.focusOnSearchField = function () {
  View.searchField.focus();
};

/**
 * Clear the search field.
 */
View.clearSearchField = function () {
  Query.update('');
  View.focusOnSearchField();
  Search.performIfSearchStringHasChanged();
};

/**
 * Create the view elements and add them to the current document.
 * @param {EventHandlers} eventHandlers The event handlers.
 */
View._create = function (eventHandlers) {
  var tableElement = document.createElement('table');
  var tableRowElementOne = document.createElement('tr');
  var tableDataCellElementOne = document.createElement('td');
  var tableRowElementTwo = document.createElement('tr');
  var tableDataCellElementTwo = document.createElement('td');

  View.searchField = View._createSearchField(eventHandlers);
  var webKitBrowser = RegExp(' AppleWebKit/').test(navigator.userAgent);
  if (View.searchField.type === 'text' || !webKitBrowser) {
    // Three cases:
    // 1) WebKit browsers, e.g. Google Chrome and Safari, will display an
    //    erase button on type="search" input fields once some text has been
    //    entered into the field. Do not manually add an erase button.
    // 2) If searchType.type === 'text', this is a pre-HTML5 browser that is
    //    unaware of the search type. Manually add an erase button.
    // 3) HTML5-aware versions of Mozilla Firefox will report that
    //    searchType.type is 'search', but will not change the behaviour of the
    //    field. Manually add an erase button.
    var eraseButton = View._createEraseButton(eventHandlers);
  }
  var optionsLink = View._createOptionsLink(eventHandlers);
  var helpLink = View._createHelpLink(eventHandlers);
  View.contentNodeParent = tableRowElementTwo;
  View.contentNode = tableDataCellElementTwo;

  tableElement.appendChild(tableRowElementOne);
  tableRowElementOne.appendChild(tableDataCellElementOne);
  tableDataCellElementOne.appendChild(View.searchField);
  if (eraseButton) {
    tableDataCellElementOne.appendChild(eraseButton);
  }
  tableDataCellElementOne.appendChild(document.createElement('br'));
  tableDataCellElementOne.appendChild(optionsLink);
  var nbsp = '\u00A0';
  tableDataCellElementOne.appendChild(document.createTextNode(nbsp + nbsp));
  tableDataCellElementOne.appendChild(helpLink);
  tableElement.appendChild(tableRowElementTwo);
  tableRowElementTwo.appendChild(tableDataCellElementTwo);

  [
    tableElement,
    tableRowElementOne,
    tableDataCellElementOne,
    tableRowElementTwo,
    tableDataCellElementTwo,
  ].forEach(function (element) {
    element.style.border = '0';
    element.style.width = '100%';
    element.style.padding = '4px';
  });

  tableElement.style['border-collapse'] = 'collapse';

  tableDataCellElementOne.style['position'] = 'fixed';
  tableDataCellElementOne.style['background-color'] = 'white';

  tableDataCellElementTwo.style['padding-top'] = '4em';

  while (document.body.firstChild) {
    document.body.removeChild(document.body.firstChild);
  }
  document.body.appendChild(tableElement);
};

/**
 * @param {EventHandlers} eventHandlers The event handlers.
 * @return {Element} The search field element.
 */
View._createSearchField = function (eventHandlers) {
  var searchField = document.createElement('input');
  searchField.setAttribute('type', 'search');
  searchField.setAttribute('spellcheck', 'false');
  searchField.setAttribute('autofocus', 'true');
  searchField.addEventListener('keyup', eventHandlers.searchFieldKeyup, false);
  searchField.addEventListener(
    'keydown',
    eventHandlers.searchFieldKeydown,
    false
  );
  searchField.addEventListener(
    'input',
    eventHandlers.searchFieldChanged,
    false
  );
  searchField.addEventListener('focus', eventHandlers.searchFieldFocus, false);
  if (View.searchAccessKey) {
    searchField.setAttribute('accesskey', View.searchAccessKey);
  }
  return searchField;
};

/**
 * @param {EventHandlers} eventHandlers The event handlers.
 * @return {Element} The erase button element.
 */
View._createEraseButton = function (eventHandlers) {
  var eraseButton = document.createElement('input');
  eraseButton.setAttribute('type', 'image');
  eraseButton.setAttribute(
    'src',
    'data:image/gif;base64,' +
      'R0lGODlhDQANAJEDAM%2FPz%2F%2F%2F%2F93d3UpihSH5BAEAAAMALAAAAAANAA0AAAI' +
      'wnCegcpcg4nIw2sRGDZYnBAWiIHJQRZbec5XXEqnrmXIupMWdZGCXlAGhJg0h7lAAADs%' +
      '3D'
  );
  eraseButton.setAttribute('style', 'margin-left: 2px');
  eraseButton.addEventListener('click', eventHandlers.eraseButtonClick, false);
  if (View.eraseAccessKey) {
    eraseButton.setAttribute('accesskey', View.eraseAccessKey);
  }
  return eraseButton;
};

/**
 * @param {EventHandlers} eventHandlers The event handlers.
 * @return {Element} The options page link element.
 */
View._createOptionsLink = function (eventHandlers) {
  var anchorElement = document.createElement('a');
  anchorElement.setAttribute('href', 'javascript:void(0);');
  anchorElement.textContent = Messages.get('optionsAnchor');
  anchorElement.addEventListener(
    'click',
    eventHandlers.optionsLinkClicked,
    false
  );
  var fontElement = document.createElement('font');
  fontElement.setAttribute('size', '-2');
  fontElement.appendChild(anchorElement);
  return fontElement;
};

/**
 * @param {EventHandlers} eventHandlers The event handlers.
 * @return {Element} The help link element.
 */
View._createHelpLink = function (eventHandlers) {
  var anchorElement = document.createElement('a');
  anchorElement.setAttribute('href', 'javascript:void(0);');
  anchorElement.textContent = Messages.get('helpAnchor');
  anchorElement.addEventListener('click', eventHandlers.helpLinkClicked, false);
  var fontElement = document.createElement('font');
  fontElement.setAttribute('size', '-2');
  fontElement.appendChild(anchorElement);
  return fontElement;
};

/*
 * ----------------------------------------------------------------------------
 * Query
 * ----------------------------------------------------------------------------
 */

/**
 * @class Query Constructs the text entered into the search field into a search
 *     query.
 */
Query = {
  packageOrClassSearchString: '',
  memberOrKeywordSearchString: null,
  menuSearchString: null,
  timeoutId: null,
};

/**
 * @return {string} The portion of the search query that relates to the
 *     packages and classes search.
 */
Query.getPackageOrClassSearchString = function () {
  return Query.packageOrClassSearchString;
};

/**
 * @return {string} The portion of the search query that relates to the class
 *     members and keywords search.
 */
Query.getMemberOrKeywordSearchString = function () {
  return Query.memberOrKeywordSearchString;
};

/**
 * @return {string} The portion of the search query that relates to the
 *     package menu or class menu.
 */
Query.getMenuSearchString = function () {
  return Query.menuSearchString;
};

/**
 * @return {string} The entire search query.
 */
Query.getEntireSearchString = function () {
  var searchString = Query.packageOrClassSearchString;
  if (Query.memberOrKeywordSearchString !== null) {
    searchString += '#';
    searchString += Query.memberOrKeywordSearchString;
  }
  if (Query.menuSearchString !== null) {
    searchString += '@';
    searchString += Query.menuSearchString;
  }
  return searchString;
};

/**
 * Update this query based on the contents of the search field.
 * @param {string} searchFieldContents The contents of the search field.
 */
Query.update = function (searchFieldContents) {
  Query._processInput(searchFieldContents);

  /*
   * Update the view on a timer (see r204) as a workaround for a Webkit bug:
   * https://bugs.webkit.org/show_bug.cgi?id=34374
   *
   * This workaround is no longer necessary since at least Google Chrome
   * 12.0.742.112 and Safari 5.1.
   *
   * However, it shouldn't be removed, because the script may be running under
   * an older version of Safari. This isn't a problem for Google Chrome, which
   * will only install extensions that are compatible with the browser version.
   */
  if (Query.timeoutId !== null) {
    window.clearTimeout(Query.timeoutId);
  }
  Query.timeoutId = window.setTimeout(function () {
    Query._updateView.apply(Query);
  }, 0);
};

/**
 * Process the search field input.
 * @param {string} searchFieldContents The contents of the search field.
 */
Query._processInput = function (searchFieldContents) {
  var searchString;
  if (Query.menuSearchString !== null) {
    searchString = Query.packageOrClassSearchString;
    if (Query.memberOrKeywordSearchString !== null) {
      searchString += '#' + Query.memberOrKeywordSearchString;
    }
    if (searchFieldContents.indexOf('@') !== -1) {
      searchString += searchFieldContents;
    }
  } else if (Query.memberOrKeywordSearchString !== null) {
    searchString = Query.packageOrClassSearchString + searchFieldContents;
  } else {
    searchString = searchFieldContents;
  }

  var tokens = [];
  var splitOnPrefix;
  ['@', '#'].forEach(function (prefix) {
    if (searchString.indexOf(prefix) !== -1) {
      splitOnPrefix = searchString.split(prefix, 2);
      tokens.push(splitOnPrefix[1]);
      searchString = splitOnPrefix[0];
    } else {
      tokens.push(null);
    }
  });

  Query.packageOrClassSearchString = searchString;
  Query.memberOrKeywordSearchString = tokens[1];
  Query.menuSearchString = tokens[0];
};

/**
 * Update the view.
 */
Query._updateView = function () {
  var fieldValue = Query.getEntireSearchString();
  ['#', '@'].forEach(function (prefix) {
    if (fieldValue.indexOf(prefix) !== -1) {
      fieldValue = prefix + fieldValue.split(prefix, 2)[1];
    }
  });

  View.setSearchFieldValue(fieldValue);
};

/*
 * ----------------------------------------------------------------------------
 * RegexLibrary
 * ----------------------------------------------------------------------------
 */

/**
 * @class RegexLibrary Library of regular expressions used by this script.
 */
RegexLibrary = {};

/**
 * Create and return a function that will take a {PackageLink}, {ClassLink},
 * {MemberLink} or {KeywordLink} as an argument and return whether that link
 * matches the given search string.
 * @param {string} searchString The search string.
 * @return {function(PackageLink|ClassLink|MemberLink|KeywordLink): boolean}
 *     The condition function.
 */
RegexLibrary.createCondition = function (searchString) {
  if (searchString.length === 0 || searchString === '*') {
    return function (link) {
      return true;
    };
  }

  var pattern = RegexLibrary._getRegex(searchString);

  return function (link) {
    return link.matches(pattern);
  };
};

/**
 * Create and return a function that will take a {PackageLink}, {ClassLink},
 * {MemberLink} or {KeywordLink} as an argument and return whether that link
 * is a case-sensitive exact match for the given search string.
 * @param {string} searchString The search string.
 * @return {function(PackageLink|ClassLink|MemberLink|KeywordLink): boolean}
 *     The condition function.
 */
RegexLibrary.createCaseInsensitiveExactMatchCondition = function (
  searchString
) {
  return RegexLibrary._createExactMatchCondition(searchString, false);
};

/**
 * Create and return a function that will take a {PackageLink}, {ClassLink},
 * {MemberLink} or {KeywordLink} as an argument and return whether that link
 * is a case-sensitive exact match for the given search string.
 * @param {string} searchString The search string.
 * @return {function(PackageLink|ClassLink|MemberLink|KeywordLink): boolean}
 *     The condition function.
 */
RegexLibrary.createCaseSensitiveExactMatchCondition = function (searchString) {
  return RegexLibrary._createExactMatchCondition(searchString, true);
};

/**
 * @param {string} searchString The search string.
 * @param {boolean} caseSensitive True for a case-sensitive match, false for
 *                  case-insensitive.
 * @return {function(PackageLink|ClassLink|MemberLink|KeywordLink): boolean}
 *     The condition function.
 */
RegexLibrary._createExactMatchCondition = function (
  searchString,
  caseSensitive
) {
  if (searchString.length === 0 || searchString.indexOf('*') !== -1) {
    return function (link) {
      return false;
    };
  }

  var pattern = RegexLibrary._getExactMatchRegex(searchString, caseSensitive);

  return function (link) {
    return link.matches(pattern);
  };
};

/**
 * @param {string} searchString The search string.
 * @return {RegExp} The regular expression for the search string.
 */
RegexLibrary._getRegex = function (searchString) {
  // Replace repeated wildcards with a single wildcard character.
  searchString = searchString.replace(/\*{2,}/g, '*');
  // If the search string ends with a wildcard, remove it.
  searchString = searchString.replace(/\*$/, '');

  // Construct the camel case regular expression.
  var camelCasePattern = '';
  var i = 0;
  while (i < searchString.length) {
    if (searchString[i] === '*') {
      // The '*' character is a wildcard.
      i++;
      camelCasePattern += '.*';
    } else if (searchString[i] === '.') {
      // The '.' character is matched directly. The next character (unless it
      // is a wildcard) starts a new camel case expression. This allows, e.g.
      // "j.l.O" to match "java.lang.Object".
      i++;
      camelCasePattern += '\\' + '.';
    } else {
      // A camel case expression, which continues until an uppercase or digit
      // character starts the next camel case expression, or a '*' or '.'
      // character is found. Digits can start a new camel case expression, so
      // that e.g. "P2D" will match "Point2D".
      var endOfCamelCase = searchString.substring(i + 1).search(/[A-Z\d\*\.]/);
      var expression;
      if (endOfCamelCase === -1) {
        expression = searchString.substring(i);
      } else {
        expression = searchString.substring(i, i + endOfCamelCase + 1);
      }
      i += expression.length;

      camelCasePattern +=
        RegexLibrary._escape(expression[0]) +
        RegexLibrary._caseInsensitivePattern(expression.substring(1));
      // The camel case regular expression will match any number of lowercase
      // or digit characters following the search term. Digits are accepted so
      // that, e.g. "PD" will match "Point2D".
      camelCasePattern += '[a-z\\d]*';
      if (/[A-Z]/.test(expression[0])) {
        // This camel case expression starts with an uppercase character, so
        // it will match classes. Accept an optional trailing '.' character to
        // make it easier to search for inner classes, e.g. "CUB" will match
        // "Character.UnicodeBlock".
        camelCasePattern += '\\.?';
      }
    }
  }

  // The search term can also be matched without camel case.
  var textPattern = RegexLibrary._caseInsensitivePattern(searchString, '*');
  textPattern = textPattern.replace(/\*/g, '.*');

  var pattern = '^((' + camelCasePattern + ')|(' + textPattern + ')).*$';
  return new RegExp(pattern);
};

/**
 * @param {string} searchString The search string.
 * @param {boolean} caseSensitive True for a case-sensitive match, false for
 *                  case-insensitive.
 * @return {RegExp} The exact match regular expression for the search string.
 */
RegexLibrary._getExactMatchRegex = function (searchString, caseSensitive) {
  var pattern = '^' + RegexLibrary._escape(searchString) + '$';
  return caseSensitive ? new RegExp(pattern) : new RegExp(pattern, 'i');
};

/**
 * Return a regular expression pattern that will perform a case-insensitive
 * match of the given string.
 * @param {string} str The input string.
 * @param {string} charactersToIgnore These characters will be left in the
 *                 string without being replaced.
 * @return {string} The regular expression pattern.
 */
RegexLibrary._caseInsensitivePattern = function (str, charactersToIgnore) {
  var result = [];
  for (var i = 0; i < str.length; i++) {
    var c = str[i];
    if (charactersToIgnore && charactersToIgnore.indexOf(c) !== -1) {
      result.push(c);
    } else {
      if (/[A-Za-z]/.test(c)) {
        result.push('[' + c.toUpperCase() + c.toLowerCase() + ']');
      } else {
        result.push(RegexLibrary._escape(c));
      }
    }
  }
  return result.join('');
};

/**
 * Escape the given string so that it will be treated as a literal within a
 * regular expression.
 * @param {string} str The input string.
 * @return {string} The escaped string.
 */
RegexLibrary._escape = function (str) {
  return str.replace(/([.*+?^=!:${}()|\[\]\/\\])/g, '\\$1');
};

/*
 * ----------------------------------------------------------------------------
 * Callback
 * ----------------------------------------------------------------------------
 */

/**
 * A callback function in the context of a specified object.
 * @param {function(*): *} callbackFunction The callback function.
 * @param {*} thisObject The "this" object used when calling the function.
 * @constructor
 */
Callback = function (callbackFunction, thisObject) {
  this.callbackFunction = callbackFunction;
  this.thisObject = thisObject;
};

/**
 * Invoke this callback function with the given arguments.
 * @param {Array.<*>=} opt_argsArray An array of arguments to pass to the
 *     callback function. If not provided, no arguments will be passed to the
 *     callback function.
 * @return {*} The function result.
 */
Callback.prototype.invoke = function (opt_argsArray) {
  return this.callbackFunction.apply(this.thisObject, opt_argsArray);
};

/*
 * ----------------------------------------------------------------------------
 * Search
 * ----------------------------------------------------------------------------
 */

/**
 * @class Search The searching functionality.
 */
Search = {
  previousEntireSearchString: null,
  timeoutId: null,
  topLink: null,
};

/**
 * Perform a search.
 */
Search.perform = function () {
  var entireSearchString = Query.getEntireSearchString();
  Search._performSearch(entireSearchString);
  Search.previousEntireSearchString = entireSearchString;
};

/**
 * Perform a search after a short delay only if the search string has changed.
 */
Search.performIfSearchStringHasChanged = function () {
  var entireSearchString = Query.getEntireSearchString();
  if (entireSearchString !== Search.previousEntireSearchString) {
    if (Search.timeoutId !== null) {
      window.clearTimeout(Search.timeoutId);
    }
    Search.timeoutId = window.setTimeout(function () {
      Search.perform.apply(Search);
    }, 100);
  }
  Search.previousEntireSearchString = entireSearchString;
};

/**
 * @return {string} The URL of the link currently displayed at the top of the
 *     list, or null if no links are currently displayed.
 */
Search.getTopLinkUrl = function () {
  if (Search.topLink) {
    return Search.topLink.getUrl();
  }
  return null;
};

/**
 * @param {string} entireSearchString The search string.
 */
Search._performSearch = function (entireSearchString) {
  Storage.get(Option.CLASS_MENU, function (classMenu) {
    Storage.get(Option.PACKAGE_MENU, function (packageMenu) {
      var searchContext = {};
      searchContext.classMenu = classMenu;
      searchContext.packageMenu = packageMenu;

      Search._PackagesAndClasses._perform(
        searchContext,
        Query.getPackageOrClassSearchString()
      );
      Search._ClassMembersAndKeywords._perform(
        searchContext,
        Query.getMemberOrKeywordSearchString()
      );
      Search._Menu._perform(searchContext, Query.getMenuSearchString());

      if (searchContext.getContentsHtmlCallback) {
        var contentsHtml = searchContext.getContentsHtmlCallback.invoke();
        View.setContentsHtml(contentsHtml);
      }

      Search.topLink =
        searchContext.topMemberOrKeywordLink ||
        searchContext.topPackageOrClassLink;

      if (searchContext.menuPageOpened) {
        Search._collapseMenu();
      }
    });
  });
};

/**
 * Collapse the menu after an external page has been opened.
 */
Search._collapseMenu = function () {
  Query.update('');
  Search.perform();
};

/*
 * ----------------------------------------------------------------------------
 * Search._PackagesAndClasses
 * ----------------------------------------------------------------------------
 */

/**
 * @class Search._PackagesAndClasses Component of the search functionality that
 *     deals with package and class links.
 */
Search._PackagesAndClasses = {
  previousQuery: null,
  currentLinks: null,
  bestMatch: null,
  topLink: null,
};

/**
 * Perform this portion of the search.
 * @param {Object} searchContext Object which allows this search component to
 *     store a result and to inspect results provided by earlier search
 *     components.
 * @param {string} searchString The search string.
 */
Search._PackagesAndClasses._perform = function (searchContext, searchString) {
  var module = Search._PackagesAndClasses;

  if (module.previousQuery === null || module.previousQuery !== searchString) {
    if (
      module.previousQuery !== null &&
      searchString.indexOf(module.previousQuery) === 0
    ) {
      // Characters have been added to the end of the previous query. Start
      // with the current search list and filter out any links that do not
      // match.
    } else {
      // Otherwise, start with the complete search list.
      module.currentLinks = ALL_PACKAGE_AND_CLASS_LINKS.concat();
    }

    var condition = RegexLibrary.createCondition(searchString);
    module.currentLinks = module.currentLinks.filter(condition);
    module.bestMatch = module._getBestMatch(searchString, module.currentLinks);
    module.topLink = module._getTopLink(module.currentLinks, module.bestMatch);
  }

  module.previousQuery = searchString;

  searchContext.topPackageOrClassLink = module.topLink;
  searchContext.getContentsHtmlCallback = new Callback(
    module._constructHtml,
    module
  );
};

/**
 * @param {Array.<PackageLink|ClassLink>} links The package and class links
 *     matched by the current search.
 * @param {PackageLink|ClassLink} bestMatch The best match link.
 * @return {PackageLink|ClassLink} The top link.
 */
Search._PackagesAndClasses._getTopLink = function (links, bestMatch) {
  if (bestMatch) {
    return bestMatch;
  }
  if (links.length > 0) {
    return links[0];
  }
  return null;
};

/**
 * Get the best match (if any) from the given array of links.
 * @param {string} searchString The search string.
 * @param {Array.<PackageLink|ClassLink>} links The package and class links
 *     matched by the current search.
 * @return {PackageLink|ClassLink=} The best match.
 */
Search._PackagesAndClasses._getBestMatch = function (searchString, links) {
  if (links.length <= 1 || searchString === '') {
    // No need to display a best match.
    return null;
  }

  function filterBestMatches(condition) {
    var result = links.filter(condition);
    if (result.length > 0) {
      links = result;
    }
  }

  // Look for a case-insensitive exact match.
  filterBestMatches(
    RegexLibrary.createCaseInsensitiveExactMatchCondition(searchString)
  );

  // Look for a case-sensitive exact match.
  filterBestMatches(
    RegexLibrary.createCaseSensitiveExactMatchCondition(searchString)
  );

  // Keep only the links with the lowest package depth.
  var lowestPackageDepth = 1000000;
  function getDepth(link) {
    var name =
      link.getType() === LinkType.PACKAGE
        ? link.getPackageName()
        : link.getCanonicalName();
    return name.split('.').length;
  }
  links.forEach(function (link) {
    lowestPackageDepth = Math.min(lowestPackageDepth, getDepth(link));
  });
  filterBestMatches(function (link) {
    return getDepth(link) === lowestPackageDepth;
  });

  // When searching for "List", select java.util.List instead of java.awt.List.
  var javaUtilList = new ClassLink(LinkType.INTERFACE, 'java.util', 'List');
  var javaAwtList = new ClassLink(LinkType.CLASS, 'java.awt', 'List');
  if (
    links.length === 2 &&
    links[0].equals(javaUtilList) &&
    links[1].equals(javaAwtList)
  ) {
    return javaUtilList;
  }

  // If the list has been reduced to one item, then that is the best match.
  return links.length == 1 ? links[0] : null;
};

/**
 * @return {string} The HTML to display the search results.
 */
Search._PackagesAndClasses._constructHtml = function () {
  var module = Search._PackagesAndClasses;
  if (module.currentLinks.length === 0) {
    return 'No search results.';
  }
  var html = '';
  if (module.bestMatch) {
    html += '<br/><b><i>Best Match</i></b><br/>';
    html += module.bestMatch.getType().getSingularName().toLowerCase();
    html += '<br/>';
    html += module.bestMatch.getHtml();
    html += '<br/>';
  }
  var type;
  var newType;
  module.currentLinks.forEach(function (link) {
    newType = link.getType();
    if (type !== newType) {
      html += '<br/><b>' + newType.getPluralName() + '</b><br/>';
      type = newType;
    }
    html += link.getHtml();
    html += '<br/>';
  });
  return html;
};

/*
 * ----------------------------------------------------------------------------
 * Search._ClassMembersAndKeywords
 * ----------------------------------------------------------------------------
 */

/**
 * @class Search._ClassMembersAndKeywords Component of the search functionality
 *     that deals with class members and keyword links.
 */
Search._ClassMembersAndKeywords = {
  httpRequest: new HttpRequest(),

  keywords: {
    'field summary': 1,
    'nested class summary': 1,
    'constructor summary': 1,
    'constructor detail': 1,
    'method summary': 1,
    'method detail': 1,
    'field detail': 1,
  },

  keywordsToIgnore: {
    'navbar top': 1,
    'navbar top firstrow': 1,
    'skip navbar top': 1,
    'navbar bottom': 1,
    'navbar bottom firstrow': 1,
    'skip navbar bottom': 1,
  },

  keywordPrefixes: [
    'methods inherited from class ',
    'fields inherited from class ',
    'nested classes inherited from class ',
  ],
};

/**
 * Perform this portion of the search.
 * @param {Object} searchContext Object which allows this search component to
 *     store a result and to inspect results provided by earlier search
 *     components.
 * @param {string} searchString The search string.
 */
Search._ClassMembersAndKeywords._perform = function (
  searchContext,
  searchString
) {
  var module = Search._ClassMembersAndKeywords;
  var topPackageOrClassLink = searchContext.topPackageOrClassLink;
  if (searchString === null || !topPackageOrClassLink) {
    module.httpRequest.abort();
    return;
  }

  var progressCallback = function () {
    Search.perform.apply(Search);
  };

  module.httpRequest.load(topPackageOrClassLink.getUrl(), progressCallback);
  if (module.httpRequest.isComplete()) {
    var packageOrClassPageHtml = module.httpRequest.getResource();
    var memberAndKeywordLinks = module._getMemberAndKeywordLinks(
      topPackageOrClassLink.getUrl(),
      packageOrClassPageHtml
    );
    var condition = RegexLibrary.createCondition(searchString);

    var matchingMemberAndKeywordLinks = memberAndKeywordLinks.filter(condition);
    searchContext.topMemberOrKeywordLink =
      matchingMemberAndKeywordLinks.length > 0
        ? matchingMemberAndKeywordLinks[0]
        : null;

    searchContext.getContentsHtmlCallback = new Callback(function () {
      var html = '';
      if (matchingMemberAndKeywordLinks.length === 0) {
        html += 'No search results.';
      } else {
        matchingMemberAndKeywordLinks.forEach(function (memberOrKeywordLink) {
          html += memberOrKeywordLink.getHtml();
        });
      }
      return topPackageOrClassLink.getHtml() + '<p>' + html + '</p>';
    }, module);
  } else {
    searchContext.getContentsHtmlCallback = new Callback(function () {
      return (
        topPackageOrClassLink.getHtml() +
        '<p>' +
        module.httpRequest.getStatusMessage() +
        '</p>'
      );
    }, module);
    searchContext.memberAndKeywordLinksLoading = true;
  }
};

/**
 * Retrieve the member and keyword links from the given package or class page.
 * @param {string} baseUrl The URL of the page.
 * @param {string} packageOrClassPageHtml The contents of the page.
 * @return {Array.<MemberLink|KeywordLink>} The links.
 */
Search._ClassMembersAndKeywords._getMemberAndKeywordLinks = function (
  baseUrl,
  packageOrClassPageHtml
) {
  // Starting with Java 9, the "id" attribute is used instead of "name".
  var anchorRegex = /<a (name|id)=\"([^\"]+)\"/gi;
  var matches;
  var links = [];
  while ((matches = anchorRegex.exec(packageOrClassPageHtml)) !== null) {
    var name = matches[2];
    var link = Search._ClassMembersAndKeywords._createLink(baseUrl, name);
    if (link) {
      links.push(link);
    }
  }
  return Search._ClassMembersAndKeywords._sortLinks(links);
};

/**
 * Create a class member or keyword link from the given anchor name.
 * @param {string} baseUrl The URL of the package or class page.
 * @param {string} anchorName The anchor name.
 * @return {MemberLink|KeywordLink|null} The created link.
 */
Search._ClassMembersAndKeywords._createLink = function (baseUrl, anchorName) {
  var module = Search._ClassMembersAndKeywords;
  // Starting with Java 8, the keyword anchors use '.' to delimit words. Prior
  // to that, a combination of '-' and '_' was used. Replace all with spaces.
  var keywordDisplayName = anchorName.replace(/[-_.]/g, ' ');
  if (module.keywordsToIgnore[keywordDisplayName] === 1) {
    return null;
  }
  if (module.keywords[keywordDisplayName] === 1) {
    return new KeywordLink(baseUrl, anchorName, keywordDisplayName);
  }
  for (var i = 0; i < module.keywordPrefixes.length; i++) {
    var prefix = module.keywordPrefixes[i];
    if (keywordDisplayName.indexOf(prefix) === 0) {
      // Retain the original anchor name after the prefix which contains the
      // class name, e.g. 'java.awt.Window'.
      keywordDisplayName = prefix + anchorName.substring(prefix.length);
      return new KeywordLink(baseUrl, anchorName, keywordDisplayName);
    }
  }
  var displayName = anchorName;
  if ((anchorName.match(/-/g) || []).length >= 2) {
    // Starting with Java 8, the method anchors contain dashes in place of
    // brackets and between the method arguments.
    displayName = displayName.replace('-', '(');
    displayName = displayName.replace(/-$/, ')');
    displayName = displayName.replace(/-/g, ', ');
  }
  return new MemberLink(baseUrl, anchorName, displayName);
};

/**
 * Sort class member and keyword links such that the keyword links appear at
 * the end.
 * @param {Array.<MemberLink|KeywordLink>} links The links to sort.
 * @return {Array.<MemberLink|KeywordLink>} The sorted links.
 */
Search._ClassMembersAndKeywords._sortLinks = function (links) {
  var memberLinks = [];
  var keywordLinks = [];
  links.forEach(function (link) {
    if (link.getType() === LinkType.CLASS_MEMBER) {
      memberLinks.push(link);
    } else {
      keywordLinks.push(link);
    }
  }, Search._ClassMembersAndKeywords);
  return memberLinks.concat(keywordLinks);
};

/*
 * ----------------------------------------------------------------------------
 * Search._Menu
 * ----------------------------------------------------------------------------
 */

/**
 * @class Search._Menu Component of the search functionality that deals with
 *     the package menu and class menu.
 */
Search._Menu = {
  menuReplacement: null,
};

/**
 * Perform this portion of the search.
 * @param {Object} searchContext Object which allows this search component to
 *     store a result and to inspect results provided by earlier search
 *     components.
 * @param {string} searchString The search string.
 */
Search._Menu._perform = function (searchContext, searchString) {
  var module = Search._Menu;
  var topPackageOrClassLink = searchContext.topPackageOrClassLink;
  var topMemberOrKeywordLink = searchContext.topMemberOrKeywordLink;

  var performMenuSearch =
    searchString !== null &&
    topPackageOrClassLink &&
    !searchContext.memberAndKeywordLinksLoading &&
    topMemberOrKeywordLink !== null;
  if (!performMenuSearch) {
    return;
  }

  var menuReplacement = module._getMenuReplacement();
  var menu = module._constructMenu(
    searchContext,
    menuReplacement,
    topPackageOrClassLink,
    topMemberOrKeywordLink
  );

  searchContext.getContentsHtmlCallback = new Callback(function () {
    var html = topPackageOrClassLink.getHtml();
    if (topMemberOrKeywordLink) {
      html += '<br/>' + topMemberOrKeywordLink.getHtml();
    }
    html += '<p>' + module._constructMenuHtml(menu) + '</p>';
    return html;
  }, module);

  if (!searchString) {
    return;
  }

  for (var i = 0; i < menu.length; i++) {
    var menuElement = menu[i];
    if (menuElement.mnemonic === '@' + searchString) {
      Frames.openLinkInNewTab(menuElement.url);
    }
  }

  searchContext.menuPageOpened = true;
};

/**
 * Construct the menu.
 * @param {Object} searchContext The search context.
 * @param {{Object.<function(ClassLink|PackageLink,MemberLink)>}}
 *     memberReplacement An object containing, for each placeholder value, a
 *     function to resolve that value.
 * @param {ClassLink|PackageLink} classOrPackageLink The current class link or
 *     package link.
 * @param {MemberLink|KeywordLink} memberOrKeywordLink The current member link
 *     or keyword link.
 * @return {Array.<{mnemonic: string, label: string, url: string}>} The menu
 *     items.
 */
Search._Menu._constructMenu = function (
  searchContext,
  menuReplacement,
  classOrPackageLink,
  memberOrKeywordLink
) {
  var classMemberLink;
  if (
    memberOrKeywordLink &&
    memberOrKeywordLink.getType() === LinkType.CLASS_MEMBER
  ) {
    classMemberLink = memberOrKeywordLink;
  }

  var menuDefinition;
  if (classOrPackageLink && classOrPackageLink.getType() === LinkType.PACKAGE) {
    menuDefinition = searchContext.packageMenu;
  } else {
    menuDefinition = searchContext.classMenu;
  }

  var menu = [];
  menuDefinition.split('\n').forEach(function (menuAnchorDefinition) {
    var splitOnArrow = splitOnFirst(menuAnchorDefinition, '->');
    if (splitOnArrow.length === 2) {
      var mnemonicAndLabel = splitOnFirst(splitOnArrow[0], ':');
      if (mnemonicAndLabel.length === 2) {
        var mnemonic = mnemonicAndLabel[0];
        var label = mnemonicAndLabel[1];
        var url = splitOnArrow[1];

        var matches;
        while ((matches = /##(\w+)##/.exec(url)) !== null) {
          var f = menuReplacement[matches[1]];
          var rx2 = new RegExp(matches[0], 'g');
          if (f) {
            url = url.replace(rx2, f(classOrPackageLink, classMemberLink));
          } else {
            url = url.replace(rx2, '');
          }
        }

        menu.push({ mnemonic: mnemonic, label: label, url: url });
      }
    }
  });

  return menu;
};

/**
 * Placeholder values that can be entered into the class_menu or package_menu
 * options and will, when the menu is opened, be replaced with data relevant
 * to the current package or class.
 * @return {Object.<function(ClassLink|PackageLink,MemberLink)>} An object
 *     containing, for each placeholder value, a function to resolve that
 *     value.
 */
Search._Menu._getMenuReplacement = function () {
  if (!Search._Menu.menuReplacement) {
    var memberNameFunction = function (classOrPackageLink, classMemberLink) {
      return classMemberLink ? classMemberLink.getName() : '';
    };
    Search._Menu.menuReplacement = {
      CLASS_NAME: function (classLink) {
        return classLink ? classLink.getClassName() : '';
      },
      PACKAGE_NAME: function (classOrPackageLink) {
        return classOrPackageLink ? classOrPackageLink.getPackageName() : '';
      },
      PACKAGE_PATH: function (classOrPackageLink) {
        return classOrPackageLink
          ? classOrPackageLink.getPackageName().replace(/\./g, '/')
          : '';
      },
      MEMBER_NAME: memberNameFunction,
      METHOD_NAME: memberNameFunction, // Synonym for MEMBER_NAME.
      FIELD_NAME: memberNameFunction, // Synonym for MEMBER_NAME.
      ANCHOR_NAME: memberNameFunction, // Deprecated synonym for MEMBER_NAME.
    };
  }
  return Search._Menu.menuReplacement;
};

/**
 * @param {{Array.<{mnemonic: string, label: string, url: string}>}} menu The
 *     menu items.
 * @return {string} An HTML representation of the menu items.
 */
Search._Menu._constructMenuHtml = function (menu) {
  var menuHtml = '';
  menu.forEach(function (menuElement) {
    menuHtml +=
      '<A HREF="' +
      menuElement.url +
      '">' +
      menuElement.mnemonic +
      ':' +
      menuElement.label +
      '</A><BR/>';
  });
  return menuHtml;
};

/*
 * ----------------------------------------------------------------------------
 * Main script
 * ----------------------------------------------------------------------------
 */

/**
 * Initialise this script.
 * @param {boolean} packageFrameHidden Whether the package frame has been hidden.
 */
function init(packageFrameHidden) {
  // Retrieve the inner HTML of the class frame.
  var classesInnerHtml = getClassesInnerHtml();

  // Initialise stored package and class links.
  var classLinks = getClassLinks(classesInnerHtml);
  var packageAndClassLinks;
  if (packageFrameHidden) {
    var packageLinks = getPackageLinks(classLinks);
    packageAndClassLinks = packageLinks.concat(classLinks);
  } else {
    packageAndClassLinks = classLinks;
  }
  if (packageAndClassLinks.length === 0) {
    // Another instance of this script is already running and it has not yet
    // added the package and class links to the page.
    return;
  }
  ALL_PACKAGE_AND_CLASS_LINKS = packageAndClassLinks;

  // Initialise class frame.
  View.initialise(EventHandlers);

  // Perform an initial search. This will populate the class frame with the
  // entire list of packages and classes.
  Search.perform();

  // Give focus to the search field.
  View.focusOnSearchField();
}

/**
 * Parse packages from the given array of {ClassLink} objects.
 * @param {Array.<ClassLink>} classLinks The class links.
 * @return {Array.<PackageLink>} The package links.
 */
function getPackageLinks(classLinks) {
  var packageLinks = [];
  var packageLinksAdded = {};
  var packageName;

  classLinks.forEach(function (classLink) {
    packageName = classLink.getPackageName();
    if (!packageLinksAdded[packageName]) {
      packageLinks.push(new PackageLink(packageName));
      packageLinksAdded[packageName] = true;
    }
  });

  packageLinks.sort(function (packageLinkOne, packageLinkTwo) {
    var packageNameOneComponents = packageLinkOne.getPackageName().split(/\./);
    var packageNameTwoComponents = packageLinkTwo.getPackageName().split(/\./);
    var smallerLength = Math.min(
      packageNameOneComponents.length,
      packageNameTwoComponents.length
    );
    for (i = 0; i < smallerLength; i++) {
      if (packageNameOneComponents[i] < packageNameTwoComponents[i]) {
        return -1;
      }
      if (packageNameOneComponents[i] > packageNameTwoComponents[i]) {
        return 1;
      }
    }
    return packageNameOneComponents.length - packageNameTwoComponents.length;
  });

  return packageLinks;
}

/**
 * @return {string} The inner HTML of the body element of the classes list
 *    frame, or undefined if the element does not exist.
 */
function getClassesInnerHtml() {
  var classesInnerHtml;
  if (document && document.body) {
    classesInnerHtml = document.body.innerHTML;
  }
  return classesInnerHtml;
}

/**
 * Parse interfaces, classes, enumerations, and annotations from the inner HTML
 * of the body element of the classes list frame.
 * @param {string} classesInnerHtml The inner HTML of the body element of the
 *     classes list frame.
 * @return {Array.<ClassLink>} The class links.
 */
function getClassLinks(classesInnerHtml) {
  if (!classesInnerHtml) {
    return [];
  }

  var matches;
  var classLinksMap = {};
  var classLinkTypes = [
    LinkType.PACKAGE,
    LinkType.INTERFACE,
    LinkType.CLASS,
    LinkType.ENUM,
    LinkType.EXCEPTION,
    LinkType.ERROR,
    LinkType.ANNOTATION,
    LinkType.SCRIPT,
  ];
  classLinkTypes.forEach(function (type) {
    classLinksMap[type] = [];
  });

  function checkForExceptionOrErrorType(type, className) {
    if (type === LinkType.CLASS) {
      if (endsWith(className, 'Exception')) {
        type = LinkType.EXCEPTION;
      } else if (endsWith(className, 'Error')) {
        type = LinkType.ERROR;
      }
    }
    return type;
  }

  var classesRegexWithTitle =
    /<a\s+href\s*=\s*\"([^\"]+)\.html?\"\s*title\s*=\s*\"\s*([^\s]+)\s/gi;
  var anchorWithTitleFound = false;
  while ((matches = classesRegexWithTitle.exec(classesInnerHtml)) !== null) {
    var hrefTokens = matches[1].split('/');
    if (hrefTokens.length >= 2) {
      var packageName = hrefTokens.slice(0, -1).join('.');
      var className = hrefTokens[hrefTokens.length - 1];
      var type = LinkType.getByName(matches[2]);
      if (type) {
        type = checkForExceptionOrErrorType(type, className);

        var classLink = new ClassLink(type, packageName, className);
        classLinksMap[type].push(classLink);
        anchorWithTitleFound = true;
      }
    }
  }

  if (!anchorWithTitleFound) {
    // Javadoc created with Java 1.2 or 1.3. The anchors do not have title
    // attributes, but the interfaces can be identified by looking for a
    // surrounding italic element. There are no enumerations or annotations.
    var classesWithoutTitleRegex =
      /<a\s+href\s*=\s*\"([^\"]+)\.html?\"\s*[^>]*>(\s*<i\s*>)?\s*(?:[^<]+)(?:<\/i\s*>\s*)?<\/a\s*>/gi;
    while (
      (matches = classesWithoutTitleRegex.exec(classesInnerHtml)) !== null
    ) {
      var hrefTokens = matches[1].split('/');
      if (hrefTokens.length >= 2) {
        var packageName = hrefTokens.slice(0, -1).join('.');
        var className = hrefTokens[hrefTokens.length - 1];
        var openingItalicTag = matches[2];
        var type = openingItalicTag ? LinkType.INTERFACE : LinkType.CLASS;
        type = checkForExceptionOrErrorType(type, className);

        var classLink = new ClassLink(type, packageName, className);
        classLinksMap[type].push(classLink);
      }
    }
  }

  var classLinks = [];
  classLinkTypes.forEach(function (type) {
    classLinks = classLinks.concat(classLinksMap[type]);
  });
  return classLinks;
}

/**
 * Determine whether stringOne ends with stringTwo.
 * @param {string} stringOne The first string.
 * @param {string} stringTwo The second string.
 * @return {boolean} Whether stringOne ends with stringTwo.
 */
function endsWith(stringOne, stringTwo) {
  if (!stringOne) {
    return false;
  }
  var strIndex = stringOne.length - stringTwo.length;
  return strIndex >= 0 && stringOne.substring(strIndex) === stringTwo;
}

/**
 * Trim whitespace from the start of the given string.
 * @param {string} stringToTrim The string to trim.
 * @return {string} The trimmed string.
 */
function trimFromStart(stringToTrim) {
  return stringToTrim.replace(/^\s+/, '');
}

/**
 * Trim whitespace from the end of the given string.
 * @param {string} stringToTrim The string to trim.
 * @return {string} The trimmed string.
 */
function trimFromEnd(stringToTrim) {
  return stringToTrim.replace(/\s+$/, '');
}

/**
 * Split the given string on the first occurence of the given separator string.
 * Any whitespace surrounding the first occurence of the separator will be
 * removed.
 * @param {string} stringToSplit The string to split.
 * @param {string} separator The separator string.
 * @return {Array.<string>} An array containing two elements: the portion of
 *     the string found before the first occurence of the separator, and the
 *     portion of the string found after the first occurence of the separator.
 */
function splitOnFirst(stringToSplit, separator) {
  var firstOccurrence = stringToSplit.indexOf(separator);
  if (firstOccurrence === -1) {
    return [stringToSplit, ''];
  }
  return [
    trimFromEnd(stringToSplit.substring(0, firstOccurrence)),
    trimFromStart(
      stringToSplit.substring(
        firstOccurrence + separator.length,
        stringToSplit.length
      )
    ),
  ];
}

/*
 * ----------------------------------------------------------------------------
 * EventHandlers
 * ----------------------------------------------------------------------------
 */

/**
 * @class EventHandlers Called by the view to handle UI events.
 */
EventHandlers = {};

/**
 * Called on a 'keyup' event while the search field has focus.
 * @param {Event} evt The event.
 */
EventHandlers.searchFieldKeyup = function (evt) {
  var code = evt.keyCode;
  if (code === 13) {
    EventHandlers._returnKeyPressed(evt.ctrlKey);
  } else if (code === 27) {
    EventHandlers._escapeKeyPressed();
  }
};

/**
 * Called on a 'keydown' event while the search field has focus.
 * @param {Event} evt The event.
 */
EventHandlers.searchFieldKeydown = function (evt) {
  // Disable the default behaviour of completely clearing the search field when
  // the ESCAPE key is pressed. Instead, the ESCAPE key will clear only the
  // currently displayed portion of the search query.
  if (evt.keyCode === 27) {
    evt.preventDefault();
  }
};

/**
 * Called when the contents of the search field has changed.
 */
EventHandlers.searchFieldChanged = function () {
  var searchFieldContents = View.getSearchFieldValue();
  Query.update(searchFieldContents);
  Search.performIfSearchStringHasChanged();
};

/**
 * Called when the search field has gained focus.
 */
EventHandlers.searchFieldFocus = function () {
  document.body.scrollLeft = 0;
};

/**
 * Caled when the erase button has been clicked.
 */
EventHandlers.eraseButtonClick = function () {
  View.clearSearchField();
};

/**
 * Called when the Options link has been clicked.
 * @param {Event} evt The event.
 */
EventHandlers.optionsLinkClicked = function (evt) {
  OptionsPage.open();
  evt.preventDefault();
};

/**
 * Called when the help link has been clicked.
 * @param {Event} evt The event.
 */
EventHandlers.helpLinkClicked = function (evt) {
  var url =
    'https://github.com/StevenGBrown/javadoc-search-frame/wiki/Features';
  Frames.openLinkInNewTab(url);
  evt.preventDefault();
};

/**
 * Called when the return key has been pressed while the search field has
 * focus.
 * @param {boolean} ctrlModifier Whether the CTRL key was held down when the
 *     return key was pressed.
 */
EventHandlers._returnKeyPressed = function (ctrlModifier) {
  var searchFieldValue = View.getSearchFieldValue();
  Query.update(searchFieldValue);
  Search.performIfSearchStringHasChanged();

  var url = Search.getTopLinkUrl();
  if (url) {
    if (ctrlModifier) {
      Frames.openLinkInNewTab(url);
    } else {
      Frames.openLinkInSummaryFrameOrNewTab(url);
    }
  }
};

/**
 * Called when the escape key has been pressed while the search field has
 * focus.
 */
EventHandlers._escapeKeyPressed = function () {
  View.clearSearchField();
};