blob: 90ffc7fe0b0986647254f608b07eb37c80a1bf8d [file] [log] [blame]
package org.wordpress.android.editor;
import android.text.Editable;
import android.text.Spannable;
import android.text.TextWatcher;
import org.wordpress.android.util.AppLog;
import org.wordpress.android.util.AppLog.T;
public class HtmlStyleTextWatcher implements TextWatcher {
private enum Operation {
INSERT, DELETE, REPLACE, NONE
}
private int mOffset;
private CharSequence mModifiedText;
private Operation mLastOperation;
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
if (s == null) {
return;
}
int lastCharacterLocation = start + count - 1;
if (s.length() > lastCharacterLocation && lastCharacterLocation >= 0) {
if (after < count) {
if (after > 0) {
// Text was deleted and replaced by some other text
mLastOperation = Operation.REPLACE;
} else {
// Text was deleted only
mLastOperation = Operation.DELETE;
}
mOffset = start;
mModifiedText = s.subSequence(start + after, start + count);
}
}
}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
if (s == null) {
return;
}
int lastCharacterLocation = start + count - 1;
if (s.length() > lastCharacterLocation) {
if (count > 0) {
if (before > 0) {
// Text was added, replacing some existing text
mLastOperation = Operation.REPLACE;
mModifiedText = s.subSequence(start, start + count);
} else {
// Text was added only
mLastOperation = Operation.INSERT;
mOffset = start;
mModifiedText = s.subSequence(start + before, start + count);
}
}
}
}
@Override
public void afterTextChanged(Editable s) {
if (mModifiedText == null || s == null) {
return;
}
SpanRange spanRange;
// If the modified text included a tag or entity symbol ("<", ">", "&" or ";"), find its match and restyle
if (mModifiedText.toString().contains("<")) {
spanRange = getRespanRangeForChangedOpeningSymbol(s, "<");
} else if (mModifiedText.toString().contains(">")) {
spanRange = getRespanRangeForChangedClosingSymbol(s, ">");
} else if (mModifiedText.toString().contains("&")) {
spanRange = getRespanRangeForChangedOpeningSymbol(s, "&");
} else if (mModifiedText.toString().contains(";")) {
spanRange = getRespanRangeForChangedClosingSymbol(s, ";");
} else {
// If the modified text didn't include any tag or entity symbols, restyle if the modified text is inside
// a tag or entity
spanRange = getRespanRangeForNormalText(s, "<");
if (spanRange == null) {
spanRange = getRespanRangeForNormalText(s, "&");
}
}
if (spanRange != null) {
updateSpans(s, spanRange);
}
mModifiedText = null;
mLastOperation = Operation.NONE;
}
/**
* For changes made which contain at least one opening symbol (e.g. '<' or '&'), whether added or deleted, returns
* the range of text which should have its style reapplied.
* @param content the content after modification
* @param openingSymbol the opening symbol recognized (e.g. '<' or '&')
* @return the range of characters to re-apply spans to
*/
protected SpanRange getRespanRangeForChangedOpeningSymbol(Editable content, String openingSymbol) {
// For simplicity, re-parse the document if text was replaced
if (mLastOperation == Operation.REPLACE) {
return new SpanRange(0, content.length());
}
String closingSymbol = getMatchingSymbol(openingSymbol);
int firstOpeningTagLoc = mOffset + mModifiedText.toString().indexOf(openingSymbol);
int closingTagLoc;
if (mLastOperation == Operation.INSERT) {
// Apply span from the first added opening symbol until the closing symbol in the content matching the
// last added opening symbol
// e.g. pasting "<b><" before "/b>" - we want the span to be applied to all of "<b></b>"
int lastOpeningTagLoc = mOffset + mModifiedText.toString().lastIndexOf(openingSymbol);
closingTagLoc = content.toString().indexOf(closingSymbol, lastOpeningTagLoc);
} else {
// Apply span until the first closing tag that appears after the deleted text
closingTagLoc = content.toString().indexOf(closingSymbol, mOffset);
}
if (closingTagLoc > 0) {
return new SpanRange(firstOpeningTagLoc, closingTagLoc + 1);
}
return null;
}
/**
* For changes made which contain at least one closing symbol (e.g. '>' or ';') and no opening symbols, whether
* added or deleted, returns the range of text which should have its style reapplied.
* @param content the content after modification
* @param closingSymbol the closing symbol recognized (e.g. '>' or ';')
* @return the range of characters to re-apply spans to
*/
protected SpanRange getRespanRangeForChangedClosingSymbol(Editable content, String closingSymbol) {
// For simplicity, re-parse the document if text was replaced
if (mLastOperation == Operation.REPLACE) {
return new SpanRange(0, content.length());
}
String openingSymbol = getMatchingSymbol(closingSymbol);
int firstClosingTagInModLoc = mOffset + mModifiedText.toString().indexOf(closingSymbol);
int firstClosingTagAfterModLoc = content.toString().indexOf(closingSymbol, mOffset + mModifiedText.length());
int openingTagLoc = content.toString().lastIndexOf(openingSymbol, firstClosingTagInModLoc - 1);
if (openingTagLoc >= 0) {
if (firstClosingTagAfterModLoc >= 0) {
return new SpanRange(openingTagLoc, firstClosingTagAfterModLoc + 1);
} else {
return new SpanRange(openingTagLoc, content.length());
}
}
return null;
}
/**
* For changes made which contain no opening or closing symbols, checks whether the changed text is inside a tag,
* and if so returns the range of text which should have its style reapplied.
* @param content the content after modification
* @param openingSymbol the opening symbol of the tag to check for (e.g. '<' or '&')
* @return the range of characters to re-apply spans to
*/
protected SpanRange getRespanRangeForNormalText(Editable content, String openingSymbol) {
String closingSymbol = getMatchingSymbol(openingSymbol);
int openingTagLoc = content.toString().lastIndexOf(openingSymbol, mOffset);
if (openingTagLoc >= 0) {
int closingTagLoc = content.toString().indexOf(closingSymbol, openingTagLoc);
if (closingTagLoc >= mOffset) {
return new SpanRange(openingTagLoc, closingTagLoc + 1);
}
}
return null;
}
/**
* Clears and re-applies spans to {@code content} within range {@code spanRange} according to rules in
* {@link HtmlStyleUtils}.
* @param content the content to re-style
* @param spanRange the range within {@code content} to be re-styled
*/
protected void updateSpans(Spannable content, SpanRange spanRange) {
int spanStart = spanRange.getOpeningTagLoc();
int spanEnd = spanRange.getClosingTagLoc();
if (spanStart > content.length() || spanEnd > content.length()) {
AppLog.d(T.EDITOR, "The specified span range was beyond the Spannable's length");
return;
} else if (spanStart >= spanEnd) {
// If the span start is after the end position (probably due to a multi-line deletion), selective
// re-styling won't work
// Instead, do a clean re-styling of the whole document
spanStart = 0;
spanEnd = content.length();
}
HtmlStyleUtils.clearSpans(content, spanStart, spanEnd);
HtmlStyleUtils.styleHtmlForDisplay(content, spanStart, spanEnd);
}
/**
* Returns the closing/opening symbol corresponding to the given opening/closing symbol.
*/
private String getMatchingSymbol(String symbol) {
switch(symbol) {
case "<":
return ">";
case ">":
return "<";
case "&":
return ";";
case ";":
return "&";
default:
return "";
}
}
/**
* Stores a pair of integers describing a range of values.
*/
protected static class SpanRange {
private final int mOpeningTagLoc;
private final int mClosingTagLoc;
public SpanRange(int openingTagLoc, int closingTagLoc) {
mOpeningTagLoc = openingTagLoc;
mClosingTagLoc = closingTagLoc;
}
public int getOpeningTagLoc() {
return mOpeningTagLoc;
}
public int getClosingTagLoc() {
return mClosingTagLoc;
}
}
}