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