KeySpecParser.java revision d9c6b332090c90e4d4840e62fe3eb45c834b2e14
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 */ 45public final class KeySpecParser { 46 // Constants for parsing. 47 private static final char BACKSLASH = Constants.CODE_BACKSLASH; 48 private static final char VERTICAL_BAR = Constants.CODE_VERTICAL_BAR; 49 private static final String PREFIX_HEX = "0x"; 50 51 private KeySpecParser() { 52 // Intentional empty constructor for utility class. 53 } 54 55 private static boolean hasIcon(final String keySpec) { 56 return keySpec.startsWith(KeyboardIconsSet.PREFIX_ICON); 57 } 58 59 private static boolean hasCode(final String keySpec, final int labelEnd) { 60 if (labelEnd > 0 && labelEnd + 1 < keySpec.length() 61 && keySpec.startsWith(KeyboardCodesSet.PREFIX_CODE, labelEnd + 1)) { 62 return true; 63 } 64 return false; 65 } 66 67 private static String parseEscape(final String text) { 68 if (text.indexOf(BACKSLASH) < 0) { 69 return text; 70 } 71 final int length = text.length(); 72 final StringBuilder sb = new StringBuilder(); 73 for (int pos = 0; pos < length; pos++) { 74 final char c = text.charAt(pos); 75 if (c == BACKSLASH && pos + 1 < length) { 76 // Skip escape char 77 pos++; 78 sb.append(text.charAt(pos)); 79 } else { 80 sb.append(c); 81 } 82 } 83 return sb.toString(); 84 } 85 86 private static int indexOfLabelEnd(final String keySpec) { 87 if (keySpec.indexOf(BACKSLASH) < 0) { 88 final int labelEnd = keySpec.indexOf(VERTICAL_BAR); 89 if (labelEnd == 0) { 90 throw new KeySpecParserError("Empty label"); 91 } 92 return labelEnd; 93 } 94 final int length = keySpec.length(); 95 for (int pos = 0; pos < length; pos++) { 96 final char c = keySpec.charAt(pos); 97 if (c == BACKSLASH && pos + 1 < length) { 98 // Skip escape char 99 pos++; 100 } else if (c == VERTICAL_BAR) { 101 return pos; 102 } 103 } 104 return -1; 105 } 106 107 private static String getBeforeLabelEnd(final String keySpec, final int labelEnd) { 108 return (labelEnd < 0) ? keySpec : keySpec.substring(0, labelEnd); 109 } 110 111 private static String getAfterLabelEnd(final String keySpec, final int labelEnd) { 112 return keySpec.substring(labelEnd + /* VERTICAL_BAR */1); 113 } 114 115 private static void checkDoubleLabelEnd(final String keySpec, final int labelEnd) { 116 if (indexOfLabelEnd(getAfterLabelEnd(keySpec, labelEnd)) < 0) { 117 return; 118 } 119 throw new KeySpecParserError("Multiple " + VERTICAL_BAR + ": " + keySpec); 120 } 121 122 public static String getLabel(final String keySpec) { 123 if (keySpec == null) { 124 // TODO: Throw {@link KeySpecParserError} once Key.keyLabel attribute becomes mandatory. 125 return null; 126 } 127 if (hasIcon(keySpec)) { 128 return null; 129 } 130 final int labelEnd = indexOfLabelEnd(keySpec); 131 final String label = parseEscape(getBeforeLabelEnd(keySpec, labelEnd)); 132 if (label.isEmpty()) { 133 throw new KeySpecParserError("Empty label: " + keySpec); 134 } 135 return label; 136 } 137 138 private static String getOutputTextInternal(final String keySpec, final int labelEnd) { 139 if (labelEnd <= 0) { 140 return null; 141 } 142 checkDoubleLabelEnd(keySpec, labelEnd); 143 return parseEscape(getAfterLabelEnd(keySpec, labelEnd)); 144 } 145 146 public static String getOutputText(final String keySpec) { 147 if (keySpec == null) { 148 // TODO: Throw {@link KeySpecParserError} once Key.keyLabel attribute becomes mandatory. 149 return null; 150 } 151 final int labelEnd = indexOfLabelEnd(keySpec); 152 if (hasCode(keySpec, labelEnd)) { 153 return null; 154 } 155 final String outputText = getOutputTextInternal(keySpec, labelEnd); 156 if (outputText != null) { 157 if (StringUtils.codePointCount(outputText) == 1) { 158 // If output text is one code point, it should be treated as a code. 159 // See {@link #getCode(Resources, String)}. 160 return null; 161 } 162 if (outputText.isEmpty()) { 163 throw new KeySpecParserError("Empty outputText: " + keySpec); 164 } 165 return outputText; 166 } 167 final String label = getLabel(keySpec); 168 if (label == null) { 169 throw new KeySpecParserError("Empty label: " + keySpec); 170 } 171 // Code is automatically generated for one letter label. See {@link getCode()}. 172 return (StringUtils.codePointCount(label) == 1) ? null : label; 173 } 174 175 public static int getCode(final String keySpec, final KeyboardCodesSet codesSet) { 176 if (keySpec == null) { 177 // TODO: Throw {@link KeySpecParserError} once Key.keyLabel attribute becomes mandatory. 178 return CODE_UNSPECIFIED; 179 } 180 final int labelEnd = indexOfLabelEnd(keySpec); 181 if (hasCode(keySpec, labelEnd)) { 182 checkDoubleLabelEnd(keySpec, labelEnd); 183 return parseCode(getAfterLabelEnd(keySpec, labelEnd), codesSet, CODE_UNSPECIFIED); 184 } 185 final String outputText = getOutputTextInternal(keySpec, labelEnd); 186 if (outputText != null) { 187 // If output text is one code point, it should be treated as a code. 188 // See {@link #getOutputText(String)}. 189 if (StringUtils.codePointCount(outputText) == 1) { 190 return outputText.codePointAt(0); 191 } 192 return CODE_OUTPUT_TEXT; 193 } 194 final String label = getLabel(keySpec); 195 if (label == null) { 196 throw new KeySpecParserError("Empty label: " + keySpec); 197 } 198 // Code is automatically generated for one letter label. 199 return (StringUtils.codePointCount(label) == 1) ? label.codePointAt(0) : CODE_OUTPUT_TEXT; 200 } 201 202 // TODO: Make this method private once Key.code attribute is removed. 203 public static int parseCode(final String text, final KeyboardCodesSet codesSet, 204 final int defCode) { 205 if (text == null) { 206 return defCode; 207 } 208 if (text.startsWith(KeyboardCodesSet.PREFIX_CODE)) { 209 return codesSet.getCode(text.substring(KeyboardCodesSet.PREFIX_CODE.length())); 210 } 211 if (text.startsWith(PREFIX_HEX)) { 212 return Integer.parseInt(text.substring(PREFIX_HEX.length()), 16); 213 } 214 return Integer.parseInt(text); 215 } 216 217 public static int getIconId(final String keySpec) { 218 if (keySpec == null) { 219 // TODO: Throw {@link KeySpecParserError} once Key.keyLabel attribute becomes mandatory. 220 return KeyboardIconsSet.ICON_UNDEFINED; 221 } 222 if (!hasIcon(keySpec)) { 223 return KeyboardIconsSet.ICON_UNDEFINED; 224 } 225 final int labelEnd = indexOfLabelEnd(keySpec); 226 final String iconName = getBeforeLabelEnd(keySpec, labelEnd) 227 .substring(KeyboardIconsSet.PREFIX_ICON.length()); 228 return KeyboardIconsSet.getIconId(iconName); 229 } 230 231 @SuppressWarnings("serial") 232 public static final class KeySpecParserError extends RuntimeException { 233 public KeySpecParserError(final String message) { 234 super(message); 235 } 236 } 237} 238