package com.door43.translationstudio.rendering;
import android.text.Editable;
import android.text.Html;
import android.text.Layout;
import android.text.Spannable;
import android.text.style.AlignmentSpan;
import android.text.style.BulletSpan;
import android.text.style.LeadingMarginSpan;
import android.text.style.TypefaceSpan;
import android.util.Log;
import com.door43.translationstudio.spannables.LinkSpan;
import com.door43.translationstudio.spannables.Span;
import org.eclipse.jgit.diff.Edit;
import org.xml.sax.XMLReader;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Vector;
/**
* Some parts of this code are based on android.text.Html
*/
public class HtmlTagHandler implements Html.TagHandler {
public static final String TAG = "HtmlTagHandler";
private final Span.OnClickListener clickListener;
private int mListItemCount = 0;
private static final boolean DEBUG = true;
private Vector<String> mListParents = new Vector<>();
final HashMap<String, String> attributes = new HashMap<>();
public HtmlTagHandler(Span.OnClickListener clickListener) {
this.clickListener = clickListener;
}
private static class Code {
}
private static class Center {
}
private static class AppLink {
}
/**
* http://stackoverflow.com/questions/6952243/how-to-get-an-attribute-from-an-xmlreader
* @param xmlReader
*/
private void processAttributes(final XMLReader xmlReader) {
try {
Field elementField = xmlReader.getClass().getDeclaredField("theNewElement");
elementField.setAccessible(true);
Object element = elementField.get(xmlReader);
Field attsField = element.getClass().getDeclaredField("theAtts");
attsField.setAccessible(true);
Object atts = attsField.get(element);
Field dataField = atts.getClass().getDeclaredField("data");
dataField.setAccessible(true);
String[] data = (String[])dataField.get(atts);
Field lengthField = atts.getClass().getDeclaredField("length");
lengthField.setAccessible(true);
int len = (Integer)lengthField.get(atts);
/**
* MSH: Look for supported attributes and add to hash map.
* This is as tight as things can get :)
* The data index is "just" where the keys and values are stored.
*/
for(int i = 0; i < len; i++)
attributes.put(data[i * 5 + 1], data[i * 5 + 4]);
}
catch (Exception e) {
Log.d(TAG, "Exception: " + e);
}
}
@Override
public void handleTag(final boolean opening, final String tag, Editable output, final XMLReader xmlReader) {
processAttributes(xmlReader);
if (opening) {
// opening tag
if (DEBUG) {
Log.d(TAG, "opening, output: " + output.toString());
}
if (tag.equalsIgnoreCase("ul") || tag.equalsIgnoreCase("ol") || tag.equalsIgnoreCase("dd")) {
mListParents.add(tag);
mListItemCount = 0;
} else if (tag.equalsIgnoreCase("code")) {
start(output, new Code());
} else if (tag.equalsIgnoreCase("center")) {
start(output, new Center());
} else if (tag.equalsIgnoreCase("app-link")) {
start(output, new AppLink());
}
} else {
// closing tag
if (DEBUG) {
Log.d(TAG, "closing, output: " + output.toString());
}
if (tag.equalsIgnoreCase("ul") || tag.equalsIgnoreCase("ol") || tag.equalsIgnoreCase("dd")) {
mListParents.remove(tag);
mListItemCount = 0;
} else if (tag.equalsIgnoreCase("li")) {
handleListTag(output);
} else if (tag.equalsIgnoreCase("code")) {
end(output, Code.class, new TypefaceSpan("monospace"), false);
} else if (tag.equalsIgnoreCase("center")) {
end(output, Center.class, new AlignmentSpan.Standard(Layout.Alignment.ALIGN_CENTER), true);
} else if (tag.equalsIgnoreCase("app-link")) {
handleAppLinkTag(output);
}
}
}
/**
* Mark the opening tag by using private classes
*
* @param output
* @param mark
*/
private void start(Editable output, Object mark) {
int len = output.length();
output.setSpan(mark, len, len, Spannable.SPAN_MARK_MARK);
if (DEBUG) {
Log.d(TAG, "len: " + len);
}
}
private void end(Editable output, Class kind, Object repl, boolean paragraphStyle) {
Object obj = getLast(output, kind);
// start of the tag
int where = output.getSpanStart(obj);
// end of the tag
int len = output.length();
output.removeSpan(obj);
if (where != len) {
// paragraph styles like AlignmentSpan need to end with a new line!
if (paragraphStyle) {
output.append("\n");
len++;
}
output.setSpan(repl, where, len, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
}
if (DEBUG) {
Log.d(TAG, "where: " + where);
Log.d(TAG, "len: " + len);
}
}
/**
* Get last marked position of a specific tag kind (private class)
*
* @param text
* @param kind
* @return
*/
private Object getLast(Editable text, Class kind) {
Object[] objs = text.getSpans(0, text.length(), kind);
if (objs.length == 0) {
return null;
} else {
for (int i = objs.length; i > 0; i--) {
if (text.getSpanFlags(objs[i - 1]) == Spannable.SPAN_MARK_MARK) {
return objs[i - 1];
}
}
return null;
}
}
private void handleAppLinkTag(Editable output) {
Object obj = getLast(output, AppLink.class);
// start of the tag
int where = output.getSpanStart(obj);
// end of the tag
int len = output.length();
output.removeSpan(obj);
CharSequence title = output.subSequence(where, len);
LinkSpan span = new LinkSpan(title.toString(), attributes.get("href"), attributes.get("type"));
span.setOnClickListener(this.clickListener);
if(where != len) {
output.replace(where, len, span.toCharSequence());
}
if (DEBUG) {
Log.d(TAG, "where: " + where);
Log.d(TAG, "len: " + len);
}
}
private void handleListTag(Editable output) {
if (mListParents.lastElement().equals("ul")) {
output.append("\n");
String[] split = output.toString().split("\n");
int lastIndex = split.length - 1;
int start = output.length() - split[lastIndex].length() - 1;
output.setSpan(new BulletSpan(15 * mListParents.size()), start, output.length(), 0);
} else if (mListParents.lastElement().equals("ol")) {
mListItemCount++;
output.append("\n");
String[] split = output.toString().split("\n");
int lastIndex = split.length - 1;
int start = output.length() - split[lastIndex].length() - 1;
output.insert(start, mListItemCount + ". ");
output.setSpan(new LeadingMarginSpan.Standard(15 * mListParents.size()), start, output.length(), 0);
}
}
}