• 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"); you may not
5  * use this file except in compliance with the License. You may obtain a copy of
6  * 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, WITHOUT
12  * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13  * License for the specific language governing permissions and limitations under
14  * the License.
15  */
16 
17 package com.android.inputmethod.keyboard.internal;
18 
19 import static com.android.inputmethod.keyboard.Keyboard.CODE_UNSPECIFIED;
20 
21 import android.text.TextUtils;
22 
23 import com.android.inputmethod.keyboard.Keyboard;
24 import com.android.inputmethod.latin.LatinImeLogger;
25 import com.android.inputmethod.latin.StringUtils;
26 
27 import java.util.ArrayList;
28 import java.util.Arrays;
29 import java.util.Locale;
30 
31 /**
32  * The string parser of more keys specification.
33  * The specification is comma separated texts each of which represents one "more key".
34  * The specification might have label or string resource reference in it. These references are
35  * expanded before parsing comma.
36  * - Label reference should be a string representation of label (!text/label_name)
37  * - String resource reference should be a string representation of resource (!text/resource_name)
38  * Each "more key" specification is one of the following:
39  * - Label optionally followed by keyOutputText or code (keyLabel|keyOutputText).
40  * - Icon followed by keyOutputText or code (!icon/icon_name|!code/code_name)
41  *   - Icon should be a string representation of icon (!icon/icon_name).
42  *   - Code should be a code point presented by hexadecimal string prefixed with "0x", or a string
43  *     representation of code (!code/code_name).
44  * Special character, comma ',' backslash '\', and bar '|' can be escaped by '\' character.
45  * Note that the '\' is also parsed by XML parser and CSV parser as well.
46  * See {@link KeyboardIconsSet} about icon_name.
47  */
48 public class KeySpecParser {
49     private static final boolean DEBUG = LatinImeLogger.sDBG;
50 
51     private static final int MAX_STRING_REFERENCE_INDIRECTION = 10;
52 
53     // Constants for parsing.
54     private static int COMMA = ',';
55     private static final char ESCAPE_CHAR = '\\';
56     private static final char LABEL_END = '|';
57     private static final String PREFIX_TEXT = "!text/";
58     private static final String PREFIX_ICON = "!icon/";
59     private static final String PREFIX_CODE = "!code/";
60     private static final String PREFIX_HEX = "0x";
61     private static final String ADDITIONAL_MORE_KEY_MARKER = "%";
62 
63     public static class MoreKeySpec {
64         public final int mCode;
65         public final String mLabel;
66         public final String mOutputText;
67         public final int mIconId;
68 
MoreKeySpec(final String moreKeySpec, boolean needsToUpperCase, Locale locale, final KeyboardCodesSet codesSet)69         public MoreKeySpec(final String moreKeySpec, boolean needsToUpperCase, Locale locale,
70                 final KeyboardCodesSet codesSet) {
71             mCode = toUpperCaseOfCodeForLocale(getCode(moreKeySpec, codesSet),
72                     needsToUpperCase, locale);
73             mLabel = toUpperCaseOfStringForLocale(getLabel(moreKeySpec),
74                     needsToUpperCase, locale);
75             mOutputText = toUpperCaseOfStringForLocale(getOutputText(moreKeySpec),
76                     needsToUpperCase, locale);
77             mIconId = getIconId(moreKeySpec);
78         }
79     }
80 
KeySpecParser()81     private KeySpecParser() {
82         // Intentional empty constructor for utility class.
83     }
84 
hasIcon(String moreKeySpec)85     private static boolean hasIcon(String moreKeySpec) {
86         return moreKeySpec.startsWith(PREFIX_ICON);
87     }
88 
hasCode(String moreKeySpec)89     private static boolean hasCode(String moreKeySpec) {
90         final int end = indexOfLabelEnd(moreKeySpec, 0);
91         if (end > 0 && end + 1 < moreKeySpec.length() && moreKeySpec.startsWith(
92                 PREFIX_CODE, end + 1)) {
93             return true;
94         }
95         return false;
96     }
97 
parseEscape(String text)98     private static String parseEscape(String text) {
99         if (text.indexOf(ESCAPE_CHAR) < 0) {
100             return text;
101         }
102         final int length = text.length();
103         final StringBuilder sb = new StringBuilder();
104         for (int pos = 0; pos < length; pos++) {
105             final char c = text.charAt(pos);
106             if (c == ESCAPE_CHAR && pos + 1 < length) {
107                 // Skip escape char
108                 pos++;
109                 sb.append(text.charAt(pos));
110             } else {
111                 sb.append(c);
112             }
113         }
114         return sb.toString();
115     }
116 
indexOfLabelEnd(String moreKeySpec, int start)117     private static int indexOfLabelEnd(String moreKeySpec, int start) {
118         if (moreKeySpec.indexOf(ESCAPE_CHAR, start) < 0) {
119             final int end = moreKeySpec.indexOf(LABEL_END, start);
120             if (end == 0) {
121                 throw new KeySpecParserError(LABEL_END + " at " + start + ": " + moreKeySpec);
122             }
123             return end;
124         }
125         final int length = moreKeySpec.length();
126         for (int pos = start; pos < length; pos++) {
127             final char c = moreKeySpec.charAt(pos);
128             if (c == ESCAPE_CHAR && pos + 1 < length) {
129                 // Skip escape char
130                 pos++;
131             } else if (c == LABEL_END) {
132                 return pos;
133             }
134         }
135         return -1;
136     }
137 
getLabel(String moreKeySpec)138     public static String getLabel(String moreKeySpec) {
139         if (hasIcon(moreKeySpec)) {
140             return null;
141         }
142         final int end = indexOfLabelEnd(moreKeySpec, 0);
143         final String label = (end > 0) ? parseEscape(moreKeySpec.substring(0, end))
144                 : parseEscape(moreKeySpec);
145         if (TextUtils.isEmpty(label)) {
146             throw new KeySpecParserError("Empty label: " + moreKeySpec);
147         }
148         return label;
149     }
150 
getOutputTextInternal(String moreKeySpec)151     private static String getOutputTextInternal(String moreKeySpec) {
152         final int end = indexOfLabelEnd(moreKeySpec, 0);
153         if (end <= 0) {
154             return null;
155         }
156         if (indexOfLabelEnd(moreKeySpec, end + 1) >= 0) {
157             throw new KeySpecParserError("Multiple " + LABEL_END + ": " + moreKeySpec);
158         }
159         return parseEscape(moreKeySpec.substring(end + /* LABEL_END */1));
160     }
161 
getOutputText(String moreKeySpec)162     static String getOutputText(String moreKeySpec) {
163         if (hasCode(moreKeySpec)) {
164             return null;
165         }
166         final String outputText = getOutputTextInternal(moreKeySpec);
167         if (outputText != null) {
168             if (StringUtils.codePointCount(outputText) == 1) {
169                 // If output text is one code point, it should be treated as a code.
170                 // See {@link #getCode(Resources, String)}.
171                 return null;
172             }
173             if (!TextUtils.isEmpty(outputText)) {
174                 return outputText;
175             }
176             throw new KeySpecParserError("Empty outputText: " + moreKeySpec);
177         }
178         final String label = getLabel(moreKeySpec);
179         if (label == null) {
180             throw new KeySpecParserError("Empty label: " + moreKeySpec);
181         }
182         // Code is automatically generated for one letter label. See {@link getCode()}.
183         return (StringUtils.codePointCount(label) == 1) ? null : label;
184     }
185 
getCode(String moreKeySpec, KeyboardCodesSet codesSet)186     static int getCode(String moreKeySpec, KeyboardCodesSet codesSet) {
187         if (hasCode(moreKeySpec)) {
188             final int end = indexOfLabelEnd(moreKeySpec, 0);
189             if (indexOfLabelEnd(moreKeySpec, end + 1) >= 0) {
190                 throw new KeySpecParserError("Multiple " + LABEL_END + ": " + moreKeySpec);
191             }
192             return parseCode(moreKeySpec.substring(end + 1), codesSet, Keyboard.CODE_UNSPECIFIED);
193         }
194         final String outputText = getOutputTextInternal(moreKeySpec);
195         if (outputText != null) {
196             // If output text is one code point, it should be treated as a code.
197             // See {@link #getOutputText(String)}.
198             if (StringUtils.codePointCount(outputText) == 1) {
199                 return outputText.codePointAt(0);
200             }
201             return Keyboard.CODE_OUTPUT_TEXT;
202         }
203         final String label = getLabel(moreKeySpec);
204         // Code is automatically generated for one letter label.
205         if (StringUtils.codePointCount(label) == 1) {
206             return label.codePointAt(0);
207         }
208         return Keyboard.CODE_OUTPUT_TEXT;
209     }
210 
parseCode(String text, KeyboardCodesSet codesSet, int defCode)211     public static int parseCode(String text, KeyboardCodesSet codesSet, int defCode) {
212         if (text == null) return defCode;
213         if (text.startsWith(PREFIX_CODE)) {
214             return codesSet.getCode(text.substring(PREFIX_CODE.length()));
215         } else if (text.startsWith(PREFIX_HEX)) {
216             return Integer.parseInt(text.substring(PREFIX_HEX.length()), 16);
217         } else {
218             return Integer.parseInt(text);
219         }
220     }
221 
getIconId(String moreKeySpec)222     public static int getIconId(String moreKeySpec) {
223         if (moreKeySpec != null && hasIcon(moreKeySpec)) {
224             final int end = moreKeySpec.indexOf(LABEL_END, PREFIX_ICON.length());
225             final String name = (end < 0) ? moreKeySpec.substring(PREFIX_ICON.length())
226                     : moreKeySpec.substring(PREFIX_ICON.length(), end);
227             return KeyboardIconsSet.getIconId(name);
228         }
229         return KeyboardIconsSet.ICON_UNDEFINED;
230     }
231 
arrayAsList(T[] array, int start, int end)232     private static <T> ArrayList<T> arrayAsList(T[] array, int start, int end) {
233         if (array == null) {
234             throw new NullPointerException();
235         }
236         if (start < 0 || start > end || end > array.length) {
237             throw new IllegalArgumentException();
238         }
239 
240         final ArrayList<T> list = new ArrayList<T>(end - start);
241         for (int i = start; i < end; i++) {
242             list.add(array[i]);
243         }
244         return list;
245     }
246 
247     private static final String[] EMPTY_STRING_ARRAY = new String[0];
248 
filterOutEmptyString(String[] array)249     private static String[] filterOutEmptyString(String[] array) {
250         if (array == null) {
251             return EMPTY_STRING_ARRAY;
252         }
253         ArrayList<String> out = null;
254         for (int i = 0; i < array.length; i++) {
255             final String entry = array[i];
256             if (TextUtils.isEmpty(entry)) {
257                 if (out == null) {
258                     out = arrayAsList(array, 0, i);
259                 }
260             } else if (out != null) {
261                 out.add(entry);
262             }
263         }
264         if (out == null) {
265             return array;
266         }
267         return out.toArray(new String[out.size()]);
268     }
269 
insertAdditionalMoreKeys(String[] moreKeySpecs, String[] additionalMoreKeySpecs)270     public static String[] insertAdditionalMoreKeys(String[] moreKeySpecs,
271             String[] additionalMoreKeySpecs) {
272         final String[] moreKeys = filterOutEmptyString(moreKeySpecs);
273         final String[] additionalMoreKeys = filterOutEmptyString(additionalMoreKeySpecs);
274         final int moreKeysCount = moreKeys.length;
275         final int additionalCount = additionalMoreKeys.length;
276         ArrayList<String> out = null;
277         int additionalIndex = 0;
278         for (int moreKeyIndex = 0; moreKeyIndex < moreKeysCount; moreKeyIndex++) {
279             final String moreKeySpec = moreKeys[moreKeyIndex];
280             if (moreKeySpec.equals(ADDITIONAL_MORE_KEY_MARKER)) {
281                 if (additionalIndex < additionalCount) {
282                     // Replace '%' marker with additional more key specification.
283                     final String additionalMoreKey = additionalMoreKeys[additionalIndex];
284                     if (out != null) {
285                         out.add(additionalMoreKey);
286                     } else {
287                         moreKeys[moreKeyIndex] = additionalMoreKey;
288                     }
289                     additionalIndex++;
290                 } else {
291                     // Filter out excessive '%' marker.
292                     if (out == null) {
293                         out = arrayAsList(moreKeys, 0, moreKeyIndex);
294                     }
295                 }
296             } else {
297                 if (out != null) {
298                     out.add(moreKeySpec);
299                 }
300             }
301         }
302         if (additionalCount > 0 && additionalIndex == 0) {
303             // No '%' marker is found in more keys.
304             // Insert all additional more keys to the head of more keys.
305             if (DEBUG && out != null) {
306                 throw new RuntimeException("Internal logic error:"
307                         + " moreKeys=" + Arrays.toString(moreKeys)
308                         + " additionalMoreKeys=" + Arrays.toString(additionalMoreKeys));
309             }
310             out = arrayAsList(additionalMoreKeys, additionalIndex, additionalCount);
311             for (int i = 0; i < moreKeysCount; i++) {
312                 out.add(moreKeys[i]);
313             }
314         } else if (additionalIndex < additionalCount) {
315             // The number of '%' markers are less than additional more keys.
316             // Append remained additional more keys to the tail of more keys.
317             if (DEBUG && out != null) {
318                 throw new RuntimeException("Internal logic error:"
319                         + " moreKeys=" + Arrays.toString(moreKeys)
320                         + " additionalMoreKeys=" + Arrays.toString(additionalMoreKeys));
321             }
322             out = arrayAsList(moreKeys, 0, moreKeysCount);
323             for (int i = additionalIndex; i < additionalCount; i++) {
324                 out.add(additionalMoreKeys[additionalIndex]);
325             }
326         }
327         if (out == null && moreKeysCount > 0) {
328             return moreKeys;
329         } else if (out != null && out.size() > 0) {
330             return out.toArray(new String[out.size()]);
331         } else {
332             return null;
333         }
334     }
335 
336     @SuppressWarnings("serial")
337     public static class KeySpecParserError extends RuntimeException {
KeySpecParserError(String message)338         public KeySpecParserError(String message) {
339             super(message);
340         }
341     }
342 
resolveTextReference(String rawText, KeyboardTextsSet textsSet)343     public static String resolveTextReference(String rawText, KeyboardTextsSet textsSet) {
344         int level = 0;
345         String text = rawText;
346         StringBuilder sb;
347         do {
348             level++;
349             if (level >= MAX_STRING_REFERENCE_INDIRECTION) {
350                 throw new RuntimeException("too many @string/resource indirection: " + text);
351             }
352 
353             final int prefixLen = PREFIX_TEXT.length();
354             final int size = text.length();
355             if (size < prefixLen) {
356                 return text;
357             }
358 
359             sb = null;
360             for (int pos = 0; pos < size; pos++) {
361                 final char c = text.charAt(pos);
362                 if (text.startsWith(PREFIX_TEXT, pos) && textsSet != null) {
363                     if (sb == null) {
364                         sb = new StringBuilder(text.substring(0, pos));
365                     }
366                     final int end = searchTextNameEnd(text, pos + prefixLen);
367                     final String name = text.substring(pos + prefixLen, end);
368                     sb.append(textsSet.getText(name));
369                     pos = end - 1;
370                 } else if (c == ESCAPE_CHAR) {
371                     if (sb != null) {
372                         // Append both escape character and escaped character.
373                         sb.append(text.substring(pos, Math.min(pos + 2, size)));
374                     }
375                     pos++;
376                 } else if (sb != null) {
377                     sb.append(c);
378                 }
379             }
380 
381             if (sb != null) {
382                 text = sb.toString();
383             }
384         } while (sb != null);
385 
386         return text;
387     }
388 
searchTextNameEnd(String text, int start)389     private static int searchTextNameEnd(String text, int start) {
390         final int size = text.length();
391         for (int pos = start; pos < size; pos++) {
392             final char c = text.charAt(pos);
393             // Label name should be consisted of [a-zA-Z_0-9].
394             if ((c >= 'a' && c <= 'z') || c == '_' || (c >= '0' && c <= '9')) {
395                 continue;
396             }
397             return pos;
398         }
399         return size;
400     }
401 
parseCsvString(String rawText, KeyboardTextsSet textsSet)402     public static String[] parseCsvString(String rawText, KeyboardTextsSet textsSet) {
403         final String text = resolveTextReference(rawText, textsSet);
404         final int size = text.length();
405         if (size == 0) {
406             return null;
407         }
408         if (StringUtils.codePointCount(text) == 1) {
409             return text.codePointAt(0) == COMMA ? null : new String[] { text };
410         }
411 
412         ArrayList<String> list = null;
413         int start = 0;
414         for (int pos = 0; pos < size; pos++) {
415             final char c = text.charAt(pos);
416             if (c == COMMA) {
417                 // Skip empty entry.
418                 if (pos - start > 0) {
419                     if (list == null) {
420                         list = new ArrayList<String>();
421                     }
422                     list.add(text.substring(start, pos));
423                 }
424                 // Skip comma
425                 start = pos + 1;
426             } else if (c == ESCAPE_CHAR) {
427                 // Skip escape character and escaped character.
428                 pos++;
429             }
430         }
431         final String remain = (size - start > 0) ? text.substring(start) : null;
432         if (list == null) {
433             return remain != null ? new String[] { remain } : null;
434         }
435         if (remain != null) {
436             list.add(remain);
437         }
438         return list.toArray(new String[list.size()]);
439     }
440 
getIntValue(String[] moreKeys, String key, int defaultValue)441     public static int getIntValue(String[] moreKeys, String key, int defaultValue) {
442         if (moreKeys == null) {
443             return defaultValue;
444         }
445         final int keyLen = key.length();
446         boolean foundValue = false;
447         int value = defaultValue;
448         for (int i = 0; i < moreKeys.length; i++) {
449             final String moreKeySpec = moreKeys[i];
450             if (moreKeySpec == null || !moreKeySpec.startsWith(key)) {
451                 continue;
452             }
453             moreKeys[i] = null;
454             try {
455                 if (!foundValue) {
456                     value = Integer.parseInt(moreKeySpec.substring(keyLen));
457                     foundValue = true;
458                 }
459             } catch (NumberFormatException e) {
460                 throw new RuntimeException(
461                         "integer should follow after " + key + ": " + moreKeySpec);
462             }
463         }
464         return value;
465     }
466 
getBooleanValue(String[] moreKeys, String key)467     public static boolean getBooleanValue(String[] moreKeys, String key) {
468         if (moreKeys == null) {
469             return false;
470         }
471         boolean value = false;
472         for (int i = 0; i < moreKeys.length; i++) {
473             final String moreKeySpec = moreKeys[i];
474             if (moreKeySpec == null || !moreKeySpec.equals(key)) {
475                 continue;
476             }
477             moreKeys[i] = null;
478             value = true;
479         }
480         return value;
481     }
482 
toUpperCaseOfCodeForLocale(int code, boolean needsToUpperCase, Locale locale)483     public static int toUpperCaseOfCodeForLocale(int code, boolean needsToUpperCase,
484             Locale locale) {
485         if (!Keyboard.isLetterCode(code) || !needsToUpperCase) return code;
486         final String text = new String(new int[] { code } , 0, 1);
487         final String casedText = KeySpecParser.toUpperCaseOfStringForLocale(
488                 text, needsToUpperCase, locale);
489         return StringUtils.codePointCount(casedText) == 1
490                 ? casedText.codePointAt(0) : CODE_UNSPECIFIED;
491     }
492 
toUpperCaseOfStringForLocale(String text, boolean needsToUpperCase, Locale locale)493     public static String toUpperCaseOfStringForLocale(String text, boolean needsToUpperCase,
494             Locale locale) {
495         if (text == null || !needsToUpperCase) return text;
496         return text.toUpperCase(locale);
497     }
498 }
499