chromium/chrome/browser/resources/omnibox/omnibox_util.ts

// Copyright 2023 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import type {Signals} from './omnibox.mojom-webui.js';

export function clearChildren(element: Element) {
  while (element.firstChild) {
    element.firstChild.remove();
  }
}

export function createEl<K extends keyof HTMLElementTagNameMap>(
    tagName: K, parentEl: Element|null = null, classes: string[] = [],
    text: string = ''): HTMLElementTagNameMap[K] {
  const el = document.createElement(tagName);
  el.classList.add(...classes);
  el.textContent = text;
  if (parentEl) {
    parentEl.appendChild(el);
  }
  return el;
}

// Keep consistent:
// - omnibox_event.proto `ScoringSignals`
// - omnibox_scoring_signals.proto `OmniboxScoringSignals`
// - autocomplete_scoring_model_handler.cc
//   `AutocompleteScoringModelHandler::ExtractInputFromScoringSignals()`
// - autocomplete_match.cc `AutocompleteMatch::MergeScoringSignals()`
// - autocomplete_controller.cc `RecordScoringSignalCoverageForProvider()`
// - omnibox_metrics_provider.cc `GetScoringSignalsForLogging()`
// - omnibox.mojom `struct Signals`
// - omnibox_page_handler.cc
//   `TypeConverter<AutocompleteMatch::ScoringSignals, mojom::SignalsPtr>`
// - omnibox_page_handler.cc `TypeConverter<mojom::SignalsPtr,
//   AutocompleteMatch::ScoringSignals>`
// - omnibox_util.ts `signalNames`
// - omnibox/histograms.xml
//   `Omnibox.URLScoringModelExecuted.ScoringSignalCoverage`
export const signalNames: Array<keyof Signals> = [
  'typedCount',
  'visitCount',
  'elapsedTimeLastVisitSecs',
  'shortcutVisitCount',
  'shortestShortcutLen',
  'elapsedTimeLastShortcutVisitSec',
  'isHostOnly',
  'numBookmarksOfUrl',
  'firstBookmarkTitleMatchPosition',
  'totalBookmarkTitleMatchLength',
  'numInputTermsMatchedByBookmarkTitle',
  'firstUrlMatchPosition',
  'totalUrlMatchLength',
  'hostMatchAtWordBoundary',
  'totalHostMatchLength',
  'totalPathMatchLength',
  'totalQueryOrRefMatchLength',
  'totalTitleMatchLength',
  'hasNonSchemeWwwMatch',
  'numInputTermsMatchedByTitle',
  'numInputTermsMatchedByUrl',
  'lengthOfUrl',
  'siteEngagement',
  'allowedToBeDefaultMatch',
  'searchSuggestRelevance',
  'isSearchSuggestEntity',
  'isVerbatim',
  'isNavsuggest',
  'isSearchSuggestTail',
  'isAnswerSuggest',
  'isCalculatorSuggest',
];

export function clamp(value: number, min: number, max: number) {
  return Math.min(Math.max(value, min), max);
}

export class MlVersionObj {
  version: number;
  string: string;
  url: string;

  constructor(version: number) {
    this.version = version;
    this.string = version === -1 ?
        String(version) :
        `${version} (${new Date(version * 1000).toLocaleDateString()})`;
    const codeSearchPrefix =
        'https://source.corp.google.com/search?q=file:google3/googledata/chrome/breve/cacao/models/data/omnibox/url_scoring/';
    this.url = `${codeSearchPrefix} ${version}`;
  }
}

function setFormattedClipboard(text: string) {
  let styleCount = 0;
  const addStyle = (style: string) => {
    styleCount++;
    return `<span style="${style}">`;
  };
  const clearStyles = () => {
    const n = styleCount;
    styleCount = 0;
    return '</span>'.repeat(n);
  };
  let linkStep = 0;
  const addLink = () => {
    linkStep = (linkStep + 1) % 3;
    switch (linkStep) {
      case 1:
        return '<a href="';
      case 2:
        return '">';
      case 3:
      default:
        return '</a>';
    }
  };

  type StyleMap = Record<string, () => string>;
  const htmlMap: StyleMap = {
    $$: () => '$',
    $n: () => '<br>',
    $h: () => addStyle('font-weight:bold'),
    $r: () => addStyle('color:red'),
    $g: () => addStyle('color:green'),
    $b: () => addStyle('color:blue'),
    $p: () => addStyle('color:purple'),
    $0: clearStyles,
    $l: addLink,
  };
  const plainMap: StyleMap = {
    $$: () => '$',
    $n: () => '\n',
  };
  const applyMap = (map: StyleMap) => {
    return text.split(/(\$.)/g)
        .map((part, i) => i % 2 ? map[part]?.() ?? '' : part)
        .join('');
  };

  const clipboardEntries: Array<[string, StyleMap]> = [
    ['text/html', htmlMap],
    ['text/plain', plainMap],
  ];
  const clipboardItem =
      new ClipboardItem(Object.fromEntries(clipboardEntries.map(
          ([type, map]) => [type, new Blob([applyMap(map)], {type})])));
  return navigator.clipboard.write([clipboardItem]);
}

export function setFormattedClipboardForMl(
    matchDetails: Record<string, any>, signals: Signals, shareUrl: string,
    version: MlVersionObj) {
  return setFormattedClipboard([
    // clang-format off
    ...Object.entries(matchDetails)
        .flatMap(([k, v]) => ['$h$g', k, ': $0$b', v, '$0$n']),
    ...Object.entries(signals)
        .filter(([, v]) => v)
        .flatMap(([k, v]) => ['$h$p', k, ': $0$b', v, '$0$n']),
    ...shareUrl ? ['$r', shareUrl, '$0$n'] : [],
    '$h$r', 'Version: ', '$0', '$l', version.url, '$l', version.string, '$l$n',
    // clang-format on
  ].join(''));
}