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