Home | History | Annotate | Download | only in internal
      1 /*
      2  * Copyright (C) 2010 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.keyboard.internal;
     18 
     19 import static com.android.inputmethod.latin.Constants.CODE_OUTPUT_TEXT;
     20 import static com.android.inputmethod.latin.Constants.CODE_UNSPECIFIED;
     21 
     22 import com.android.inputmethod.latin.Constants;
     23 import com.android.inputmethod.latin.utils.StringUtils;
     24 
     25 /**
     26  * The string parser of the key specification.
     27  *
     28  * Each key specification is one of the following:
     29  * - Label optionally followed by keyOutputText (keyLabel|keyOutputText).
     30  * - Label optionally followed by code point (keyLabel|!code/code_name).
     31  * - Icon followed by keyOutputText (!icon/icon_name|keyOutputText).
     32  * - Icon followed by code point (!icon/icon_name|!code/code_name).
     33  * Label and keyOutputText are one of the following:
     34  * - Literal string.
     35  * - Label reference represented by (!text/label_name), see {@link KeyboardTextsSet}.
     36  * - String resource reference represented by (!text/resource_name), see {@link KeyboardTextsSet}.
     37  * Icon is represented by (!icon/icon_name), see {@link KeyboardIconsSet}.
     38  * Code is one of the following:
     39  * - Code point presented by hexadecimal string prefixed with "0x"
     40  * - Code reference represented by (!code/code_name), see {@link KeyboardCodesSet}.
     41  * Special character, comma ',' backslash '\', and bar '|' can be escaped by '\' character.
     42  * Note that the '\' is also parsed by XML parser and {@link MoreKeySpec#splitKeySpecs(String)}
     43  * as well.
     44  */
     45 // TODO: Rename to KeySpec and make this class to the key specification object.
     46 public final class KeySpecParser {
     47     // Constants for parsing.
     48     private static final char BACKSLASH = Constants.CODE_BACKSLASH;
     49     private static final char VERTICAL_BAR = Constants.CODE_VERTICAL_BAR;
     50     private static final String PREFIX_HEX = "0x";
     51 
     52     private KeySpecParser() {
     53         // Intentional empty constructor for utility class.
     54     }
     55 
     56     private static boolean hasIcon(final String keySpec) {
     57         return keySpec.startsWith(KeyboardIconsSet.PREFIX_ICON);
     58     }
     59 
     60     private static boolean hasCode(final String keySpec, final int labelEnd) {
     61         if (labelEnd <= 0 || labelEnd + 1 >= keySpec.length()) {
     62             return false;
     63         }
     64         if (keySpec.startsWith(KeyboardCodesSet.PREFIX_CODE, labelEnd + 1)) {
     65             return true;
     66         }
     67         // This is a workaround to have a key that has a supplementary code point. We can't put a
     68         // string in resource as a XML entity of a supplementary code point or a surrogate pair.
     69         if (keySpec.startsWith(PREFIX_HEX, labelEnd + 1)) {
     70             return true;
     71         }
     72         return false;
     73     }
     74 
     75     private static String parseEscape(final String text) {
     76         if (text.indexOf(BACKSLASH) < 0) {
     77             return text;
     78         }
     79         final int length = text.length();
     80         final StringBuilder sb = new StringBuilder();
     81         for (int pos = 0; pos < length; pos++) {
     82             final char c = text.charAt(pos);
     83             if (c == BACKSLASH && pos + 1 < length) {
     84                 // Skip escape char
     85                 pos++;
     86                 sb.append(text.charAt(pos));
     87             } else {
     88                 sb.append(c);
     89             }
     90         }
     91         return sb.toString();
     92     }
     93 
     94     private static int indexOfLabelEnd(final String keySpec) {
     95         final int length = keySpec.length();
     96         if (keySpec.indexOf(BACKSLASH) < 0) {
     97             final int labelEnd = keySpec.indexOf(VERTICAL_BAR);
     98             if (labelEnd == 0) {
     99                 if (length == 1) {
    100                     // Treat a sole vertical bar as a special case of key label.
    101                     return -1;
    102                 }
    103                 throw new KeySpecParserError("Empty label");
    104             }
    105             return labelEnd;
    106         }
    107         for (int pos = 0; pos < length; pos++) {
    108             final char c = keySpec.charAt(pos);
    109             if (c == BACKSLASH && pos + 1 < length) {
    110                 // Skip escape char
    111                 pos++;
    112             } else if (c == VERTICAL_BAR) {
    113                 return pos;
    114             }
    115         }
    116         return -1;
    117     }
    118 
    119     private static String getBeforeLabelEnd(final String keySpec, final int labelEnd) {
    120         return (labelEnd < 0) ? keySpec : keySpec.substring(0, labelEnd);
    121     }
    122 
    123     private static String getAfterLabelEnd(final String keySpec, final int labelEnd) {
    124         return keySpec.substring(labelEnd + /* VERTICAL_BAR */1);
    125     }
    126 
    127     private static void checkDoubleLabelEnd(final String keySpec, final int labelEnd) {
    128         if (indexOfLabelEnd(getAfterLabelEnd(keySpec, labelEnd)) < 0) {
    129             return;
    130         }
    131         throw new KeySpecParserError("Multiple " + VERTICAL_BAR + ": " + keySpec);
    132     }
    133 
    134     public static String getLabel(final String keySpec) {
    135         if (keySpec == null) {
    136             // TODO: Throw {@link KeySpecParserError} once Key.keyLabel attribute becomes mandatory.
    137             return null;
    138         }
    139         if (hasIcon(keySpec)) {
    140             return null;
    141         }
    142         final int labelEnd = indexOfLabelEnd(keySpec);
    143         final String label = parseEscape(getBeforeLabelEnd(keySpec, labelEnd));
    144         if (label.isEmpty()) {
    145             throw new KeySpecParserError("Empty label: " + keySpec);
    146         }
    147         return label;
    148     }
    149 
    150     private static String getOutputTextInternal(final String keySpec, final int labelEnd) {
    151         if (labelEnd <= 0) {
    152             return null;
    153         }
    154         checkDoubleLabelEnd(keySpec, labelEnd);
    155         return parseEscape(getAfterLabelEnd(keySpec, labelEnd));
    156     }
    157 
    158     public static String getOutputText(final String keySpec) {
    159         if (keySpec == null) {
    160             // TODO: Throw {@link KeySpecParserError} once Key.keyLabel attribute becomes mandatory.
    161             return null;
    162         }
    163         final int labelEnd = indexOfLabelEnd(keySpec);
    164         if (hasCode(keySpec, labelEnd)) {
    165             return null;
    166         }
    167         final String outputText = getOutputTextInternal(keySpec, labelEnd);
    168         if (outputText != null) {
    169             if (StringUtils.codePointCount(outputText) == 1) {
    170                 // If output text is one code point, it should be treated as a code.
    171                 // See {@link #getCode(Resources, String)}.
    172                 return null;
    173             }
    174             if (outputText.isEmpty()) {
    175                 throw new KeySpecParserError("Empty outputText: " + keySpec);
    176             }
    177             return outputText;
    178         }
    179         final String label = getLabel(keySpec);
    180         if (label == null) {
    181             throw new KeySpecParserError("Empty label: " + keySpec);
    182         }
    183         // Code is automatically generated for one letter label. See {@link getCode()}.
    184         return (StringUtils.codePointCount(label) == 1) ? null : label;
    185     }
    186 
    187     public static int getCode(final String keySpec) {
    188         if (keySpec == null) {
    189             // TODO: Throw {@link KeySpecParserError} once Key.keyLabel attribute becomes mandatory.
    190             return CODE_UNSPECIFIED;
    191         }
    192         final int labelEnd = indexOfLabelEnd(keySpec);
    193         if (hasCode(keySpec, labelEnd)) {
    194             checkDoubleLabelEnd(keySpec, labelEnd);
    195             return parseCode(getAfterLabelEnd(keySpec, labelEnd), CODE_UNSPECIFIED);
    196         }
    197         final String outputText = getOutputTextInternal(keySpec, labelEnd);
    198         if (outputText != null) {
    199             // If output text is one code point, it should be treated as a code.
    200             // See {@link #getOutputText(String)}.
    201             if (StringUtils.codePointCount(outputText) == 1) {
    202                 return outputText.codePointAt(0);
    203             }
    204             return CODE_OUTPUT_TEXT;
    205         }
    206         final String label = getLabel(keySpec);
    207         if (label == null) {
    208             throw new KeySpecParserError("Empty label: " + keySpec);
    209         }
    210         // Code is automatically generated for one letter label.
    211         return (StringUtils.codePointCount(label) == 1) ? label.codePointAt(0) : CODE_OUTPUT_TEXT;
    212     }
    213 
    214     public static int parseCode(final String text, final int defaultCode) {
    215         if (text == null) {
    216             return defaultCode;
    217         }
    218         if (text.startsWith(KeyboardCodesSet.PREFIX_CODE)) {
    219             return KeyboardCodesSet.getCode(text.substring(KeyboardCodesSet.PREFIX_CODE.length()));
    220         }
    221         // This is a workaround to have a key that has a supplementary code point. We can't put a
    222         // string in resource as a XML entity of a supplementary code point or a surrogate pair.
    223         if (text.startsWith(PREFIX_HEX)) {
    224             return Integer.parseInt(text.substring(PREFIX_HEX.length()), 16);
    225         }
    226         return defaultCode;
    227     }
    228 
    229     public static int getIconId(final String keySpec) {
    230         if (keySpec == null) {
    231             // TODO: Throw {@link KeySpecParserError} once Key.keyLabel attribute becomes mandatory.
    232             return KeyboardIconsSet.ICON_UNDEFINED;
    233         }
    234         if (!hasIcon(keySpec)) {
    235             return KeyboardIconsSet.ICON_UNDEFINED;
    236         }
    237         final int labelEnd = indexOfLabelEnd(keySpec);
    238         final String iconName = getBeforeLabelEnd(keySpec, labelEnd)
    239                 .substring(KeyboardIconsSet.PREFIX_ICON.length());
    240         return KeyboardIconsSet.getIconId(iconName);
    241     }
    242 
    243     @SuppressWarnings("serial")
    244     public static final class KeySpecParserError extends RuntimeException {
    245         public KeySpecParserError(final String message) {
    246             super(message);
    247         }
    248     }
    249 }
    250