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 = i->second; | 40 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 == '.' || | |
| 73 ch == '-' || | |
| 74 ch == '_' || | |
| 75 ch == '/' || | |
| 76 ch == '~'; | |
| 77 } | |
| 78 | |
| 67 } // namespace | 79 } // namespace |
| 68 | 80 |
| 69 // Inheritance structure: | 81 // Inheritance structure: |
| 70 // Queries are represented as trees of QueryNodes. | 82 // Queries are represented as trees of QueryNodes. |
| 71 // QueryNodes are either a collection of subnodes (a QueryNodeList) | 83 // QueryNodes are either a collection of subnodes (a QueryNodeList) |
| 72 // or a single word (a QueryNodeWord). | 84 // or a single word (a QueryNodeWord). |
| 73 | 85 |
| 74 // A QueryNodeWord is a single word in the query. | 86 // A QueryNodeWord is a single word in the query. |
|
sky
2012/02/08 23:31:28
Update description.
Patrick Dubroy
2012/02/09 14:47:00
Done.
| |
| 75 class QueryNodeWord : public QueryNode { | 87 class QueryNodeWord : public QueryNode { |
| 76 public: | 88 public: |
| 77 explicit QueryNodeWord(const string16& word); | 89 explicit QueryNodeWord(const string16& word); |
| 78 virtual ~QueryNodeWord(); | 90 virtual ~QueryNodeWord(); |
| 79 | 91 |
| 80 const string16& word() const { return word_; } | 92 const string16& word() const { return word_; } |
| 81 | 93 |
| 82 void set_literal(bool literal) { literal_ = literal; } | 94 void set_literal(bool literal) { literal_ = literal; } |
| 83 | 95 |
| 84 // QueryNode: | 96 // QueryNode: |
| 85 virtual int AppendToSQLiteQuery(string16* query) const OVERRIDE; | 97 virtual int AppendToSQLiteQuery(string16* query) const OVERRIDE; |
| 86 virtual bool IsWord() const OVERRIDE; | 98 virtual bool IsWord() const OVERRIDE; |
| 87 virtual bool Matches(const string16& word, bool exact) const OVERRIDE; | 99 virtual bool Matches(const string16& word, bool exact) const OVERRIDE; |
| 88 virtual bool HasMatchIn( | 100 virtual bool HasMatchIn( |
| 89 const std::vector<QueryWord>& words, | 101 const std::vector<QueryWord>& words, |
| 90 Snippet::MatchPositions* match_positions) const OVERRIDE; | 102 Snippet::MatchPositions* match_positions) const OVERRIDE; |
| 91 virtual void AppendWords(std::vector<string16>* words) const OVERRIDE; | 103 virtual void AppendWords(std::vector<string16>* words) const OVERRIDE; |
| 104 virtual void Append(const string16& word); | |
|
sky
2012/02/08 23:31:28
This isn't part of QueryNode, so move it some wher
Patrick Dubroy
2012/02/09 14:47:00
Whoops, yeah. Done.
| |
| 92 | 105 |
| 93 private: | 106 private: |
| 94 string16 word_; | 107 string16 word_; |
| 95 bool literal_; | 108 bool literal_; |
| 96 | 109 |
| 97 DISALLOW_COPY_AND_ASSIGN(QueryNodeWord); | 110 DISALLOW_COPY_AND_ASSIGN(QueryNodeWord); |
| 98 }; | 111 }; |
| 99 | 112 |
| 100 QueryNodeWord::QueryNodeWord(const string16& word) | 113 QueryNodeWord::QueryNodeWord(const string16& word) |
| 101 : word_(word), | 114 : word_(word), |
| (...skipping 32 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 134 return true; | 147 return true; |
| 135 } | 148 } |
| 136 } | 149 } |
| 137 return false; | 150 return false; |
| 138 } | 151 } |
| 139 | 152 |
| 140 void QueryNodeWord::AppendWords(std::vector<string16>* words) const { | 153 void QueryNodeWord::AppendWords(std::vector<string16>* words) const { |
| 141 words->push_back(word_); | 154 words->push_back(word_); |
| 142 } | 155 } |
| 143 | 156 |
| 157 void QueryNodeWord::Append(const string16& str) { | |
| 158 word_ += str; | |
| 159 } | |
| 160 | |
| 144 // A QueryNodeList has a collection of QueryNodes which are deleted in the end. | 161 // A QueryNodeList has a collection of QueryNodes which are deleted in the end. |
| 145 class QueryNodeList : public QueryNode { | 162 class QueryNodeList : public QueryNode { |
| 146 public: | 163 public: |
| 147 typedef std::vector<QueryNode*> QueryNodeVector; | 164 typedef std::vector<QueryNode*> QueryNodeVector; |
| 148 | 165 |
| 149 QueryNodeList(); | 166 QueryNodeList(); |
| 150 virtual ~QueryNodeList(); | 167 virtual ~QueryNodeList(); |
| 151 | 168 |
| 152 QueryNodeVector* children() { return &children_; } | 169 QueryNodeVector* children() { return &children_; } |
| 153 | 170 |
| (...skipping 186 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 340 if (!query_nodes[i]->HasMatchIn(query_words, &matches)) | 357 if (!query_nodes[i]->HasMatchIn(query_words, &matches)) |
| 341 return false; | 358 return false; |
| 342 } | 359 } |
| 343 if (lower_text.length() != text.length()) { | 360 if (lower_text.length() != text.length()) { |
| 344 // The lower case string differs from the original string. The matches are | 361 // The lower case string differs from the original string. The matches are |
| 345 // meaningless. | 362 // meaningless. |
| 346 // TODO(sky): we need a better way to align the positions so that we don't | 363 // TODO(sky): we need a better way to align the positions so that we don't |
| 347 // completely punt here. | 364 // completely punt here. |
| 348 match_positions->clear(); | 365 match_positions->clear(); |
| 349 } else { | 366 } else { |
| 350 CoalseAndSortMatchPositions(&matches); | 367 CoalesceAndSortMatchPositions(&matches); |
| 351 match_positions->swap(matches); | 368 match_positions->swap(matches); |
| 352 } | 369 } |
| 353 return true; | 370 return true; |
| 354 } | 371 } |
| 355 | 372 |
| 356 bool QueryParser::ParseQueryImpl(const string16& query, QueryNodeList* root) { | 373 bool QueryParser::ParseQueryImpl(const string16& query, QueryNodeList* root) { |
| 357 base::i18n::BreakIterator iter(query, base::i18n::BreakIterator::BREAK_WORD); | 374 base::i18n::BreakIterator iter(query, base::i18n::BreakIterator::BREAK_WORD); |
| 358 // TODO(evanm): support a locale here | 375 // TODO(evanm): support a locale here |
| 359 if (!iter.Init()) | 376 if (!iter.Init()) |
| 360 return false; | 377 return false; |
| 361 | 378 |
| 362 // To handle nesting, we maintain a stack of QueryNodeLists. | 379 // To handle nesting, we maintain a stack of QueryNodeLists. |
| 363 // The last element (back) of the stack contains the current, deepest node. | 380 // The last element (back) of the stack contains the current, deepest node. |
| 364 std::vector<QueryNodeList*> query_stack; | 381 std::vector<QueryNodeList*> query_stack; |
| 365 query_stack.push_back(root); | 382 query_stack.push_back(root); |
| 366 | 383 |
| 367 bool in_quotes = false; // whether we're currently in a quoted phrase | 384 bool in_quotes = false; // whether we're currently in a quoted phrase |
| 385 QueryNodeWord* current_word = NULL; | |
| 368 while (iter.Advance()) { | 386 while (iter.Advance()) { |
| 369 // Just found a span between 'prev' (inclusive) and 'pos' (exclusive). It | 387 // Just found a span between 'prev' (inclusive) and 'pos' (exclusive). It |
| 370 // is not necessarily a word, but could also be a sequence of punctuation | 388 // is not necessarily a word, it could also be a punctuation or whitespace |
| 371 // or whitespace. | 389 // character. Punctuation is preserved inside quotes, and otherwise removed |
| 372 if (iter.IsWord()) { | 390 // except if it is a non-breaking character in the middle of a word. |
| 373 QueryNodeWord* word_node = new QueryNodeWord(iter.GetString()); | 391 |
| 374 if (in_quotes) | 392 wchar_t last_char = query[iter.prev()]; |
| 375 word_node->set_literal(true); | 393 if (IsQueryQuote(last_char)) { |
| 376 query_stack.back()->AddChild(word_node); | 394 if (!in_quotes) { |
| 377 } else { // Punctuation. | 395 QueryNodeList* quotes_node = new QueryNodePhrase; |
| 378 if (IsQueryQuote(query[iter.prev()])) { | 396 query_stack.back()->AddChild(quotes_node); |
| 379 if (!in_quotes) { | 397 query_stack.push_back(quotes_node); |
| 380 QueryNodeList* quotes_node = new QueryNodePhrase; | 398 in_quotes = true; |
| 381 query_stack.back()->AddChild(quotes_node); | 399 } else { |
| 382 query_stack.push_back(quotes_node); | 400 query_stack.pop_back(); // Stop adding to the quoted phrase. |
| 383 in_quotes = true; | 401 in_quotes = false; |
| 384 } else { | |
| 385 query_stack.pop_back(); // Stop adding to the quoted phrase. | |
| 386 in_quotes = false; | |
| 387 } | |
| 388 } | 402 } |
| 403 current_word = NULL; | |
| 404 } else if (iter.IsWord() || (in_quotes && !IsWhitespace(last_char))) { | |
| 405 // Append to the current word if the new token is a word or a non- | |
| 406 // whitespace character inside quotes. | |
| 407 if (current_word) { | |
| 408 current_word->Append(iter.GetString()); | |
| 409 } else { | |
| 410 current_word = new QueryNodeWord(iter.GetString()); | |
| 411 current_word->set_literal(in_quotes); | |
| 412 query_stack.back()->AddChild(current_word); | |
| 413 } | |
| 414 } else if (current_word != NULL) { | |
| 415 // Allow non-breaking symbols inside a word. | |
| 416 // Any other punctuation or whitespace character ends the current word. | |
| 417 if (IsNonBreakingSymbol(last_char)) | |
| 418 current_word->Append(iter.GetString()); | |
| 419 else | |
| 420 current_word = NULL; | |
| 389 } | 421 } |
| 390 } | 422 } |
| 391 | 423 |
| 392 root->RemoveEmptySubnodes(); | 424 root->RemoveEmptySubnodes(); |
| 393 return true; | 425 return true; |
| 394 } | 426 } |
| 395 | 427 |
| 396 void QueryParser::ExtractQueryWords(const string16& text, | 428 void QueryParser::ExtractQueryWords(const string16& text, |
| 397 std::vector<QueryWord>* words) { | 429 std::vector<QueryWord>* words) { |
| 398 base::i18n::BreakIterator iter(text, base::i18n::BreakIterator::BREAK_WORD); | 430 base::i18n::BreakIterator iter(text, base::i18n::BreakIterator::BREAK_WORD); |
| 399 // TODO(evanm): support a locale here | 431 // TODO(evanm): support a locale here |
| 400 if (!iter.Init()) | 432 if (!iter.Init()) |
| 401 return; | 433 return; |
| 402 | 434 |
| 403 while (iter.Advance()) { | 435 while (iter.Advance()) { |
| 404 // Just found a span between 'prev' (inclusive) and 'pos' (exclusive). It | 436 // Just found a span between 'prev' (inclusive) and 'pos' (exclusive). It |
| 405 // is not necessarily a word, but could also be a sequence of punctuation | 437 // is not necessarily a word, but could also be a sequence of punctuation |
| 406 // or whitespace. | 438 // or whitespace. |
| 407 if (iter.IsWord()) { | 439 if (iter.IsWord()) { |
| 408 string16 word = iter.GetString(); | 440 string16 word = iter.GetString(); |
| 409 if (!word.empty()) { | 441 if (!word.empty()) { |
| 410 words->push_back(QueryWord()); | 442 words->push_back(QueryWord()); |
| 411 words->back().word = word; | 443 words->back().word = word; |
| 412 words->back().position = iter.prev(); | 444 words->back().position = iter.prev(); |
| 413 } | 445 } |
| 414 } | 446 } |
| 415 } | 447 } |
| 416 } | 448 } |
| OLD | NEW |