chromium/chromeos/ash/components/local_search_service/content_extraction_utils.cc

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

#include "chromeos/ash/components/local_search_service/content_extraction_utils.h"
#include <memory>
#include <string>
#include <unordered_map>
#include <vector>

#include "base/check.h"
#include "base/containers/contains.h"
#include "base/containers/flat_set.h"
#include "base/i18n/case_conversion.h"
#include "base/i18n/unicodestring.h"
#include "base/memory/ptr_util.h"
#include "base/no_destructor.h"
#include "base/strings/utf_string_conversions.h"
#include "chromeos/ash/components/string_matching/tokenized_string.h"
#include "third_party/icu/source/i18n/unicode/translit.h"

namespace ash::local_search_service {

namespace {

using string_matching::TokenizedString;

std::unique_ptr<icu::Transliterator> CreateDiacriticRemover() {
  UErrorCode status = U_ZERO_ERROR;
  UParseError parse_error;

  // Adds a rule to remove diacritic from text. Adds a few characters that are
  // not handled by ICU (ł > l; ø > o; đ > d).
  return base::WrapUnique(icu::Transliterator::createFromRules(
      UNICODE_STRING_SIMPLE("RemoveDiacritic"),
      icu::UnicodeString::fromUTF8("::NFD; ::[:Nonspacing Mark:] Remove; "
                                   "::NFC; ł > l; ø > o; đ > d;"),
      UTRANS_FORWARD, parse_error, status));
}

std::unique_ptr<icu::Transliterator> CreateHyphenRemover() {
  UErrorCode status = U_ZERO_ERROR;
  UParseError parse_error;

  // Hyphen characters list is taken from here: http://jkorpela.fi/dashes.html
  // U+002D(-), U+007E(~), U+058A(֊), U+05BE(־), U+1806(᠆), U+2010(‐),
  // U+2011(‑), U+2012(‒), U+2013(–), U+2014(—), U+2015(―), U+2053(⁓),
  // U+207B(⁻), U+208B(₋), U+2212(−), U+2E3A(⸺ ), U+2E3B(⸻  ), U+301C(〜),
  // U+3030(〰), U+30A0(゠), U+FE58(﹘), U+FE63(﹣), U+FF0D(-).
  return base::WrapUnique(icu::Transliterator::createFromRules(
      UNICODE_STRING_SIMPLE("RemoveHyphen"),
      icu::UnicodeString::fromUTF8("::[-~֊־᠆‐‑‒–—―⁓⁻₋−⸺⸻〜〰゠﹘﹣-] Remove;"),
      UTRANS_FORWARD, parse_error, status));
}

}  // namespace

std::vector<Token> ConsolidateToken(const std::vector<Token>& tokens) {
  std::unordered_map<std::u16string, std::vector<WeightedPosition>> dictionary;
  for (const auto& token : tokens) {
    dictionary[token.content].insert(dictionary[token.content].end(),
                                     token.positions.begin(),
                                     token.positions.end());
  }

  std::vector<Token> results;
  for (const auto& item : dictionary) {
    results.push_back(Token(item.first, item.second));
  }
  return results;
}

std::vector<Token> ExtractContent(const std::string& content_id,
                                  const std::u16string& text,
                                  double weight,
                                  const std::string& locale) {
  // Use two different string tokenizing algorithms for Latin and non Latin
  // locale.
  TokenizedString::Mode mode;
  if (IsNonLatinLocale(locale)) {
    mode = TokenizedString::Mode::kCamelCase;
  } else {
    mode = TokenizedString::Mode::kWords;
  }

  const TokenizedString tokenized_string(text, mode);
  DCHECK(tokenized_string.tokens().size() ==
         tokenized_string.mappings().size());

  const size_t num_tokens = tokenized_string.tokens().size();
  std::vector<Token> tokens;

  for (size_t i = 0; i < num_tokens; i++) {
    const std::u16string word = Normalizer(tokenized_string.tokens()[i]);
    if (IsStopword(word, locale))
      continue;
    tokens.push_back(Token(
        word,
        {WeightedPosition(
            weight, Position(content_id, tokenized_string.mappings()[i].start(),
                             tokenized_string.mappings()[i].end() -
                                 tokenized_string.mappings()[i].start()))}));
  }

  return tokens;
}

bool IsNonLatinLocale(const std::string& locale) {
  static const base::NoDestructor<base::flat_set<std::string>>
      non_latin_locales({"am", "ar", "be", "bg", "bn", "el", "fa", "gu",
                         "hi", "hy", "iw", "ja", "ka", "kk", "km", "kn",
                         "ko", "ky", "lo", "mk", "ml", "mn", "mr", "my",
                         "pa", "ru", "sr", "ta", "te", "th", "uk", "zh"});
  return base::Contains(*non_latin_locales, locale.substr(0, 2));
}

bool IsStopword(const std::u16string& word, const std::string& locale) {
  // TODO(thanhdng): Currently we support stopword list for English only. In the
  // future, when we need to support other languages, creates resource files to
  // store the stopwords.
  if (locale.substr(0, 2) != "en")
    return false;

  // A set of stopwords in English. This set is taken from NLTK library.
  static const base::NoDestructor<base::flat_set<std::string>>
      english_stopwords(
          {"i",         "me",         "my",        "myself",  "we",
           "our",       "ours",       "ourselves", "you",     "you're",
           "you've",    "you'll",     "you'd",     "your",    "yours",
           "yourself",  "yourselves", "he",        "him",     "his",
           "himself",   "she",        "she's",     "her",     "hers",
           "herself",   "it",         "it's",      "its",     "itself",
           "they",      "them",       "their",     "theirs",  "themselves",
           "what",      "which",      "who",       "whom",    "this",
           "that",      "that'll",    "these",     "those",   "am",
           "is",        "are",        "was",       "were",    "be",
           "been",      "being",      "have",      "has",     "had",
           "having",    "do",         "does",      "did",     "doing",
           "a",         "an",         "the",       "and",     "but",
           "if",        "or",         "because",   "as",      "until",
           "while",     "of",         "at",        "by",      "for",
           "with",      "about",      "against",   "between", "into",
           "through",   "during",     "before",    "after",   "above",
           "below",     "to",         "from",      "up",      "down",
           "in",        "out",        "on",        "off",     "over",
           "under",     "again",      "further",   "then",    "once",
           "here",      "there",      "when",      "where",   "why",
           "how",       "all",        "any",       "both",    "each",
           "few",       "more",       "most",      "other",   "some",
           "such",      "no",         "nor",       "not",     "only",
           "own",       "same",       "so",        "than",    "too",
           "very",      "s",          "t",         "can",     "will",
           "just",      "don",        "don't",     "should",  "should've",
           "now",       "d",          "ll",        "m",       "o",
           "re",        "ve",         "y",         "ain",     "aren",
           "aren't",    "couldn",     "couldn't",  "didn",    "didn't",
           "doesn",     "doesn't",    "hadn",      "hadn't",  "hasn",
           "hasn't",    "haven",      "haven't",   "isn",     "isn't",
           "ma",        "mightn",     "mightn't",  "mustn",   "mustn't",
           "needn",     "needn't",    "shan",      "shan't",  "shouldn",
           "shouldn't", "wasn",       "wasn't",    "weren",   "weren't",
           "won",       "won't",      "wouldn",    "wouldn't"});
  return base::Contains(*english_stopwords, base::UTF16ToUTF8(word));
}

std::u16string Normalizer(const std::u16string& word, bool remove_hyphen) {
  // Case folding.
  icu::UnicodeString source = icu::UnicodeString::fromUTF8(
      base::UTF16ToUTF8(base::i18n::FoldCase(word)));

  // Removes diacritic.
  static base::NoDestructor<std::unique_ptr<icu::Transliterator>>
      diacritic_remover(CreateDiacriticRemover());
  (*diacritic_remover)->transliterate(source);

  // Removes hyphen.
  if (remove_hyphen) {
    static base::NoDestructor<std::unique_ptr<icu::Transliterator>>
        hyphen_remover(CreateHyphenRemover());
    (*hyphen_remover)->transliterate(source);
  }

  return base::i18n::UnicodeStringToString16(source);
}

}  // namespace ash::local_search_service