package be.digitalia.fosdem.utils;
import android.content.res.Resources;
import android.support.annotation.NonNull;
import android.support.v4.util.CircularIntArray;
import android.text.Editable;
import android.text.Html;
import android.text.Spanned;
import android.text.TextUtils;
import android.text.style.BulletSpan;
import android.text.style.LeadingMarginSpan;
import org.xml.sax.XMLReader;
import java.util.Iterator;
import java.util.Locale;
/**
* Various methods to transform strings
*
* @author Christophe Beyls
*/
public class StringUtils {
/**
* Mirror of the unicode table from 00c0 to 017f without diacritics.
*/
private static final String tab00c0 = "AAAAAAACEEEEIIII" + "DNOOOOO\u00d7\u00d8UUUUYI\u00df" + "aaaaaaaceeeeiiii" + "\u00f0nooooo\u00f7\u00f8uuuuy\u00fey"
+ "AaAaAaCcCcCcCcDd" + "DdEeEeEeEeEeGgGg" + "GgGgHhHhIiIiIiIi" + "IiJjJjKkkLlLlLlL" + "lLlNnNnNnnNnOoOo" + "OoOoRrRrRrSsSsSs" + "SsTtTtTtUuUuUuUu"
+ "UuUuWwYyYZzZzZzF";
private static final String ROOM_DRAWABLE_PREFIX = "room_";
/**
* Returns string without diacritics - 7 bit approximation.
*
* @param source string to convert
* @return corresponding string without diacritics
*/
public static String removeDiacritics(@NonNull String source) {
final int length = source.length();
char[] result = new char[length];
char c;
for (int i = 0; i < length; i++) {
c = source.charAt(i);
if (c >= '\u00c0' && c <= '\u017f') {
c = tab00c0.charAt((int) c - '\u00c0');
}
result[i] = c;
}
return new String(result);
}
public static String remove(String str, final char remove) {
if (TextUtils.isEmpty(str) || str.indexOf(remove) == -1) {
return str;
}
final char[] chars = str.toCharArray();
int pos = 0;
for (int i = 0; i < chars.length; i++) {
if (chars[i] != remove) {
chars[pos++] = chars[i];
}
}
return new String(chars, 0, pos);
}
/**
* Replaces all groups of non-alphanumeric chars in source with a single replacement char.
*/
private static String replaceNonAlphaGroups(String source, char replacement) {
final int length = source.length();
char[] result = new char[length];
char c;
boolean replaced = false;
int size = 0;
for (int i = 0; i < length; i++) {
c = source.charAt(i);
if (isLetterOrDigitOrUnderscore(c)) {
result[size++] = c;
replaced = false;
} else {
// Skip quote
if ((c != '’') && !replaced) {
result[size++] = replacement;
replaced = true;
}
}
}
return new String(result, 0, size);
}
/**
* Removes all non-alphanumeric chars at the beginning and end of source.
*/
private static String trimNonAlpha(String source) {
int st = 0;
int len = source.length();
while ((st < len) && !isLetterOrDigitOrUnderscore(source.charAt(st))) {
st++;
}
while ((st < len) && !isLetterOrDigitOrUnderscore(source.charAt(len - 1))) {
len--;
}
return ((st > 0) || (len < source.length())) ? source.substring(st, len) : source;
}
private static boolean isLetterOrDigitOrUnderscore(char c) {
return Character.isLetterOrDigit(c) || c == '_';
}
/**
* Transforms a name to a slug identifier to be used in a FOSDEM URL.
*/
public static String toSlug(@NonNull String source) {
source = remove(source, '.');
source = removeDiacritics(source);
source = source.replace("ß", "ss");
source = trimNonAlpha(source);
source = replaceNonAlphaGroups(source, '_');
source = source.toLowerCase(Locale.US);
return source;
}
@SuppressWarnings("deprecation")
public static String stripHtml(@NonNull String html) {
return trimEnd(Html.fromHtml(html)).toString();
}
@SuppressWarnings("deprecation")
public static CharSequence parseHtml(@NonNull String html, Resources res) {
return trimEnd(Html.fromHtml(html, null, new ListsTagHandler(res)));
}
public static CharSequence trimEnd(@NonNull CharSequence source) {
int pos = source.length() - 1;
while ((pos >= 0) && Character.isWhitespace(source.charAt(pos))) {
pos--;
}
pos++;
return (pos < source.length()) ? source.subSequence(0, pos) : source;
}
/**
* Converts a room name to a local drawable resource name, by stripping non-alpha chars and converting to lower case. Any letter following a digit will be
* ignored, along with the rest of the string.
*/
public static String roomNameToResourceName(@NonNull String roomName) {
StringBuilder builder = new StringBuilder(ROOM_DRAWABLE_PREFIX.length() + roomName.length());
builder.append(ROOM_DRAWABLE_PREFIX);
int size = roomName.length();
boolean lastDigit = false;
for (int i = 0; i < size; ++i) {
char c = roomName.charAt(i);
if (Character.isLetter(c)) {
if (lastDigit) {
break;
}
builder.append(Character.toLowerCase(c));
} else if (Character.isDigit(c)) {
builder.append(c);
lastDigit = true;
}
}
return builder.toString();
}
static class ListsTagHandler implements Html.TagHandler {
private static final float LEADING_MARGIN_DIPS = 2f;
private static final float BULLET_GAP_WIDTH_DIPS = 8f;
private final CircularIntArray liStarts = new CircularIntArray(4);
private final int leadingMargin;
private final int bulletGapWidth;
public ListsTagHandler(Resources res) {
final float density = res.getDisplayMetrics().density;
leadingMargin = (int) (density * LEADING_MARGIN_DIPS + 0.5f);
bulletGapWidth = (int) (density * BULLET_GAP_WIDTH_DIPS + 0.5f);
}
/**
* @return final output length
*/
private static int ensureParagraphBoundary(Editable output) {
int length = output.length();
if ((length != 0) && output.charAt(length - 1) != '\n') {
output.insert(length, "\n");
length++;
}
return length;
}
private static void trimStart(Editable output, final int start) {
int end = start;
final int length = output.length();
while ((end < length) && Character.isWhitespace(output.charAt(end))) {
end++;
}
if (start < end) {
output.delete(start, end);
}
}
@Override
public void handleTag(boolean opening, String tag, Editable output, XMLReader xmlReader) {
switch (tag) {
case "pre":
case "PRE":
ensureParagraphBoundary(output);
break;
// Unfortunately the following code will be ignored in API 24+ and the native rendering is inferior
case "li":
case "LI":
if (opening) {
liStarts.addLast(ensureParagraphBoundary(output));
} else if (!liStarts.isEmpty()) {
int start = liStarts.popLast();
trimStart(output, start);
int end = ensureParagraphBoundary(output);
// Add leading margin to ensure the bullet is not cut off
output.setSpan(new LeadingMarginSpan.Standard(leadingMargin), start, end, Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
output.setSpan(new BulletSpan(bulletGapWidth), start, end, Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
}
break;
}
}
}
/**
* A version of Android's SimpleStringSplitter using a String as delimiter.
*/
public static class SimpleStringSplitter implements TextUtils.StringSplitter, Iterator<String> {
private final String mDelimiter;
private String mString;
private int mPosition;
private int mLength;
/**
* Initializes the splitter. setString may be called later.
*
* @param delimiter the delimiter on which to split
*/
public SimpleStringSplitter(String delimiter) {
mDelimiter = delimiter;
}
/**
* Sets the string to split
*
* @param string the string to split
*/
public void setString(String string) {
mString = string;
mPosition = 0;
mLength = mString.length();
}
public Iterator<String> iterator() {
return this;
}
public boolean hasNext() {
return mPosition < mLength;
}
public String next() {
int end = mString.indexOf(mDelimiter, mPosition);
if (end == -1) {
end = mLength;
}
String nextString = mString.substring(mPosition, end);
mPosition = end + mDelimiter.length(); // Skip the delimiter.
return nextString;
}
public void remove() {
throw new UnsupportedOperationException();
}
}
}