summaryrefslogtreecommitdiffstats
path: root/browser/components/urlbar/UrlbarResult.sys.mjs
diff options
context:
space:
mode:
Diffstat (limited to 'browser/components/urlbar/UrlbarResult.sys.mjs')
-rw-r--r--browser/components/urlbar/UrlbarResult.sys.mjs382
1 files changed, 382 insertions, 0 deletions
diff --git a/browser/components/urlbar/UrlbarResult.sys.mjs b/browser/components/urlbar/UrlbarResult.sys.mjs
new file mode 100644
index 0000000000..81f9aa118d
--- /dev/null
+++ b/browser/components/urlbar/UrlbarResult.sys.mjs
@@ -0,0 +1,382 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/**
+ * This module exports a urlbar result class, each representing a single result
+ * found by a provider that can be passed from the model to the view through
+ * the controller. It is mainly defined by a result type, and a payload,
+ * containing the data. A few getters allow to retrieve information common to all
+ * the result types.
+ */
+
+import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
+
+const lazy = {};
+
+ChromeUtils.defineESModuleGetters(lazy, {
+ JsonSchemaValidator:
+ "resource://gre/modules/components-utils/JsonSchemaValidator.sys.mjs",
+ UrlbarPrefs: "resource:///modules/UrlbarPrefs.sys.mjs",
+ UrlbarUtils: "resource:///modules/UrlbarUtils.sys.mjs",
+});
+
+XPCOMUtils.defineLazyModuleGetters(lazy, {
+ BrowserUIUtils: "resource:///modules/BrowserUIUtils.jsm",
+});
+
+/**
+ * Class used to create a single result.
+ */
+export class UrlbarResult {
+ /**
+ * Creates a result.
+ *
+ * @param {integer} resultType one of UrlbarUtils.RESULT_TYPE.* values
+ * @param {integer} resultSource one of UrlbarUtils.RESULT_SOURCE.* values
+ * @param {object} payload data for this result. A payload should always
+ * contain a way to extract a final url to visit. The url getter
+ * should have a case for each of the types.
+ * @param {object} [payloadHighlights] payload highlights, if any. Each
+ * property in the payload may have a corresponding property in this
+ * object. The value of each property should be an array of [index,
+ * length] tuples. Each tuple indicates a substring in the corresponding
+ * payload property.
+ */
+ constructor(resultType, resultSource, payload, payloadHighlights = {}) {
+ // Type describes the payload and visualization that should be used for
+ // this result.
+ if (!Object.values(lazy.UrlbarUtils.RESULT_TYPE).includes(resultType)) {
+ throw new Error("Invalid result type");
+ }
+ this.type = resultType;
+
+ // Source describes which data has been used to derive this result. In case
+ // multiple sources are involved, use the more privacy restricted.
+ if (!Object.values(lazy.UrlbarUtils.RESULT_SOURCE).includes(resultSource)) {
+ throw new Error("Invalid result source");
+ }
+ this.source = resultSource;
+
+ // UrlbarView is responsible for updating this.
+ this.rowIndex = -1;
+
+ // May be used to indicate an heuristic result. Heuristic results can bypass
+ // source filters in the ProvidersManager, that otherwise may skip them.
+ this.heuristic = false;
+
+ // Exposure specific properties. These allow us to track the exposure
+ // of a result through the query process.
+ // A non-zero value here indicates that this result's exposure should be
+ // recorded in the exposure event.
+ this.exposureResultType = "";
+
+ // Determines if the exposure result should be hidden from the view.
+ this.exposureResultHidden = false;
+
+ // The payload contains result data. Some of the data is common across
+ // multiple types, but most of it will vary.
+ if (!payload || typeof payload != "object") {
+ throw new Error("Invalid result payload");
+ }
+ this.payload = this.validatePayload(payload);
+
+ if (!payloadHighlights || typeof payloadHighlights != "object") {
+ throw new Error("Invalid result payload highlights");
+ }
+ this.payloadHighlights = payloadHighlights;
+
+ // Make sure every property in the payload has an array of highlights. If a
+ // payload property does not have a highlights array, then give it one now.
+ // That way the consumer doesn't need to check whether it exists.
+ for (let name in payload) {
+ if (!(name in this.payloadHighlights)) {
+ this.payloadHighlights[name] = [];
+ }
+ }
+ }
+
+ /**
+ * Returns a title that could be used as a label for this result.
+ *
+ * @returns {string} The label to show in a simplified title / url view.
+ */
+ get title() {
+ return this._titleAndHighlights[0];
+ }
+
+ /**
+ * Returns an array of highlights for the title.
+ *
+ * @returns {Array} The array of highlights.
+ */
+ get titleHighlights() {
+ return this._titleAndHighlights[1];
+ }
+
+ /**
+ * Returns an array [title, highlights].
+ *
+ * @returns {Array} The title and array of highlights.
+ */
+ get _titleAndHighlights() {
+ switch (this.type) {
+ case lazy.UrlbarUtils.RESULT_TYPE.KEYWORD:
+ case lazy.UrlbarUtils.RESULT_TYPE.TAB_SWITCH:
+ case lazy.UrlbarUtils.RESULT_TYPE.URL:
+ case lazy.UrlbarUtils.RESULT_TYPE.OMNIBOX:
+ case lazy.UrlbarUtils.RESULT_TYPE.REMOTE_TAB:
+ if (this.payload.qsSuggestion) {
+ return [
+ // We will initially only be targeting en-US users with this experiment
+ // but will need to change this to work properly with l10n.
+ this.payload.qsSuggestion + " — " + this.payload.title,
+ this.payloadHighlights.qsSuggestion,
+ ];
+ }
+
+ if (this.payload.fallbackTitle) {
+ return [
+ this.payload.fallbackTitle,
+ this.payloadHighlights.fallbackTitle,
+ ];
+ }
+
+ if (this.payload.title) {
+ return [this.payload.title, this.payloadHighlights.title];
+ }
+
+ return [this.payload.url ?? "", this.payloadHighlights.url ?? []];
+ case lazy.UrlbarUtils.RESULT_TYPE.SEARCH:
+ if (this.payload.providesSearchMode) {
+ return ["", []];
+ }
+ if (this.payload.tail && this.payload.tailOffsetIndex >= 0) {
+ return [this.payload.tail, this.payloadHighlights.tail];
+ } else if (this.payload.suggestion) {
+ return [this.payload.suggestion, this.payloadHighlights.suggestion];
+ }
+ return [this.payload.query, this.payloadHighlights.query];
+ default:
+ return ["", []];
+ }
+ }
+
+ /**
+ * Returns an icon url.
+ *
+ * @returns {string} url of the icon.
+ */
+ get icon() {
+ return this.payload.icon;
+ }
+
+ /**
+ * Returns whether the result's `suggestedIndex` property is defined.
+ * `suggestedIndex` is an optional hint to the muxer that can be set to
+ * suggest a specific position among the results.
+ *
+ * @returns {boolean} Whether `suggestedIndex` is defined.
+ */
+ get hasSuggestedIndex() {
+ return typeof this.suggestedIndex == "number";
+ }
+
+ /**
+ * Returns the given payload if it's valid or throws an error if it's not.
+ * The schemas in UrlbarUtils.RESULT_PAYLOAD_SCHEMA are used for validation.
+ *
+ * @param {object} payload The payload object.
+ * @returns {object} `payload` if it's valid.
+ */
+ validatePayload(payload) {
+ let schema = lazy.UrlbarUtils.getPayloadSchema(this.type);
+ if (!schema) {
+ throw new Error(`Unrecognized result type: ${this.type}`);
+ }
+ let result = lazy.JsonSchemaValidator.validate(payload, schema, {
+ allowExplicitUndefinedProperties: true,
+ allowNullAsUndefinedProperties: true,
+ allowExtraProperties: this.type == lazy.UrlbarUtils.RESULT_TYPE.DYNAMIC,
+ });
+ if (!result.valid) {
+ throw result.error;
+ }
+ return payload;
+ }
+
+ /**
+ * A convenience function that takes a payload annotated with
+ * UrlbarUtils.HIGHLIGHT enums and returns the payload and the payload's
+ * highlights. Use this function when the highlighting required by your
+ * payload is based on simple substring matching, as done by
+ * UrlbarUtils.getTokenMatches(). Pass the return values as the `payload` and
+ * `payloadHighlights` params of the UrlbarResult constructor.
+ * `payloadHighlights` is optional. If omitted, payload will not be
+ * highlighted.
+ *
+ * If the payload doesn't have a title or has an empty title, and it also has
+ * a URL, then this function also sets the title to the URL's domain.
+ *
+ * @param {Array} tokens The tokens that should be highlighted in each of the
+ * payload properties.
+ * @param {object} payloadInfo An object that looks like this:
+ * { payloadPropertyName: payloadPropertyInfo }
+ *
+ * Each payloadPropertyInfo may be either a string or an array. If
+ * it's a string, then the property value will be that string, and no
+ * highlighting will be applied to it. If it's an array, then it
+ * should look like this: [payloadPropertyValue, highlightType].
+ * payloadPropertyValue may be a string or an array of strings. If
+ * it's a string, then the payloadHighlights in the return value will
+ * be an array of match highlights as described in
+ * UrlbarUtils.getTokenMatches(). If it's an array, then
+ * payloadHighlights will be an array of arrays of match highlights,
+ * one element per element in payloadPropertyValue.
+ * @returns {Array} An array [payload, payloadHighlights].
+ */
+ static payloadAndSimpleHighlights(tokens, payloadInfo) {
+ // Convert scalar values in payloadInfo to [value] arrays.
+ for (let [name, info] of Object.entries(payloadInfo)) {
+ if (!Array.isArray(info)) {
+ payloadInfo[name] = [info];
+ }
+ }
+
+ if (
+ (!payloadInfo.title || !payloadInfo.title[0]) &&
+ !payloadInfo.fallbackTitle &&
+ payloadInfo.url &&
+ typeof payloadInfo.url[0] == "string"
+ ) {
+ // If there's no title, show the domain as the title. Not all valid URLs
+ // have a domain.
+ payloadInfo.title = payloadInfo.title || [
+ "",
+ lazy.UrlbarUtils.HIGHLIGHT.TYPED,
+ ];
+ try {
+ payloadInfo.title[0] = new URL(payloadInfo.url[0]).host;
+ } catch (e) {}
+ }
+
+ if (payloadInfo.url) {
+ // For display purposes we need to unescape the url.
+ payloadInfo.displayUrl = [...payloadInfo.url];
+ let url = payloadInfo.displayUrl[0];
+ if (url && lazy.UrlbarPrefs.get("trimURLs")) {
+ url = lazy.BrowserUIUtils.removeSingleTrailingSlashFromURL(url);
+ if (url.startsWith("https://")) {
+ url = url.substring(8);
+ if (url.startsWith("www.")) {
+ url = url.substring(4);
+ }
+ }
+ }
+ payloadInfo.displayUrl[0] = lazy.UrlbarUtils.unEscapeURIForUI(url);
+ }
+
+ // For performance reasons limit excessive string lengths, to reduce the
+ // amount of string matching we do here, and avoid wasting resources to
+ // handle long textruns that the user would never see anyway.
+ for (let prop of ["displayUrl", "title", "suggestion"]) {
+ let val = payloadInfo[prop]?.[0];
+ if (typeof val == "string") {
+ payloadInfo[prop][0] = val.substring(
+ 0,
+ lazy.UrlbarUtils.MAX_TEXT_LENGTH
+ );
+ }
+ }
+
+ let entries = Object.entries(payloadInfo);
+ return [
+ entries.reduce((payload, [name, [val, _]]) => {
+ payload[name] = val;
+ return payload;
+ }, {}),
+ entries.reduce((highlights, [name, [val, highlightType]]) => {
+ if (highlightType) {
+ highlights[name] = !Array.isArray(val)
+ ? lazy.UrlbarUtils.getTokenMatches(tokens, val || "", highlightType)
+ : val.map(subval =>
+ lazy.UrlbarUtils.getTokenMatches(tokens, subval, highlightType)
+ );
+ }
+ return highlights;
+ }, {}),
+ ];
+ }
+
+ static _dynamicResultTypesByName = new Map();
+
+ /**
+ * Registers a dynamic result type. Dynamic result types are types that are
+ * created at runtime, for example by an extension. A particular type should
+ * be added only once; if this method is called for a type more than once, the
+ * `type` in the last call overrides those in previous calls.
+ *
+ * @param {string} name
+ * The name of the type. This is used in CSS selectors, so it shouldn't
+ * contain any spaces or punctuation except for -, _, etc.
+ * @param {object} type
+ * An object that describes the type. Currently types do not have any
+ * associated metadata, so this object should be empty.
+ */
+ static addDynamicResultType(name, type = {}) {
+ if (/[^a-z0-9_-]/i.test(name)) {
+ this.logger.error(`Illegal dynamic type name: ${name}`);
+ return;
+ }
+ this._dynamicResultTypesByName.set(name, type);
+ }
+
+ /**
+ * Unregisters a dynamic result type.
+ *
+ * @param {string} name
+ * The name of the type.
+ */
+ static removeDynamicResultType(name) {
+ let type = this._dynamicResultTypesByName.get(name);
+ if (type) {
+ this._dynamicResultTypesByName.delete(name);
+ }
+ }
+
+ /**
+ * Returns an object describing a registered dynamic result type.
+ *
+ * @param {string} name
+ * The name of the type.
+ * @returns {object}
+ * Currently types do not have any associated metadata, so the return value
+ * is an empty object if the type exists. If the type doesn't exist,
+ * undefined is returned.
+ */
+ static getDynamicResultType(name) {
+ return this._dynamicResultTypesByName.get(name);
+ }
+
+ /**
+ * This is useful for logging results. If you need the full payload, then it's
+ * better to JSON.stringify the result object itself.
+ *
+ * @returns {string} string representation of the result.
+ */
+ toString() {
+ if (this.payload.url) {
+ return this.payload.title + " - " + this.payload.url.substr(0, 100);
+ }
+ if (this.payload.keyword) {
+ return this.payload.keyword + " - " + this.payload.query;
+ }
+ if (this.payload.suggestion) {
+ return this.payload.engine + " - " + this.payload.suggestion;
+ }
+ if (this.payload.engine) {
+ return this.payload.engine + " - " + this.payload.query;
+ }
+ return JSON.stringify(this);
+ }
+}