Implement legacy code parsing and rendering into ansi + markdown for console

This commit is contained in:
Vankka 2023-09-13 21:07:42 +03:00
parent 7193100047
commit 81c109c2d2
No known key found for this signature in database
GPG Key ID: 6E50CB7A29B96AD0
4 changed files with 206 additions and 49 deletions

View File

@ -33,13 +33,15 @@ import dev.vankka.mcdiscordreserializer.minecraft.MinecraftSerializerOptions;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.TranslatableComponent;
import net.kyori.adventure.text.flattener.ComponentFlattener;
import net.kyori.adventure.text.format.NamedTextColor;
import net.kyori.adventure.text.format.TextColor;
import net.kyori.adventure.text.serializer.ansi.ANSIComponentSerializer;
import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer;
import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer;
import net.kyori.ansi.ColorLevel;
import org.jetbrains.annotations.NotNull;
import java.util.Locale;
import java.util.*;
public class ComponentFactory implements MinecraftComponentFactory {
@ -58,6 +60,7 @@ public class ComponentFactory implements MinecraftComponentFactory {
private final DiscordSerializer discordSerializer;
private final PlainTextComponentSerializer plainSerializer;
private final ANSIComponentSerializer ansiSerializer;
private final ANSIComponentSerializerWrapper ansiWrapper = new ANSIComponentSerializerWrapper();
// Not the same as Adventure's TranslationRegistry
private final TranslationRegistry translationRegistry = new TranslationRegistry();
@ -131,10 +134,47 @@ public class ComponentFactory implements MinecraftComponentFactory {
}
public ANSIComponentSerializer ansiSerializer() {
return ansiSerializer;
return ansiWrapper;
}
public TranslationRegistry translationRegistry() {
return translationRegistry;
}
private static final Set<TextColor> ANSI_SUPPORTED_COLORS = new HashSet<>(Arrays.asList(
NamedTextColor.BLACK,
NamedTextColor.DARK_BLUE,
NamedTextColor.DARK_GREEN,
NamedTextColor.DARK_AQUA,
NamedTextColor.DARK_RED,
NamedTextColor.DARK_PURPLE,
NamedTextColor.GOLD,
NamedTextColor.GRAY
));
/**
* "Fix" for Discord only supporting 8 colors.
* <a href="https://github.com/KyoriPowered/ansi/issues/35">KyoriPowered/ansi issue</a>
*/
private class ANSIComponentSerializerWrapper implements ANSIComponentSerializer {
@Override
public @NotNull String serialize(@NotNull Component component) {
return ansiSerializer.serialize(recursivelyCheckColor(component));
}
private Component recursivelyCheckColor(Component component) {
if (!ANSI_SUPPORTED_COLORS.contains(component.color())) {
component = component.color(null);
}
List<Component> children = component.children();
List<Component> newChildren = new ArrayList<>(children.size());
for (Component child : children) {
newChildren.add(recursivelyCheckColor(child));
}
return component.children(newChildren);
}
}
}

View File

@ -2,10 +2,12 @@ package com.discordsrv.common.console;
import com.discordsrv.api.discord.entity.channel.DiscordGuildMessageChannel;
import com.discordsrv.api.discord.entity.message.SendableDiscordMessage;
import com.discordsrv.api.placeholder.provider.SinglePlaceholder;
import com.discordsrv.common.DiscordSRV;
import com.discordsrv.common.config.main.ConsoleConfig;
import com.discordsrv.common.console.entry.LogEntry;
import com.discordsrv.common.console.entry.LogMessage;
import com.discordsrv.common.console.message.ConsoleMessage;
import com.discordsrv.common.logging.LogLevel;
import net.dv8tion.jda.api.entities.Message;
import org.apache.commons.lang3.StringUtils;
@ -18,26 +20,10 @@ import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class SingleConsoleHandler {
private static final int MESSAGE_MAX_LENGTH = Message.MAX_CONTENT_LENGTH;
private static final String ESCAPE = "\u001B";
private static final Pattern ANSI_PATTERN = Pattern.compile(
ESCAPE
+ "\\["
+ "(\\d{1,3}"
+ "(;\\d{1,3}"
+ "(;\\d{1,3}"
+ "(?:(?:;\\d{1,3}){2})?"
+ ")?"
+ ")?"
+ ")"
+ "m"
);
private static final Pattern COLOR_CODE_PATTERN = Pattern.compile("\\u007F[0-9a-fk-orx]");
private final DiscordSRV discordSRV;
private final ConsoleConfig config;
@ -187,41 +173,28 @@ public class SingleConsoleHandler {
}
}
private String getAnsiEscapeSequence(String codePart) {
String[] split = codePart.split(";");
int[] numbers = new int[split.length];
for (int i = 0; i < split.length; i++) {
numbers[i] = Integer.parseInt(split[i]);
}
if (numbers.length == 1) {
return String.valueOf(numbers[0]);
} else if (numbers.length == 2) {
return numbers[0] + ";" + numbers[1];
} else {
// longer than supported by Discord, so drop the ansi here
return null;
}
}
private List<String> formatEntry(LogEntry entry, ConsoleConfig.OutputMode outputMode) {
int blockLength = outputMode.blockLength();
int maximumPart = MESSAGE_MAX_LENGTH - blockLength - "\n".length();
String message = discordSRV.placeholderService().replacePlaceholders(config.appender.lineFormat, entry) + "\n";
String parsedMessage;
switch (outputMode) {
case ANSI:
parsedMessage = new ConsoleMessage(discordSRV, entry.message()).asAnsi();
break;
case MARKDOWN:
parsedMessage = new ConsoleMessage(discordSRV, entry.message()).asMarkdown();
break;
default:
parsedMessage = entry.message();
break;
}
// TODO: make a parser for ANSI + color codes that makes a intermediary format that can be converted to
// TODO: either 16 color ansi (ANSI mode) or just bold/italics/underline/strikethrough markdown (MARKDOWN mode)
Matcher matcher = ANSI_PATTERN.matcher(message);
while (matcher.find()) {
String codes = matcher.group(1);
String escapeSequence = getAnsiEscapeSequence(codes);
if (escapeSequence != null && outputMode == ConsoleConfig.OutputMode.ANSI) {
message = matcher.replaceAll(ESCAPE + escapeSequence + "m");
} else {
message = matcher.replaceAll("");
}
}
message = message.replaceAll(COLOR_CODE_PATTERN.pattern(), "");
String message = discordSRV.placeholderService().replacePlaceholders(
config.appender.lineFormat,
entry,
new SinglePlaceholder("message", parsedMessage)
) + "\n";
if (outputMode == ConsoleConfig.OutputMode.DIFF) {
message = getLogLevelDiffCharacter(entry.level()) + message;

View File

@ -35,7 +35,6 @@ public class LogEntry {
return level;
}
@Placeholder("message")
public String message() {
return message;
}

View File

@ -0,0 +1,145 @@
package com.discordsrv.common.console.message;
import com.discordsrv.common.DiscordSRV;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.TextComponent;
import net.kyori.adventure.text.format.*;
import java.util.EnumSet;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class ConsoleMessage {
private static final String ANSI_ESCAPE = "\u001B";
// Paper uses 007F as an intermediary
private static final String SECTION = "\u007F]";
// Regex pattern for matching ANSI + Legacy
private static final Pattern PATTERN = Pattern.compile(
// ANSI
ANSI_ESCAPE
+ "\\["
+ "(?<ansi>\\d{1,3}"
+ "(;\\d{1,3}"
+ "(;\\d{1,3}"
+ "(?:(?:;\\d{1,3}){2})?"
+ ")?"
+ ")?"
+ ")"
+ "m"
+ "|"
+ "(?<legacy>"
// Legacy color/formatting
+ "(?:" + SECTION + "[0-9a-fk-or])"
+ "|"
// Bungee/Spigot legacy
+ "(?:" + SECTION + "x(?:" + SECTION + "[0-9a-f]){6})"
+ ")"
);
private final TextComponent.Builder builder = Component.text();
private final DiscordSRV discordSRV;
public ConsoleMessage(DiscordSRV discordSRV, String input) {
this.discordSRV = discordSRV;
parse(input);
}
public String asMarkdown() {
Component component = builder.build();
return discordSRV.componentFactory().discordSerializer().serialize(component);
}
public String asAnsi() {
Component component = builder.build();
return discordSRV.componentFactory().ansiSerializer().serialize(component);
}
private void parse(String input) {
Matcher matcher = PATTERN.matcher(input);
Style.Builder style = Style.style();
int lastMatchEnd = 0;
while (matcher.find()) {
int start = matcher.start();
int end = matcher.end();
if (start != lastMatchEnd) {
builder.append(Component.text(input.substring(lastMatchEnd, start), style.build()));
}
String ansi = matcher.group("ansi");
if (ansi != null) {
parseAnsi(ansi, style);
}
String legacy = matcher.group("legacy");
if (legacy != null) {
parseLegacy(legacy, style);
}
lastMatchEnd = end;
}
int length = input.length();
if (lastMatchEnd != length) {
builder.append(Component.text(input.substring(lastMatchEnd, length), style.build()));
}
}
private void parseAnsi(String ansiEscape, Style.Builder style) {
// TODO: implement
}
private void parseLegacy(String legacy, Style.Builder style) {
if (legacy.length() == 2) {
char character = legacy.toCharArray()[1];
if (character == 'r') {
style.color(null).decorations(EnumSet.allOf(TextDecoration.class), false);
} else {
TextFormat format = getFormat(character);
if (format instanceof TextColor) {
style.color((TextColor) format);
} else if (format instanceof TextDecoration) {
style.decorate((TextDecoration) format);
}
}
} else {
char[] characters = legacy.toCharArray();
StringBuilder hex = new StringBuilder(7).append(TextColor.HEX_PREFIX);
for (int i = 2; i < characters.length; i += 2) {
hex.append(characters[i]);
}
style.color(TextColor.fromHexString(hex.toString()));
}
}
private TextFormat getFormat(char character) {
switch (character) {
case '0': return NamedTextColor.BLACK;
case '1': return NamedTextColor.DARK_BLUE;
case '2': return NamedTextColor.DARK_GREEN;
case '3': return NamedTextColor.DARK_AQUA;
case '4': return NamedTextColor.DARK_RED;
case '5': return NamedTextColor.DARK_PURPLE;
case '6': return NamedTextColor.GOLD;
case '7': return NamedTextColor.GRAY;
case '8': return NamedTextColor.DARK_GRAY;
case '9': return NamedTextColor.BLUE;
case 'a': return NamedTextColor.GREEN;
case 'b': return NamedTextColor.AQUA;
case 'c': return NamedTextColor.RED;
case 'd': return NamedTextColor.LIGHT_PURPLE;
case 'e': return NamedTextColor.YELLOW;
case 'f': return NamedTextColor.WHITE;
case 'k': return TextDecoration.OBFUSCATED;
case 'l': return TextDecoration.BOLD;
case 'm': return TextDecoration.STRIKETHROUGH;
case 'n': return TextDecoration.UNDERLINED;
case 'o': return TextDecoration.ITALIC;
default: return null;
}
}
}