1/*
2 * Copyright (C) 2013 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.latin.utils;
18
19import android.text.Spannable;
20import android.text.SpannableString;
21import android.text.Spanned;
22import android.text.SpannedString;
23import android.text.TextUtils;
24import android.text.style.SuggestionSpan;
25import android.text.style.URLSpan;
26
27import com.android.inputmethod.annotations.UsedForTesting;
28
29import java.util.ArrayList;
30import java.util.regex.Matcher;
31import java.util.regex.Pattern;
32
33public final class SpannableStringUtils {
34    /**
35     * Copies the spans from the region <code>start...end</code> in
36     * <code>source</code> to the region
37     * <code>destoff...destoff+end-start</code> in <code>dest</code>.
38     * Spans in <code>source</code> that begin before <code>start</code>
39     * or end after <code>end</code> but overlap this range are trimmed
40     * as if they began at <code>start</code> or ended at <code>end</code>.
41     * Only SuggestionSpans that don't have the SPAN_PARAGRAPH span are copied.
42     *
43     * This code is almost entirely taken from {@link TextUtils#copySpansFrom}, except for the
44     * kind of span that is copied.
45     *
46     * @throws IndexOutOfBoundsException if any of the copied spans
47     * are out of range in <code>dest</code>.
48     */
49    public static void copyNonParagraphSuggestionSpansFrom(Spanned source, int start, int end,
50            Spannable dest, int destoff) {
51        Object[] spans = source.getSpans(start, end, SuggestionSpan.class);
52
53        for (int i = 0; i < spans.length; i++) {
54            int fl = source.getSpanFlags(spans[i]);
55            // We don't care about the PARAGRAPH flag in LatinIME code. However, if this flag
56            // is set, Spannable#setSpan will throw an exception unless the span is on the edge
57            // of a word. But the spans have been split into two by the getText{Before,After}Cursor
58            // methods, so after concatenation they may end in the middle of a word.
59            // Since we don't use them, we can just remove them and avoid crashing.
60            fl &= ~Spanned.SPAN_PARAGRAPH;
61
62            int st = source.getSpanStart(spans[i]);
63            int en = source.getSpanEnd(spans[i]);
64
65            if (st < start)
66                st = start;
67            if (en > end)
68                en = end;
69
70            dest.setSpan(spans[i], st - start + destoff, en - start + destoff,
71                         fl);
72        }
73    }
74
75    /**
76     * Returns a CharSequence concatenating the specified CharSequences, retaining their
77     * SuggestionSpans that don't have the PARAGRAPH flag, but not other spans.
78     *
79     * This code is almost entirely taken from {@link TextUtils#concat(CharSequence...)}, except
80     * it calls copyNonParagraphSuggestionSpansFrom instead of {@link TextUtils#copySpansFrom}.
81     */
82    public static CharSequence concatWithNonParagraphSuggestionSpansOnly(CharSequence... text) {
83        if (text.length == 0) {
84            return "";
85        }
86
87        if (text.length == 1) {
88            return text[0];
89        }
90
91        boolean spanned = false;
92        for (int i = 0; i < text.length; i++) {
93            if (text[i] instanceof Spanned) {
94                spanned = true;
95                break;
96            }
97        }
98
99        StringBuilder sb = new StringBuilder();
100        for (int i = 0; i < text.length; i++) {
101            sb.append(text[i]);
102        }
103
104        if (!spanned) {
105            return sb.toString();
106        }
107
108        SpannableString ss = new SpannableString(sb);
109        int off = 0;
110        for (int i = 0; i < text.length; i++) {
111            int len = text[i].length();
112
113            if (text[i] instanceof Spanned) {
114                copyNonParagraphSuggestionSpansFrom((Spanned) text[i], 0, len, ss, off);
115            }
116
117            off += len;
118        }
119
120        return new SpannedString(ss);
121    }
122
123    public static boolean hasUrlSpans(final CharSequence text,
124            final int startIndex, final int endIndex) {
125        if (!(text instanceof Spanned)) {
126            return false; // Not spanned, so no link
127        }
128        final Spanned spanned = (Spanned)text;
129        // getSpans(x, y) does not return spans that start on x or end on y. x-1, y+1 does the
130        // trick, and works in all cases even if startIndex <= 0 or endIndex >= text.length().
131        final URLSpan[] spans = spanned.getSpans(startIndex - 1, endIndex + 1, URLSpan.class);
132        return null != spans && spans.length > 0;
133    }
134
135    /**
136     * Splits the given {@code charSequence} with at occurrences of the given {@code regex}.
137     * <p>
138     * This is equivalent to
139     * {@code charSequence.toString().split(regex, preserveTrailingEmptySegments ? -1 : 0)}
140     * except that the spans are preserved in the result array.
141     * </p>
142     * @param charSequence the character sequence to be split.
143     * @param regex the regex pattern to be used as the separator.
144     * @param preserveTrailingEmptySegments {@code true} to preserve the trailing empty
145     * segments. Otherwise, trailing empty segments will be removed before being returned.
146     * @return the array which contains the result. All the spans in the <code>charSequence</code>
147     * is preserved.
148     */
149    @UsedForTesting
150    public static CharSequence[] split(final CharSequence charSequence, final String regex,
151            final boolean preserveTrailingEmptySegments) {
152        // A short-cut for non-spanned strings.
153        if (!(charSequence instanceof Spanned)) {
154            // -1 means that trailing empty segments will be preserved.
155            return charSequence.toString().split(regex, preserveTrailingEmptySegments ? -1 : 0);
156        }
157
158        // Hereafter, emulate String.split for CharSequence.
159        final ArrayList<CharSequence> sequences = new ArrayList<>();
160        final Matcher matcher = Pattern.compile(regex).matcher(charSequence);
161        int nextStart = 0;
162        boolean matched = false;
163        while (matcher.find()) {
164            sequences.add(charSequence.subSequence(nextStart, matcher.start()));
165            nextStart = matcher.end();
166            matched = true;
167        }
168        if (!matched) {
169            // never matched. preserveTrailingEmptySegments is ignored in this case.
170            return new CharSequence[] { charSequence };
171        }
172        sequences.add(charSequence.subSequence(nextStart, charSequence.length()));
173        if (!preserveTrailingEmptySegments) {
174            for (int i = sequences.size() - 1; i >= 0; --i) {
175                if (!TextUtils.isEmpty(sequences.get(i))) {
176                    break;
177                }
178                sequences.remove(i);
179            }
180        }
181        return sequences.toArray(new CharSequence[sequences.size()]);
182    }
183}
184