1 // Copyright (c) 2012 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.
5 #include "chrome/browser/autocomplete/autocomplete_match.h"
7 #include "base/i18n/time_formatting.h"
8 #include "base/logging.h"
9 #include "base/strings/string16.h"
10 #include "base/strings/string_number_conversions.h"
11 #include "base/strings/string_util.h"
12 #include "base/strings/utf_string_conversions.h"
13 #include "base/time/time.h"
14 #include "chrome/browser/autocomplete/autocomplete_provider.h"
15 #include "chrome/browser/search_engines/template_url.h"
16 #include "chrome/browser/search_engines/template_url_service.h"
17 #include "chrome/browser/search_engines/template_url_service_factory.h"
18 #include "content/public/common/url_constants.h"
19 #include "grit/theme_resources.h"
23 bool IsTrivialClassification(const ACMatchClassifications
& classifications
) {
24 return classifications
.empty() ||
25 ((classifications
.size() == 1) &&
26 (classifications
.back().style
== ACMatchClassification::NONE
));
31 // AutocompleteMatch ----------------------------------------------------------
34 const base::char16
AutocompleteMatch::kInvalidChars
[] = {
36 0x2028, // Line separator
37 0x2029, // Paragraph separator
41 AutocompleteMatch::AutocompleteMatch()
46 allowed_to_be_default_match(false),
47 transition(content::PAGE_TRANSITION_GENERATED
),
48 is_history_what_you_typed_match(false),
49 type(AutocompleteMatchType::SEARCH_WHAT_YOU_TYPED
),
51 from_previous(false) {
54 AutocompleteMatch::AutocompleteMatch(AutocompleteProvider
* provider
,
62 allowed_to_be_default_match(false),
63 transition(content::PAGE_TRANSITION_TYPED
),
64 is_history_what_you_typed_match(false),
67 from_previous(false) {
70 AutocompleteMatch::AutocompleteMatch(const AutocompleteMatch
& match
)
71 : provider(match
.provider
),
72 relevance(match
.relevance
),
73 typed_count(match
.typed_count
),
74 deletable(match
.deletable
),
75 fill_into_edit(match
.fill_into_edit
),
76 inline_autocompletion(match
.inline_autocompletion
),
77 allowed_to_be_default_match(match
.allowed_to_be_default_match
),
78 destination_url(match
.destination_url
),
79 stripped_destination_url(match
.stripped_destination_url
),
80 contents(match
.contents
),
81 contents_class(match
.contents_class
),
82 description(match
.description
),
83 description_class(match
.description_class
),
84 answer_contents(match
.answer_contents
),
85 answer_type(match
.answer_type
),
86 transition(match
.transition
),
87 is_history_what_you_typed_match(match
.is_history_what_you_typed_match
),
89 associated_keyword(match
.associated_keyword
.get() ?
90 new AutocompleteMatch(*match
.associated_keyword
) : NULL
),
91 keyword(match
.keyword
),
92 starred(match
.starred
),
93 from_previous(match
.from_previous
),
94 search_terms_args(match
.search_terms_args
.get() ?
95 new TemplateURLRef::SearchTermsArgs(*match
.search_terms_args
) :
97 additional_info(match
.additional_info
),
98 duplicate_matches(match
.duplicate_matches
) {
101 AutocompleteMatch::~AutocompleteMatch() {
104 AutocompleteMatch
& AutocompleteMatch::operator=(
105 const AutocompleteMatch
& match
) {
109 provider
= match
.provider
;
110 relevance
= match
.relevance
;
111 typed_count
= match
.typed_count
;
112 deletable
= match
.deletable
;
113 fill_into_edit
= match
.fill_into_edit
;
114 inline_autocompletion
= match
.inline_autocompletion
;
115 allowed_to_be_default_match
= match
.allowed_to_be_default_match
;
116 destination_url
= match
.destination_url
;
117 stripped_destination_url
= match
.stripped_destination_url
;
118 contents
= match
.contents
;
119 contents_class
= match
.contents_class
;
120 description
= match
.description
;
121 description_class
= match
.description_class
;
122 answer_contents
= match
.answer_contents
;
123 answer_type
= match
.answer_type
;
124 transition
= match
.transition
;
125 is_history_what_you_typed_match
= match
.is_history_what_you_typed_match
;
127 associated_keyword
.reset(match
.associated_keyword
.get() ?
128 new AutocompleteMatch(*match
.associated_keyword
) : NULL
);
129 keyword
= match
.keyword
;
130 starred
= match
.starred
;
131 from_previous
= match
.from_previous
;
132 search_terms_args
.reset(match
.search_terms_args
.get() ?
133 new TemplateURLRef::SearchTermsArgs(*match
.search_terms_args
) : NULL
);
134 additional_info
= match
.additional_info
;
135 duplicate_matches
= match
.duplicate_matches
;
140 int AutocompleteMatch::TypeToIcon(Type type
) {
156 IDR_OMNIBOX_EXTENSION_APP
,
161 COMPILE_ASSERT(arraysize(icons
) == AutocompleteMatchType::NUM_TYPES
,
162 icons_array_must_match_type_enum
);
167 int AutocompleteMatch::TypeToLocationBarIcon(Type type
) {
168 int id
= TypeToIcon(type
);
169 if (id
== IDR_OMNIBOX_HTTP
)
170 return IDR_LOCATION_BAR_HTTP
;
175 bool AutocompleteMatch::MoreRelevant(const AutocompleteMatch
& elem1
,
176 const AutocompleteMatch
& elem2
) {
177 // For equal-relevance matches, we sort alphabetically, so that providers
178 // who return multiple elements at the same priority get a "stable" sort
179 // across multiple updates.
180 return (elem1
.relevance
== elem2
.relevance
) ?
181 (elem1
.contents
< elem2
.contents
) : (elem1
.relevance
> elem2
.relevance
);
185 bool AutocompleteMatch::DestinationsEqual(const AutocompleteMatch
& elem1
,
186 const AutocompleteMatch
& elem2
) {
187 if (elem1
.stripped_destination_url
.is_empty() &&
188 elem2
.stripped_destination_url
.is_empty())
190 return elem1
.stripped_destination_url
== elem2
.stripped_destination_url
;
194 void AutocompleteMatch::ClassifyMatchInString(
195 const base::string16
& find_text
,
196 const base::string16
& text
,
198 ACMatchClassifications
* classification
) {
199 ClassifyLocationInString(text
.find(find_text
), find_text
.length(),
200 text
.length(), style
, classification
);
204 void AutocompleteMatch::ClassifyLocationInString(
205 size_t match_location
,
207 size_t overall_length
,
209 ACMatchClassifications
* classification
) {
210 classification
->clear();
212 // Don't classify anything about an empty string
213 // (AutocompleteMatch::Validate() checks this).
214 if (overall_length
== 0)
217 // Mark pre-match portion of string (if any).
218 if (match_location
!= 0) {
219 classification
->push_back(ACMatchClassification(0, style
));
222 // Mark matching portion of string.
223 if (match_location
== base::string16::npos
) {
224 // No match, above classification will suffice for whole string.
227 // Classifying an empty match makes no sense and will lead to validation
229 DCHECK_GT(match_length
, 0U);
230 classification
->push_back(ACMatchClassification(match_location
,
231 (style
| ACMatchClassification::MATCH
) & ~ACMatchClassification::DIM
));
233 // Mark post-match portion of string (if any).
234 const size_t after_match(match_location
+ match_length
);
235 if (after_match
< overall_length
) {
236 classification
->push_back(ACMatchClassification(after_match
, style
));
241 AutocompleteMatch::ACMatchClassifications
242 AutocompleteMatch::MergeClassifications(
243 const ACMatchClassifications
& classifications1
,
244 const ACMatchClassifications
& classifications2
) {
245 // We must return the empty vector only if both inputs are truly empty.
246 // The result of merging an empty vector with a single (0, NONE)
247 // classification is the latter one-entry vector.
248 if (IsTrivialClassification(classifications1
))
249 return classifications2
.empty() ? classifications1
: classifications2
;
250 if (IsTrivialClassification(classifications2
))
251 return classifications1
;
253 ACMatchClassifications output
;
254 for (ACMatchClassifications::const_iterator i
= classifications1
.begin(),
255 j
= classifications2
.begin(); i
!= classifications1
.end();) {
256 AutocompleteMatch::AddLastClassificationIfNecessary(&output
,
257 std::max(i
->offset
, j
->offset
), i
->style
| j
->style
);
258 const size_t next_i_offset
= (i
+ 1) == classifications1
.end() ?
259 static_cast<size_t>(-1) : (i
+ 1)->offset
;
260 const size_t next_j_offset
= (j
+ 1) == classifications2
.end() ?
261 static_cast<size_t>(-1) : (j
+ 1)->offset
;
262 if (next_i_offset
>= next_j_offset
)
264 if (next_j_offset
>= next_i_offset
)
272 std::string
AutocompleteMatch::ClassificationsToString(
273 const ACMatchClassifications
& classifications
) {
274 std::string serialized_classifications
;
275 for (size_t i
= 0; i
< classifications
.size(); ++i
) {
277 serialized_classifications
+= ',';
278 serialized_classifications
+= base::IntToString(classifications
[i
].offset
) +
279 ',' + base::IntToString(classifications
[i
].style
);
281 return serialized_classifications
;
285 ACMatchClassifications
AutocompleteMatch::ClassificationsFromString(
286 const std::string
& serialized_classifications
) {
287 ACMatchClassifications classifications
;
288 std::vector
<std::string
> tokens
;
289 Tokenize(serialized_classifications
, ",", &tokens
);
290 DCHECK(!(tokens
.size() & 1)); // The number of tokens should be even.
291 for (size_t i
= 0; i
< tokens
.size(); i
+= 2) {
292 int classification_offset
= 0;
293 int classification_style
= ACMatchClassification::NONE
;
294 if (!base::StringToInt(tokens
[i
], &classification_offset
) ||
295 !base::StringToInt(tokens
[i
+ 1], &classification_style
)) {
297 return classifications
;
299 classifications
.push_back(ACMatchClassification(classification_offset
,
300 classification_style
));
302 return classifications
;
306 void AutocompleteMatch::AddLastClassificationIfNecessary(
307 ACMatchClassifications
* classifications
,
310 DCHECK(classifications
);
311 if (classifications
->empty() || classifications
->back().style
!= style
) {
312 DCHECK(classifications
->empty() ||
313 (offset
> classifications
->back().offset
));
314 classifications
->push_back(ACMatchClassification(offset
, style
));
319 base::string16
AutocompleteMatch::SanitizeString(const base::string16
& text
) {
320 // NOTE: This logic is mirrored by |sanitizeString()| in
321 // omnibox_custom_bindings.js.
322 base::string16 result
;
323 base::TrimWhitespace(text
, base::TRIM_LEADING
, &result
);
324 base::RemoveChars(result
, kInvalidChars
, &result
);
329 bool AutocompleteMatch::IsSearchType(Type type
) {
330 return type
== AutocompleteMatchType::SEARCH_WHAT_YOU_TYPED
||
331 type
== AutocompleteMatchType::SEARCH_HISTORY
||
332 type
== AutocompleteMatchType::SEARCH_SUGGEST
||
333 type
== AutocompleteMatchType::SEARCH_OTHER_ENGINE
||
334 IsSpecializedSearchType(type
);
338 bool AutocompleteMatch::IsSpecializedSearchType(Type type
) {
339 return type
== AutocompleteMatchType::SEARCH_SUGGEST_ENTITY
||
340 type
== AutocompleteMatchType::SEARCH_SUGGEST_INFINITE
||
341 type
== AutocompleteMatchType::SEARCH_SUGGEST_PERSONALIZED
||
342 type
== AutocompleteMatchType::SEARCH_SUGGEST_PROFILE
;
345 void AutocompleteMatch::ComputeStrippedDestinationURL(Profile
* profile
) {
346 stripped_destination_url
= destination_url
;
347 if (!stripped_destination_url
.is_valid())
350 // If the destination URL looks like it was generated from a TemplateURL,
351 // remove all substitutions other than the search terms. This allows us
352 // to eliminate cases like past search URLs from history that differ only
353 // by some obscure query param from each other or from the search/keyword
355 TemplateURL
* template_url
= GetTemplateURL(profile
, true);
356 if (template_url
!= NULL
&& template_url
->SupportsReplacement()) {
357 base::string16 search_terms
;
358 if (template_url
->ExtractSearchTermsFromURL(stripped_destination_url
,
360 stripped_destination_url
=
361 GURL(template_url
->url_ref().ReplaceSearchTerms(
362 TemplateURLRef::SearchTermsArgs(search_terms
)));
366 // |replacements| keeps all the substitions we're going to make to
367 // from {destination_url} to {stripped_destination_url}. |need_replacement|
368 // is a helper variable that helps us keep track of whether we need
369 // to apply the replacement.
370 bool needs_replacement
= false;
371 GURL::Replacements replacements
;
373 // Remove the www. prefix from the host.
374 static const char prefix
[] = "www.";
375 static const size_t prefix_len
= arraysize(prefix
) - 1;
376 std::string host
= stripped_destination_url
.host();
377 if (host
.compare(0, prefix_len
, prefix
) == 0) {
378 host
= host
.substr(prefix_len
);
379 replacements
.SetHostStr(host
);
380 needs_replacement
= true;
383 // Replace https protocol with http protocol.
384 if (stripped_destination_url
.SchemeIs(url::kHttpsScheme
)) {
385 replacements
.SetScheme(url::kHttpScheme
,
386 url::Component(0, strlen(url::kHttpScheme
)));
387 needs_replacement
= true;
390 if (needs_replacement
)
391 stripped_destination_url
= stripped_destination_url
.ReplaceComponents(
395 void AutocompleteMatch::GetKeywordUIState(Profile
* profile
,
396 base::string16
* keyword
,
397 bool* is_keyword_hint
) const {
398 *is_keyword_hint
= associated_keyword
.get() != NULL
;
399 keyword
->assign(*is_keyword_hint
? associated_keyword
->keyword
:
400 GetSubstitutingExplicitlyInvokedKeyword(profile
));
403 base::string16
AutocompleteMatch::GetSubstitutingExplicitlyInvokedKeyword(
404 Profile
* profile
) const {
405 if (transition
!= content::PAGE_TRANSITION_KEYWORD
)
406 return base::string16();
407 const TemplateURL
* t_url
= GetTemplateURL(profile
, false);
408 return (t_url
&& t_url
->SupportsReplacement()) ? keyword
: base::string16();
411 TemplateURL
* AutocompleteMatch::GetTemplateURL(
412 Profile
* profile
, bool allow_fallback_to_destination_host
) const {
414 TemplateURLService
* template_url_service
=
415 TemplateURLServiceFactory::GetForProfile(profile
);
416 if (template_url_service
== NULL
)
418 TemplateURL
* template_url
= keyword
.empty() ? NULL
:
419 template_url_service
->GetTemplateURLForKeyword(keyword
);
420 if (template_url
== NULL
&& allow_fallback_to_destination_host
) {
421 template_url
= template_url_service
->GetTemplateURLForHost(
422 destination_url
.host());
427 void AutocompleteMatch::RecordAdditionalInfo(const std::string
& property
,
428 const std::string
& value
) {
429 DCHECK(!property
.empty());
430 DCHECK(!value
.empty());
431 additional_info
[property
] = value
;
434 void AutocompleteMatch::RecordAdditionalInfo(const std::string
& property
,
436 RecordAdditionalInfo(property
, base::IntToString(value
));
439 void AutocompleteMatch::RecordAdditionalInfo(const std::string
& property
,
440 const base::Time
& value
) {
441 RecordAdditionalInfo(property
,
443 base::TimeFormatShortDateAndTime(value
)));
446 std::string
AutocompleteMatch::GetAdditionalInfo(
447 const std::string
& property
) const {
448 AdditionalInfo::const_iterator
i(additional_info
.find(property
));
449 return (i
== additional_info
.end()) ? std::string() : i
->second
;
452 bool AutocompleteMatch::IsVerbatimType() const {
453 const bool is_keyword_verbatim_match
=
454 (type
== AutocompleteMatchType::SEARCH_OTHER_ENGINE
&&
456 provider
->type() == AutocompleteProvider::TYPE_SEARCH
);
457 return type
== AutocompleteMatchType::SEARCH_WHAT_YOU_TYPED
||
458 type
== AutocompleteMatchType::URL_WHAT_YOU_TYPED
||
459 is_keyword_verbatim_match
;
462 bool AutocompleteMatch::SupportsDeletion() const {
466 for (ACMatches::const_iterator
it(duplicate_matches
.begin());
467 it
!= duplicate_matches
.end(); ++it
) {
475 void AutocompleteMatch::Validate() const {
476 ValidateClassifications(contents
, contents_class
);
477 ValidateClassifications(description
, description_class
);
480 void AutocompleteMatch::ValidateClassifications(
481 const base::string16
& text
,
482 const ACMatchClassifications
& classifications
) const {
484 DCHECK(classifications
.empty());
488 // The classifications should always cover the whole string.
489 DCHECK(!classifications
.empty()) << "No classification for \"" << text
<< '"';
490 DCHECK_EQ(0U, classifications
[0].offset
)
491 << "Classification misses beginning for \"" << text
<< '"';
492 if (classifications
.size() == 1)
495 // The classifications should always be sorted.
496 size_t last_offset
= classifications
[0].offset
;
497 for (ACMatchClassifications::const_iterator
i(classifications
.begin() + 1);
498 i
!= classifications
.end(); ++i
) {
499 const char* provider_name
= provider
? provider
->GetName() : "None";
500 DCHECK_GT(i
->offset
, last_offset
)
501 << " Classification for \"" << text
<< "\" with offset of " << i
->offset
502 << " is unsorted in relation to last offset of " << last_offset
503 << ". Provider: " << provider_name
<< ".";
504 DCHECK_LT(i
->offset
, text
.length())
505 << " Classification of [" << i
->offset
<< "," << text
.length()
506 << "] is out of bounds for \"" << text
<< "\". Provider: "
507 << provider_name
<< ".";
508 last_offset
= i
->offset
;