package org.wikipedia.edit.richtext;
import android.content.Context;
import android.os.Handler;
import android.os.Looper;
import android.support.annotation.Nullable;
import android.support.annotation.VisibleForTesting;
import android.text.Editable;
import android.text.Spanned;
import android.text.TextWatcher;
import android.text.format.DateUtils;
import android.widget.EditText;
import org.wikipedia.concurrency.SaneAsyncTask;
import org.wikipedia.util.log.L;
import java.util.ArrayList;
import java.util.List;
import java.util.Stack;
public class SyntaxHighlighter {
@VisibleForTesting
interface OnSyntaxHighlightListener {
void syntaxHighlightResults(List<SpanExtents> spanExtents);
}
private Context context;
private EditText textBox;
private List<SyntaxRule> syntaxRules;
private Handler handler;
@Nullable private OnSyntaxHighlightListener syntaxHighlightListener;
private Runnable syntaxHighlightCallback = new Runnable() {
private SyntaxHighlightTask currentTask;
@Override public void run() {
if (context != null) {
if (currentTask != null) {
currentTask.cancel();
}
currentTask = new SyntaxHighlightTask(textBox.getText());
currentTask.execute();
}
}
};
public SyntaxHighlighter(Context context, EditText textBox) {
this(context, textBox, null);
}
public SyntaxHighlighter(Context parentContext,
EditText textBox,
@Nullable OnSyntaxHighlightListener listener) {
this.context = parentContext;
this.textBox = textBox;
this.syntaxHighlightListener = listener;
syntaxRules = new ArrayList<>();
// create our list of syntax rules for Wikipedia markup:
syntaxRules.add(new SyntaxRule("{{", "}}", SyntaxRuleStyle.TEMPLATE));
syntaxRules.add(new SyntaxRule("[[", "]]", SyntaxRuleStyle.INTERNAL_LINK));
syntaxRules.add(new SyntaxRule("[", "]", SyntaxRuleStyle.EXTERNAL_LINK));
syntaxRules.add(new SyntaxRule("<", ">", SyntaxRuleStyle.REF));
syntaxRules.add(new SyntaxRule("'''''", "'''''", SyntaxRuleStyle.BOLD_ITALIC));
syntaxRules.add(new SyntaxRule("'''", "'''", SyntaxRuleStyle.BOLD));
syntaxRules.add(new SyntaxRule("''", "''", SyntaxRuleStyle.ITALIC));
// TODO: reevaluate colors/styles for other syntax elements.
/*
// section level 4:
syntaxRules.add(new SyntaxRule("====", "====", new SyntaxRule.SyntaxRuleStyle() {
@Override
public SpanExtents createSpan(int spanStart, SyntaxRule syntaxItem) {
return new ColorSpanEx(parentActivity.getResources().getColor(R.color.syntax_highlight_sectiontext),
parentActivity.getResources().getColor(R.color.syntax_highlight_sectionbgd),
spanStart, syntaxItem);
}
}));
// section level 3:
syntaxRules.add(new SyntaxRule("===", "===", new SyntaxRule.SyntaxRuleStyle() {
@Override
public SpanExtents createSpan(int spanStart, SyntaxRule syntaxItem) {
return new ColorSpanEx(parentActivity.getResources().getColor(R.color.syntax_highlight_sectiontext),
parentActivity.getResources().getColor(R.color.syntax_highlight_sectionbgd),
spanStart, syntaxItem);
}
}));
// section level 2:
syntaxRules.add(new SyntaxRule("==", "==", new SyntaxRule.SyntaxRuleStyle() {
@Override
public SpanExtents createSpan(int spanStart, SyntaxRule syntaxItem) {
return new ColorSpanEx(parentActivity.getResources().getColor(R.color.syntax_highlight_sectiontext),
parentActivity.getResources().getColor(R.color.syntax_highlight_sectionbgd),
spanStart, syntaxItem);
}
}));
*/
handler = new Handler(Looper.getMainLooper());
// add a text-change listener that will trigger syntax highlighting
// whenever text is modified.
textBox.addTextChangedListener(new TextWatcher() {
@Override
public void beforeTextChanged(CharSequence charSequence, int i, int i2, int i3) {
}
@Override
public void onTextChanged(CharSequence charSequence, int i, int i2, int i3) {
}
@Override
public void afterTextChanged(final Editable editable) {
// queue up syntax highlighting.
// if the user adds more text within 1/2 second, the previous request
// is cancelled, and a new one is placed.
handler.removeCallbacks(syntaxHighlightCallback);
handler.postDelayed(syntaxHighlightCallback, DateUtils.SECOND_IN_MILLIS / 2);
}
});
}
public void cleanup() {
if (context != null) {
handler.removeCallbacks(syntaxHighlightCallback);
textBox.getText().clearSpans();
textBox = null;
context = null;
}
}
private class SyntaxHighlightTask extends SaneAsyncTask<List<SpanExtents>> {
SyntaxHighlightTask(Editable text) {
this.text = text;
}
private Editable text;
@Override
public List<SpanExtents> performTask() throws Throwable {
Stack<SpanExtents> spanStack = new Stack<>();
List<SpanExtents> spansToSet = new ArrayList<>();
/*
The (naïve) algorithm:
Iterate through the text string, and maintain a stack of matched syntax rules.
When the "start" and "end" symbol of a rule are matched in sequence, create a new
Span to be added to the EditText at the corresponding location.
*/
for (int i = 0; i < text.length();) {
SpanExtents newSpanInfo;
boolean incrementDone = false;
for (SyntaxRule syntaxItem : syntaxRules) {
if (i + syntaxItem.getStartSymbol().length() > text.length()) {
continue;
}
if (syntaxItem.isStartEndSame()) {
boolean pass = true;
for (int j = 0; j < syntaxItem.getStartSymbol().length(); j++) {
if (text.charAt(i + j) != syntaxItem.getStartSymbol().charAt(j)) {
pass = false;
break;
}
}
if (pass) {
if (spanStack.size() > 0 && spanStack.peek().getSyntaxRule().equals(syntaxItem)) {
newSpanInfo = spanStack.pop();
newSpanInfo.setEnd(i + syntaxItem.getStartSymbol().length());
spansToSet.add(newSpanInfo);
} else {
SpanExtents sp = syntaxItem.getSpanStyle().createSpan(context, i, syntaxItem);
spanStack.push(sp);
}
i += syntaxItem.getStartSymbol().length();
incrementDone = true;
}
} else {
boolean pass = true;
for (int j = 0; j < syntaxItem.getStartSymbol().length(); j++) {
if (text.charAt(i + j) != syntaxItem.getStartSymbol().charAt(j)) {
pass = false;
break;
}
}
if (pass) {
SpanExtents sp = syntaxItem.getSpanStyle().createSpan(context, i, syntaxItem);
spanStack.push(sp);
i += syntaxItem.getStartSymbol().length();
incrementDone = true;
}
//skip the check of end symbol when start symbol is found at end of the text
if (i + syntaxItem.getStartSymbol().length() > text.length()) {
continue;
}
pass = true;
for (int j = 0; j < syntaxItem.getEndSymbol().length(); j++) {
if (text.charAt(i + j) != syntaxItem.getEndSymbol().charAt(j)) {
pass = false;
break;
}
}
if (pass) {
if (spanStack.size() > 0 && spanStack.peek().getSyntaxRule().equals(syntaxItem)) {
newSpanInfo = spanStack.pop();
newSpanInfo.setEnd(i + syntaxItem.getEndSymbol().length());
spansToSet.add(newSpanInfo);
}
i += syntaxItem.getEndSymbol().length();
incrementDone = true;
}
}
}
if (!incrementDone) {
i++;
}
if (isCancelled()) {
break;
}
}
return spansToSet;
}
@Override
public void onFinish(List<SpanExtents> result) {
if (context == null) {
return;
}
if (syntaxHighlightListener != null) {
syntaxHighlightListener.syntaxHighlightResults(result);
}
// TODO: probably possible to make this more efficient...
// Right now, on longer articles, this is quite heavy on the UI thread.
// remove any of our custom spans from the previous cycle...
long time = System.currentTimeMillis();
Object[] prevSpans = textBox.getText().getSpans(0, text.length(), SpanExtents.class);
for (Object sp : prevSpans) {
textBox.getText().removeSpan(sp);
}
// and add our new spans
for (SpanExtents spanEx : result) {
textBox.getText().setSpan(spanEx, spanEx.getStart(), spanEx.getEnd(), Spanned.SPAN_INCLUSIVE_INCLUSIVE);
}
time = System.currentTimeMillis() - time;
L.v("That took " + time + "ms");
}
}
}