/*
* This file is part of LanternServer, licensed under the MIT License (MIT).
*
* Copyright (c) LanternPowered <https://www.lanternpowered.org>
* Copyright (c) SpongePowered <https://www.spongepowered.org>
* Copyright (c) contributors
*
* 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 org.lanternpowered.server.text;
import static com.google.common.base.Preconditions.checkNotNull;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Lists;
import it.unimi.dsi.fastutil.chars.Char2ObjectMap;
import it.unimi.dsi.fastutil.chars.Char2ObjectOpenHashMap;
import it.unimi.dsi.fastutil.objects.Object2CharMap;
import it.unimi.dsi.fastutil.objects.Object2CharOpenHashMap;
import org.lanternpowered.server.text.format.FormattingCodeHolder;
import org.spongepowered.api.text.LiteralText;
import org.spongepowered.api.text.ScoreText;
import org.spongepowered.api.text.SelectorText;
import org.spongepowered.api.text.Text;
import org.spongepowered.api.text.TextRepresentable;
import org.spongepowered.api.text.TranslatableText;
import org.spongepowered.api.text.format.TextColor;
import org.spongepowered.api.text.format.TextColors;
import org.spongepowered.api.text.format.TextFormat;
import org.spongepowered.api.text.format.TextStyle;
import org.spongepowered.api.text.format.TextStyles;
import org.spongepowered.api.text.serializer.TextParseException;
import org.spongepowered.api.text.translation.Translation;
import org.spongepowered.api.text.translation.locale.Locales;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import java.util.Optional;
import javax.annotation.Nullable;
public class FormattingCodeTextSerializer implements org.spongepowered.api.text.serializer.FormattingCodeTextSerializer, LanternTextSerializer {
public static final Object2CharMap<Object> FORMATS_TO_CODE = new Object2CharOpenHashMap<>();
public static final Char2ObjectMap<Object> CODE_TO_FORMATS = new Char2ObjectOpenHashMap<>();
private static void addFormat(Object format, char code) {
FORMATS_TO_CODE.put(format, code);
CODE_TO_FORMATS.put(code, format);
}
static {
addFormat(TextColors.BLACK, TextConstants.BLACK);
addFormat(TextColors.DARK_BLUE, TextConstants.DARK_BLUE);
addFormat(TextColors.DARK_GREEN, TextConstants.DARK_GREEN);
addFormat(TextColors.DARK_AQUA, TextConstants.DARK_AQUA);
addFormat(TextColors.DARK_RED, TextConstants.DARK_RED);
addFormat(TextColors.DARK_PURPLE, TextConstants.DARK_PURPLE);
addFormat(TextColors.GOLD, TextConstants.GOLD);
addFormat(TextColors.GRAY, TextConstants.GRAY);
addFormat(TextColors.DARK_GRAY, TextConstants.DARK_GRAY);
addFormat(TextColors.BLUE, TextConstants.BLUE);
addFormat(TextColors.GREEN, TextConstants.GREEN);
addFormat(TextColors.AQUA, TextConstants.AQUA);
addFormat(TextColors.RED, TextConstants.RED);
addFormat(TextColors.LIGHT_PURPLE, TextConstants.LIGHT_PURPLE);
addFormat(TextColors.YELLOW, TextConstants.YELLOW);
addFormat(TextColors.WHITE, TextConstants.WHITE);
addFormat(TextColors.RESET, TextConstants.RESET);
addFormat(TextStyles.OBFUSCATED, TextConstants.OBFUSCATED);
addFormat(TextStyles.BOLD, TextConstants.BOLD);
addFormat(TextStyles.STRIKETHROUGH, TextConstants.STRIKETHROUGH);
addFormat(TextStyles.UNDERLINE, TextConstants.UNDERLINE);
addFormat(TextStyles.ITALIC, TextConstants.ITALIC);
}
private static boolean isFormat(char format) {
boolean flag = CODE_TO_FORMATS.containsValue(format);
if (!flag) {
flag = CODE_TO_FORMATS.containsValue(Character.toLowerCase(format));
}
return flag;
}
@Nullable
private static Object getFormat(char format) {
Object obj = CODE_TO_FORMATS.get(format);
if (obj == null) {
obj = CODE_TO_FORMATS.get(Character.toLowerCase(format));
}
return obj;
}
private final char formattingCode;
public FormattingCodeTextSerializer(char formattingCode) {
this.formattingCode = formattingCode;
}
@Override
public char getCharacter() {
return this.formattingCode;
}
@Override
public String stripCodes(String text) {
return strip(text, this.formattingCode);
}
@Override
public String replaceCodes(String text, char to) {
return replace(text, this.formattingCode, to);
}
@Override
public String serialize(Text text) {
return this.serialize(text, Locales.DEFAULT);
}
@Override
public String serialize(Text text, Locale locale) {
return to(checkNotNull(text, "text"), checkNotNull(locale, "locale"), new StringBuilder(), this.formattingCode).toString();
}
@Override
public Text deserialize(String input) throws TextParseException {
return this.deserializeUnchecked(input);
}
@Override
public Text deserializeUnchecked(String input) {
checkNotNull(input, "input");
int next = input.lastIndexOf(this.formattingCode, input.length() - 2);
if (next == -1) {
return Text.of(input);
}
List<Text> parts = Lists.newArrayList();
LiteralText.Builder current = null;
boolean reset = false;
int pos = input.length();
do {
Object format = getFormat(input.charAt(next + 1));
if (format != null) {
int from = next + 2;
if (from != pos) {
if (current != null) {
if (reset) {
parts.add(current.build());
reset = false;
current = Text.builder("");
} else {
current = Text.builder("").append(current.build());
}
} else {
current = Text.builder("");
}
current.content(input.substring(from, pos));
} else if (current == null) {
current = Text.builder("");
}
reset |= applyStyle(current, format);
pos = next;
}
next = input.lastIndexOf(this.formattingCode, next - 1);
} while (next != -1);
if (current != null) {
parts.add(current.build());
}
Collections.reverse(parts);
return Text.builder(pos > 0 ? input.substring(0, pos) : "").append(parts).build();
}
private static boolean applyStyle(Text.Builder builder, Object format) {
if (format instanceof TextStyle) {
builder.style((TextStyle) format);
return false;
} else if (format == TextColors.RESET) {
return true;
} else {
if (builder.getColor() == TextColors.NONE) {
builder.color((TextColor) format);
}
return true;
}
}
static StringBuilder to(Text text, Locale locale, StringBuilder builder, @Nullable Character colorCode) {
return to(text, locale, builder, colorCode, null);
}
private static StringBuilder to(Text text, Locale locale, StringBuilder builder, @Nullable Character colorCode,
@Nullable ResolvedChatStyle current) {
ResolvedChatStyle style = null;
if (colorCode != null) {
style = resolve(current, text.getFormat());
if (current == null || (current.color != style.color) || (current.bold && !style.bold) ||
(current.italic && !style.italic) || (current.underlined && !style.underlined) ||
(current.strikethrough && !style.strikethrough) || (current.obfuscated && !style.obfuscated)) {
if (style.color != null) {
apply(builder, colorCode, ((FormattingCodeHolder) style.color).getCode());
} else if (current != null) {
apply(builder, colorCode, TextConstants.RESET);
}
apply(builder, colorCode, TextConstants.BOLD, style.bold);
apply(builder, colorCode, TextConstants.ITALIC, style.italic);
apply(builder, colorCode, TextConstants.UNDERLINE, style.underlined);
apply(builder, colorCode, TextConstants.STRIKETHROUGH, style.strikethrough);
apply(builder, colorCode, TextConstants.OBFUSCATED, style.obfuscated);
} else {
apply(builder, colorCode, TextConstants.BOLD, current.bold != style.bold);
apply(builder, colorCode, TextConstants.ITALIC, current.italic != style.italic);
apply(builder, colorCode, TextConstants.UNDERLINE, current.underlined != style.underlined);
apply(builder, colorCode, TextConstants.STRIKETHROUGH, current.strikethrough != style.strikethrough);
apply(builder, colorCode, TextConstants.OBFUSCATED, current.obfuscated != style.obfuscated);
}
}
if (text instanceof LiteralText) {
builder.append(((LiteralText) text).getContent());
} else if (text instanceof SelectorText) {
builder.append(((SelectorText) text).getSelector().toPlain());
} else if (text instanceof TranslatableText) {
TranslatableText text0 = (TranslatableText) text;
Translation translation = text0.getTranslation();
ImmutableList<Object> args = text0.getArguments();
Object[] args0 = new Object[args.size()];
for (int i = 0; i < args0.length; i++) {
Object object = args.get(i);
if (object instanceof Text || object instanceof Text.Builder || object instanceof TextRepresentable) {
if (object instanceof Text) {
// Ignore
} else if (object instanceof Text.Builder) {
object = ((Text.Builder) object).build();
} else {
object = ((TextRepresentable) object).toText();
}
args0[i] = to((Text) object, locale, new StringBuilder(), colorCode).toString();
} else {
args0[i] = object;
}
}
builder.append(translation.get(locale, args0));
} else if (text instanceof ScoreText) {
ScoreText text0 = (ScoreText) text;
Optional<String> override = text0.getOverride();
if (override.isPresent()) {
builder.append(override.get());
} else {
builder.append(text0.getScore().getScore());
}
}
for (Text child : text.getChildren()) {
to(child, locale, builder, colorCode, style);
}
return builder;
}
private static void apply(StringBuilder builder, char code, char formattingCode) {
builder.append(code).append(formattingCode);
}
private static void apply(StringBuilder builder, char code, char formattingCode, boolean state) {
if (state) {
apply(builder, code, formattingCode);
}
}
private static ResolvedChatStyle resolve(@Nullable ResolvedChatStyle current, TextFormat format) {
TextColor color = format.getColor();
TextStyle style = format.getStyle();
if (current == null) {
if (color == TextColors.NONE) {
color = null;
}
return new ResolvedChatStyle(color, style.isBold().orElse(false), style.isItalic().orElse(false),
style.hasUnderline().orElse(false), style.hasStrikethrough().orElse(false), style.isObfuscated().orElse(false));
}
if (color == TextColors.NONE) {
color = current.color;
}
return new ResolvedChatStyle(color, style.isBold().orElse(current.bold), style.isItalic().orElse(current.italic),
style.hasUnderline().orElse(current.underlined), style.hasStrikethrough().orElse(current.strikethrough),
style.isObfuscated().orElse(current.obfuscated));
}
private static class ResolvedChatStyle {
@Nullable public final TextColor color;
public final boolean bold;
public final boolean italic;
public final boolean underlined;
public final boolean strikethrough;
public final boolean obfuscated;
public ResolvedChatStyle(@Nullable TextColor color, boolean bold, boolean italic,
boolean underlined, boolean strikethrough, boolean obfuscated) {
this.color = color;
this.bold = bold;
this.italic = italic;
this.underlined = underlined;
this.strikethrough = strikethrough;
this.obfuscated = obfuscated;
}
}
static String replace(String text, char from, char to) {
int pos = text.indexOf(from);
int last = text.length() - 1;
if (pos == -1 || pos == last) {
return text;
}
char[] result = text.toCharArray();
for (; pos < last; pos++) {
if (result[pos] == from && isFormat(result[pos + 1])) {
result[pos] = to;
}
}
return new String(result);
}
public static String strip(String text, char code) {
return strip(text, code, false);
}
public static String strip(String text, char code, boolean all) {
int next = text.indexOf(code);
int last = text.length() - 1;
if (next == -1 || next == last) {
return text;
}
StringBuilder result = new StringBuilder(text.length());
int pos = 0;
do {
if (pos != next) {
result.append(text, pos, next);
}
pos = next;
if (isFormat(text.charAt(next + 1))) {
pos = next += 2; // Skip formatting
} else if (all) {
pos = next += 1; // Skip code only
} else {
next++;
}
next = text.indexOf(code, next);
} while (next != -1 && next < last);
return result.append(text, pos, text.length()).toString();
}
}