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 // Implements a custom word iterator used for our spellchecker.
7 #include "chrome/renderer/spellchecker/spellcheck_worditerator.h"
12 #include "base/basictypes.h"
13 #include "base/logging.h"
14 #include "base/strings/stringprintf.h"
15 #include "base/strings/utf_string_conversions.h"
16 #include "chrome/renderer/spellchecker/spellcheck.h"
17 #include "third_party/icu/source/common/unicode/normlzr.h"
18 #include "third_party/icu/source/common/unicode/schriter.h"
19 #include "third_party/icu/source/common/unicode/uscript.h"
20 #include "third_party/icu/source/i18n/unicode/ulocdata.h"
22 // SpellcheckCharAttribute implementation:
24 SpellcheckCharAttribute::SpellcheckCharAttribute()
25 : script_code_(USCRIPT_LATIN) {
28 SpellcheckCharAttribute::~SpellcheckCharAttribute() {
31 void SpellcheckCharAttribute::SetDefaultLanguage(const std::string& language) {
32 CreateRuleSets(language);
35 string16 SpellcheckCharAttribute::GetRuleSet(bool allow_contraction) const {
36 return allow_contraction ?
37 ruleset_allow_contraction_ : ruleset_disallow_contraction_;
40 void SpellcheckCharAttribute::CreateRuleSets(const std::string& language) {
41 // The template for our custom rule sets, which is based on the word-break
43 // <http://source.icu-project.org/repos/icu/icu/tags/release-4-0/source/data/brkitr/word.txt>.
44 // The major differences from the original one are listed below:
45 // * It discards comments in the original rules.
46 // * It discards characters not needed by our spellchecker (e.g. numbers,
47 // punctuation characters, Hiraganas, Katakanas, CJK Ideographs, and so on).
48 // * It allows customization of the $ALetter value (i.e. word characters).
49 // * It allows customization of the $ALetterPlus value (i.e. whether or not to
50 // use the dictionary data).
51 // * It allows choosing whether or not to split a text at contraction
53 // This template only changes the forward-iteration rules. So, calling
54 // ubrk_prev() returns the same results as the original template.
55 static const char kRuleTemplate[] =
57 "$CR = [\\p{Word_Break = CR}];"
58 "$LF = [\\p{Word_Break = LF}];"
59 "$Newline = [\\p{Word_Break = Newline}];"
60 "$Extend = [\\p{Word_Break = Extend}];"
61 "$Format = [\\p{Word_Break = Format}];"
62 "$Katakana = [\\p{Word_Break = Katakana}];"
63 // Not all the characters in a given script are ALetter.
64 // For instance, U+05F4 is MidLetter. So, this may be
65 // better, but it leads to an empty set error in Thai.
66 // "$ALetter = [[\\p{script=%s}] & [\\p{Word_Break = ALetter}]];"
67 "$ALetter = [\\p{script=%s}%s];"
68 "$MidNumLet = [\\p{Word_Break = MidNumLet}];"
69 "$MidLetter = [\\p{Word_Break = MidLetter}%s];"
70 "$MidNum = [\\p{Word_Break = MidNum}];"
71 "$Numeric = [\\p{Word_Break = Numeric}];"
72 "$ExtendNumLet = [\\p{Word_Break = ExtendNumLet}];"
74 "$Control = [\\p{Grapheme_Cluster_Break = Control}]; "
77 "$KatakanaEx = $Katakana ($Extend | $Format)*;"
78 "$ALetterEx = $ALetterPlus ($Extend | $Format)*;"
79 "$MidNumLetEx = $MidNumLet ($Extend | $Format)*;"
80 "$MidLetterEx = $MidLetter ($Extend | $Format)*;"
81 "$MidNumEx = $MidNum ($Extend | $Format)*;"
82 "$NumericEx = $Numeric ($Extend | $Format)*;"
83 "$ExtendNumLetEx = $ExtendNumLet ($Extend | $Format)*;"
85 "$Hiragana = [\\p{script=Hiragana}];"
86 "$Ideographic = [\\p{Ideographic}];"
87 "$HiraganaEx = $Hiragana ($Extend | $Format)*;"
88 "$IdeographicEx = $Ideographic ($Extend | $Format)*;"
92 "[^$CR $LF $Newline]? ($Extend | $Format)+;"
94 "$ALetterEx $ALetterEx {200};"
95 "%s" // (Allow|Disallow) Contraction
98 "$BackALetterEx = ($Format | $Extend)* $ALetterPlus;"
99 "$BackMidNumLetEx = ($Format | $Extend)* $MidNumLet;"
100 "$BackNumericEx = ($Format | $Extend)* $Numeric;"
101 "$BackMidNumEx = ($Format | $Extend)* $MidNum;"
102 "$BackMidLetterEx = ($Format | $Extend)* $MidLetter;"
103 "$BackKatakanaEx = ($Format | $Extend)* $Katakana;"
104 "$BackExtendNumLetEx= ($Format | $Extend)* $ExtendNumLet;"
106 "($Format | $Extend)* [^$CR $LF $Newline]?;"
107 "$BackALetterEx $BackALetterEx;"
108 "$BackALetterEx ($BackMidLetterEx | $BackMidNumLetEx) $BackALetterEx;"
109 "$BackNumericEx $BackNumericEx;"
110 "$BackNumericEx $BackALetterEx;"
111 "$BackALetterEx $BackNumericEx;"
112 "$BackNumericEx ($BackMidNumEx | $BackMidNumLetEx) $BackNumericEx;"
113 "$BackKatakanaEx $BackKatakanaEx;"
114 "$BackExtendNumLetEx ($BackALetterEx | $BackNumericEx |"
115 " $BackKatakanaEx | $BackExtendNumLetEx);"
116 "($BackALetterEx | $BackNumericEx | $BackKatakanaEx)"
117 " $BackExtendNumLetEx;"
120 "($Extend | $Format)+ .?;"
121 "($MidLetter | $MidNumLet) $BackALetterEx;"
122 "($MidNum | $MidNumLet) $BackNumericEx;"
125 "($Extend | $Format)+ .?;"
126 "($MidLetterEx | $MidNumLetEx) $ALetterEx;"
127 "($MidNumEx | $MidNumLetEx) $NumericEx;";
129 // Retrieve the script codes used by the given language from ICU. When the
130 // given language consists of two or more scripts, we just use the first
131 // script. The size of returned script codes is always < 8. Therefore, we use
132 // an array of size 8 so we can include all script codes without insufficient
134 UErrorCode error = U_ZERO_ERROR;
135 UScriptCode script_code[8];
136 int scripts = uscript_getCode(language.c_str(), script_code,
137 arraysize(script_code), &error);
138 if (U_SUCCESS(error) && scripts >= 1)
139 script_code_ = script_code[0];
141 // Retrieve the values for $ALetter and $ALetterPlus. We use the dictionary
142 // only for the languages which need it (i.e. Korean and Thai) to prevent ICU
143 // from returning dictionary words (i.e. Korean or Thai words) for languages
144 // which don't need them.
145 const char* aletter = uscript_getName(script_code_);
149 const char kWithDictionary[] =
150 "$dictionary = [:LineBreak = Complex_Context:];"
151 "$ALetterPlus = [$ALetter [$dictionary-$Extend-$Control]];";
152 const char kWithoutDictionary[] = "$ALetterPlus = $ALetter;";
153 const char* aletter_plus = kWithoutDictionary;
154 if (script_code_ == USCRIPT_HANGUL || script_code_ == USCRIPT_THAI)
155 aletter_plus = kWithDictionary;
157 // Treat numbers as word characters except for Arabic and Hebrew.
158 const char* aletter_extra = " [0123456789]";
159 if (script_code_ == USCRIPT_HEBREW || script_code_ == USCRIPT_ARABIC)
162 const char kMidLetterExtra[] = "";
163 // For Hebrew, treat single/double quoation marks as MidLetter.
164 const char kMidLetterExtraHebrew[] = "\"'";
165 const char* midletter_extra = kMidLetterExtra;
166 if (script_code_ == USCRIPT_HEBREW)
167 midletter_extra = kMidLetterExtraHebrew;
169 // Create two custom rule-sets: one allows contraction and the other does not.
170 // We save these strings in UTF-16 so we can use it without conversions. (ICU
171 // needs UTF-16 strings.)
172 const char kAllowContraction[] =
173 "$ALetterEx ($MidLetterEx | $MidNumLetEx) $ALetterEx {200};";
174 const char kDisallowContraction[] = "";
176 ruleset_allow_contraction_ = ASCIIToUTF16(
177 base::StringPrintf(kRuleTemplate,
183 ruleset_disallow_contraction_ = ASCIIToUTF16(
184 base::StringPrintf(kRuleTemplate,
189 kDisallowContraction));
192 bool SpellcheckCharAttribute::OutputChar(UChar c, string16* output) const {
193 // Call the language-specific function if necessary.
194 // Otherwise, we call the default one.
195 switch (script_code_) {
197 return OutputArabic(c, output);
200 return OutputHangul(c, output);
203 return OutputHebrew(c, output);
206 return OutputDefault(c, output);
210 bool SpellcheckCharAttribute::OutputArabic(UChar c, string16* output) const {
211 // Discard characters not from Arabic alphabets. We also discard vowel marks
212 // of Arabic (Damma, Fatha, Kasra, etc.) to prevent our Arabic dictionary from
213 // marking an Arabic word including vowel marks as misspelled. (We need to
214 // check these vowel marks manually and filter them out since their script
215 // codes are USCRIPT_ARABIC.)
216 if (0x0621 <= c && c <= 0x064D)
217 output->push_back(c);
221 bool SpellcheckCharAttribute::OutputHangul(UChar c, string16* output) const {
222 // Decompose a Hangul character to a Hangul vowel and consonants used by our
223 // spellchecker. A Hangul character of Unicode is a ligature consisting of a
224 // Hangul vowel and consonants, e.g. U+AC01 "Gag" consists of U+1100 "G",
225 // U+1161 "a", and U+11A8 "g". That is, we can treat each Hangul character as
226 // a point of a cubic linear space consisting of (first consonant, vowel, last
227 // consonant). Therefore, we can compose a Hangul character from a vowel and
228 // two consonants with linear composition:
229 // character = 0xAC00 +
230 // (first consonant - 0x1100) * 28 * 21 +
231 // (vowel - 0x1161) * 28 +
232 // (last consonant - 0x11A7);
233 // We can also decompose a Hangul character with linear decomposition:
234 // first consonant = (character - 0xAC00) / 28 / 21;
235 // vowel = (character - 0xAC00) / 28 % 21;
236 // last consonant = (character - 0xAC00) % 28;
237 // This code is copied from Unicode Standard Annex #15
238 // <http://unicode.org/reports/tr15> and added some comments.
239 const int kSBase = 0xAC00; // U+AC00: the top of Hangul characters.
240 const int kLBase = 0x1100; // U+1100: the top of Hangul first consonants.
241 const int kVBase = 0x1161; // U+1161: the top of Hangul vowels.
242 const int kTBase = 0x11A7; // U+11A7: the top of Hangul last consonants.
243 const int kLCount = 19; // The number of Hangul first consonants.
244 const int kVCount = 21; // The number of Hangul vowels.
245 const int kTCount = 28; // The number of Hangul last consonants.
246 const int kNCount = kVCount * kTCount;
247 const int kSCount = kLCount * kNCount;
249 int index = c - kSBase;
250 if (index < 0 || index >= kSBase + kSCount) {
251 // This is not a Hangul syllable. Call the default output function since we
252 // should output this character when it is a Hangul syllable.
253 return OutputDefault(c, output);
256 // This is a Hangul character. Decompose this characters into Hangul vowels
258 int l = kLBase + index / kNCount;
259 int v = kVBase + (index % kNCount) / kTCount;
260 int t = kTBase + index % kTCount;
261 output->push_back(l);
262 output->push_back(v);
264 output->push_back(t);
268 bool SpellcheckCharAttribute::OutputHebrew(UChar c, string16* output) const {
269 // Discard characters except Hebrew alphabets. We also discard Hebrew niqquds
270 // to prevent our Hebrew dictionary from marking a Hebrew word including
271 // niqquds as misspelled. (Same as Arabic vowel marks, we need to check
272 // niqquds manually and filter them out since their script codes are
274 // Pass through ASCII single/double quotation marks and Hebrew Geresh and
276 if ((0x05D0 <= c && c <= 0x05EA) || c == 0x22 || c == 0x27 ||
277 c == 0x05F4 || c == 0x05F3)
278 output->push_back(c);
282 bool SpellcheckCharAttribute::OutputDefault(UChar c, string16* output) const {
283 // Check the script code of this character and output only if it is the one
284 // used by the spellchecker language.
285 UErrorCode status = U_ZERO_ERROR;
286 UScriptCode script_code = uscript_getScript(c, &status);
287 if (script_code == script_code_ || script_code == USCRIPT_COMMON)
288 output->push_back(c);
292 // SpellcheckWordIterator implementation:
294 SpellcheckWordIterator::SpellcheckWordIterator()
297 position_(UBRK_DONE),
302 SpellcheckWordIterator::~SpellcheckWordIterator() {
306 bool SpellcheckWordIterator::Initialize(
307 const SpellcheckCharAttribute* attribute,
308 bool allow_contraction) {
309 // Create a custom ICU break iterator with empty text used in this object. (We
310 // allow setting text later so we can re-use this iterator.)
312 UErrorCode open_status = U_ZERO_ERROR;
313 UParseError parse_status;
314 string16 rule(attribute->GetRuleSet(allow_contraction));
316 // If there is no rule set, the attributes were invalid.
320 iterator_ = ubrk_openRules(rule.c_str(), rule.length(), NULL, 0,
321 &parse_status, &open_status);
322 if (U_FAILURE(open_status))
325 // Set the character attributes so we can normalize the words extracted by
327 attribute_ = attribute;
331 bool SpellcheckWordIterator::IsInitialized() const {
332 // Return true if we have an ICU custom iterator.
336 bool SpellcheckWordIterator::SetText(const char16* text, size_t length) {
339 // Set the text to be split by this iterator.
340 UErrorCode status = U_ZERO_ERROR;
341 ubrk_setText(iterator_, text, length, &status);
342 if (U_FAILURE(status))
345 // Retrieve the position to the first word in this text. We return false if
346 // this text does not have any words. (For example, The input text consists
347 // only of Chinese characters while the spellchecker language is English.)
348 position_ = ubrk_first(iterator_);
349 if (position_ == UBRK_DONE)
353 length_ = static_cast<int>(length);
357 bool SpellcheckWordIterator::GetNextWord(string16* word_string,
360 DCHECK(!!text_ && length_ > 0);
362 word_string->clear();
366 if (!text_ || position_ == UBRK_DONE)
369 // Find a word that can be checked for spelling. Our rule sets filter out
370 // invalid words (e.g. numbers and characters not supported by the
371 // spellchecker language) so this ubrk_getRuleStatus() call returns
372 // UBRK_WORD_NONE when this iterator finds an invalid word. So, we skip such
373 // words until we can find a valid word or reach the end of the input string.
374 int next = ubrk_next(iterator_);
375 while (next != UBRK_DONE) {
376 if (ubrk_getRuleStatus(iterator_) != UBRK_WORD_NONE) {
377 if (Normalize(position_, next - position_, word_string)) {
378 *word_start = position_;
379 *word_length = next - position_;
385 next = ubrk_next(iterator_);
388 // There aren't any more words in the given text. Set the position to
389 // UBRK_DONE to prevent from calling ubrk_next() next time when this function
391 position_ = UBRK_DONE;
395 void SpellcheckWordIterator::Reset() {
397 ubrk_close(iterator_);
402 bool SpellcheckWordIterator::Normalize(int input_start,
404 string16* output_string) const {
405 // We use NFKC (Normalization Form, Compatible decomposition, followed by
406 // canonical Composition) defined in Unicode Standard Annex #15 to normalize
407 // this token because it it the most suitable normalization algorithm for our
408 // spellchecker. Nevertheless, it is not a perfect algorithm for our
409 // spellchecker and we need manual normalization as well. The normalized
410 // text does not have to be NUL-terminated since its characters are copied to
411 // string16, which adds a NUL character when we need.
412 icu::UnicodeString input(FALSE, &text_[input_start], input_length);
413 UErrorCode status = U_ZERO_ERROR;
414 icu::UnicodeString output;
415 icu::Normalizer::normalize(input, UNORM_NFKC, 0, output, status);
416 if (status != U_ZERO_ERROR && status != U_STRING_NOT_TERMINATED_WARNING)
419 // Copy the normalized text to the output.
420 icu::StringCharacterIterator it(output);
421 for (UChar c = it.first(); c != icu::CharacterIterator::DONE; c = it.next())
422 attribute_->OutputChar(c, output_string);
424 return !output_string->empty();