| // Copyright 2014 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| package org.chromium.ui.text; |
| |
| import android.support.annotation.Nullable; |
| import android.text.SpannableString; |
| |
| import java.util.Arrays; |
| |
| /** |
| * Applies spans to an HTML-looking string and returns the resulting SpannableString. |
| * Note: This does not support duplicate, nested or overlapping spans. |
| * |
| * Example: |
| * |
| * String input = "Click to view the <tos>terms of service</tos> or <pn>privacy notice</pn>"; |
| * ClickableSpan tosSpan = ...; |
| * ClickableSpan privacySpan = ...; |
| * SpannableString output = SpanApplier.applySpans(input, |
| * new Span("<tos>", "</tos>", tosSpan), new Span("<pn>", "</pn>", privacySpan)); |
| */ |
| public class SpanApplier { |
| |
| /** |
| * Associates a span with the range of text between a start and an end tag. |
| */ |
| public static final class SpanInfo implements Comparable<SpanInfo> { |
| final String mStartTag; |
| final String mEndTag; |
| final @Nullable Object[] mSpans; |
| int mStartTagIndex; |
| int mEndTagIndex; |
| |
| /** |
| * @param startTag The start tag, e.g. "<tos>". |
| * @param endTag The end tag, e.g. "</tos>". |
| * @param span The span to apply to the text between the start and end tags. May be null, |
| * then SpanApplier will not apply any span. |
| */ |
| public SpanInfo(String startTag, String endTag, @Nullable Object span) { |
| mStartTag = startTag; |
| mEndTag = endTag; |
| mSpans = span == null ? null : new Object[] {span}; |
| } |
| |
| /** |
| * @param startTag The start tag, e.g. "<tos>". |
| * @param endTag The end tag, e.g. "</tos>". |
| * @param spans A vararg list of spans to be applied. |
| */ |
| public SpanInfo(String startTag, String endTag, Object... spans) { |
| mStartTag = startTag; |
| mEndTag = endTag; |
| mSpans = spans; |
| } |
| |
| @Override |
| public int compareTo(SpanInfo other) { |
| return this.mStartTagIndex < other.mStartTagIndex ? -1 |
| : (this.mStartTagIndex == other.mStartTagIndex ? 0 : 1); |
| } |
| |
| @Override |
| public boolean equals(Object other) { |
| if (!(other instanceof SpanInfo)) return false; |
| |
| return compareTo((SpanInfo) other) == 0; |
| } |
| |
| @Override |
| public int hashCode() { |
| return 0; |
| } |
| } |
| |
| /** |
| * Applies spans to an HTML-looking string and returns the resulting SpannableString. |
| * If a span cannot be applied (e.g. because the start tag isn't in the input string), then |
| * a RuntimeException will be thrown. |
| * |
| * @param input The input string. |
| * @param spans The Spans which will be applied to the string. |
| * @return A SpannableString with the given spans applied. |
| * @throws IllegalArgumentException if the span cannot be applied. |
| */ |
| public static SpannableString applySpans(String input, SpanInfo... spans) { |
| for (SpanInfo span : spans) { |
| span.mStartTagIndex = input.indexOf(span.mStartTag); |
| span.mEndTagIndex = input.indexOf(span.mEndTag, |
| span.mStartTagIndex + span.mStartTag.length()); |
| } |
| |
| // Sort the spans from first to last in the order they appear in the input string. |
| Arrays.sort(spans); |
| |
| // Copy the input text to the output, but omit the start and end tags. |
| // Update startTagIndex and endTagIndex for each Span as we go. |
| int inputIndex = 0; |
| StringBuilder output = new StringBuilder(input.length()); |
| |
| for (SpanInfo span : spans) { |
| // Fail if there is a span without a start or end tag or if there are nested |
| // or overlapping spans. |
| if (span.mStartTagIndex == -1 || span.mEndTagIndex == -1 |
| || span.mStartTagIndex < inputIndex) { |
| span.mStartTagIndex = -1; |
| String error = String.format("Input string is missing tags %s%s: %s", |
| span.mStartTag, span.mEndTag, input); |
| throw new IllegalArgumentException(error); |
| } |
| |
| output.append(input, inputIndex, span.mStartTagIndex); |
| inputIndex = span.mStartTagIndex + span.mStartTag.length(); |
| span.mStartTagIndex = output.length(); |
| |
| output.append(input, inputIndex, span.mEndTagIndex); |
| inputIndex = span.mEndTagIndex + span.mEndTag.length(); |
| span.mEndTagIndex = output.length(); |
| } |
| output.append(input, inputIndex, input.length()); |
| |
| SpannableString spannableString = new SpannableString(output); |
| for (SpanInfo span : spans) { |
| if (span.mStartTagIndex == -1 || span.mSpans == null || span.mSpans.length == 0) { |
| continue; |
| } |
| |
| for (Object s : span.mSpans) { |
| if (s == null) continue; |
| spannableString.setSpan(s, span.mStartTagIndex, span.mEndTagIndex, 0); |
| } |
| } |
| |
| return spannableString; |
| } |
| } |
| |