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