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); } } } }