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 
KeySpecParser()55     private KeySpecParser() {
56         // Intentional empty constructor for utility class.
57     }
58 
hasIcon(@onnull final String keySpec)59     private static boolean hasIcon(@Nonnull final String keySpec) {
60         return keySpec.startsWith(KeyboardIconsSet.PREFIX_ICON);
61     }
62 
hasCode(@onnull final String keySpec, final int labelEnd)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
parseEscape(@onnull final String text)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 
indexOfLabelEnd(@onnull final String keySpec)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
getBeforeLabelEnd(@onnull final String keySpec, final int labelEnd)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
getAfterLabelEnd(@onnull final String keySpec, final int labelEnd)129     private static String getAfterLabelEnd(@Nonnull final String keySpec, final int labelEnd) {
130         return keySpec.substring(labelEnd + /* VERTICAL_BAR */1);
131     }
132 
checkDoubleLabelEnd(@onnull final String keySpec, final int labelEnd)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
getLabel(@ullable final String keySpec)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
getOutputTextInternal(@onnull final String keySpec, final int labelEnd)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
getOutputText(@ullable final String keySpec)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 
getCode(@ullable final String keySpec)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 
parseCode(@ullable final String text, final int defaultCode)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 
getIconId(@ullable final String keySpec)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 {
KeySpecParserError(final String message)254         public KeySpecParserError(final String message) {
255             super(message);
256         }
257     }
258 }
259