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