package org.wordpress.android.editor;
import android.graphics.Color;
import android.graphics.Typeface;
import android.os.Build;
import android.support.annotation.NonNull;
import android.text.Spannable;
import android.text.style.CharacterStyle;
import android.text.style.ForegroundColorSpan;
import android.text.style.RelativeSizeSpan;
import android.text.style.StyleSpan;
import org.wordpress.android.util.AppLog;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class HtmlStyleUtils {
public static final int TAG_COLOR = Color.rgb(0, 80, 130);
public static final int ATTRIBUTE_COLOR = Color.rgb(158, 158, 158);
public static final String REGEX_HTML_TAGS = "(<\\/?[a-z][^<>]*>)";
public static final String REGEX_HTML_ATTRIBUTES = "(?<==)('|\")(.*?\\1)(?=.*?>)";
public static final String REGEX_HTML_COMMENTS = "(<!--.*?-->)";
public static final String REGEX_HTML_ENTITIES = "("|&|'|<|>| |¡|¢|£" +
"|¤|¥|¦|§|¨|©|ª|«|¬||®|¯|°|±" +
"|²|³|´|µ|¶|·|¸|¹|º|»|¼|½|¾|¿" +
"|À|Á|Â|Ã|Ä|Å|Æ|Ç|È|É|Ê|Ë|Ì|Í" +
"|Î|Ï|Ð|Ñ|Ò|Ó|Ô|Õ|Ö|×|Ø|Ù|Ú|Û" +
"|Ü|Ý|Þ|ß|à|á|â|ã|ä|å|æ|ç|è|é" +
"|ê|ë|ì|í|î|ï|ð|ñ|ò|ó|ô|õ|ö|÷" +
"|ø|ù|ú|û|ü|ý|þ|ÿ|Œ|œ|Š|š|Ÿ|ƒ" +
"|ˆ|˜|Α|Β|Γ|Δ|Ε|Ζ|Η|Θ|Ι|Κ|Λ|Μ" +
"|Ν|Ξ|Ο|Π|Ρ|Σ|Τ|Υ|Φ|Χ|Ψ|Ω|α|β" +
"|γ|δ|ε|ζ|η|θ|ι|κ|λ|μ|ν|ξ|ο|π" +
"|ρ|ς|σ|τ|υ|φ|χ|ψ|ω|ϑ|ϒ|ϖ| | " +
"| |||||–|—|‘|’|‚|“|”|„" +
"|†|‡|•|…|‰|′|″|‹|›|‾|⁄|€|ℑ" +
"|℘|ℜ|™|ℵ|←|↑|→|↓|↔|↵|⇐|⇑|⇒" +
"|⇓|⇔|∀|∂|∃|∅|∇|∈|∉|∋|∏|∑|−" +
"|∗|√|∝|∞|∠|∧|∨|∩|∪|∫|∴|∼|≅" +
"|≈|≠|≡|≤|≥|⊂|⊃|⊄|⊆|⊇|⊕|⊗|⊥" +
"|⋅|⌈|⌉|⌊|⌋|〈|〉|◊|♠|♣|♥|♦|"" +
"|&|'|<|>| |¡|¢|£|¤|¥|¦|§|¨|©|ª" +
"|«|¬||®|¯|°|±|²|³|´|µ|¶|·|¸" +
"|¹|º|»|¼|½|¾|¿|À|Á|Â|Ã|Ä" +
"|Å|Æ|Ç|È|É|Ê|Ë|Ì|Í|Î|Ï|Ð" +
"|Ñ|Ò|Ó|Ô|Õ|Ö|×|Ø|Ù|Ú|Û|Ü" +
"|Ý|Þ|ß|à|á|â|ã|ä|å|æ|ç|è" +
"|é|ê|ë|ì|í|î|ï|ð|ñ|ò|ó|ô" +
"|õ|ö|÷|ø|Ù|Ú|Û|Ü|ý|þ|ÿ|Œ" +
"|œ|Š|š|Ÿ|ƒ|ˆ|˜|Α|Β|Γ|Δ|Ε|Ζ" +
"|Η|Θ|Ι|Κ|Λ|Μ|Ν|Ξ|Ο|Π|Ρ|Σ|Τ|Υ|Φ" +
"|Χ|Ψ|Ω|α|β|γ|δ|ε|ζ|η|θ|ι|κ" +
"|λ|μ|ν|ξ|ο|π|ρ|ς|σ|τ|υ|φ|χ|ψ|ω" +
"|ϑ|&Upsih;|ϖ| | | |||||–|—|‘" +
"|’|‚|“|”|„|†|‡|•|…|‰|′|″" +
"|‹|›|‾|⁄|€|ℑ|℘|ℜ|™|ℵ|←|↑|→" +
"|↓|↔|↵|⇐|&UArr;|⇒|⇓|⇔|∀|∂|∃|∅|∇|∈" +
"|∉|∋|∏|∑|−|∗|√|∝|∞|∠|∧|∨|∩|∪|∫" +
"|∴|∼|≅|≈|≠|≡|≤|≥|⊂|⊃|⊄|⊆|⊇|⊕|⊗" +
"|⊥|⋅|⌈|⌉|⌊|⌋|〈|〉|◊|♠|♣|♥|♦)";
public static final int SPANNABLE_FLAGS = Spannable.SPAN_EXCLUSIVE_EXCLUSIVE;
/**
* Apply styling rules to {@code content}.
*/
public static void styleHtmlForDisplay(@NonNull Spannable content) {
styleHtmlForDisplay(content, 0, content.length());
}
/**
* Apply styling rules to {@code content} inside the range from {@code start} to {@code end}.
*
* @param content the Spannable to apply style rules to
* @param start the index in {@code content} to start styling from
* @param end the index in {@code content} to style until
*/
public static void styleHtmlForDisplay(@NonNull Spannable content, int start, int end) {
if (Build.VERSION.RELEASE.equals("4.1") || Build.VERSION.RELEASE.equals("4.1.1")) {
// Avoids crashing bug in Android 4.1 and 4.1.1 triggered when spanned text is line-wrapped
// AOSP issue: https://code.google.com/p/android/issues/detail?id=35466
return;
}
applySpansByRegex(content, start, end, REGEX_HTML_TAGS);
applySpansByRegex(content, start, end, REGEX_HTML_ATTRIBUTES);
applySpansByRegex(content, start, end, REGEX_HTML_COMMENTS);
applySpansByRegex(content, start, end, REGEX_HTML_ENTITIES);
}
/**
* Applies styles to {@code content} from {@code start} to {@code end}, based on rule {@code regex}.
* @param content the Spannable to apply style rules to
* @param start the index in {@code content} to start styling from
* @param end the index in {@code content} to style until
* @param regex the pattern to match for styling
*/
private static void applySpansByRegex(Spannable content, int start, int end, String regex) {
if (content == null || start < 0 || end < 0 || start > content.length() || end > content.length() ||
start >= end) {
AppLog.d(AppLog.T.EDITOR, "applySpansByRegex() received invalid input");
return;
}
Pattern pattern = Pattern.compile(regex);
Matcher matcher = pattern.matcher(content.subSequence(start, end));
while (matcher.find()) {
int matchStart = matcher.start() + start;
int matchEnd = matcher.end() + start;
switch(regex) {
case REGEX_HTML_TAGS:
content.setSpan(new ForegroundColorSpan(TAG_COLOR), matchStart, matchEnd, SPANNABLE_FLAGS);
break;
case REGEX_HTML_ATTRIBUTES:
content.setSpan(new ForegroundColorSpan(ATTRIBUTE_COLOR), matchStart, matchEnd, SPANNABLE_FLAGS);
break;
case REGEX_HTML_COMMENTS:
content.setSpan(new ForegroundColorSpan(ATTRIBUTE_COLOR), matchStart, matchEnd, SPANNABLE_FLAGS);
content.setSpan(new StyleSpan(Typeface.ITALIC), matchStart, matchEnd, SPANNABLE_FLAGS);
content.setSpan(new RelativeSizeSpan(0.75f), matchStart, matchEnd, SPANNABLE_FLAGS);
break;
case REGEX_HTML_ENTITIES:
content.setSpan(new ForegroundColorSpan(TAG_COLOR), matchStart, matchEnd, SPANNABLE_FLAGS);
content.setSpan(new StyleSpan(Typeface.BOLD), matchStart, matchEnd, SPANNABLE_FLAGS);
content.setSpan(new RelativeSizeSpan(0.75f), matchStart, matchEnd, SPANNABLE_FLAGS);
break;
}
}
}
/**
* Clears all relevant spans in {@code content} from {@code start} to {@code end}. Relevant spans are the subclasses
* of {@link CharacterStyle} applied by {@link HtmlStyleUtils#applySpansByRegex(Spannable, int, int, String)}.
* @param content the Spannable to clear styles from
* @param spanStart the index in {@code content} to start clearing styles from
* @param spanEnd the index in {@code content} to clear styles until
*/
public static void clearSpans(Spannable content, int spanStart, int spanEnd) {
CharacterStyle[] spans = content.getSpans(spanStart, spanEnd, CharacterStyle.class);
for (CharacterStyle span : spans) {
if (span instanceof ForegroundColorSpan || span instanceof StyleSpan || span instanceof RelativeSizeSpan) {
content.removeSpan(span);
}
}
}
}