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 == '.' || 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); | |
| 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 |
| (...skipping 41 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 134 return true; | 144 return true; |
| 135 } | 145 } |
| 136 } | 146 } |
| 137 return false; | 147 return false; |
| 138 } | 148 } |
| 139 | 149 |
| 140 void QueryNodeWord::AppendWords(std::vector<string16>* words) const { | 150 void QueryNodeWord::AppendWords(std::vector<string16>* words) const { |
| 141 words->push_back(word_); | 151 words->push_back(word_); |
| 142 } | 152 } |
| 143 | 153 |
| 154 void QueryNodeWord::Append(const string16& str) { | |
|
sky
2012/02/10 16:53:35
Definition should match declaration order (this sh
Patrick Dubroy
2012/02/20 11:00:36
Done.
| |
| 155 word_ += str; | |
| 156 } | |
| 157 | |
| 144 // A QueryNodeList has a collection of QueryNodes which are deleted in the end. | 158 // A QueryNodeList has a collection of QueryNodes which are deleted in the end. |
| 145 class QueryNodeList : public QueryNode { | 159 class QueryNodeList : public QueryNode { |
| 146 public: | 160 public: |
| 147 typedef std::vector<QueryNode*> QueryNodeVector; | 161 typedef std::vector<QueryNode*> QueryNodeVector; |
| 148 | 162 |
| 149 QueryNodeList(); | 163 QueryNodeList(); |
| 150 virtual ~QueryNodeList(); | 164 virtual ~QueryNodeList(); |
| 151 | 165 |
| 152 QueryNodeVector* children() { return &children_; } | 166 QueryNodeVector* children() { return &children_; } |
| 153 | 167 |
| (...skipping 186 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 340 if (!query_nodes[i]->HasMatchIn(query_words, &matches)) | 354 if (!query_nodes[i]->HasMatchIn(query_words, &matches)) |
| 341 return false; | 355 return false; |
| 342 } | 356 } |
| 343 if (lower_text.length() != text.length()) { | 357 if (lower_text.length() != text.length()) { |
| 344 // The lower case string differs from the original string. The matches are | 358 // The lower case string differs from the original string. The matches are |
| 345 // meaningless. | 359 // meaningless. |
| 346 // TODO(sky): we need a better way to align the positions so that we don't | 360 // TODO(sky): we need a better way to align the positions so that we don't |
| 347 // completely punt here. | 361 // completely punt here. |
| 348 match_positions->clear(); | 362 match_positions->clear(); |
| 349 } else { | 363 } else { |
| 350 CoalseAndSortMatchPositions(&matches); | 364 CoalesceAndSortMatchPositions(&matches); |
| 351 match_positions->swap(matches); | 365 match_positions->swap(matches); |
| 352 } | 366 } |
| 353 return true; | 367 return true; |
| 354 } | 368 } |
| 355 | 369 |
| 356 bool QueryParser::ParseQueryImpl(const string16& query, QueryNodeList* root) { | 370 bool QueryParser::ParseQueryImpl(const string16& query, QueryNodeList* root) { |
| 357 base::i18n::BreakIterator iter(query, base::i18n::BreakIterator::BREAK_WORD); | 371 base::i18n::BreakIterator iter(query, base::i18n::BreakIterator::BREAK_WORD); |
| 358 // TODO(evanm): support a locale here | 372 // TODO(evanm): support a locale here |
| 359 if (!iter.Init()) | 373 if (!iter.Init()) |
| 360 return false; | 374 return false; |
| 361 | 375 |
| 362 // To handle nesting, we maintain a stack of QueryNodeLists. | 376 // To handle nesting, we maintain a stack of QueryNodeLists. |
| 363 // The last element (back) of the stack contains the current, deepest node. | 377 // The last element (back) of the stack contains the current, deepest node. |
| 364 std::vector<QueryNodeList*> query_stack; | 378 std::vector<QueryNodeList*> query_stack; |
| 365 query_stack.push_back(root); | 379 query_stack.push_back(root); |
|
Scott Hess - ex-Googler
2012/02/17 22:34:59
I don't understand why a stack is needed to parse
Patrick Dubroy
2012/02/20 11:00:36
I agree. I can fix this up in a follow-up CL if yo
| |
| 366 | 380 |
| 367 bool in_quotes = false; // whether we're currently in a quoted phrase | 381 bool in_quotes = false; // whether we're currently in a quoted phrase |
| 382 QueryNodeWord* current_word = NULL; | |
| 368 while (iter.Advance()) { | 383 while (iter.Advance()) { |
| 369 // Just found a span between 'prev' (inclusive) and 'pos' (exclusive). It | 384 // 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 | 385 // is not necessarily a word, it could also be a punctuation or whitespace |
| 371 // or whitespace. | 386 // character. Punctuation is preserved inside quotes, and otherwise removed |
| 372 if (iter.IsWord()) { | 387 // except if it is a non-breaking character in the middle of a word. |
| 373 QueryNodeWord* word_node = new QueryNodeWord(iter.GetString()); | 388 |
| 374 if (in_quotes) | 389 wchar_t last_char = query[iter.prev()]; |
|
Scott Hess - ex-Googler
2012/02/17 22:34:59
I'm not sure "last_char" is the best description.
Patrick Dubroy
2012/02/20 11:00:36
You're right. Not sure why I named it like that. D
| |
| 375 word_node->set_literal(true); | 390 if (IsQueryQuote(last_char)) { |
|
sky
2012/02/10 16:53:35
Should we require the trailing quote character to
mrossetti
2012/02/10 17:33:43
I almost commented on that as well but the existin
Patrick Dubroy
2012/02/20 11:00:36
I think we should. I can address this in a follow-
| |
| 376 query_stack.back()->AddChild(word_node); | 391 if (!in_quotes) { |
| 377 } else { // Punctuation. | 392 QueryNodeList* quotes_node = new QueryNodePhrase; |
| 378 if (IsQueryQuote(query[iter.prev()])) { | 393 query_stack.back()->AddChild(quotes_node); |
| 379 if (!in_quotes) { | 394 query_stack.push_back(quotes_node); |
| 380 QueryNodeList* quotes_node = new QueryNodePhrase; | 395 in_quotes = true; |
| 381 query_stack.back()->AddChild(quotes_node); | 396 } else { |
| 382 query_stack.push_back(quotes_node); | 397 query_stack.pop_back(); // Stop adding to the quoted phrase. |
| 383 in_quotes = true; | 398 in_quotes = false; |
| 384 } else { | |
| 385 query_stack.pop_back(); // Stop adding to the quoted phrase. | |
| 386 in_quotes = false; | |
| 387 } | |
| 388 } | 399 } |
| 400 current_word = NULL; | |
| 401 } else if (iter.IsWord() || (in_quotes && !IsWhitespace(last_char))) { | |
| 402 // Append to the current word if the new token is a word or a non- | |
| 403 // whitespace character inside quotes. | |
|
Scott Hess - ex-Googler
2012/02/17 22:34:59
If it's not a word, is it always a single characte
Patrick Dubroy
2012/02/20 11:00:36
Done.
| |
| 404 if (current_word) { | |
| 405 current_word->Append(iter.GetString()); | |
| 406 } else { | |
| 407 current_word = new QueryNodeWord(iter.GetString()); | |
| 408 current_word->set_literal(in_quotes); | |
| 409 query_stack.back()->AddChild(current_word); | |
| 410 } | |
| 411 } else if (current_word != NULL) { | |
| 412 // Allow non-breaking symbols inside a word. | |
| 413 // Any other punctuation or whitespace character ends the current word. | |
| 414 | |
| 415 // TODO(dubroy): Consider sharing code with the omnibox to allow a more | |
| 416 // accurate "best guess" of whether a sequence of characters is a URL. | |
| 417 if (IsNonBreakingSymbol(last_char)) | |
| 418 current_word->Append(iter.GetString()); | |
|
Scott Hess - ex-Googler
2012/02/17 22:34:59
If I understand this right, I'm not sure I buy it.
Patrick Dubroy
2012/02/20 11:00:36
I agree. I don't think it's very common to include
| |
| 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 |