1 // Copyright 2014 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4
5 #include "components/omnibox/browser/keyword_provider.h"
6
7 #include <algorithm>
8 #include <vector>
9
10 #include "base/i18n/case_conversion.h"
11 #include "base/strings/string16.h"
12 #include "base/strings/string_util.h"
13 #include "base/strings/utf_string_conversions.h"
14 #include "base/trace_event/trace_event.h"
15 #include "components/omnibox/browser/autocomplete_match.h"
16 #include "components/omnibox/browser/autocomplete_provider_client.h"
17 #include "components/omnibox/browser/autocomplete_provider_listener.h"
18 #include "components/omnibox/browser/keyword_extensions_delegate.h"
19 #include "components/omnibox/browser/omnibox_field_trial.h"
20 #include "components/omnibox/browser/search_provider.h"
21 #include "components/search_engines/omnibox_focus_type.h"
22 #include "components/search_engines/template_url.h"
23 #include "components/search_engines/template_url_service.h"
24 #include "components/strings/grit/components_strings.h"
25 #include "components/url_formatter/url_formatter.h"
26 #include "net/base/escape.h"
27 #include "third_party/metrics_proto/omnibox_input_type.pb.h"
28 #include "ui/base/l10n/l10n_util.h"
29
30 namespace {
31
32 // Helper functor for Start(), for sorting keyword matches by quality.
33 class CompareQuality {
34 public:
35 // A keyword is of higher quality when a greater fraction of the important
36 // part of it has been typed, that is, when the meaningful keyword length is
37 // shorter.
38 //
39 // TODO(pkasting): Most recent and most frequent keywords are probably
40 // better rankings than the fraction of the keyword typed. We should
41 // always put any exact matches first no matter what, since the code in
42 // Start() assumes this (and it makes sense).
operator ()(const TemplateURLService::TURLAndMeaningfulLength t_url_match1,const TemplateURLService::TURLAndMeaningfulLength t_url_match2) const43 bool operator()(
44 const TemplateURLService::TURLAndMeaningfulLength t_url_match1,
45 const TemplateURLService::TURLAndMeaningfulLength t_url_match2) const {
46 return t_url_match1.second < t_url_match2.second;
47 }
48 };
49
50 // Helper for KeywordProvider::Start(), for ending keyword mode unless
51 // explicitly told otherwise.
52 class ScopedEndExtensionKeywordMode {
53 public:
54 explicit ScopedEndExtensionKeywordMode(KeywordExtensionsDelegate* delegate);
55 ~ScopedEndExtensionKeywordMode();
56 ScopedEndExtensionKeywordMode(const ScopedEndExtensionKeywordMode&) = delete;
57 ScopedEndExtensionKeywordMode& operator=(
58 const ScopedEndExtensionKeywordMode&) = delete;
59
60 void StayInKeywordMode();
61
62 private:
63 KeywordExtensionsDelegate* delegate_;
64 };
65
ScopedEndExtensionKeywordMode(KeywordExtensionsDelegate * delegate)66 ScopedEndExtensionKeywordMode::ScopedEndExtensionKeywordMode(
67 KeywordExtensionsDelegate* delegate)
68 : delegate_(delegate) {
69 }
70
~ScopedEndExtensionKeywordMode()71 ScopedEndExtensionKeywordMode::~ScopedEndExtensionKeywordMode() {
72 if (delegate_)
73 delegate_->MaybeEndExtensionKeywordMode();
74 }
75
StayInKeywordMode()76 void ScopedEndExtensionKeywordMode::StayInKeywordMode() {
77 delegate_ = nullptr;
78 }
79
80 } // namespace
81
KeywordProvider(AutocompleteProviderClient * client,AutocompleteProviderListener * listener)82 KeywordProvider::KeywordProvider(AutocompleteProviderClient* client,
83 AutocompleteProviderListener* listener)
84 : AutocompleteProvider(AutocompleteProvider::TYPE_KEYWORD),
85 listener_(listener),
86 model_(client->GetTemplateURLService()),
87 extensions_delegate_(client->GetKeywordExtensionsDelegate(this)) {}
88
89 // static
SplitKeywordFromInput(const base::string16 & input,bool trim_leading_whitespace,base::string16 * remaining_input)90 base::string16 KeywordProvider::SplitKeywordFromInput(
91 const base::string16& input,
92 bool trim_leading_whitespace,
93 base::string16* remaining_input) {
94 // Find end of first token. The AutocompleteController has trimmed leading
95 // whitespace, so we need not skip over that.
96 const size_t first_white(input.find_first_of(base::kWhitespaceUTF16));
97 DCHECK_NE(0U, first_white);
98 if (first_white == base::string16::npos)
99 return input; // Only one token provided.
100
101 // Set |remaining_input| to everything after the first token.
102 DCHECK(remaining_input != nullptr);
103 const size_t remaining_start = trim_leading_whitespace ?
104 input.find_first_not_of(base::kWhitespaceUTF16, first_white) :
105 first_white + 1;
106
107 if (remaining_start < input.length())
108 remaining_input->assign(input.begin() + remaining_start, input.end());
109
110 // Return first token as keyword.
111 return input.substr(0, first_white);
112 }
113
114 // static
SplitReplacementStringFromInput(const base::string16 & input,bool trim_leading_whitespace)115 base::string16 KeywordProvider::SplitReplacementStringFromInput(
116 const base::string16& input,
117 bool trim_leading_whitespace) {
118 // The input may contain leading whitespace, strip it.
119 base::string16 trimmed_input;
120 base::TrimWhitespace(input, base::TRIM_LEADING, &trimmed_input);
121
122 // And extract the replacement string.
123 base::string16 remaining_input;
124 SplitKeywordFromInput(trimmed_input, trim_leading_whitespace,
125 &remaining_input);
126 return remaining_input;
127 }
128
129 // static
GetSubstitutingTemplateURLForInput(TemplateURLService * model,AutocompleteInput * input)130 const TemplateURL* KeywordProvider::GetSubstitutingTemplateURLForInput(
131 TemplateURLService* model,
132 AutocompleteInput* input) {
133 if (!input->allow_exact_keyword_match())
134 return nullptr;
135
136 DCHECK(model);
137 base::string16 keyword, remaining_input;
138 if (!ExtractKeywordFromInput(*input, model, &keyword, &remaining_input))
139 return nullptr;
140
141 const TemplateURL* template_url = model->GetTemplateURLForKeyword(keyword);
142 if (template_url &&
143 template_url->SupportsReplacement(model->search_terms_data())) {
144 // Adjust cursor position iff it was set before, otherwise leave it as is.
145 size_t cursor_position = base::string16::npos;
146 // The adjustment assumes that the keyword was stripped from the beginning
147 // of the original input.
148 if (input->cursor_position() != base::string16::npos &&
149 !remaining_input.empty() &&
150 base::EndsWith(input->text(), remaining_input,
151 base::CompareCase::SENSITIVE)) {
152 int offset = input->text().length() - input->cursor_position();
153 // The cursor should never be past the last character or before the
154 // first character.
155 DCHECK_GE(offset, 0);
156 DCHECK_LE(offset, static_cast<int>(input->text().length()));
157 if (offset <= 0) {
158 // Normalize the cursor to be exactly after the last character.
159 cursor_position = remaining_input.length();
160 } else {
161 // If somehow the cursor was before the remaining text, set it to 0,
162 // otherwise adjust it relative to the remaining text.
163 cursor_position = offset > static_cast<int>(remaining_input.length()) ?
164 0u : remaining_input.length() - offset;
165 }
166 }
167 input->UpdateText(remaining_input, cursor_position, input->parts());
168 return template_url;
169 }
170
171 return nullptr;
172 }
173
GetKeywordForText(const base::string16 & text) const174 base::string16 KeywordProvider::GetKeywordForText(
175 const base::string16& text) const {
176 TemplateURLService* url_service = GetTemplateURLService();
177 if (!url_service)
178 return base::string16();
179
180 const base::string16 keyword(CleanUserInputKeyword(url_service, text));
181
182 if (keyword.empty())
183 return keyword;
184
185 // Don't provide a keyword if it doesn't support replacement.
186 const TemplateURL* const template_url =
187 url_service->GetTemplateURLForKeyword(keyword);
188 if (!template_url ||
189 !template_url->SupportsReplacement(url_service->search_terms_data()))
190 return base::string16();
191
192 // Don't provide a keyword for inactive/disabled extension keywords.
193 if ((template_url->type() == TemplateURL::OMNIBOX_API_EXTENSION) &&
194 extensions_delegate_ &&
195 !extensions_delegate_->IsEnabledExtension(template_url->GetExtensionId()))
196 return base::string16();
197
198 return keyword;
199 }
200
CreateVerbatimMatch(const base::string16 & text,const base::string16 & keyword,const AutocompleteInput & input)201 AutocompleteMatch KeywordProvider::CreateVerbatimMatch(
202 const base::string16& text,
203 const base::string16& keyword,
204 const AutocompleteInput& input) {
205 // A verbatim match is allowed to be the default match when appropriate.
206 return CreateAutocompleteMatch(
207 GetTemplateURLService()->GetTemplateURLForKeyword(keyword),
208 keyword.length(), input, keyword.length(),
209 SplitReplacementStringFromInput(text, true),
210 input.allow_exact_keyword_match(), 0, false);
211 }
212
DeleteMatch(const AutocompleteMatch & match)213 void KeywordProvider::DeleteMatch(const AutocompleteMatch& match) {
214 const base::string16& suggestion_text = match.contents;
215
216 const auto pred = [&match](const AutocompleteMatch& i) {
217 return i.keyword == match.keyword &&
218 i.fill_into_edit == match.fill_into_edit;
219 };
220 base::EraseIf(matches_, pred);
221
222 base::string16 keyword, remaining_input;
223 if (!ExtractKeywordFromInput(
224 keyword_input_, GetTemplateURLService(), &keyword, &remaining_input))
225 return;
226 const TemplateURL* const template_url =
227 GetTemplateURLService()->GetTemplateURLForKeyword(keyword);
228
229 if ((template_url->type() == TemplateURL::OMNIBOX_API_EXTENSION) &&
230 extensions_delegate_ &&
231 extensions_delegate_->IsEnabledExtension(
232 template_url->GetExtensionId())) {
233 extensions_delegate_->DeleteSuggestion(template_url, suggestion_text);
234 }
235 }
236
Start(const AutocompleteInput & input,bool minimal_changes)237 void KeywordProvider::Start(const AutocompleteInput& input,
238 bool minimal_changes) {
239 TRACE_EVENT0("omnibox", "KeywordProvider::Start");
240 // This object ensures we end keyword mode if we exit the function without
241 // toggling keyword mode to on.
242 ScopedEndExtensionKeywordMode keyword_mode_toggle(extensions_delegate_.get());
243
244 matches_.clear();
245
246 if (!minimal_changes) {
247 done_ = true;
248
249 // Input has changed. Increment the input ID so that we can discard any
250 // stale extension suggestions that may be incoming.
251 if (extensions_delegate_)
252 extensions_delegate_->IncrementInputId();
253 }
254
255 if (input.focus_type() != OmniboxFocusType::DEFAULT)
256 return;
257
258 GetTemplateURLService();
259 DCHECK(model_);
260 // Split user input into a keyword and some query input.
261 //
262 // We want to suggest keywords even when users have started typing URLs, on
263 // the assumption that they might not realize they no longer need to go to a
264 // site to be able to search it. So we call CleanUserInputKeyword() to strip
265 // any initial scheme and/or "www.". NOTE: Any heuristics or UI used to
266 // automatically/manually create keywords will need to be in sync with
267 // whatever we do here!
268 //
269 // TODO(pkasting): http://crbug/347744 If someday we remember usage frequency
270 // for keywords, we might suggest keywords that haven't even been partially
271 // typed, if the user uses them enough and isn't obviously typing something
272 // else. In this case we'd consider all input here to be query input.
273 base::string16 keyword, remaining_input;
274 if (!ExtractKeywordFromInput(input, model_, &keyword,
275 &remaining_input))
276 return;
277
278 keyword_input_ = input;
279
280 // Get the best matches for this keyword.
281 //
282 // NOTE: We could cache the previous keywords and reuse them here in the
283 // |minimal_changes| case, but since we'd still have to recalculate their
284 // relevances and we can just recreate the results synchronously anyway, we
285 // don't bother.
286 TemplateURLService::TURLsAndMeaningfulLengths matches;
287 model_->AddMatchingKeywords(keyword, !remaining_input.empty(), &matches);
288
289 for (auto i(matches.begin()); i != matches.end();) {
290 const TemplateURL* template_url = i->first;
291
292 // Prune any extension keywords that are disallowed in incognito mode (if
293 // we're incognito), or disabled.
294 if (template_url->type() == TemplateURL::OMNIBOX_API_EXTENSION &&
295 extensions_delegate_ &&
296 !extensions_delegate_->IsEnabledExtension(
297 template_url->GetExtensionId())) {
298 i = matches.erase(i);
299 continue;
300 }
301
302 // Prune any substituting keywords if there is no substitution.
303 if (template_url->SupportsReplacement(
304 model_->search_terms_data()) &&
305 remaining_input.empty() &&
306 !input.allow_exact_keyword_match()) {
307 i = matches.erase(i);
308 continue;
309 }
310
311 ++i;
312 }
313 if (matches.empty())
314 return;
315 std::sort(matches.begin(), matches.end(), CompareQuality());
316
317 // Limit to one exact or three inexact matches, and mark them up for display
318 // in the autocomplete popup.
319 // Any exact match is going to be the highest quality match, and thus at the
320 // front of our vector.
321 if (matches.front().first->keyword() == keyword) {
322 const TemplateURL* template_url = matches.front().first;
323 const size_t meaningful_keyword_length = matches.front().second;
324 const bool is_extension_keyword =
325 template_url->type() == TemplateURL::OMNIBOX_API_EXTENSION;
326
327 // Only create an exact match if |remaining_input| is empty or if
328 // this is an extension keyword. If |remaining_input| is a
329 // non-empty non-extension keyword (i.e., a regular keyword that
330 // supports replacement and that has extra text following it),
331 // then SearchProvider creates the exact (a.k.a. verbatim) match.
332 if (!remaining_input.empty() && !is_extension_keyword)
333 return;
334
335 // TODO(pkasting): We should probably check that if the user explicitly
336 // typed a scheme, that scheme matches the one in |template_url|.
337
338 // When creating an exact match (either for the keyword itself, no
339 // remaining query or an extension keyword, possibly with remaining
340 // input), allow the match to be the default match when appropriate.
341 // For exactly-typed non-substituting keywords, it's always appropriate.
342 matches_.push_back(CreateAutocompleteMatch(
343 template_url, meaningful_keyword_length, input, keyword.length(),
344 remaining_input,
345 input.allow_exact_keyword_match() ||
346 !template_url->SupportsReplacement(model_->search_terms_data()),
347 -1, false));
348
349 // Having extension-provided suggestions appear outside keyword mode can
350 // be surprising, so only query for suggestions when in keyword mode.
351 if (is_extension_keyword && extensions_delegate_ &&
352 input.allow_exact_keyword_match()) {
353 if (extensions_delegate_->Start(input, minimal_changes, template_url,
354 remaining_input))
355 keyword_mode_toggle.StayInKeywordMode();
356 }
357 } else {
358 for (TemplateURLService::TURLsAndMeaningfulLengths::const_iterator i(
359 matches.begin());
360 (i != matches.end()) && (matches_.size() < provider_max_matches_);
361 ++i) {
362 // Skip keywords that we've already added. It's possible we may have
363 // retrieved the same keyword twice. For example, the keyword
364 // "abc.abc.com" may be retrieved for the input "abc" from the full
365 // keyword matching and the domain matching passes.
366 ACMatches::const_iterator duplicate = std::find_if(
367 matches_.begin(), matches_.end(),
368 [&i] (const AutocompleteMatch& m) {
369 return m.keyword == i->first->keyword();
370 });
371 if (duplicate == matches_.end()) {
372 matches_.push_back(CreateAutocompleteMatch(
373 i->first, i->second, input, keyword.length(), remaining_input,
374 false, -1, false));
375 }
376 }
377 }
378 }
379
Stop(bool clear_cached_results,bool due_to_user_inactivity)380 void KeywordProvider::Stop(bool clear_cached_results,
381 bool due_to_user_inactivity) {
382 done_ = true;
383 // Only end an extension's request if the user did something to explicitly
384 // cancel it; mere inactivity shouldn't terminate long-running extension
385 // operations since the user likely explicitly requested them.
386 if (extensions_delegate_ && !due_to_user_inactivity)
387 extensions_delegate_->MaybeEndExtensionKeywordMode();
388 }
389
~KeywordProvider()390 KeywordProvider::~KeywordProvider() {}
391
392 // static
ExtractKeywordFromInput(const AutocompleteInput & input,const TemplateURLService * template_url_service,base::string16 * keyword,base::string16 * remaining_input)393 bool KeywordProvider::ExtractKeywordFromInput(
394 const AutocompleteInput& input,
395 const TemplateURLService* template_url_service,
396 base::string16* keyword,
397 base::string16* remaining_input) {
398 if ((input.type() == metrics::OmniboxInputType::EMPTY))
399 return false;
400
401 DCHECK(template_url_service);
402 *keyword = CleanUserInputKeyword(
403 template_url_service,
404 SplitKeywordFromInput(input.text(), true, remaining_input));
405 return !keyword->empty();
406 }
407
408 // static
CalculateRelevance(metrics::OmniboxInputType type,bool complete,bool sufficiently_complete,bool supports_replacement,bool prefer_keyword,bool allow_exact_keyword_match)409 int KeywordProvider::CalculateRelevance(metrics::OmniboxInputType type,
410 bool complete,
411 bool sufficiently_complete,
412 bool supports_replacement,
413 bool prefer_keyword,
414 bool allow_exact_keyword_match) {
415 if (!complete) {
416 const int sufficiently_complete_score =
417 OmniboxFieldTrial::KeywordScoreForSufficientlyCompleteMatch();
418 // If we have a special score to apply for sufficiently-complete matches,
419 // do so.
420 if (sufficiently_complete && (sufficiently_complete_score > -1))
421 return sufficiently_complete_score;
422 return (type == metrics::OmniboxInputType::URL) ? 700 : 450;
423 }
424 if (!supports_replacement)
425 return 1500;
426 return SearchProvider::CalculateRelevanceForKeywordVerbatim(
427 type, allow_exact_keyword_match, prefer_keyword);
428 }
429
CreateAutocompleteMatch(const TemplateURL * template_url,const size_t meaningful_keyword_length,const AutocompleteInput & input,size_t prefix_length,const base::string16 & remaining_input,bool allowed_to_be_default_match,int relevance,bool deletable)430 AutocompleteMatch KeywordProvider::CreateAutocompleteMatch(
431 const TemplateURL* template_url,
432 const size_t meaningful_keyword_length,
433 const AutocompleteInput& input,
434 size_t prefix_length,
435 const base::string16& remaining_input,
436 bool allowed_to_be_default_match,
437 int relevance,
438 bool deletable) {
439 DCHECK(template_url);
440 const bool supports_replacement =
441 template_url->url_ref().SupportsReplacement(
442 GetTemplateURLService()->search_terms_data());
443
444 // Create an edit entry of "[keyword] [remaining input]". This is helpful
445 // even when [remaining input] is empty, as the user can select the popup
446 // choice and immediately begin typing in query input.
447 const base::string16& keyword = template_url->keyword();
448 const bool keyword_complete = (prefix_length == keyword.length());
449 const bool sufficiently_complete =
450 (prefix_length >= meaningful_keyword_length);
451 if (relevance < 0) {
452 relevance =
453 CalculateRelevance(input.type(), keyword_complete,
454 sufficiently_complete,
455 // When the user wants keyword matches to take
456 // preference, score them highly regardless of
457 // whether the input provides query text.
458 supports_replacement, input.prefer_keyword(),
459 input.allow_exact_keyword_match());
460 }
461
462 AutocompleteMatch match(this, relevance, deletable,
463 supports_replacement
464 ? AutocompleteMatchType::SEARCH_OTHER_ENGINE
465 : AutocompleteMatchType::HISTORY_KEYWORD);
466 match.allowed_to_be_default_match = allowed_to_be_default_match;
467 match.fill_into_edit = keyword;
468 if (!remaining_input.empty() || supports_replacement)
469 match.fill_into_edit.push_back(L' ');
470 match.fill_into_edit.append(remaining_input);
471 // If we wanted to set |result.inline_autocompletion| correctly, we'd need
472 // CleanUserInputKeyword() to return the amount of adjustment it's made to
473 // the user's input. Because right now inexact keyword matches can't score
474 // more highly than a "what you typed" match from one of the other providers,
475 // we just don't bother to do this, and leave inline autocompletion off.
476
477 // Create destination URL and popup entry content by substituting user input
478 // into keyword templates.
479 FillInURLAndContents(remaining_input, template_url, &match);
480
481 match.keyword = keyword;
482 match.from_keyword = true;
483 match.transition = ui::PAGE_TRANSITION_KEYWORD;
484
485 return match;
486 }
487
FillInURLAndContents(const base::string16 & remaining_input,const TemplateURL * element,AutocompleteMatch * match) const488 void KeywordProvider::FillInURLAndContents(
489 const base::string16& remaining_input,
490 const TemplateURL* element,
491 AutocompleteMatch* match) const {
492 DCHECK(!element->short_name().empty());
493 const TemplateURLRef& element_ref = element->url_ref();
494 DCHECK(element_ref.IsValid(GetTemplateURLService()->search_terms_data()));
495 if (remaining_input.empty()) {
496 // Allow extension keyword providers to accept empty string input. This is
497 // useful to allow extensions to do something in the case where no input is
498 // entered.
499 if (element_ref.SupportsReplacement(
500 GetTemplateURLService()->search_terms_data()) &&
501 (element->type() != TemplateURL::OMNIBOX_API_EXTENSION)) {
502 // No query input; return a generic, no-destination placeholder.
503 match->contents.assign(
504 l10n_util::GetStringUTF16(IDS_EMPTY_KEYWORD_VALUE));
505 match->contents_class.emplace_back(0, ACMatchClassification::DIM);
506 } else {
507 // Keyword or extension that has no replacement text (aka a shorthand for
508 // a URL).
509 match->destination_url = GURL(element->url());
510 match->contents.assign(element->short_name());
511 if (!element->short_name().empty())
512 match->contents_class.emplace_back(0, ACMatchClassification::MATCH);
513 }
514 } else {
515 // Create destination URL by escaping user input and substituting into
516 // keyword template URL. The escaping here handles whitespace in user
517 // input, but we rely on later canonicalization functions to do more
518 // fixup to make the URL valid if necessary.
519 DCHECK(element_ref.SupportsReplacement(
520 GetTemplateURLService()->search_terms_data()));
521 TemplateURLRef::SearchTermsArgs search_terms_args(remaining_input);
522 search_terms_args.append_extra_query_params_from_command_line =
523 element == GetTemplateURLService()->GetDefaultSearchProvider();
524 match->destination_url = GURL(element_ref.ReplaceSearchTerms(
525 search_terms_args, GetTemplateURLService()->search_terms_data()));
526 match->contents = remaining_input;
527 match->contents_class.emplace_back(0, ACMatchClassification::NONE);
528 }
529 }
530
GetTemplateURLService() const531 TemplateURLService* KeywordProvider::GetTemplateURLService() const {
532 // Make sure the model is loaded. This is cheap and quickly bails out if
533 // the model is already loaded.
534 model_->Load();
535 return model_;
536 }
537
538 // static
CleanUserInputKeyword(const TemplateURLService * template_url_service,const base::string16 & keyword)539 base::string16 KeywordProvider::CleanUserInputKeyword(
540 const TemplateURLService* template_url_service,
541 const base::string16& keyword) {
542 DCHECK(template_url_service);
543 base::string16 result(base::i18n::ToLower(keyword));
544 base::TrimWhitespace(result, base::TRIM_ALL, &result);
545 // If this keyword is found with no additional cleaning of input, return it.
546 if (template_url_service->GetTemplateURLForKeyword(result) != nullptr)
547 return result;
548
549 // If keyword is not found, try removing a "http" or "https" scheme if any.
550 url::Component scheme_component;
551 if (url::ExtractScheme(base::UTF16ToUTF8(result).c_str(),
552 static_cast<int>(result.length()),
553 &scheme_component) &&
554 (!result.compare(0, scheme_component.end(),
555 base::ASCIIToUTF16(url::kHttpScheme)) ||
556 !result.compare(0, scheme_component.end(),
557 base::ASCIIToUTF16(url::kHttpsScheme)))) {
558 // Remove the scheme and the trailing ':'.
559 result.erase(0, scheme_component.end() + 1);
560 if (template_url_service->GetTemplateURLForKeyword(result) != nullptr)
561 return result;
562 // Many schemes usually have "//" after them, so strip it too.
563 const base::string16 after_scheme(base::ASCIIToUTF16("//"));
564 if (result.compare(0, after_scheme.length(), after_scheme) == 0)
565 result.erase(0, after_scheme.length());
566 if (template_url_service->GetTemplateURLForKeyword(result) != nullptr)
567 return result;
568 }
569
570 // Remove leading "www.", if any, and again try to find a matching keyword.
571 // The 'www.' stripping is done directly here instead of calling
572 // url_formatter::StripWWW because we're not assuming that the keyword is a
573 // hostname.
574 const base::string16 kWww(base::ASCIIToUTF16("www."));
575 constexpr size_t kWwwLength = 4;
576 result = base::StartsWith(result, kWww, base::CompareCase::SENSITIVE)
577 ? result.substr(kWwwLength)
578 : result;
579 if (template_url_service->GetTemplateURLForKeyword(result) != nullptr)
580 return result;
581
582 // Remove trailing "/", if any.
583 if (!result.empty() && result.back() == '/')
584 result.pop_back();
585 return result;
586 }
587