Chromium Code Reviews| OLD | NEW |
|---|---|
| 1 // Copyright (c) 2011 The Chromium Authors. All rights reserved. | 1 // Copyright (c) 2011 The Chromium Authors. All rights reserved. |
| 2 // Use of this source code is governed by a BSD-style license that can be | 2 // Use of this source code is governed by a BSD-style license that can be |
| 3 // found in the LICENSE file. | 3 // found in the LICENSE file. |
| 4 | 4 |
| 5 #include "chrome/browser/history/query_parser.h" | 5 #include "chrome/browser/history/query_parser.h" |
| 6 | 6 |
| 7 #include <algorithm> | 7 #include <algorithm> |
| 8 | 8 |
| 9 #include "base/compiler_specific.h" | 9 #include "base/compiler_specific.h" |
| 10 #include "base/i18n/break_iterator.h" | 10 #include "base/i18n/break_iterator.h" |
| 11 #include "base/i18n/case_conversion.h" | 11 #include "base/i18n/case_conversion.h" |
| 12 #include "base/logging.h" | 12 #include "base/logging.h" |
| 13 #include "base/stl_util.h" | 13 #include "base/stl_util.h" |
| 14 #include "base/string_util.h" | |
| 14 | 15 |
| 15 namespace { | 16 namespace { |
| 16 | 17 |
| 17 // Returns true if |mp1.first| is less than |mp2.first|. This is used to | 18 // Returns true if |mp1.first| is less than |mp2.first|. This is used to |
| 18 // sort match positions. | 19 // sort match positions. |
| 19 int CompareMatchPosition(const Snippet::MatchPosition& mp1, | 20 int CompareMatchPosition(const Snippet::MatchPosition& mp1, |
| 20 const Snippet::MatchPosition& mp2) { | 21 const Snippet::MatchPosition& mp2) { |
| 21 return mp1.first < mp2.first; | 22 return mp1.first < mp2.first; |
| 22 } | 23 } |
| 23 | 24 |
| (...skipping 15 matching lines...) Expand all Loading... | |
| 39 mp.second = std::max(mp.second, i->second); | 40 mp.second = std::max(mp.second, i->second); |
| 40 i = matches->erase(i); | 41 i = matches->erase(i); |
| 41 } else { | 42 } else { |
| 42 return; | 43 return; |
| 43 } | 44 } |
| 44 } | 45 } |
| 45 } | 46 } |
| 46 | 47 |
| 47 // Sorts the match positions in |matches| by their first index, then coalesces | 48 // Sorts the match positions in |matches| by their first index, then coalesces |
| 48 // any match positions that intersect each other. | 49 // any match positions that intersect each other. |
| 49 void CoalseAndSortMatchPositions(Snippet::MatchPositions* matches) { | 50 void CoalesceAndSortMatchPositions(Snippet::MatchPositions* matches) { |
| 50 std::sort(matches->begin(), matches->end(), &CompareMatchPosition); | 51 std::sort(matches->begin(), matches->end(), &CompareMatchPosition); |
| 51 // WARNING: we don't use iterator here as CoalesceMatchesFrom may remove | 52 // WARNING: we don't use iterator here as CoalesceMatchesFrom may remove |
| 52 // from matches. | 53 // from matches. |
| 53 for (size_t i = 0; i < matches->size(); ++i) | 54 for (size_t i = 0; i < matches->size(); ++i) |
| 54 CoalesceMatchesFrom(i, matches); | 55 CoalesceMatchesFrom(i, matches); |
| 55 } | 56 } |
| 56 | 57 |
| 57 // Returns true if the character is considered a quote. | 58 // Returns true if the character is considered a quote. |
| 58 bool IsQueryQuote(wchar_t ch) { | 59 bool IsQueryQuote(wchar_t ch) { |
| 59 return ch == '"' || | 60 return ch == '"' || |
| 60 ch == 0xab || // left pointing double angle bracket | 61 ch == 0xab || // left pointing double angle bracket |
| 61 ch == 0xbb || // right pointing double angle bracket | 62 ch == 0xbb || // right pointing double angle bracket |
| 62 ch == 0x201c || // left double quotation mark | 63 ch == 0x201c || // left double quotation mark |
| 63 ch == 0x201d || // right double quotation mark | 64 ch == 0x201d || // right double quotation mark |
| 64 ch == 0x201e; // double low-9 quotation mark | 65 ch == 0x201e; // double low-9 quotation mark |
| 65 } | 66 } |
| 66 | 67 |
| 68 // Returns true if the character is considered non-breaking when it appears in | |
| 69 // the middle of a word. This can be used to prevent an URL-like query from | |
| 70 // being broken into multiple words. | |
| 71 bool IsNonBreakingSymbol(wchar_t ch) { | |
| 72 return ch == '.' || ch == '-'; | |
| 73 } | |
| 74 | |
| 67 } // namespace | 75 } // namespace |
| 68 | 76 |
| 69 // Inheritance structure: | 77 // Inheritance structure: |
| 70 // Queries are represented as trees of QueryNodes. | 78 // Queries are represented as trees of QueryNodes. |
| 71 // QueryNodes are either a collection of subnodes (a QueryNodeList) | 79 // QueryNodes are either a collection of subnodes (a QueryNodeList) |
| 72 // or a single word (a QueryNodeWord). | 80 // or a single word (a QueryNodeWord). |
| 73 | 81 |
| 74 // A QueryNodeWord is a single word in the query. | 82 // A QueryNodeWord is a sequence of consecutive characters in a query. |
| 83 // It can be an actual word or an URL-like sequence of characters. | |
| 75 class QueryNodeWord : public QueryNode { | 84 class QueryNodeWord : public QueryNode { |
| 76 public: | 85 public: |
| 77 explicit QueryNodeWord(const string16& word); | 86 explicit QueryNodeWord(const string16& word); |
| 78 virtual ~QueryNodeWord(); | 87 virtual ~QueryNodeWord(); |
| 79 | 88 |
| 80 const string16& word() const { return word_; } | 89 const string16& word() const { return word_; } |
| 81 | 90 |
| 82 void set_literal(bool literal) { literal_ = literal; } | 91 void set_literal(bool literal) { literal_ = literal; } |
| 92 void Append(const string16& word); | |
|
Scott Hess - ex-Googler
2013/04/17 18:14:02
I think this sense of "Append" is enough different
| |
| 83 | 93 |
| 84 // QueryNode: | 94 // QueryNode: |
| 85 virtual int AppendToSQLiteQuery(string16* query) const OVERRIDE; | 95 virtual int AppendToSQLiteQuery(string16* query) const OVERRIDE; |
| 86 virtual bool IsWord() const OVERRIDE; | 96 virtual bool IsWord() const OVERRIDE; |
| 87 virtual bool Matches(const string16& word, bool exact) const OVERRIDE; | 97 virtual bool Matches(const string16& word, bool exact) const OVERRIDE; |
| 88 virtual bool HasMatchIn( | 98 virtual bool HasMatchIn( |
| 89 const std::vector<QueryWord>& words, | 99 const std::vector<QueryWord>& words, |
| 90 Snippet::MatchPositions* match_positions) const OVERRIDE; | 100 Snippet::MatchPositions* match_positions) const OVERRIDE; |
| 91 virtual void AppendWords(std::vector<string16>* words) const OVERRIDE; | 101 virtual void AppendWords(std::vector<string16>* words) const OVERRIDE; |
| 92 | 102 |
| 93 private: | 103 private: |
| 94 string16 word_; | 104 string16 word_; |
| 95 bool literal_; | 105 bool literal_; |
| 96 | 106 |
| 97 DISALLOW_COPY_AND_ASSIGN(QueryNodeWord); | 107 DISALLOW_COPY_AND_ASSIGN(QueryNodeWord); |
| 98 }; | 108 }; |
| 99 | 109 |
| 100 QueryNodeWord::QueryNodeWord(const string16& word) | 110 QueryNodeWord::QueryNodeWord(const string16& word) |
| 101 : word_(word), | 111 : word_(word), |
| 102 literal_(false) {} | 112 literal_(false) {} |
| 103 | 113 |
| 104 QueryNodeWord::~QueryNodeWord() {} | 114 QueryNodeWord::~QueryNodeWord() {} |
| 105 | 115 |
| 116 void QueryNodeWord::Append(const string16& str) { | |
| 117 word_ += str; | |
| 118 } | |
| 119 | |
| 106 int QueryNodeWord::AppendToSQLiteQuery(string16* query) const { | 120 int QueryNodeWord::AppendToSQLiteQuery(string16* query) const { |
| 107 query->append(word_); | 121 query->append(word_); |
| 108 | 122 |
| 109 // Use prefix search if we're not literal and long enough. | 123 // Use prefix search if we're not literal and long enough. |
| 110 if (!literal_ && QueryParser::IsWordLongEnoughForPrefixSearch(word_)) | 124 if (!literal_ && QueryParser::IsWordLongEnoughForPrefixSearch(word_)) |
| 111 *query += L'*'; | 125 *query += L'*'; |
| 112 return 1; | 126 return 1; |
| 113 } | 127 } |
| 114 | 128 |
| 115 bool QueryNodeWord::IsWord() const { | 129 bool QueryNodeWord::IsWord() const { |
| (...skipping 225 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 341 if (!query_nodes[i]->HasMatchIn(query_words, &matches)) | 355 if (!query_nodes[i]->HasMatchIn(query_words, &matches)) |
| 342 return false; | 356 return false; |
| 343 } | 357 } |
| 344 if (lower_text.length() != text.length()) { | 358 if (lower_text.length() != text.length()) { |
| 345 // The lower case string differs from the original string. The matches are | 359 // The lower case string differs from the original string. The matches are |
| 346 // meaningless. | 360 // meaningless. |
| 347 // TODO(sky): we need a better way to align the positions so that we don't | 361 // TODO(sky): we need a better way to align the positions so that we don't |
| 348 // completely punt here. | 362 // completely punt here. |
| 349 match_positions->clear(); | 363 match_positions->clear(); |
| 350 } else { | 364 } else { |
| 351 CoalseAndSortMatchPositions(&matches); | 365 CoalesceAndSortMatchPositions(&matches); |
| 352 match_positions->swap(matches); | 366 match_positions->swap(matches); |
| 353 } | 367 } |
| 354 return true; | 368 return true; |
| 355 } | 369 } |
| 356 | 370 |
| 357 bool QueryParser::ParseQueryImpl(const string16& query, QueryNodeList* root) { | 371 bool QueryParser::ParseQueryImpl(const string16& query, QueryNodeList* root) { |
| 358 base::i18n::BreakIterator iter(query, base::i18n::BreakIterator::BREAK_WORD); | 372 base::i18n::BreakIterator iter(query, base::i18n::BreakIterator::BREAK_WORD); |
| 359 // TODO(evanm): support a locale here | 373 // TODO(evanm): support a locale here |
| 360 if (!iter.Init()) | 374 if (!iter.Init()) |
| 361 return false; | 375 return false; |
| 362 | 376 |
| 363 // To handle nesting, we maintain a stack of QueryNodeLists. | 377 // To handle nesting, we maintain a stack of QueryNodeLists. |
| 364 // The last element (back) of the stack contains the current, deepest node. | 378 // The last element (back) of the stack contains the current, deepest node. |
| 365 std::vector<QueryNodeList*> query_stack; | 379 std::vector<QueryNodeList*> query_stack; |
| 366 query_stack.push_back(root); | 380 query_stack.push_back(root); |
| 367 | 381 |
| 368 bool in_quotes = false; // whether we're currently in a quoted phrase | 382 bool in_quotes = false; // whether we're currently in a quoted phrase |
| 383 QueryNodeWord* current_word = NULL; | |
| 369 while (iter.Advance()) { | 384 while (iter.Advance()) { |
| 370 // Just found a span between 'prev' (inclusive) and 'pos' (exclusive). It | 385 // Just found a span between 'prev' (inclusive) and 'pos' (exclusive). It |
| 371 // is not necessarily a word, but could also be a sequence of punctuation | 386 // is not necessarily a word, it could also be a punctuation or whitespace |
| 372 // or whitespace. | 387 // character. Punctuation is preserved inside quotes, and otherwise removed |
| 373 if (iter.IsWord()) { | 388 // except if it is a non-breaking character in the middle of a word. |
| 374 QueryNodeWord* word_node = new QueryNodeWord(iter.GetString()); | 389 |
| 375 if (in_quotes) | 390 wchar_t first_char = query[iter.prev()]; |
| 376 word_node->set_literal(true); | 391 if (IsQueryQuote(first_char)) { |
| 377 query_stack.back()->AddChild(word_node); | 392 if (!in_quotes) { |
| 378 } else { // Punctuation. | 393 QueryNodeList* quotes_node = new QueryNodePhrase; |
| 379 if (IsQueryQuote(query[iter.prev()])) { | 394 query_stack.back()->AddChild(quotes_node); |
| 380 if (!in_quotes) { | 395 query_stack.push_back(quotes_node); |
| 381 QueryNodeList* quotes_node = new QueryNodePhrase; | 396 in_quotes = true; |
| 382 query_stack.back()->AddChild(quotes_node); | 397 } else { |
| 383 query_stack.push_back(quotes_node); | 398 query_stack.pop_back(); // Stop adding to the quoted phrase. |
| 384 in_quotes = true; | 399 in_quotes = false; |
| 385 } else { | |
| 386 query_stack.pop_back(); // Stop adding to the quoted phrase. | |
| 387 in_quotes = false; | |
| 388 } | |
| 389 } | 400 } |
| 401 current_word = NULL; | |
| 402 } else if (iter.IsWord() || (in_quotes && !IsWhitespace(first_char))) { | |
|
Scott Hess - ex-Googler
2013/04/17 18:14:02
Previously, if there were a sequence of IsWord() t
| |
| 403 // Append to the current word if the new token is a word or a non- | |
| 404 // whitespace token inside quotes. | |
| 405 if (current_word) { | |
| 406 current_word->Append(iter.GetString()); | |
| 407 } else { | |
| 408 current_word = new QueryNodeWord(iter.GetString()); | |
| 409 current_word->set_literal(in_quotes); | |
| 410 query_stack.back()->AddChild(current_word); | |
| 411 } | |
| 412 } else if (current_word != NULL) { | |
| 413 // Allow non-breaking symbols inside a word. | |
| 414 // Any other punctuation or whitespace character ends the current word. | |
| 415 | |
| 416 // TODO(dubroy): Consider sharing code with the omnibox to allow a more | |
| 417 // accurate "best guess" of whether a sequence of characters is a URL. | |
| 418 if (IsNonBreakingSymbol(first_char)) | |
| 419 current_word->Append(iter.GetString()); | |
|
Scott Hess - ex-Googler
2013/04/17 18:14:02
I believe this won't back out the symbol(s) if the
| |
| 420 else | |
| 421 current_word = NULL; | |
| 390 } | 422 } |
| 391 } | 423 } |
| 392 | 424 |
| 393 root->RemoveEmptySubnodes(); | 425 root->RemoveEmptySubnodes(); |
| 394 return true; | 426 return true; |
| 395 } | 427 } |
| 396 | 428 |
| 397 void QueryParser::ExtractQueryWords(const string16& text, | 429 void QueryParser::ExtractQueryWords(const string16& text, |
| 398 std::vector<QueryWord>* words) { | 430 std::vector<QueryWord>* words) { |
| 399 base::i18n::BreakIterator iter(text, base::i18n::BreakIterator::BREAK_WORD); | 431 base::i18n::BreakIterator iter(text, base::i18n::BreakIterator::BREAK_WORD); |
| 400 // TODO(evanm): support a locale here | 432 // TODO(evanm): support a locale here |
| 401 if (!iter.Init()) | 433 if (!iter.Init()) |
| 402 return; | 434 return; |
| 403 | 435 |
| 404 while (iter.Advance()) { | 436 while (iter.Advance()) { |
| 405 // Just found a span between 'prev' (inclusive) and 'pos' (exclusive). It | 437 // Just found a span between 'prev' (inclusive) and 'pos' (exclusive). It |
| 406 // is not necessarily a word, but could also be a sequence of punctuation | 438 // is not necessarily a word, but could also be a sequence of punctuation |
| 407 // or whitespace. | 439 // or whitespace. |
| 408 if (iter.IsWord()) { | 440 if (iter.IsWord()) { |
| 409 string16 word = iter.GetString(); | 441 string16 word = iter.GetString(); |
| 410 if (!word.empty()) { | 442 if (!word.empty()) { |
| 411 words->push_back(QueryWord()); | 443 words->push_back(QueryWord()); |
| 412 words->back().word = word; | 444 words->back().word = word; |
| 413 words->back().position = iter.prev(); | 445 words->back().position = iter.prev(); |
| 414 } | 446 } |
| 415 } | 447 } |
| 416 } | 448 } |
| 417 } | 449 } |
| OLD | NEW |