/*
* Copyright 2012 GitHub Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.github.mobile.util;
import static android.graphics.Paint.Style.FILL;
import static android.os.Build.VERSION.SDK_INT;
import static android.os.Build.VERSION_CODES.ICE_CREAM_SANDWICH;
import static android.text.Spanned.SPAN_EXCLUSIVE_EXCLUSIVE;
import static android.text.Spanned.SPAN_MARK_MARK;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Paint.Style;
import android.text.Editable;
import android.text.Html.ImageGetter;
import android.text.Html.TagHandler;
import android.text.Layout;
import android.text.Spanned;
import android.text.TextUtils;
import android.text.style.LeadingMarginSpan;
import android.text.style.QuoteSpan;
import android.text.style.StrikethroughSpan;
import android.text.style.TypefaceSpan;
import java.util.LinkedList;
import org.xml.sax.XMLReader;
/**
* HTML Utilities
*/
public class HtmlUtils {
private static class ReplySpan implements LeadingMarginSpan {
private final int color;
public ReplySpan() {
color = 0xffDDDDDD;
}
@Override
public int getLeadingMargin(boolean first) {
return 18;
}
public void drawLeadingMargin(Canvas c, Paint p, int x, int dir,
int top, int baseline, int bottom, CharSequence text,
int start, int end, boolean first, Layout layout) {
final Style style = p.getStyle();
final int color = p.getColor();
p.setStyle(FILL);
p.setColor(this.color);
c.drawRect(x, top, x + dir * 6, bottom, p);
p.setStyle(style);
p.setColor(color);
}
}
private static final String TAG_ROOT = "githubroot";
private static final String ROOT_START = '<' + TAG_ROOT + '>';
private static final String ROOT_END = "</" + TAG_ROOT + '>';
private static final String TAG_DEL = "del";
private static final String TAG_UL = "ul";
private static final String TAG_OL = "ol";
private static final String TAG_LI = "li";
private static final String TAG_CODE = "code";
private static final String TAG_PRE = "pre";
private static final String TOGGLE_START = "<span class=\"email-hidden-toggle\">";
private static final String TOGGLE_END = "</span>";
private static final String REPLY_START = "<div class=\"email-quoted-reply\">";
private static final String REPLY_END = "</div>";
private static final String SIGNATURE_START = "<div class=\"email-signature-reply\">";
private static final String SIGNATURE_END = "</div>";
private static final String EMAIL_START = "<div class=\"email-fragment\">";
private static final String EMAIL_END = "</div>";
private static final String HIDDEN_REPLY_START = "<div class=\"email-hidden-reply\" style=\" display:none\">";
private static final String HIDDEN_REPLY_END = "</div>";
private static final String BREAK = "<br>";
private static final String PARAGRAPH_START = "<p>";
private static final String PARAGRAPH_END = "</p>";
private static final String BLOCKQUOTE_START = "<blockquote>";
private static final String BLOCKQUOTE_END = "</blockquote>";
private static final String SPACE = " ";
private static final String PRE_START = "<pre>";
private static final String PRE_END = "</pre>";
private static final String CODE_START = "<code>";
private static final String CODE_END = "</code>";
private static class ListSeparator {
private int count;
public ListSeparator(boolean ordered) {
if (ordered)
count = 1;
else
count = -1;
}
public ListSeparator append(Editable output, int indentLevel) {
output.append('\n');
for (int i = 0; i < indentLevel * 2; i++)
output.append(' ');
if (count != -1) {
output.append(Integer.toString(count)).append('.');
count++;
} else
output.append('\u2022');
output.append(' ').append(' ');
return this;
}
}
private static final TagHandler TAG_HANDLER = new TagHandler() {
private int indentLevel;
private LinkedList<ListSeparator> listElements = new LinkedList<ListSeparator>();
public void handleTag(final boolean opening, final String tag,
final Editable output, final XMLReader xmlReader) {
if (TAG_DEL.equalsIgnoreCase(tag)) {
if (opening)
startSpan(new StrikethroughSpan(), output);
else
endSpan(StrikethroughSpan.class, output);
return;
}
if (TAG_UL.equalsIgnoreCase(tag)) {
if (opening) {
listElements.addFirst(new ListSeparator(false));
indentLevel++;
} else {
listElements.removeFirst();
indentLevel--;
}
if (!opening && indentLevel == 0)
output.append('\n');
return;
}
if (TAG_OL.equalsIgnoreCase(tag)) {
if (opening) {
listElements.addFirst(new ListSeparator(true));
indentLevel++;
} else {
listElements.removeFirst();
indentLevel--;
}
if (!opening && indentLevel == 0)
output.append('\n');
return;
}
if (TAG_LI.equalsIgnoreCase(tag) && opening) {
listElements.getFirst().append(output, indentLevel);
return;
}
if (TAG_CODE.equalsIgnoreCase(tag)) {
if (opening)
startSpan(new TypefaceSpan("monospace"), output);
else
endSpan(TypefaceSpan.class, output);
return;
}
if (TAG_PRE.equalsIgnoreCase(tag)) {
output.append('\n');
if (opening)
startSpan(new TypefaceSpan("monospace"), output);
else
endSpan(TypefaceSpan.class, output);
return;
}
if (TAG_ROOT.equalsIgnoreCase(tag) && !opening) {
// Remove leading newlines
while (output.length() > 0 && output.charAt(0) == '\n')
output.delete(0, 1);
// Remove trailing newlines
int last = output.length() - 1;
while (last >= 0 && output.charAt(last) == '\n') {
output.delete(last, last + 1);
last = output.length() - 1;
}
QuoteSpan[] quoteSpans = output.getSpans(0, output.length(),
QuoteSpan.class);
for (QuoteSpan span : quoteSpans) {
int start = output.getSpanStart(span);
int end = output.getSpanEnd(span);
output.removeSpan(span);
output.setSpan(new ReplySpan(), start, end,
SPAN_EXCLUSIVE_EXCLUSIVE);
}
return;
}
}
};
private static Object getLast(final Spanned text, final Class<?> kind) {
Object[] spans = text.getSpans(0, text.length(), kind);
return spans.length > 0 ? spans[spans.length - 1] : null;
}
private static void startSpan(Object span, Editable output) {
int length = output.length();
output.setSpan(span, length, length, SPAN_MARK_MARK);
}
private static void endSpan(Class<?> type, Editable output) {
int length = output.length();
Object span = getLast(output, type);
int start = output.getSpanStart(span);
output.removeSpan(span);
if (start != length)
output.setSpan(span, start, length, SPAN_EXCLUSIVE_EXCLUSIVE);
}
/**
* Encode HTML
*
* @param html
* @return html
*/
public static CharSequence encode(final String html) {
return encode(html, null);
}
/**
* Encode HTML
*
* @param html
* @param imageGetter
* @return html
*/
public static CharSequence encode(final String html,
final ImageGetter imageGetter) {
if (TextUtils.isEmpty(html))
return "";
return android.text.Html.fromHtml(html, imageGetter, TAG_HANDLER);
}
/**
* Format given HTML string so it is ready to be presented in a text view
*
* @param html
* @return formatted HTML
*/
public static final CharSequence format(final String html) {
if (html == null)
return "";
if (html.length() == 0)
return "";
StringBuilder formatted = new StringBuilder(html);
// Remove e-mail toggle link
strip(formatted, TOGGLE_START, TOGGLE_END);
// Remove signature
strip(formatted, SIGNATURE_START, SIGNATURE_END);
// Replace div with e-mail content with block quote
replace(formatted, REPLY_START, REPLY_END, BLOCKQUOTE_START,
BLOCKQUOTE_END);
// Remove hidden div
strip(formatted, HIDDEN_REPLY_START, HIDDEN_REPLY_END);
// Replace paragraphs with breaks
if (replace(formatted, PARAGRAPH_START, BREAK))
replace(formatted, PARAGRAPH_END, BREAK);
formatPres(formatted);
formatEmailFragments(formatted);
formatIncorrectStyles(formatted);
trim(formatted);
formatted.insert(0, ROOT_START);
formatted.append(ROOT_END);
return formatted;
}
private static void formatIncorrectStyles(final StringBuilder input) {
// em and strong tag styles are swapped on pre-4.0 so swap them back
// using alternate tags that don't exhibit the incorrect styling.
// http://code.google.com/p/android/issues/detail?id=3473
if (SDK_INT < ICE_CREAM_SANDWICH) {
replaceTag(input, "em", "i");
replaceTag(input, "strong", "b");
}
}
private static StringBuilder strip(final StringBuilder input,
final String prefix, final String suffix) {
int start = input.indexOf(prefix);
while (start != -1) {
int end = input.indexOf(suffix, start + prefix.length());
if (end == -1)
end = input.length();
input.delete(start, end + suffix.length());
start = input.indexOf(prefix, start);
}
return input;
}
private static boolean replace(final StringBuilder input,
final String from, final String to) {
int start = input.indexOf(from);
if (start == -1)
return false;
final int fromLength = from.length();
final int toLength = to.length();
while (start != -1) {
input.replace(start, start + fromLength, to);
start = input.indexOf(from, start + toLength);
}
return true;
}
private static void replaceTag(final StringBuilder input,
final String from, final String to) {
if (replace(input, '<' + from + '>', '<' + to + '>'))
replace(input, "</" + from + '>', "</" + to + '>');
}
private static StringBuilder replace(final StringBuilder input,
final String fromStart, final String fromEnd, final String toStart,
final String toEnd) {
int start = input.indexOf(fromStart);
if (start == -1)
return input;
final int fromStartLength = fromStart.length();
final int fromEndLength = fromEnd.length();
final int toStartLength = toStart.length();
while (start != -1) {
input.replace(start, start + fromStartLength, toStart);
int end = input.indexOf(fromEnd, start + toStartLength);
if (end != -1)
input.replace(end, end + fromEndLength, toEnd);
start = input.indexOf(fromStart);
}
return input;
}
private static StringBuilder formatPres(final StringBuilder input) {
int start = input.indexOf(PRE_START);
final int spaceAdvance = SPACE.length() - 1;
final int breakAdvance = BREAK.length() - 1;
while (start != -1) {
int end = input.indexOf(PRE_END, start + PRE_START.length());
if (end == -1)
break;
// Skip over code element
if (input.indexOf(CODE_START, start) == start)
start += CODE_START.length();
if (input.indexOf(CODE_END, start) == end - CODE_END.length())
end -= CODE_END.length();
for (int i = start; i < end; i++) {
switch (input.charAt(i)) {
case ' ':
input.deleteCharAt(i);
input.insert(i, SPACE);
start += spaceAdvance;
end += spaceAdvance;
break;
case '\t':
input.deleteCharAt(i);
input.insert(i, SPACE);
start += spaceAdvance;
end += spaceAdvance;
for (int j = 0; j < 3; j++) {
input.insert(i, SPACE);
start += spaceAdvance + 1;
end += spaceAdvance + 1;
}
break;
case '\n':
input.deleteCharAt(i);
// Ignore if last character is a newline
if (i + 1 < end) {
input.insert(i, BREAK);
start += breakAdvance;
end += breakAdvance;
}
break;
}
}
start = input.indexOf(PRE_START, end + PRE_END.length());
}
return input;
}
/**
* Remove email fragment 'div' tag and replace newlines with 'br' tags
*
* @param input
* @return input
*/
private static StringBuilder formatEmailFragments(final StringBuilder input) {
int emailStart = input.indexOf(EMAIL_START);
int breakAdvance = BREAK.length() - 1;
while (emailStart != -1) {
int startLength = EMAIL_START.length();
int emailEnd = input.indexOf(EMAIL_END, emailStart + startLength);
if (emailEnd == -1)
break;
input.delete(emailEnd, emailEnd + EMAIL_END.length());
input.delete(emailStart, emailStart + startLength);
int fullEmail = emailEnd - startLength;
for (int i = emailStart; i < fullEmail; i++)
if (input.charAt(i) == '\n') {
input.deleteCharAt(i);
input.insert(i, BREAK);
i += breakAdvance;
fullEmail += breakAdvance;
}
emailStart = input.indexOf(EMAIL_START, fullEmail);
}
return input;
}
/**
* Remove leading and trailing whitespace
*
* @param input
*/
private static StringBuilder trim(final StringBuilder input) {
int length = input.length();
int breakLength = BREAK.length();
while (length > 0) {
if (input.indexOf(BREAK) == 0)
input.delete(0, breakLength);
else if (length >= breakLength
&& input.lastIndexOf(BREAK) == length - breakLength)
input.delete(length - breakLength, length);
else if (Character.isWhitespace(input.charAt(0)))
input.deleteCharAt(0);
else if (Character.isWhitespace(input.charAt(length - 1)))
input.deleteCharAt(length - 1);
else
break;
length = input.length();
}
return input;
}
}