blob: 90d80d0e45843fe5e19c3c501dd2bdc3cf6bad94 [file] [log] [blame]
[email protected]3b63f8f42011-03-28 01:54:151// Copyright (c) 2011 The Chromium Authors. All rights reserved.
license.botbf09a502008-08-24 00:55:552// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
initial.commit09911bf2008-07-26 23:55:294
5#include "chrome/browser/history/snippet.h"
6
7#include <algorithm>
8
9#include "base/logging.h"
[email protected]3b63f8f42011-03-28 01:54:1510#include "base/memory/scoped_ptr.h"
[email protected]1988e1c2013-02-28 20:27:4211#include "base/strings/string_split.h"
[email protected]d8830562013-06-10 22:01:5412#include "base/strings/string_util.h"
[email protected]112158af2013-06-07 23:46:1813#include "base/strings/utf_string_conversions.h"
[email protected]c297d3a2013-01-07 22:33:4714#include "third_party/icu/public/common/unicode/brkiter.h"
15#include "third_party/icu/public/common/unicode/utext.h"
16#include "third_party/icu/public/common/unicode/utf8.h"
initial.commit09911bf2008-07-26 23:55:2917
18namespace {
19
[email protected]c29962f22008-12-03 00:47:5820bool PairFirstLessThan(const Snippet::MatchPosition& a,
21 const Snippet::MatchPosition& b) {
initial.commit09911bf2008-07-26 23:55:2922 return a.first < b.first;
23}
24
25// Combines all pairs after offset in match_positions that are contained
26// or touch the pair at offset.
27void CoalescePositionsFrom(size_t offset,
28 Snippet::MatchPositions* match_positions) {
29 DCHECK(offset < match_positions->size());
[email protected]c29962f22008-12-03 00:47:5830 Snippet::MatchPosition& pair((*match_positions)[offset]);
initial.commit09911bf2008-07-26 23:55:2931 ++offset;
32 while (offset < match_positions->size() &&
33 pair.second >= (*match_positions)[offset].first) {
34 pair.second = std::max(pair.second, (*match_positions)[offset].second);
35 match_positions->erase(match_positions->begin() + offset);
36 }
37}
38
39// Makes sure there is a pair in match_positions that contains the specified
40// range. This keeps the pairs ordered in match_positions by first, and makes
41// sure none of the pairs in match_positions touch each other.
[email protected]c29962f22008-12-03 00:47:5842void AddMatch(size_t start,
43 size_t end,
44 Snippet::MatchPositions* match_positions) {
45 DCHECK(start < end);
46 DCHECK(match_positions);
47 Snippet::MatchPosition pair(start, end);
initial.commit09911bf2008-07-26 23:55:2948 if (match_positions->empty()) {
49 match_positions->push_back(pair);
50 return;
51 }
52 // There's at least one match. Find the position of the new match,
53 // potentially extending pairs around it.
54 Snippet::MatchPositions::iterator i =
55 std::lower_bound(match_positions->begin(), match_positions->end(),
56 pair, &PairFirstLessThan);
57 if (i != match_positions->end() && i->first == start) {
58 // Match not at the end and there is already a pair with the same
59 // start.
60 if (end > i->second) {
61 // New pair extends beyond existing pair. Extend existing pair and
62 // coalesce matches after it.
63 i->second = end;
64 CoalescePositionsFrom(i - match_positions->begin(), match_positions);
[email protected]9fd9092f2010-03-08 23:28:4165 } // else case, new pair completely contained in existing pair, nothing
66 // to do.
initial.commit09911bf2008-07-26 23:55:2967 } else if (i == match_positions->begin()) {
68 // Match at the beginning and the first pair doesn't have the same
69 // start. Insert new pair and coalesce matches after it.
70 match_positions->insert(i, pair);
71 CoalescePositionsFrom(0, match_positions);
72 } else {
73 // Not at the beginning (but may be at the end).
74 --i;
75 if (start <= i->second && end > i->second) {
76 // Previous element contains match. Extend it and coalesce.
77 i->second = end;
78 CoalescePositionsFrom(i - match_positions->begin(), match_positions);
79 } else if (end > i->second) {
80 // Region doesn't touch previous element. See if region touches current
81 // element.
82 ++i;
83 if (i == match_positions->end() || end < i->first) {
84 match_positions->insert(i, pair);
85 } else {
86 i->first = start;
87 i->second = end;
88 CoalescePositionsFrom(i - match_positions->begin(), match_positions);
89 }
90 }
91 }
92}
93
[email protected]e53668962010-06-23 15:35:2594// Converts an index in a utf8 string into the index in the corresponding utf16
95// string and returns the utf16 index. This is intended to be called in a loop
initial.commit09911bf2008-07-26 23:55:2996// iterating through a utf8 string.
97//
98// utf8_string: the utf8 string.
99// utf8_length: length of the utf8 string.
100// offset: the utf8 offset to convert.
101// utf8_pos: current offset in the utf8 string. This is modified and on return
102// matches offset.
103// wide_pos: current index in the wide string. This is the same as the return
104// value.
[email protected]e53668962010-06-23 15:35:25105size_t AdvanceAndReturnUTF16Pos(const char* utf8_string,
106 int32_t utf8_length,
107 int32_t offset,
108 int32_t* utf8_pos,
109 size_t* utf16_pos) {
initial.commit09911bf2008-07-26 23:55:29110 DCHECK(offset >= *utf8_pos && offset <= utf8_length);
111
112 UChar32 wide_char;
113 while (*utf8_pos < offset) {
114 U8_NEXT(utf8_string, *utf8_pos, utf8_length, wide_char);
[email protected]e53668962010-06-23 15:35:25115 *utf16_pos += (wide_char <= 0xFFFF) ? 1 : 2;
initial.commit09911bf2008-07-26 23:55:29116 }
[email protected]e53668962010-06-23 15:35:25117 return *utf16_pos;
initial.commit09911bf2008-07-26 23:55:29118}
119
120// Given a character break iterator over a UTF-8 string, set the iterator
121// position to |*utf8_pos| and move by |count| characters. |count| can
122// be either positive or negative.
[email protected]b5b2385a2009-08-18 05:12:29123void MoveByNGraphemes(icu::BreakIterator* bi, int count, size_t* utf8_pos) {
initial.commit09911bf2008-07-26 23:55:29124 // Ignore the return value. A side effect of the current position
125 // being set at or following |*utf8_pos| is exploited here.
126 // It's simpler than calling following(n) and then previous().
127 // isBoundary() is not very fast, but should be good enough for the
128 // snippet generation. If not, revisit the way we scan in ComputeSnippet.
129 bi->isBoundary(*utf8_pos);
130 bi->next(count);
[email protected]c29962f22008-12-03 00:47:58131 *utf8_pos = static_cast<size_t>(bi->current());
initial.commit09911bf2008-07-26 23:55:29132}
133
134// The amount of context to include for a given hit. Note that it's counted
135// in terms of graphemes rather than bytes.
136const int kSnippetContext = 50;
137
138// Returns true if next match falls within a snippet window
139// from the previous match. The window size is counted in terms
140// of graphemes rather than bytes in UTF-8.
[email protected]b5b2385a2009-08-18 05:12:29141bool IsNextMatchWithinSnippetWindow(icu::BreakIterator* bi,
[email protected]c29962f22008-12-03 00:47:58142 size_t previous_match_end,
143 size_t next_match_start) {
initial.commit09911bf2008-07-26 23:55:29144 // If it's within a window in terms of bytes, it's certain
145 // that it's within a window in terms of graphemes as well.
146 if (next_match_start < previous_match_end + kSnippetContext)
147 return true;
148 bi->isBoundary(previous_match_end);
149 // An alternative to this is to call |bi->next()| at most
150 // kSnippetContext times, compare |bi->current()| with |next_match_start|
151 // after each call and return early if possible. There are other
152 // heuristics to speed things up if necessary, but it's not likely that
153 // we need to bother.
154 bi->next(kSnippetContext);
[email protected]65d55d82009-07-28 21:15:56155 int64 current = bi->current();
156 return (next_match_start < static_cast<uint64>(current) ||
[email protected]b5b2385a2009-08-18 05:12:29157 current == icu::BreakIterator::DONE);
initial.commit09911bf2008-07-26 23:55:29158}
159
160} // namespace
161
162// static
163void Snippet::ExtractMatchPositions(const std::string& offsets_str,
164 const std::string& column_num,
165 MatchPositions* match_positions) {
166 DCHECK(match_positions);
167 if (offsets_str.empty())
168 return;
169 std::vector<std::string> offsets;
[email protected]76eb0242010-10-14 00:35:36170 base::SplitString(offsets_str, ' ', &offsets);
initial.commit09911bf2008-07-26 23:55:29171 // SQLite offsets are sets of four integers:
172 // column, query term, match offset, match length
173 // Matches within a string are marked by (start, end) pairs.
174 for (size_t i = 0; i < offsets.size() - 3; i += 4) {
175 if (offsets[i] != column_num)
176 continue;
[email protected]c29962f22008-12-03 00:47:58177 const size_t start = atoi(offsets[i + 2].c_str());
178 const size_t end = start + atoi(offsets[i + 3].c_str());
[email protected]135b1652009-08-11 21:43:11179 // Switch to DCHECK after debugging http://crbug.com/15261.
180 CHECK(end >= start);
initial.commit09911bf2008-07-26 23:55:29181 AddMatch(start, end, match_positions);
182 }
183}
184
185// static
186void Snippet::ConvertMatchPositionsToWide(
187 const std::string& utf8_string,
188 Snippet::MatchPositions* match_positions) {
189 DCHECK(match_positions);
[email protected]c29962f22008-12-03 00:47:58190 int32_t utf8_pos = 0;
[email protected]e53668962010-06-23 15:35:25191 size_t utf16_pos = 0;
initial.commit09911bf2008-07-26 23:55:29192 const char* utf8_cstring = utf8_string.c_str();
[email protected]c29962f22008-12-03 00:47:58193 const int32_t utf8_length = static_cast<int32_t>(utf8_string.size());
initial.commit09911bf2008-07-26 23:55:29194 for (Snippet::MatchPositions::iterator i = match_positions->begin();
195 i != match_positions->end(); ++i) {
[email protected]e53668962010-06-23 15:35:25196 i->first = AdvanceAndReturnUTF16Pos(utf8_cstring, utf8_length,
197 i->first, &utf8_pos, &utf16_pos);
198 i->second = AdvanceAndReturnUTF16Pos(utf8_cstring, utf8_length,
199 i->second, &utf8_pos, &utf16_pos);
initial.commit09911bf2008-07-26 23:55:29200 }
201}
202
[email protected]20f0487a2010-09-30 20:06:30203Snippet::Snippet() {
204}
205
206Snippet::~Snippet() {
207}
208
initial.commit09911bf2008-07-26 23:55:29209void Snippet::ComputeSnippet(const MatchPositions& match_positions,
210 const std::string& document) {
211 // The length of snippets we try to produce.
212 // We can generate longer snippets but stop once we cross kSnippetMaxLength.
213 const size_t kSnippetMaxLength = 200;
[email protected]e53668962010-06-23 15:35:25214 const string16 kEllipsis = ASCIIToUTF16(" ... ");
initial.commit09911bf2008-07-26 23:55:29215
initial.commit09911bf2008-07-26 23:55:29216 UText* document_utext = NULL;
217 UErrorCode status = U_ZERO_ERROR;
218 document_utext = utext_openUTF8(document_utext, document.data(),
[email protected]c29962f22008-12-03 00:47:58219 document.size(), &status);
initial.commit09911bf2008-07-26 23:55:29220 // Locale does not matter because there's no per-locale customization
221 // for character iterator.
[email protected]b5b2385a2009-08-18 05:12:29222 scoped_ptr<icu::BreakIterator> bi(icu::BreakIterator::createCharacterInstance(
223 icu::Locale::getDefault(), status));
initial.commit09911bf2008-07-26 23:55:29224 bi->setText(document_utext, status);
225 DCHECK(U_SUCCESS(status));
226
227 // We build the snippet by iterating through the matches and then grabbing
228 // context around each match. If matches are near enough each other (within
229 // kSnippetContext), we skip the "..." between them.
[email protected]e53668962010-06-23 15:35:25230 string16 snippet;
[email protected]c29962f22008-12-03 00:47:58231 size_t start = 0;
initial.commit09911bf2008-07-26 23:55:29232 for (size_t i = 0; i < match_positions.size(); ++i) {
233 // Some shorter names for the current match.
[email protected]c29962f22008-12-03 00:47:58234 const size_t match_start = match_positions[i].first;
235 const size_t match_end = match_positions[i].second;
initial.commit09911bf2008-07-26 23:55:29236
[email protected]135b1652009-08-11 21:43:11237 // Switch to DCHECK after debugging http://crbug.com/15261.
238 CHECK(match_end > match_start);
239 CHECK(match_end <= document.size());
240
initial.commit09911bf2008-07-26 23:55:29241 // Add the context, if any, to show before the match.
[email protected]c29962f22008-12-03 00:47:58242 size_t context_start = match_start;
initial.commit09911bf2008-07-26 23:55:29243 MoveByNGraphemes(bi.get(), -kSnippetContext, &context_start);
244 start = std::max(start, context_start);
245 if (start < match_start) {
246 if (start > 0)
247 snippet += kEllipsis;
[email protected]135b1652009-08-11 21:43:11248 // Switch to DCHECK after debugging http://crbug.com/15261.
249 CHECK(start < document.size());
[email protected]e53668962010-06-23 15:35:25250 snippet += UTF8ToUTF16(document.substr(start, match_start - start));
initial.commit09911bf2008-07-26 23:55:29251 }
252
253 // Add the match.
[email protected]c29962f22008-12-03 00:47:58254 const size_t first = snippet.size();
[email protected]e53668962010-06-23 15:35:25255 snippet += UTF8ToUTF16(document.substr(match_start,
initial.commit09911bf2008-07-26 23:55:29256 match_end - match_start));
[email protected]c29962f22008-12-03 00:47:58257 matches_.push_back(std::make_pair(first, snippet.size()));
initial.commit09911bf2008-07-26 23:55:29258
259 // Compute the context, if any, to show after the match.
[email protected]c29962f22008-12-03 00:47:58260 size_t end;
initial.commit09911bf2008-07-26 23:55:29261 // Check if the next match falls within our snippet window.
262 if (i + 1 < match_positions.size() &&
263 IsNextMatchWithinSnippetWindow(bi.get(), match_end,
[email protected]c29962f22008-12-03 00:47:58264 match_positions[i + 1].first)) {
initial.commit09911bf2008-07-26 23:55:29265 // Yes, it's within the window. Make the end context extend just up
266 // to the next match.
267 end = match_positions[i + 1].first;
[email protected]135b1652009-08-11 21:43:11268 // Switch to DCHECK after debugging http://crbug.com/15261.
269 CHECK(end >= match_end);
270 CHECK(end <= document.size());
[email protected]e53668962010-06-23 15:35:25271 snippet += UTF8ToUTF16(document.substr(match_end, end - match_end));
initial.commit09911bf2008-07-26 23:55:29272 } else {
273 // No, there's either no next match or the next match is too far away.
274 end = match_end;
275 MoveByNGraphemes(bi.get(), kSnippetContext, &end);
[email protected]135b1652009-08-11 21:43:11276 // Switch to DCHECK after debugging http://crbug.com/15261.
277 CHECK(end >= match_end);
278 CHECK(end <= document.size());
[email protected]e53668962010-06-23 15:35:25279 snippet += UTF8ToUTF16(document.substr(match_end, end - match_end));
[email protected]c29962f22008-12-03 00:47:58280 if (end < document.size())
initial.commit09911bf2008-07-26 23:55:29281 snippet += kEllipsis;
282 }
283 start = end;
284
285 // Stop here if we have enough snippet computed.
286 if (snippet.size() >= kSnippetMaxLength)
287 break;
288 }
289
290 utext_close(document_utext);
291 swap(text_, snippet);
292}
[email protected]20f0487a2010-09-30 20:06:30293
294void Snippet::Swap(Snippet* other) {
295 text_.swap(other->text_);
296 matches_.swap(other->matches_);
297}