1 /* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
6 * This module exports a urlbar result class, each representing a single result
7 * found by a provider that can be passed from the model to the view through
8 * the controller. It is mainly defined by a result type, and a payload,
9 * containing the data. A few getters allow to retrieve information common to all
13 import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
17 ChromeUtils.defineESModuleGetters(lazy, {
19 "resource://gre/modules/components-utils/JsonSchemaValidator.sys.mjs",
20 UrlbarPrefs: "resource:///modules/UrlbarPrefs.sys.mjs",
21 UrlbarUtils: "resource:///modules/UrlbarUtils.sys.mjs",
24 XPCOMUtils.defineLazyModuleGetters(lazy, {
25 BrowserUIUtils: "resource:///modules/BrowserUIUtils.jsm",
29 * Class used to create a single result.
31 export class UrlbarResult {
35 * @param {integer} resultType one of UrlbarUtils.RESULT_TYPE.* values
36 * @param {integer} resultSource one of UrlbarUtils.RESULT_SOURCE.* values
37 * @param {object} payload data for this result. A payload should always
38 * contain a way to extract a final url to visit. The url getter
39 * should have a case for each of the types.
40 * @param {object} [payloadHighlights] payload highlights, if any. Each
41 * property in the payload may have a corresponding property in this
42 * object. The value of each property should be an array of [index,
43 * length] tuples. Each tuple indicates a substring in the correspoding
46 constructor(resultType, resultSource, payload, payloadHighlights = {}) {
47 // Type describes the payload and visualization that should be used for
49 if (!Object.values(lazy.UrlbarUtils.RESULT_TYPE).includes(resultType)) {
50 throw new Error("Invalid result type");
52 this.type = resultType;
54 // Source describes which data has been used to derive this result. In case
55 // multiple sources are involved, use the more privacy restricted.
56 if (!Object.values(lazy.UrlbarUtils.RESULT_SOURCE).includes(resultSource)) {
57 throw new Error("Invalid result source");
59 this.source = resultSource;
61 // UrlbarView is responsible for updating this.
64 // May be used to indicate an heuristic result. Heuristic results can bypass
65 // source filters in the ProvidersManager, that otherwise may skip them.
66 this.heuristic = false;
68 // The payload contains result data. Some of the data is common across
69 // multiple types, but most of it will vary.
70 if (!payload || typeof payload != "object") {
71 throw new Error("Invalid result payload");
73 this.payload = this.validatePayload(payload);
75 if (!payloadHighlights || typeof payloadHighlights != "object") {
76 throw new Error("Invalid result payload highlights");
78 this.payloadHighlights = payloadHighlights;
80 // Make sure every property in the payload has an array of highlights. If a
81 // payload property does not have a highlights array, then give it one now.
82 // That way the consumer doesn't need to check whether it exists.
83 for (let name in payload) {
84 if (!(name in this.payloadHighlights)) {
85 this.payloadHighlights[name] = [];
91 * Returns a title that could be used as a label for this result.
93 * @returns {string} The label to show in a simplified title / url view.
96 return this._titleAndHighlights[0];
100 * Returns an array of highlights for the title.
102 * @returns {Array} The array of highlights.
104 get titleHighlights() {
105 return this._titleAndHighlights[1];
109 * Returns an array [title, highlights].
111 * @returns {Array} The title and array of highlights.
113 get _titleAndHighlights() {
115 case lazy.UrlbarUtils.RESULT_TYPE.KEYWORD:
116 case lazy.UrlbarUtils.RESULT_TYPE.TAB_SWITCH:
117 case lazy.UrlbarUtils.RESULT_TYPE.URL:
118 case lazy.UrlbarUtils.RESULT_TYPE.OMNIBOX:
119 case lazy.UrlbarUtils.RESULT_TYPE.REMOTE_TAB:
120 if (this.payload.qsSuggestion) {
122 // We will initially only be targetting en-US users with this experiment
123 // but will need to change this to work properly with l10n.
124 this.payload.qsSuggestion + " — " + this.payload.title,
125 this.payloadHighlights.qsSuggestion,
129 if (this.payload.fallbackTitle) {
131 this.payload.fallbackTitle,
132 this.payloadHighlights.fallbackTitle,
136 if (this.payload.title) {
137 return [this.payload.title, this.payloadHighlights.title];
140 return [this.payload.url ?? "", this.payloadHighlights.url ?? []];
141 case lazy.UrlbarUtils.RESULT_TYPE.SEARCH:
142 if (this.payload.providesSearchMode) {
145 if (this.payload.tail && this.payload.tailOffsetIndex >= 0) {
146 return [this.payload.tail, this.payloadHighlights.tail];
147 } else if (this.payload.suggestion) {
148 return [this.payload.suggestion, this.payloadHighlights.suggestion];
150 return [this.payload.query, this.payloadHighlights.query];
157 * Returns an icon url.
159 * @returns {string} url of the icon.
162 return this.payload.icon;
166 * Returns whether the result's `suggestedIndex` property is defined.
167 * `suggestedIndex` is an optional hint to the muxer that can be set to
168 * suggest a specific position among the results.
170 * @returns {boolean} Whether `suggestedIndex` is defined.
172 get hasSuggestedIndex() {
173 return typeof this.suggestedIndex == "number";
177 * Returns the given payload if it's valid or throws an error if it's not.
178 * The schemas in UrlbarUtils.RESULT_PAYLOAD_SCHEMA are used for validation.
180 * @param {object} payload The payload object.
181 * @returns {object} `payload` if it's valid.
183 validatePayload(payload) {
184 let schema = lazy.UrlbarUtils.getPayloadSchema(this.type);
186 throw new Error(`Unrecognized result type: ${this.type}`);
188 let result = lazy.JsonSchemaValidator.validate(payload, schema, {
189 allowExplicitUndefinedProperties: true,
190 allowNullAsUndefinedProperties: true,
191 allowExtraProperties: this.type == lazy.UrlbarUtils.RESULT_TYPE.DYNAMIC,
200 * A convenience function that takes a payload annotated with
201 * UrlbarUtils.HIGHLIGHT enums and returns the payload and the payload's
202 * highlights. Use this function when the highlighting required by your
203 * payload is based on simple substring matching, as done by
204 * UrlbarUtils.getTokenMatches(). Pass the return values as the `payload` and
205 * `payloadHighlights` params of the UrlbarResult constructor.
206 * `payloadHighlights` is optional. If omitted, payload will not be
209 * If the payload doesn't have a title or has an empty title, and it also has
210 * a URL, then this function also sets the title to the URL's domain.
212 * @param {Array} tokens The tokens that should be highlighted in each of the
213 * payload properties.
214 * @param {object} payloadInfo An object that looks like this:
215 * { payloadPropertyName: payloadPropertyInfo }
217 * Each payloadPropertyInfo may be either a string or an array. If
218 * it's a string, then the property value will be that string, and no
219 * highlighting will be applied to it. If it's an array, then it
220 * should look like this: [payloadPropertyValue, highlightType].
221 * payloadPropertyValue may be a string or an array of strings. If
222 * it's a string, then the payloadHighlights in the return value will
223 * be an array of match highlights as described in
224 * UrlbarUtils.getTokenMatches(). If it's an array, then
225 * payloadHighlights will be an array of arrays of match highlights,
226 * one element per element in payloadPropertyValue.
227 * @returns {Array} An array [payload, payloadHighlights].
229 static payloadAndSimpleHighlights(tokens, payloadInfo) {
230 // Convert scalar values in payloadInfo to [value] arrays.
231 for (let [name, info] of Object.entries(payloadInfo)) {
232 if (!Array.isArray(info)) {
233 payloadInfo[name] = [info];
238 (!payloadInfo.title || !payloadInfo.title[0]) &&
239 !payloadInfo.fallbackTitle &&
241 typeof payloadInfo.url[0] == "string"
243 // If there's no title, show the domain as the title. Not all valid URLs
245 payloadInfo.title = payloadInfo.title || [
247 lazy.UrlbarUtils.HIGHLIGHT.TYPED,
250 payloadInfo.title[0] = new URL(payloadInfo.url[0]).host;
254 if (payloadInfo.url) {
255 // For display purposes we need to unescape the url.
256 payloadInfo.displayUrl = [...payloadInfo.url];
257 let url = payloadInfo.displayUrl[0];
258 if (url && lazy.UrlbarPrefs.get("trimURLs")) {
259 url = lazy.BrowserUIUtils.removeSingleTrailingSlashFromURL(url);
260 if (url.startsWith("https://")) {
261 url = url.substring(8);
262 if (url.startsWith("www.")) {
263 url = url.substring(4);
267 payloadInfo.displayUrl[0] = lazy.UrlbarUtils.unEscapeURIForUI(url);
270 // For performance reasons limit excessive string lengths, to reduce the
271 // amount of string matching we do here, and avoid wasting resources to
272 // handle long textruns that the user would never see anyway.
273 for (let prop of ["displayUrl", "title", "suggestion"]) {
274 let val = payloadInfo[prop]?.[0];
275 if (typeof val == "string") {
276 payloadInfo[prop][0] = val.substring(
278 lazy.UrlbarUtils.MAX_TEXT_LENGTH
283 let entries = Object.entries(payloadInfo);
285 entries.reduce((payload, [name, [val, _]]) => {
289 entries.reduce((highlights, [name, [val, highlightType]]) => {
291 highlights[name] = !Array.isArray(val)
292 ? lazy.UrlbarUtils.getTokenMatches(tokens, val || "", highlightType)
294 lazy.UrlbarUtils.getTokenMatches(tokens, subval, highlightType)
302 static _dynamicResultTypesByName = new Map();
305 * Registers a dynamic result type. Dynamic result types are types that are
306 * created at runtime, for example by an extension. A particular type should
307 * be added only once; if this method is called for a type more than once, the
308 * `type` in the last call overrides those in previous calls.
310 * @param {string} name
311 * The name of the type. This is used in CSS selectors, so it shouldn't
312 * contain any spaces or punctuation except for -, _, etc.
313 * @param {object} type
314 * An object that describes the type. Currently types do not have any
315 * associated metadata, so this object should be empty.
317 static addDynamicResultType(name, type = {}) {
318 if (/[^a-z0-9_-]/i.test(name)) {
319 this.logger.error(`Illegal dynamic type name: ${name}`);
322 this._dynamicResultTypesByName.set(name, type);
326 * Unregisters a dynamic result type.
328 * @param {string} name
329 * The name of the type.
331 static removeDynamicResultType(name) {
332 let type = this._dynamicResultTypesByName.get(name);
334 this._dynamicResultTypesByName.delete(name);
339 * Returns an object describing a registered dynamic result type.
341 * @param {string} name
342 * The name of the type.
344 * Currently types do not have any associated metadata, so the return value
345 * is an empty object if the type exists. If the type doesn't exist,
346 * undefined is returned.
348 static getDynamicResultType(name) {
349 return this._dynamicResultTypesByName.get(name);
353 * This is useful for logging results. If you need the full payload, then it's
354 * better to JSON.stringify the result object itself.
356 * @returns {string} string representation of the result.
359 if (this.payload.url) {
360 return this.payload.title + " - " + this.payload.url.substr(0, 100);
362 if (this.payload.keyword) {
363 return this.payload.keyword + " - " + this.payload.query;
365 if (this.payload.suggestion) {
366 return this.payload.engine + " - " + this.payload.suggestion;
368 if (this.payload.engine) {
369 return this.payload.engine + " - " + this.payload.query;
371 return JSON.stringify(this);