/*
* This file is part of NucleusFramework for Bukkit, licensed under the MIT License (MIT).
*
* Copyright (c) JCThePants (www.jcwhatever.com)
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package com.jcwhatever.nucleus.utils.text;
import com.jcwhatever.nucleus.collections.wrap.IteratorWrapper;
import com.jcwhatever.nucleus.utils.ArrayUtils;
import com.jcwhatever.nucleus.utils.CollectionUtils;
import com.jcwhatever.nucleus.utils.PreCon;
import com.jcwhatever.nucleus.utils.ThreadSingletons;
import com.jcwhatever.nucleus.utils.text.format.TextFormatter;
import javax.annotation.Nullable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
/**
* Represents Text formatting codes and provides related utilities.
*/
public class TextFormat {
private static Map<String, TextFormat> _nameMap;
private static Map<Character, TextFormat> _characterMap;
private static String _formatChars = "";
public static final char CHAR = '\u00A7';
public static final TextFormat BOLD = new TextFormat('l', "BOLD", "bold");
public static final TextFormat ITALIC = new TextFormat('o', "ITALIC", "italic");
public static final TextFormat MAGIC = new TextFormat('k', "MAGIC", "obfuscated");
public static final TextFormat STRIKETHROUGH = new TextFormat('m',"STRIKETHROUGH", "strikethrough");
public static final TextFormat UNDERLINE = new TextFormat('n', "UNDERLINE", "underlined");
public static final TextFormat RESET = new TextFormat('r', "RESET", "reset");
private static final ThreadSingletons<StringBuilder> SMALL_BUFFERS = new ThreadSingletons<>(
new ThreadSingletons.ISingletonFactory<StringBuilder>() {
@Override
public StringBuilder create(Thread thread) {
return new StringBuilder(6);
}
});
private static final ThreadSingletons<StringBuilder> LARGE_BUFFERS = new ThreadSingletons<>(
new ThreadSingletons.ISingletonFactory<StringBuilder>() {
@Override
public StringBuilder create(Thread thread) {
return new StringBuilder(0);
}
});
private final char _formatChar;
private final String _formatCode;
private final String _tagName;
private final String _minecraftName;
TextFormat (char formatChar, String tagName, String minecraftName) {
_formatChar = formatChar;
_formatCode = String.valueOf(CHAR) + formatChar;
_tagName = tagName;
_minecraftName = minecraftName;
if (_nameMap == null)
_nameMap = new HashMap<>(25);
_nameMap.put(tagName, this);
}
/**
* Determine if the {@link TextFormat} has a
* format code.
*
* <p>This exists in case text formats are added by Minecraft
* that do not have format codes. (i.e 16-bit color)</p>
*/
public boolean hasFormatCode() {
return true;
}
/**
* Determine if the {@link TextFormat} has a single character format
* code.
*
* <p>This exists in case text formats are added by Minecraft
* that do not have format codes. (i.e. 16-bit color)</p>
*/
public boolean hasFormatChar() {
return true;
}
/**
* Get the {@link TextFormat} format code. If there is no
* format code, an empty string is returned.
*/
public String getFormatCode() {
return _formatCode;
}
/**
* Get the {@link TextFormat} format character. If there is no
* format character, 0 is returned.
*/
public char getFormatChar() {
return _formatChar;
}
/**
* Get the formats tag name which is used by
* {@link TextFormatter} to identify formats inserted into
* a string to be formatted.
*/
public String getTagName() {
return _tagName;
}
/**
* Get the formats Minecraft name. This is the name of the
* formats property or value used in Minecraft JSON strings.
*/
public String getMinecraftName() {
return _minecraftName;
}
@Override
public String toString() {
return _formatCode;
}
/**
* Get the total number of available format codes.
*/
public static int totalCodes() {
return 22;
}
/**
* Get a {@link TextFormat} from a case tag name.
*
* @param name The tag name.
*
* @return Null if the name is invalid.
*/
@Nullable
public static TextFormat fromName(String name) {
PreCon.notNull(name);
assert _nameMap != null;
return _nameMap.get(name.toUpperCase());
}
/**
* Remove extra format codes.
*
* <p>Removes format codes at end of string as well as redundant codes.</p>
*
* @param charArray The text to trim.
*/
public static String trim(char[] charArray) {
return trim(new CharArraySequence(charArray));
}
/**
* Remove extra format codes.
*
* <p>Removes format codes at end of string as well as redundant codes.</p>
*
* @param charSequence The text to trim.
*/
public static String trim(CharSequence charSequence) {
PreCon.notNull(charSequence);
int len = charSequence.length();
if (len == 0)
return "";
StringBuilder buffer = getLargeBuffer(len);
Map<Character, TextFormat> characterMap = loadCharacterMap();
int trimLen = 0;
for (int p = 0; p < 2; p++) { // 2 passes
int candidateTrimLen = 0;
char prevChar = 0;
boolean isPrevColor = false;
boolean isModified = false;
for (int i = 0; i < len; i++) {
char current = charSequence.charAt(i);
if (current == CHAR && i < len - 1) {
char next = charSequence.charAt(i + 1);
TextFormat format = characterMap.get(next);
if (format != null) {
isModified = true;
i++;
if (prevChar == next) {
continue;
}
if (format instanceof TextColor && isPrevColor) {
buffer.setLength(buffer.length() - 2);
buffer.append(CHAR);
buffer.append(next);
} else {
buffer.append(CHAR);
buffer.append(next);
candidateTrimLen += 2;
}
prevChar = next;
isPrevColor = format instanceof TextColor;
continue;
}
}
trimLen++;
trimLen += candidateTrimLen;
candidateTrimLen = 0;
buffer.append(current);
isPrevColor = false;
}
if (isModified && p < 1) {
charSequence = trimLen == len ? buffer.toString() : buffer.substring(0, trimLen);
buffer.setLength(0);
len = charSequence.length();
trimLen = 0;
}
else {
// no modifications or final pass already run
break;
}
}
return trimLen == len ? buffer.toString() : buffer.substring(0, trimLen);
}
/**
* Remove format codes from a string.
*
* @param charArray The text to remove format codes from.
*/
public static String remove(char[] charArray) {
PreCon.notNull(charArray);
return remove(new CharArraySequence(charArray));
}
/**
* Remove format codes from a {@link java.lang.CharSequence}.
*
* @param charSequence The text to remove format codes from.
*/
public static String remove(CharSequence charSequence) {
PreCon.notNull(charSequence);
int len = charSequence.length();
StringBuilder sb = getLargeBuffer(len);
loadCharacterMap();
for (int i = 0, last = len - 1; i < len; i++) {
char ch = charSequence.charAt(i);
if (ch == CHAR && i != last) {
char next = charSequence.charAt(i + 1);
if (_formatChars.indexOf(next) != -1) {
i += 1;
continue;
}
}
sb.append(ch);
}
return sb.toString();
}
/**
* Removes formats from the supplied {@link char[]} and
* stores them in a {@link TextFormatMap} keyed to the index location
* of the format in the resulting format-less string.
*
* <p>The resulting format-less string can be retrieved from the
* {@link TextFormatMap} by invoking the {@link TextFormatMap#getText} method.</p>
*
* @param charArray The {@link java.lang.CharSequence}.
*/
public static TextFormatMap separate(char[] charArray) {
PreCon.notNull(charArray);
return separate(new CharArraySequence(charArray));
}
/**
* Removes formats from the supplied {@link java.lang.CharSequence} and
* stores them in a {@link TextFormatMap} keyed to the index location
* of the format in the resulting format-less string.
*
* <p>The resulting format-less string can be retrieved from the
* {@link TextFormatMap} by invoking the {@link TextFormatMap#getText} method.</p>
*
* @param charSequence The {@link CharSequence}.
*/
public static TextFormatMap separate(CharSequence charSequence) {
PreCon.notNull(charSequence);
int len = charSequence.length();
StringBuilder sb = getLargeBuffer(len);
StringBuilder formatBuffer = getSmallBuffer();
TextFormatMap formatMap = new TextFormatMap();
loadCharacterMap();
int virtualIndex = 0;
for (int i = 0, last = len - 1; i < len; i++) {
char ch = 0;
while (i < last) {
char next;
if ((ch = charSequence.charAt(i)) == CHAR
&& _formatChars.indexOf(next = charSequence.charAt(i + 1)) != -1) {
formatBuffer.append(CHAR);
formatBuffer.append(next);
i = Math.min(i + 2, last);
if (i == last)
ch = 0;
}
else {
break;
}
}
if (formatBuffer.length() != 0) {
formatMap.put(virtualIndex, formatBuffer.toString());
formatBuffer.setLength(0);
}
virtualIndex++;
if (ch != 0)
sb.append(ch);
}
formatMap.setText(sb.toString());
return formatMap;
}
/**
* Determine if a character is a valid formatting
* character.
*
* @param ch The character to check.
*/
public static boolean isFormatChar(char ch) {
loadCharacterMap();
return _formatChars.indexOf(ch) != -1;
}
/**
* Get the {@link TextFormat} that represents the
* format code character.
*
* @param ch The format character to check.
*
* @return The {@link TextFormat} or null if the character is not
* a recognized format code character.
*/
@Nullable
public static TextFormat fromFormatChar(char ch) {
return loadCharacterMap().get(ch);
}
/**
* Get the formats in effect at the
* beginning of a string.
*
* @param charArray The text to get the end format from.
*
* @return The format codes.
*/
public static TextFormats getFormatAt(int index, char[] charArray) {
PreCon.notNull(charArray);
return getEndFormat(new CharArraySequence(charArray), index);
}
/**
* Get the formats in effect at the
* beginning of a {@link java.lang.CharSequence}.
*
* @param charSequence The text to get the end format from.
*
* @return The format codes.
*/
public static TextFormats getFormatAt(int index, CharSequence charSequence) {
PreCon.positiveNumber(index, "index");
PreCon.lessThan(index, charSequence.length(), "index");
PreCon.notNull(charSequence);
return getEndFormat(charSequence, index);
}
/**
* Get the formats in effect at the end of a {@link char[]}.
*
* @param charArray The {@link char[]} to get the end format from.
*
* @return The format codes.
*/
public static TextFormats getEndFormat(final char[] charArray) {
PreCon.notNull(charArray);
return getEndFormat(new CharArraySequence(charArray), charArray.length);
}
/**
* Get the formats in effect at the end of a {@link CharSequence}.
*
* @param charSequence The {@link CharSequence} to get the end format from.
*
* @return The format codes.
*/
public static TextFormats getEndFormat(CharSequence charSequence) {
PreCon.notNull(charSequence);
return getEndFormat(charSequence, charSequence.length());
}
/**
* Get the formats in effect at the end of a {@link CharSequence}.
*/
static TextFormats getEndFormat(CharSequence charSequence, int len) {
PreCon.notNull(charSequence);
if (len == 0)
return new TextFormats("", null);
StringBuilder sb = getSmallBuffer();
List<TextFormat> formats = new ArrayList<>(2);
Map<Character, TextFormat> characterMap = loadCharacterMap();
for (int i = len - 1; i > -1; i--) {
char current = charSequence.charAt(i);
if (current != CHAR || i >= len - 1)
continue; // finish block
char next = charSequence.charAt(i + 1);
TextFormat format = characterMap.get(next);
if (format == null)
continue; // finish block
sb.insert(0, format.getFormatCode());
formats.add(format);
if (format instanceof TextColor || format == TextFormat.RESET) {
break;
}
}
return new TextFormats(sb.toString(), formats);
}
/**
* Translate all format characters in a character sequence that are followed
* by valid format characters into '&' character.
*
* @param charSequence The character sequence.
*
* @return The translated text.
*/
public static String translateFormatChars(CharSequence charSequence) {
PreCon.notNull(charSequence);
if (charSequence.length() == 0)
return "";
int len = charSequence.length();
StringBuilder sb = getLargeBuffer(charSequence.length());
loadCharacterMap();
for (int i=0, last = len - 1; i < len; i++) {
char ch = charSequence.charAt(i);
if (i < last && ch == CHAR &&
_formatChars.indexOf(charSequence.charAt(i + 1)) != -1) {
sb.append('&');
}
else {
sb.append(ch);
}
}
return sb.toString();
}
/**
* Translate all format characters in a character sequence that are followed
* by valid format characters into '&' character.
*
* @param charSequence The character sequence.
*
* @return The translated text.
*/
public static String untranslateFormatChars(CharSequence charSequence) {
PreCon.notNull(charSequence);
if (charSequence.length() == 0)
return "";
int len = charSequence.length();
StringBuilder sb = getLargeBuffer(len);
loadCharacterMap();
for (int i=0, last = len - 1; i < len; i++) {
char ch = charSequence.charAt(i);
if (i < last && ch == '&' &&
_formatChars.indexOf(charSequence.charAt(i + 1)) != -1) {
sb.append(CHAR);
}
else {
sb.append(ch);
}
}
return sb.toString();
}
/**
* Translate all format codes into format tags (i.e. '{RED}').
*
* @param charSequence The character sequence.
*
* @return The translated text.
*/
public static String translateCodes(CharSequence charSequence) {
PreCon.notNull(charSequence);
if (charSequence.length() == 0)
return "";
int len = charSequence.length();
StringBuilder sb = getLargeBuffer(len + 100);
for (int i=0, last = len - 1; i < len; i++) {
char ch = charSequence.charAt(i);
if (ch == CHAR && i < last) {
TextFormat format = loadCharacterMap().get(charSequence.charAt(i + 1));
if (format != null) {
sb.append('{');
sb.append(format._tagName);
sb.append('}');
i++;
continue;
}
}
sb.append(ch);
}
return sb.toString();
}
/**
* Get an iterator for the {@link TextFormat}'s with a format code.
*/
public static Iterator<TextFormat> formatIterator() {
return new IteratorWrapper<TextFormat>() {
Iterator<TextFormat> iterator = loadCharacterMap().values().iterator();
@Override
public void remove() {
throw new UnsupportedOperationException();
}
@Override
protected Iterator<TextFormat> iterator() {
return iterator;
}
};
}
// get small string builder buffer
private static StringBuilder getSmallBuffer() {
StringBuilder sb = SMALL_BUFFERS.get();
sb.setLength(0);
return sb;
}
// get large string builder buffer
private static StringBuilder getLargeBuffer(int length) {
if (length > 1024)
return new StringBuilder(length);
StringBuilder sb = LARGE_BUFFERS.get();
sb.setLength(0);
sb.ensureCapacity(length);
return sb;
}
/**
* Stores color codes and their index location within a string.
*/
public static class TextFormatMap extends TreeMap<Integer, String> {
private String _text;
/**
* The text of the color map.
*/
public String getText() {
return _text;
}
/**
* Set the text of the color map.
*
* @param text The text.
*/
protected void setText(String text) {
_text = text;
}
}
/**
* Holds a collection of {@link TextFormat}'s.
*
* <p>Calling the {@link #toString} method returns the
* format codes as a string.</p>
*/
public static class TextFormats {
private final List<TextFormat> formats;
private final Object _sync = new Object();
private String string;
TextFormats(CharSequence string, @Nullable List<TextFormat> formats) {
PreCon.notNull(string);
this.formats = formats != null
? formats
: CollectionUtils.unmodifiableList(new ArrayList<TextFormat>(0));
this.string = string.toString();
}
/**
* Constructor.
*
* @param formats The {@link TextFormat}'s.
*/
public TextFormats(TextFormat... formats) {
PreCon.notNull(formats);
this.formats = ArrayUtils.asList(formats);
}
/**
* Constructor.
*
* @param formats The collection of {@link TextFormat}'s.
*/
public TextFormats(Collection<TextFormat> formats) {
PreCon.notNull(formats);
this.formats = new ArrayList<>(formats);
}
/**
* Constructor.
*
* @param formatChars The format characters.
*/
public TextFormats(char... formatChars) {
PreCon.notNull(formatChars);
TextFormat[] formats = new TextFormat[formatChars.length];
StringBuilder buffer = new StringBuilder(formatChars.length * 2);
for (int i=0; i < formatChars.length; i++) {
formats[i] = fromFormatChar(formatChars[i]);
if (formats[i] == null) {
throw new IllegalArgumentException("'" + formatChars[i] +
"' is not a recognized format character.");
}
buffer.append(CHAR);
buffer.append(formatChars[i]);
}
this.string = buffer.toString();
this.formats = ArrayUtils.asList(formats);
}
/**
* Get the list of text formats.
*/
public List<TextFormat> getFormats() {
return CollectionUtils.unmodifiableList(formats);
}
@Override
public String toString() {
if (string == null) {
synchronized (_sync) {
if (string != null)
return string;
StringBuilder buffer = new StringBuilder(formats.size() * 2);
for (TextFormat format : formats) {
if (format.hasFormatCode())
buffer.append(format.getFormatCode());
}
string = buffer.toString();
}
}
return string;
}
}
private static Map<Character, TextFormat> loadCharacterMap() {
if (_characterMap == null) {
_characterMap = new HashMap<>(25);
register(TextColor.AQUA);
register(TextColor.BLACK);
register(TextColor.BLUE);
register(TextColor.DARK_AQUA);
register(TextColor.DARK_BLUE);
register(TextColor.DARK_GRAY);
register(TextColor.DARK_GREEN);
register(TextColor.DARK_PURPLE);
register(TextColor.DARK_RED);
register(TextColor.GOLD);
register(TextColor.GRAY);
register(TextColor.GREEN);
register(TextColor.LIGHT_PURPLE);
register(TextColor.RED);
register(TextColor.WHITE);
register(TextColor.YELLOW);
register(TextFormat.BOLD);
register(TextFormat.ITALIC);
register(TextFormat.STRIKETHROUGH);
register(TextFormat.UNDERLINE);
register(TextFormat.RESET);
register(TextFormat.MAGIC);
}
return _characterMap;
}
private static void register(TextFormat format) {
if (!format.hasFormatCode())
return;
if (_characterMap == null)
_characterMap = new HashMap<>(30);
_characterMap.put(format.getFormatChar(), format);
_formatChars += format.getFormatChar();
}
}