1 /*
2  * Copyright (C) 2013 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.inputmethod.latin.utils;
18 
19 import android.text.InputType;
20 import android.text.TextUtils;
21 
22 import com.android.inputmethod.latin.Constants;
23 import com.android.inputmethod.latin.WordComposer;
24 import com.android.inputmethod.latin.settings.SpacingAndPunctuations;
25 
26 import java.util.Locale;
27 
28 public final class CapsModeUtils {
CapsModeUtils()29     private CapsModeUtils() {
30         // This utility class is not publicly instantiable.
31     }
32 
33     /**
34      * Apply an auto-caps mode to a string.
35      *
36      * This intentionally does NOT apply manual caps mode. It only changes the capitalization if
37      * the mode is one of the auto-caps modes.
38      * @param s The string to capitalize.
39      * @param capitalizeMode The mode in which to capitalize.
40      * @param locale The locale for capitalizing.
41      * @return The capitalized string.
42      */
applyAutoCapsMode(final String s, final int capitalizeMode, final Locale locale)43     public static String applyAutoCapsMode(final String s, final int capitalizeMode,
44             final Locale locale) {
45         if (WordComposer.CAPS_MODE_AUTO_SHIFT_LOCKED == capitalizeMode) {
46             return s.toUpperCase(locale);
47         } else if (WordComposer.CAPS_MODE_AUTO_SHIFTED == capitalizeMode) {
48             return StringUtils.capitalizeFirstCodePoint(s, locale);
49         } else {
50             return s;
51         }
52     }
53 
54     /**
55      * Return whether a constant represents an auto-caps mode (either auto-shift or auto-shift-lock)
56      * @param mode The mode to test for
57      * @return true if this represents an auto-caps mode, false otherwise
58      */
isAutoCapsMode(final int mode)59     public static boolean isAutoCapsMode(final int mode) {
60         return WordComposer.CAPS_MODE_AUTO_SHIFTED == mode
61                 || WordComposer.CAPS_MODE_AUTO_SHIFT_LOCKED == mode;
62     }
63 
64     /**
65      * Helper method to find out if a code point is starting punctuation.
66      *
67      * This include the Unicode START_PUNCTUATION category, but also some other symbols that are
68      * starting, like the inverted question mark or the double quote.
69      *
70      * @param codePoint the code point
71      * @return true if it's starting punctuation, false otherwise.
72      */
isStartPunctuation(final int codePoint)73     private static boolean isStartPunctuation(final int codePoint) {
74         return (codePoint == Constants.CODE_DOUBLE_QUOTE || codePoint == Constants.CODE_SINGLE_QUOTE
75                 || codePoint == Constants.CODE_INVERTED_QUESTION_MARK
76                 || codePoint == Constants.CODE_INVERTED_EXCLAMATION_MARK
77                 || Character.getType(codePoint) == Character.START_PUNCTUATION);
78     }
79 
80     /**
81      * Determine what caps mode should be in effect at the current offset in
82      * the text. Only the mode bits set in <var>reqModes</var> will be
83      * checked. Note that the caps mode flags here are explicitly defined
84      * to match those in {@link InputType}.
85      *
86      * This code is a straight copy of TextUtils.getCapsMode (modulo namespace and formatting
87      * issues). This will change in the future as we simplify the code for our use and fix bugs.
88      *
89      * @param cs The text that should be checked for caps modes.
90      * @param reqModes The modes to be checked: may be any combination of
91      * {@link TextUtils#CAP_MODE_CHARACTERS}, {@link TextUtils#CAP_MODE_WORDS}, and
92      * {@link TextUtils#CAP_MODE_SENTENCES}.
93      * @param spacingAndPunctuations The current spacing and punctuations settings.
94      * @param hasSpaceBefore Whether we should consider there is a space inserted at the end of cs
95      *
96      * @return Returns the actual capitalization modes that can be in effect
97      * at the current position, which is any combination of
98      * {@link TextUtils#CAP_MODE_CHARACTERS}, {@link TextUtils#CAP_MODE_WORDS}, and
99      * {@link TextUtils#CAP_MODE_SENTENCES}.
100      */
getCapsMode(final CharSequence cs, final int reqModes, final SpacingAndPunctuations spacingAndPunctuations, final boolean hasSpaceBefore)101     public static int getCapsMode(final CharSequence cs, final int reqModes,
102             final SpacingAndPunctuations spacingAndPunctuations, final boolean hasSpaceBefore) {
103         // Quick description of what we want to do:
104         // CAP_MODE_CHARACTERS is always on.
105         // CAP_MODE_WORDS is on if there is some whitespace before the cursor.
106         // CAP_MODE_SENTENCES is on if there is some whitespace before the cursor, and the end
107         //   of a sentence just before that.
108         // We ignore opening parentheses and the like just before the cursor for purposes of
109         // finding whitespace for WORDS and SENTENCES modes.
110         // The end of a sentence ends with a period, question mark or exclamation mark. If it's
111         // a period, it also needs not to be an abbreviation, which means it also needs to either
112         // be immediately preceded by punctuation, or by a string of only letters with single
113         // periods interleaved.
114 
115         // Step 1 : check for cap MODE_CHARACTERS. If it's looked for, it's always on.
116         if ((reqModes & (TextUtils.CAP_MODE_WORDS | TextUtils.CAP_MODE_SENTENCES)) == 0) {
117             // Here we are not looking for MODE_WORDS or MODE_SENTENCES, so since we already
118             // evaluated MODE_CHARACTERS, we can return.
119             return TextUtils.CAP_MODE_CHARACTERS & reqModes;
120         }
121 
122         // Step 2 : Skip (ignore at the end of input) any opening punctuation. This includes
123         // opening parentheses, brackets, opening quotes, everything that *opens* a span of
124         // text in the linguistic sense. In RTL languages, this is still an opening sign, although
125         // it may look like a right parenthesis for example. We also include double quote and
126         // single quote since they aren't start punctuation in the unicode sense, but should still
127         // be skipped for English. TODO: does this depend on the language?
128         int i;
129         if (hasSpaceBefore) {
130             i = cs.length() + 1;
131         } else {
132             for (i = cs.length(); i > 0; i--) {
133                 final char c = cs.charAt(i - 1);
134                 if (!isStartPunctuation(c)) {
135                     break;
136                 }
137             }
138         }
139 
140         // We are now on the character that precedes any starting punctuation, so in the most
141         // frequent case this will be whitespace or a letter, although it may occasionally be a
142         // start of line, or some symbol.
143 
144         // Step 3 : Search for the start of a paragraph. From the starting point computed in step 2,
145         // we go back over any space or tab char sitting there. We find the start of a paragraph
146         // if the first char that's not a space or tab is a start of line (as in \n, start of text,
147         // or some other similar characters).
148         int j = i;
149         char prevChar = Constants.CODE_SPACE;
150         if (hasSpaceBefore) --j;
151         while (j > 0) {
152             prevChar = cs.charAt(j - 1);
153             if (!Character.isSpaceChar(prevChar) && prevChar != Constants.CODE_TAB) break;
154             j--;
155         }
156         if (j <= 0 || Character.isWhitespace(prevChar)) {
157             if (spacingAndPunctuations.mUsesGermanRules) {
158                 // In German typography rules, there is a specific case that the first character
159                 // of a new line should not be capitalized if the previous line ends in a comma.
160                 boolean hasNewLine = false;
161                 while (--j >= 0 && Character.isWhitespace(prevChar)) {
162                     if (Constants.CODE_ENTER == prevChar) {
163                         hasNewLine = true;
164                     }
165                     prevChar = cs.charAt(j);
166                 }
167                 if (Constants.CODE_COMMA == prevChar && hasNewLine) {
168                     return (TextUtils.CAP_MODE_CHARACTERS | TextUtils.CAP_MODE_WORDS) & reqModes;
169                 }
170             }
171             // There are only spacing chars between the start of the paragraph and the cursor,
172             // defined as a isWhitespace() char that is neither a isSpaceChar() nor a tab. Both
173             // MODE_WORDS and MODE_SENTENCES should be active.
174             return (TextUtils.CAP_MODE_CHARACTERS | TextUtils.CAP_MODE_WORDS
175                     | TextUtils.CAP_MODE_SENTENCES) & reqModes;
176         }
177         if (i == j) {
178             // If we don't have whitespace before index i, it means neither MODE_WORDS
179             // nor mode sentences should be on so we can return right away.
180             return TextUtils.CAP_MODE_CHARACTERS & reqModes;
181         }
182         if ((reqModes & TextUtils.CAP_MODE_SENTENCES) == 0) {
183             // Here we know we have whitespace before the cursor (if not, we returned in the above
184             // if i == j clause), so we need MODE_WORDS to be on. And we don't need to evaluate
185             // MODE_SENTENCES so we can return right away.
186             return (TextUtils.CAP_MODE_CHARACTERS | TextUtils.CAP_MODE_WORDS) & reqModes;
187         }
188         // Please note that because of the reqModes & CAP_MODE_SENTENCES test a few lines above,
189         // we know that MODE_SENTENCES is being requested.
190 
191         // Step 4 : Search for MODE_SENTENCES.
192         // English is a special case in that "American typography" rules, which are the most common
193         // in English, state that a sentence terminator immediately following a quotation mark
194         // should be swapped with it and de-duplicated (included in the quotation mark),
195         // e.g. <<Did he say, "let's go home?">>
196         // No other language has such a rule as far as I know, instead putting inside the quotation
197         // mark as the exact thing quoted and handling the surrounding punctuation independently,
198         // e.g. <<Did he say, "let's go home"?>>
199         if (spacingAndPunctuations.mUsesAmericanTypography) {
200             for (; j > 0; j--) {
201                 // Here we look to go over any closing punctuation. This is because in dominant
202                 // variants of English, the final period is placed within double quotes and maybe
203                 // other closing punctuation signs. This is generally not true in other languages.
204                 final char c = cs.charAt(j - 1);
205                 if (c != Constants.CODE_DOUBLE_QUOTE && c != Constants.CODE_SINGLE_QUOTE
206                         && Character.getType(c) != Character.END_PUNCTUATION) {
207                     break;
208                 }
209             }
210         }
211 
212         if (j <= 0) return TextUtils.CAP_MODE_CHARACTERS & reqModes;
213         char c = cs.charAt(--j);
214 
215         // We found the next interesting chunk of text ; next we need to determine if it's the
216         // end of a sentence. If we have a question mark or an exclamation mark, it's the end of
217         // a sentence. If it's neither, the only remaining case is the period so we get the opposite
218         // case out of the way.
219         if (c == Constants.CODE_QUESTION_MARK || c == Constants.CODE_EXCLAMATION_MARK) {
220             return (TextUtils.CAP_MODE_CHARACTERS | TextUtils.CAP_MODE_SENTENCES) & reqModes;
221         }
222         if (!spacingAndPunctuations.isSentenceSeparator(c) || j <= 0) {
223             return (TextUtils.CAP_MODE_CHARACTERS | TextUtils.CAP_MODE_WORDS) & reqModes;
224         }
225 
226         // We found out that we have a period. We need to determine if this is a full stop or
227         // otherwise sentence-ending period, or an abbreviation like "e.g.". An abbreviation
228         // looks like (\w\.){2,}. Moreover, in German, you put periods after digits for dates
229         // and some other things, and in German specifically we need to not go into autocaps after
230         // a whitespace-digits-period sequence.
231         // To find out, we will have a simple state machine with the following states :
232         // START, WORD, PERIOD, ABBREVIATION, NUMBER
233         // On START : (just before the first period)
234         //           letter => WORD
235         //           digit => NUMBER if German; end with caps otherwise
236         //           whitespace => end with no caps (it was a stand-alone period)
237         //           otherwise => end with caps (several periods/symbols in a row)
238         // On WORD : (within the word just before the first period)
239         //           letter => WORD
240         //           period => PERIOD
241         //           otherwise => end with caps (it was a word with a full stop at the end)
242         // On PERIOD : (period within a potential abbreviation)
243         //           letter => LETTER
244         //           otherwise => end with caps (it was not an abbreviation)
245         // On LETTER : (letter within a potential abbreviation)
246         //           letter => LETTER
247         //           period => PERIOD
248         //           otherwise => end with no caps (it was an abbreviation)
249         // On NUMBER : (period immediately preceded by one or more digits)
250         //           digit => NUMBER
251         //           letter => LETTER (promote to word)
252         //           otherwise => end with no caps (it was a whitespace-digits-period sequence,
253         //            or a punctuation-digits-period sequence like "11.11.")
254         // "Not an abbreviation" in the above chart essentially covers cases like "...yes.". This
255         // should capitalize.
256 
257         final int START = 0;
258         final int WORD = 1;
259         final int PERIOD = 2;
260         final int LETTER = 3;
261         final int NUMBER = 4;
262         final int caps = (TextUtils.CAP_MODE_CHARACTERS | TextUtils.CAP_MODE_WORDS
263                 | TextUtils.CAP_MODE_SENTENCES) & reqModes;
264         final int noCaps = (TextUtils.CAP_MODE_CHARACTERS | TextUtils.CAP_MODE_WORDS) & reqModes;
265         int state = START;
266         while (j > 0) {
267             c = cs.charAt(--j);
268             switch (state) {
269             case START:
270                 if (Character.isLetter(c)) {
271                     state = WORD;
272                 } else if (Character.isWhitespace(c)) {
273                     return noCaps;
274                 } else if (Character.isDigit(c) && spacingAndPunctuations.mUsesGermanRules) {
275                     state = NUMBER;
276                 } else {
277                     return caps;
278                 }
279                 break;
280             case WORD:
281                 if (Character.isLetter(c)) {
282                     state = WORD;
283                 } else if (spacingAndPunctuations.isSentenceSeparator(c)) {
284                     state = PERIOD;
285                 } else {
286                     return caps;
287                 }
288                 break;
289             case PERIOD:
290                 if (Character.isLetter(c)) {
291                     state = LETTER;
292                 } else {
293                     return caps;
294                 }
295                 break;
296             case LETTER:
297                 if (Character.isLetter(c)) {
298                     state = LETTER;
299                 } else if (spacingAndPunctuations.isSentenceSeparator(c)) {
300                     state = PERIOD;
301                 } else {
302                     return noCaps;
303                 }
304                 break;
305             case NUMBER:
306                 if (Character.isLetter(c)) {
307                     state = WORD;
308                 } else if (Character.isDigit(c)) {
309                     state = NUMBER;
310                 } else {
311                     return noCaps;
312                 }
313             }
314         }
315         // Here we arrived at the start of the line. This should behave exactly like whitespace.
316         return (START == state || LETTER == state) ? noCaps : caps;
317     }
318 }
319