From 81c109c2d22f4f1fee86656e421758391e616849 Mon Sep 17 00:00:00 2001 From: Vankka Date: Wed, 13 Sep 2023 21:07:42 +0300 Subject: [PATCH] Implement legacy code parsing and rendering into ansi + markdown for console --- .../common/component/ComponentFactory.java | 44 +++++- .../common/console/SingleConsoleHandler.java | 65 +++----- .../common/console/entry/LogEntry.java | 1 - .../console/message/ConsoleMessage.java | 145 ++++++++++++++++++ 4 files changed, 206 insertions(+), 49 deletions(-) create mode 100644 common/src/main/java/com/discordsrv/common/console/message/ConsoleMessage.java diff --git a/common/src/main/java/com/discordsrv/common/component/ComponentFactory.java b/common/src/main/java/com/discordsrv/common/component/ComponentFactory.java index 60465bfa..0c0d0a3b 100644 --- a/common/src/main/java/com/discordsrv/common/component/ComponentFactory.java +++ b/common/src/main/java/com/discordsrv/common/component/ComponentFactory.java @@ -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 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. + * KyoriPowered/ansi issue + */ + 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 children = component.children(); + List newChildren = new ArrayList<>(children.size()); + for (Component child : children) { + newChildren.add(recursivelyCheckColor(child)); + } + + return component.children(newChildren); + } + } } diff --git a/common/src/main/java/com/discordsrv/common/console/SingleConsoleHandler.java b/common/src/main/java/com/discordsrv/common/console/SingleConsoleHandler.java index 73d093aa..4583ab4d 100644 --- a/common/src/main/java/com/discordsrv/common/console/SingleConsoleHandler.java +++ b/common/src/main/java/com/discordsrv/common/console/SingleConsoleHandler.java @@ -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 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"; - - // 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(""); - } + 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; } - 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; diff --git a/common/src/main/java/com/discordsrv/common/console/entry/LogEntry.java b/common/src/main/java/com/discordsrv/common/console/entry/LogEntry.java index 55a85cb6..cc828cb3 100644 --- a/common/src/main/java/com/discordsrv/common/console/entry/LogEntry.java +++ b/common/src/main/java/com/discordsrv/common/console/entry/LogEntry.java @@ -35,7 +35,6 @@ public class LogEntry { return level; } - @Placeholder("message") public String message() { return message; } diff --git a/common/src/main/java/com/discordsrv/common/console/message/ConsoleMessage.java b/common/src/main/java/com/discordsrv/common/console/message/ConsoleMessage.java new file mode 100644 index 00000000..135dae93 --- /dev/null +++ b/common/src/main/java/com/discordsrv/common/console/message/ConsoleMessage.java @@ -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 + + "\\[" + + "(?\\d{1,3}" + + "(;\\d{1,3}" + + "(;\\d{1,3}" + + "(?:(?:;\\d{1,3}){2})?" + + ")?" + + ")?" + + ")" + + "m" + + "|" + + "(?" + // 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; + } + } +}