• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 
KeySpecParser()52     private KeySpecParser() {
53         // Intentional empty constructor for utility class.
54     }
55 
hasIcon(final String keySpec)56     private static boolean hasIcon(final String keySpec) {
57         return keySpec.startsWith(KeyboardIconsSet.PREFIX_ICON);
58     }
59 
hasCode(final String keySpec, final int labelEnd)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 
parseEscape(final String text)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 
indexOfLabelEnd(final String keySpec)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 
getBeforeLabelEnd(final String keySpec, final int labelEnd)119     private static String getBeforeLabelEnd(final String keySpec, final int labelEnd) {
120         return (labelEnd < 0) ? keySpec : keySpec.substring(0, labelEnd);
121     }
122 
getAfterLabelEnd(final String keySpec, final int labelEnd)123     private static String getAfterLabelEnd(final String keySpec, final int labelEnd) {
124         return keySpec.substring(labelEnd + /* VERTICAL_BAR */1);
125     }
126 
checkDoubleLabelEnd(final String keySpec, final int labelEnd)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 
getLabel(final String keySpec)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 
getOutputTextInternal(final String keySpec, final int labelEnd)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 
getOutputText(final String keySpec)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 
getCode(final String keySpec)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 
parseCode(final String text, final int defaultCode)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 
getIconId(final String keySpec)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 {
KeySpecParserError(final String message)245         public KeySpecParserError(final String message) {
246             super(message);
247         }
248     }
249 }
250