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.common.Constants.CODE_OUTPUT_TEXT;
20import static com.android.inputmethod.latin.common.Constants.CODE_UNSPECIFIED;
21
22import com.android.inputmethod.latin.common.Constants;
23import com.android.inputmethod.latin.common.StringUtils;
24
25import javax.annotation.Nonnull;
26import 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.
49public 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
55    private KeySpecParser() {
56        // Intentional empty constructor for utility class.
57    }
58
59    private static boolean hasIcon(@Nonnull final String keySpec) {
60        return keySpec.startsWith(KeyboardIconsSet.PREFIX_ICON);
61    }
62
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
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
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
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
129    private static String getAfterLabelEnd(@Nonnull final String keySpec, final int labelEnd) {
130        return keySpec.substring(labelEnd + /* VERTICAL_BAR */1);
131    }
132
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
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
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
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
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
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
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 {
254        public KeySpecParserError(final String message) {
255            super(message);
256        }
257    }
258}
259