diff --git a/java/src/com/android/inputmethod/keyboard/internal/KeySpecParser.java b/java/src/com/android/inputmethod/keyboard/internal/KeySpecParser.java index b1813a141..ba449eeb3 100644 --- a/java/src/com/android/inputmethod/keyboard/internal/KeySpecParser.java +++ b/java/src/com/android/inputmethod/keyboard/internal/KeySpecParser.java @@ -53,7 +53,9 @@ public final class KeySpecParser { private static final int MAX_STRING_REFERENCE_INDIRECTION = 10; // Constants for parsing. - private static final char LABEL_END = '|'; + private static final char COMMA = ','; + private static final char BACKSLASH = '\\'; + private static final char VERTICAL_BAR = '|'; private static final String PREFIX_TEXT = "!text/"; static final String PREFIX_ICON = "!icon/"; private static final String PREFIX_CODE = "!code/"; @@ -64,6 +66,59 @@ public final class KeySpecParser { // Intentional empty constructor for utility class. } + /** + * Split the text containing multiple key specifications separated by commas into an array of + * key specifications. + * A key specification can contain a character escaped by the backslash character, including a + * comma character. + * Note that an empty key specification will be eliminated from the result array. + * + * @param text the text containing multiple key specifications. + * @return an array of key specification text. Null if the specified text is empty + * or has no key specifications. + */ + public static String[] splitKeySpecs(final String text) { + final int size = text.length(); + if (size == 0) { + return null; + } + // Optimization for one-letter key specification. + if (size == 1) { + return text.charAt(0) == COMMA ? null : new String[] { text }; + } + + ArrayList list = null; + int start = 0; + // The characters in question in this loop are COMMA and BACKSLASH. These characters never + // match any high or low surrogate character. So it is OK to iterate through with char + // index. + for (int pos = 0; pos < size; pos++) { + final char c = text.charAt(pos); + if (c == COMMA) { + // Skip empty entry. + if (pos - start > 0) { + if (list == null) { + list = CollectionUtils.newArrayList(); + } + list.add(text.substring(start, pos)); + } + // Skip comma + start = pos + 1; + } else if (c == BACKSLASH) { + // Skip escape character and escaped character. + pos++; + } + } + final String remain = (size - start > 0) ? text.substring(start) : null; + if (list == null) { + return remain != null ? new String[] { remain } : null; + } + if (remain != null) { + list.add(remain); + } + return list.toArray(new String[list.size()]); + } + private static boolean hasIcon(final String moreKeySpec) { return moreKeySpec.startsWith(PREFIX_ICON); } @@ -78,14 +133,14 @@ public final class KeySpecParser { } private static String parseEscape(final String text) { - if (text.indexOf(Constants.CSV_ESCAPE) < 0) { + if (text.indexOf(BACKSLASH) < 0) { return text; } final int length = text.length(); final StringBuilder sb = new StringBuilder(); for (int pos = 0; pos < length; pos++) { final char c = text.charAt(pos); - if (c == Constants.CSV_ESCAPE && pos + 1 < length) { + if (c == BACKSLASH && pos + 1 < length) { // Skip escape char pos++; sb.append(text.charAt(pos)); @@ -97,20 +152,20 @@ public final class KeySpecParser { } private static int indexOfLabelEnd(final String moreKeySpec, final int start) { - if (moreKeySpec.indexOf(Constants.CSV_ESCAPE, start) < 0) { - final int end = moreKeySpec.indexOf(LABEL_END, start); + if (moreKeySpec.indexOf(BACKSLASH, start) < 0) { + final int end = moreKeySpec.indexOf(VERTICAL_BAR, start); if (end == 0) { - throw new KeySpecParserError(LABEL_END + " at " + start + ": " + moreKeySpec); + throw new KeySpecParserError(VERTICAL_BAR + " at " + start + ": " + moreKeySpec); } return end; } final int length = moreKeySpec.length(); for (int pos = start; pos < length; pos++) { final char c = moreKeySpec.charAt(pos); - if (c == Constants.CSV_ESCAPE && pos + 1 < length) { + if (c == BACKSLASH && pos + 1 < length) { // Skip escape char pos++; - } else if (c == LABEL_END) { + } else if (c == VERTICAL_BAR) { return pos; } } @@ -136,9 +191,9 @@ public final class KeySpecParser { return null; } if (indexOfLabelEnd(moreKeySpec, end + 1) >= 0) { - throw new KeySpecParserError("Multiple " + LABEL_END + ": " + moreKeySpec); + throw new KeySpecParserError("Multiple " + VERTICAL_BAR + ": " + moreKeySpec); } - return parseEscape(moreKeySpec.substring(end + /* LABEL_END */1)); + return parseEscape(moreKeySpec.substring(end + /* VERTICAL_BAR */1)); } static String getOutputText(final String moreKeySpec) { @@ -169,7 +224,7 @@ public final class KeySpecParser { if (hasCode(moreKeySpec)) { final int end = indexOfLabelEnd(moreKeySpec, 0); if (indexOfLabelEnd(moreKeySpec, end + 1) >= 0) { - throw new KeySpecParserError("Multiple " + LABEL_END + ": " + moreKeySpec); + throw new KeySpecParserError("Multiple " + VERTICAL_BAR + ": " + moreKeySpec); } return parseCode(moreKeySpec.substring(end + 1), codesSet, CODE_UNSPECIFIED); } @@ -204,7 +259,7 @@ public final class KeySpecParser { public static int getIconId(final String moreKeySpec) { if (moreKeySpec != null && hasIcon(moreKeySpec)) { - final int end = moreKeySpec.indexOf(LABEL_END, PREFIX_ICON.length()); + final int end = moreKeySpec.indexOf(VERTICAL_BAR, PREFIX_ICON.length()); final String name = (end < 0) ? moreKeySpec.substring(PREFIX_ICON.length()) : moreKeySpec.substring(PREFIX_ICON.length(), end); return KeyboardIconsSet.getIconId(name); @@ -351,7 +406,7 @@ public final class KeySpecParser { final String name = text.substring(pos + prefixLen, end); sb.append(textsSet.getText(name)); pos = end - 1; - } else if (c == Constants.CSV_ESCAPE) { + } else if (c == BACKSLASH) { if (sb != null) { // Append both escape character and escaped character. sb.append(text.substring(pos, Math.min(pos + 2, size))); @@ -366,7 +421,6 @@ public final class KeySpecParser { text = sb.toString(); } } while (sb != null); - return text; } diff --git a/java/src/com/android/inputmethod/keyboard/internal/KeyStyle.java b/java/src/com/android/inputmethod/keyboard/internal/KeyStyle.java index 5db3ebbd1..f65056948 100644 --- a/java/src/com/android/inputmethod/keyboard/internal/KeyStyle.java +++ b/java/src/com/android/inputmethod/keyboard/internal/KeyStyle.java @@ -18,8 +18,6 @@ package com.android.inputmethod.keyboard.internal; import android.content.res.TypedArray; -import com.android.inputmethod.latin.StringUtils; - public abstract class KeyStyle { private final KeyboardTextsSet mTextsSet; @@ -42,7 +40,7 @@ public abstract class KeyStyle { protected String[] parseStringArray(final TypedArray a, final int index) { if (a.hasValue(index)) { final String text = KeySpecParser.resolveTextReference(a.getString(index), mTextsSet); - return StringUtils.parseCsvString(text); + return KeySpecParser.splitKeySpecs(text); } return null; } diff --git a/java/src/com/android/inputmethod/latin/Constants.java b/java/src/com/android/inputmethod/latin/Constants.java index 86bb25562..64c14d32f 100644 --- a/java/src/com/android/inputmethod/latin/Constants.java +++ b/java/src/com/android/inputmethod/latin/Constants.java @@ -215,10 +215,6 @@ public final class Constants { } } - // Constants for CSV parsing. - public static final char CSV_SEPARATOR = ','; - public static final char CSV_ESCAPE = '\\'; - private Constants() { // This utility class is not publicly instantiable. } diff --git a/java/src/com/android/inputmethod/latin/SettingsValues.java b/java/src/com/android/inputmethod/latin/SettingsValues.java index 1ad8def16..09102447f 100644 --- a/java/src/com/android/inputmethod/latin/SettingsValues.java +++ b/java/src/com/android/inputmethod/latin/SettingsValues.java @@ -100,7 +100,7 @@ public final class SettingsValues { mWordConnectors = StringUtils.toCodePointArray(res.getString(R.string.symbols_word_connectors)); Arrays.sort(mWordConnectors); - final String[] suggestPuncsSpec = StringUtils.parseCsvString(res.getString( + final String[] suggestPuncsSpec = KeySpecParser.splitKeySpecs(res.getString( R.string.suggested_punctuations)); mSuggestPuncList = createSuggestPuncList(suggestPuncsSpec); mWordSeparators = res.getString(R.string.symbols_word_separators); diff --git a/java/src/com/android/inputmethod/latin/StringUtils.java b/java/src/com/android/inputmethod/latin/StringUtils.java index 988b7f60e..c2fd4fb32 100644 --- a/java/src/com/android/inputmethod/latin/StringUtils.java +++ b/java/src/com/android/inputmethod/latin/StringUtils.java @@ -153,44 +153,6 @@ public final class StringUtils { return codePoints; } - public static String[] parseCsvString(final String text) { - final int size = text.length(); - if (size == 0) { - return null; - } - if (codePointCount(text) == 1) { - return text.codePointAt(0) == Constants.CSV_SEPARATOR ? null : new String[] { text }; - } - - ArrayList list = null; - int start = 0; - for (int pos = 0; pos < size; pos++) { - final char c = text.charAt(pos); - if (c == Constants.CSV_SEPARATOR) { - // Skip empty entry. - if (pos - start > 0) { - if (list == null) { - list = CollectionUtils.newArrayList(); - } - list.add(text.substring(start, pos)); - } - // Skip comma - start = pos + 1; - } else if (c == Constants.CSV_ESCAPE) { - // Skip escape character and escaped character. - pos++; - } - } - final String remain = (size - start > 0) ? text.substring(start) : null; - if (list == null) { - return remain != null ? new String[] { remain } : null; - } - if (remain != null) { - list.add(remain); - } - return list.toArray(new String[list.size()]); - } - // This method assumes the text is not null. For the empty string, it returns CAPITALIZE_NONE. public static int getCapitalizationType(final String text) { // If the first char is not uppercase, then the word is either all lower case or diff --git a/tests/src/com/android/inputmethod/keyboard/internal/KeySpecParserCsvTests.java b/tests/src/com/android/inputmethod/keyboard/internal/KeySpecParserSplitTests.java similarity index 95% rename from tests/src/com/android/inputmethod/keyboard/internal/KeySpecParserCsvTests.java rename to tests/src/com/android/inputmethod/keyboard/internal/KeySpecParserSplitTests.java index 9014e7cc8..7a87f3722 100644 --- a/tests/src/com/android/inputmethod/keyboard/internal/KeySpecParserCsvTests.java +++ b/tests/src/com/android/inputmethod/keyboard/internal/KeySpecParserSplitTests.java @@ -21,7 +21,6 @@ import android.test.InstrumentationTestCase; import android.test.suitebuilder.annotation.MediumTest; import com.android.inputmethod.latin.CollectionUtils; -import com.android.inputmethod.latin.StringUtils; import java.lang.reflect.Field; import java.util.ArrayList; @@ -29,7 +28,7 @@ import java.util.Arrays; import java.util.Locale; @MediumTest -public class KeySpecParserCsvTests extends InstrumentationTestCase { +public class KeySpecParserSplitTests extends InstrumentationTestCase { private final KeyboardTextsSet mTextsSet = new KeyboardTextsSet(); @Override @@ -78,7 +77,7 @@ public class KeySpecParserCsvTests extends InstrumentationTestCase { private void assertTextArray(final String message, final String value, final String ... expectedArray) { final String resolvedActual = KeySpecParser.resolveTextReference(value, mTextsSet); - final String[] actual = StringUtils.parseCsvString(resolvedActual); + final String[] actual = KeySpecParser.splitKeySpecs(resolvedActual); final String[] expected = (expectedArray.length == 0) ? null : expectedArray; assertArrayEquals(message, expected, actual); } @@ -101,7 +100,7 @@ public class KeySpecParserCsvTests extends InstrumentationTestCase { private static final String SURROGATE1 = PAIR1 + PAIR2; private static final String SURROGATE2 = PAIR1 + PAIR2 + PAIR3; - public void testParseCsvTextZero() { + public void testSplitZero() { assertTextArray("Empty string", ""); assertTextArray("Empty entry", ","); assertTextArray("Empty entry at beginning", ",a", "a"); @@ -110,7 +109,7 @@ public class KeySpecParserCsvTests extends InstrumentationTestCase { assertTextArray("Empty entries with escape", ",a,b\\,c,,d,", "a", "b\\,c", "d"); } - public void testParseCsvTextSingle() { + public void testSplitSingle() { assertTextArray("Single char", "a", "a"); assertTextArray("Surrogate pair", PAIR1, PAIR1); assertTextArray("Single escape", "\\", "\\"); @@ -139,7 +138,7 @@ public class KeySpecParserCsvTests extends InstrumentationTestCase { assertTextArray("Incomplete resource reference 4", "!" + SURROGATE2, "!" + SURROGATE2); } - public void testParseCsvTextSingleEscaped() { + public void testSplitSingleEscaped() { assertTextArray("Escaped char", "\\a", "\\a"); assertTextArray("Escaped surrogate pair", "\\" + PAIR1, "\\" + PAIR1); assertTextArray("Escaped comma", "\\,", "\\,"); @@ -174,7 +173,7 @@ public class KeySpecParserCsvTests extends InstrumentationTestCase { assertTextArray("Escaped !TEXT/NAME", "\\!TEXT/EMPTY_STRING", "\\!TEXT/EMPTY_STRING"); } - public void testParseCsvTextMulti() { + public void testSplitMulti() { assertTextArray("Multiple chars", "a,b,c", "a", "b", "c"); assertTextArray("Multiple chars", "a,b,\\c", "a", "b", "\\c"); assertTextArray("Multiple chars and escape at beginning and end", @@ -189,7 +188,7 @@ public class KeySpecParserCsvTests extends InstrumentationTestCase { " abc ", " def ", " ghi "); } - public void testParseCsvTextMultiEscaped() { + public void testSplitMultiEscaped() { assertTextArray("Multiple chars with comma", "a,\\,,c", "a", "\\,", "c"); assertTextArray("Multiple chars with comma surrounded by spaces", " a , \\, , c ", " a ", " \\, ", " c "); @@ -208,17 +207,17 @@ public class KeySpecParserCsvTests extends InstrumentationTestCase { "\\!", "\\!TEXT/EMPTY_STRING"); } - public void testParseCsvResourceError() { + public void testSplitResourceError() { assertError("Incomplete resource name", "!text/", "!text/"); assertError("Non existing resource", "!text/non_existing"); } - public void testParseCsvResourceZero() { + public void testSplitResourceZero() { assertTextArray("Empty string", "!text/empty_string"); } - public void testParseCsvResourceSingle() { + public void testSplitResourceSingle() { assertTextArray("Single char", "!text/single_char", "a"); assertTextArray("Space", @@ -240,7 +239,7 @@ public class KeySpecParserCsvTests extends InstrumentationTestCase { "\\\\!text/single_char", "\\\\a"); } - public void testParseCsvResourceSingleEscaped() { + public void testSplitResourceSingleEscaped() { assertTextArray("Escaped char", "!text/escaped_char", "\\a"); assertTextArray("Escaped comma", @@ -267,7 +266,7 @@ public class KeySpecParserCsvTests extends InstrumentationTestCase { "!text/escaped_label_with_escape", "a\\\\c"); } - public void testParseCsvResourceMulti() { + public void testSplitResourceMulti() { assertTextArray("Multiple chars", "!text/multiple_chars", "a", "b", "c"); assertTextArray("Multiple chars surrounded by spaces", @@ -279,7 +278,7 @@ public class KeySpecParserCsvTests extends InstrumentationTestCase { "!text/multiple_labels_surrounded_by_spaces", " abc ", " def ", " ghi "); } - public void testParseCsvResourcetMultiEscaped() { + public void testSplitResourcetMultiEscaped() { assertTextArray("Multiple chars with comma", "!text/multiple_chars_with_comma", "a", "\\,", "c"); @@ -300,7 +299,7 @@ public class KeySpecParserCsvTests extends InstrumentationTestCase { " ab\\\\ ", " d\\\\\\, ", " g\\,i "); } - public void testParseMultipleResources() { + public void testSplitMultipleResources() { assertTextArray("Literals and resources", "1,!text/multiple_chars,z", "1", "a", "b", "c", "z"); assertTextArray("Literals and resources and escape at end", @@ -322,7 +321,7 @@ public class KeySpecParserCsvTests extends InstrumentationTestCase { "abcabc", "def", "ghi"); } - public void testParseIndirectReference() { + public void testSplitIndirectReference() { assertTextArray("Indirect", "!text/indirect_string", "a", "b", "c"); assertTextArray("Indirect with literal", @@ -331,7 +330,7 @@ public class KeySpecParserCsvTests extends InstrumentationTestCase { "!text/indirect2_string", "a", "b", "c"); } - public void testParseInfiniteIndirectReference() { + public void testSplitInfiniteIndirectReference() { assertError("Infinite indirection", "1,!text/infinite_indirection,2", "1", "infinite", "", "loop", "2"); }