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 17package com.android.inputmethod.keyboard.internal; 18 19import static com.android.inputmethod.latin.Constants.CODE_OUTPUT_TEXT; 20import static com.android.inputmethod.latin.Constants.CODE_UNSPECIFIED; 21 22import com.android.inputmethod.latin.Constants; 23import 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. 46public 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