From 815cf0ae698e82f47294196ffb124d1c5a97b2f8 Mon Sep 17 00:00:00 2001 From: Diogo Correia Date: Thu, 18 Jul 2024 18:01:42 +0100 Subject: [PATCH 1/8] wip: feat: restore legacy parser This commit adds the serializer of a Component into its String representation. --- .../triton/language/parser/LegacyParser.java | 212 ++++++++++++++++++ .../language/parser/LegacyParserTest.java | 70 ++++++ 2 files changed, 282 insertions(+) create mode 100644 core/src/main/java/com/rexcantor64/triton/language/parser/LegacyParser.java create mode 100644 core/src/test/java/com/rexcantor64/triton/language/parser/LegacyParserTest.java diff --git a/core/src/main/java/com/rexcantor64/triton/language/parser/LegacyParser.java b/core/src/main/java/com/rexcantor64/triton/language/parser/LegacyParser.java new file mode 100644 index 00000000..5e177e50 --- /dev/null +++ b/core/src/main/java/com/rexcantor64/triton/language/parser/LegacyParser.java @@ -0,0 +1,212 @@ +package com.rexcantor64.triton.language.parser; + +import com.rexcantor64.triton.api.config.FeatureSyntax; +import com.rexcantor64.triton.api.language.Localized; +import com.rexcantor64.triton.api.language.MessageParser; +import com.rexcantor64.triton.api.language.TranslationResult; +import lombok.Getter; +import lombok.val; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.KeybindComponent; +import net.kyori.adventure.text.TextComponent; +import net.kyori.adventure.text.TranslatableComponent; +import net.kyori.adventure.text.event.ClickEvent; +import net.kyori.adventure.text.event.HoverEvent; +import net.kyori.adventure.text.flattener.ComponentFlattener; +import net.kyori.adventure.text.flattener.FlattenerListener; +import net.kyori.adventure.text.format.NamedTextColor; +import net.kyori.adventure.text.format.Style; +import net.kyori.adventure.text.format.TextColor; +import net.kyori.adventure.text.format.TextDecoration; +import net.kyori.adventure.text.format.TextFormat; +import net.kyori.adventure.text.serializer.legacy.CharacterAndFormat; +import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer; +import net.kyori.adventure.text.serializer.legacy.Reset; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.jetbrains.annotations.VisibleForTesting; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.UUID; + +/** + * A message parser that has the same behaviour as the parser on Triton v3 and below. + * For backwards compatibility purposes. + * + * @since 4.0.0 + */ +public class LegacyParser implements MessageParser { + private static final char CLICK_DELIM = '\uE400'; + private static final char CLICK_END_DELIM = '\uE401'; + private static final char HOVER_DELIM = '\uE500'; + private static final char HOVER_END_DELIM = '\uE501'; + private static final char TRANSLATABLE_DELIM = '\uE600'; + private static final char KEYBIND_DELIM = '\uE700'; + private static final char FONT_START_DELIM = '\uE800'; + private static final char FONT_MID_DELIM = '\uE802'; + private static final char FONT_END_DELIM = '\uE801'; + + @Override + public @NotNull TranslationResult translateString(String text, Localized language, FeatureSyntax syntax) { + return null; + } + + @Override + public @NotNull TranslationResult translateComponent(Component component, Localized language, FeatureSyntax syntax) { + return null; + } + + /** + * Represents a {@link Component} but as a String and with additional storage for the + * values of click/hover events and translatable components. + */ + @VisibleForTesting + static class SerializedComponent { + private final ComponentFlattener FLATTENER = ComponentFlattener.builder() + .mapper(KeybindComponent.class, component -> KEYBIND_DELIM + component.keybind() + KEYBIND_DELIM) + .mapper(TextComponent.class, TextComponent::content) + .mapper(TranslatableComponent.class, component -> { + val uuid = UUID.randomUUID(); + this.translatableComponents.put(uuid, component); + + // Key is only included for backwards-compatibility + return TRANSLATABLE_DELIM + component.key() + TRANSLATABLE_DELIM + uuid + TRANSLATABLE_DELIM; + }) + // ignore unknown components + .build(); + + @Getter + private final HashMap translatableComponents = new HashMap<>(); + @Getter + private final HashMap clickEvents = new HashMap<>(); + @Getter + private final HashMap> hoverEvents = new HashMap<>(); + @Getter + private String text; + + public SerializedComponent(Component component) { + val flattenerListener = new CursedFlattenerListener(); + FLATTENER.flatten(component, flattenerListener); + this.text = flattenerListener.toString(); + } + + /** + * Uses reserved unicode characters to delimit components/styles that cannot be represented with + * legacy color codes, such as click, hover, translatable components, fonts, keybinds, etc. + * The used characters are: + *
    + *
  • \uE400 and \E401 for click events
  • + *
  • \uE500 and \E501 for hover events
  • + *
  • \uE600 for translatable components
  • + *
  • \uE700 for keybind components
  • + *
  • \uE800, \uE801 and \uE802 for fonts
  • + *
+ */ + private class CursedFlattenerListener implements FlattenerListener { + private final StringBuilder stringBuilder = new StringBuilder(); + private Style[] stack = new Style[8]; + private int topIndex = -1; + + @Override + public void component(@NotNull String text) { + stringBuilder.append(text); + } + + @Override + public void pushStyle(@NotNull Style style) { + val i = ++this.topIndex; + if (i >= this.stack.length) { + this.stack = Arrays.copyOf(this.stack, this.stack.length * 2); + } + if (i > 0) { + style = this.stack[i - 1].merge(style); + } + this.stack[i] = style; + + @Nullable val color = style.color(); + if (color == null) { + this.stringBuilder.append(formatToString(Reset.INSTANCE)); + } else { + this.stringBuilder.append(formatToString(color)); + } + + style.decorations().entrySet().stream() + .filter(entry -> entry.getValue() == TextDecoration.State.TRUE) + .forEach(entry -> this.stringBuilder.append(formatToString(entry.getKey()))); + + @Nullable val clickEvent = style.clickEvent(); + if (clickEvent != null && (i == 0 || !clickEvent.equals(this.stack[i - 1].clickEvent()))) { + val uuid = UUID.randomUUID(); + SerializedComponent.this.clickEvents.put(uuid, clickEvent); + + this.stringBuilder + .append(CLICK_DELIM) + .append(clickEvent.action().ordinal()) // backwards compatibility only + .append(uuid); + } + + @Nullable val hoverEvent = style.hoverEvent(); + if (hoverEvent != null && (i == 0 || !hoverEvent.equals(this.stack[i - 1].hoverEvent()))) { + val uuid = UUID.randomUUID(); + SerializedComponent.this.hoverEvents.put(uuid, hoverEvent); + + this.stringBuilder + .append(HOVER_DELIM) + .append(uuid); + } + + @Nullable val font = style.font(); + if (font != null && (i == 0 || !font.equals(this.stack[i - 1].font()))) { + this.stringBuilder + .append(FONT_START_DELIM) + .append(font.asString()) + .append(FONT_MID_DELIM); + } + } + + @Override + public void popStyle(@NotNull Style style) { + val i = this.topIndex--; + + @Nullable val clickEvent = style.clickEvent(); + if (clickEvent != null && (i == 0 || !clickEvent.equals(this.stack[i - 1].clickEvent()))) { + this.stringBuilder.append(CLICK_END_DELIM); + } + + @Nullable val hoverEvent = style.hoverEvent(); + if (hoverEvent != null && (i == 0 || !hoverEvent.equals(this.stack[i - 1].hoverEvent()))) { + this.stringBuilder.append(HOVER_END_DELIM); + } + + @Nullable val font = style.font(); + if (font != null && (i == 0 || !font.equals(this.stack[i - 1].font()))) { + this.stringBuilder.append(FONT_END_DELIM); + } + } + + public String toString() { + return this.stringBuilder.toString(); + } + + private @NotNull String formatToString(@NotNull TextFormat format) { + if (format instanceof TextColor && !(format instanceof NamedTextColor)) { + // this is a hex color + final TextColor color = (TextColor) format; + String hexCode = String.format("%06x", color.value()); + final StringBuilder legacy = new StringBuilder("§x"); + for (int i = 0, length = hexCode.length(); i < length; i++) { + legacy.append(LegacyComponentSerializer.SECTION_CHAR).append(hexCode.charAt(i)); + } + return legacy.toString(); + } + return CharacterAndFormat.defaults().stream() + .filter(characterAndFormat -> characterAndFormat.format().equals(format)) + .findFirst() + .map(CharacterAndFormat::character) + .map(c -> "§" + c) + .orElse(""); + } + } + } +} diff --git a/core/src/test/java/com/rexcantor64/triton/language/parser/LegacyParserTest.java b/core/src/test/java/com/rexcantor64/triton/language/parser/LegacyParserTest.java new file mode 100644 index 00000000..22cd7eae --- /dev/null +++ b/core/src/test/java/com/rexcantor64/triton/language/parser/LegacyParserTest.java @@ -0,0 +1,70 @@ +package com.rexcantor64.triton.language.parser; + +import net.kyori.adventure.key.Key; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.event.ClickEvent; +import net.kyori.adventure.text.event.HoverEvent; +import net.kyori.adventure.text.format.NamedTextColor; +import net.kyori.adventure.text.format.TextColor; +import net.kyori.adventure.text.format.TextDecoration; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class LegacyParserTest { + + private final AdventureParser parser = new AdventureParser(); + + @Test + public void testSerializingComponent() { + Component component = Component.text("Lorem ") + .append( + Component.text("ipsum dolor ") + .color(NamedTextColor.BLACK) + .decorate(TextDecoration.BOLD) + ) + .append( + Component.text("sit amet,") + .decorate(TextDecoration.BOLD) + ) + .append( + Component.text(" consectetur ") + .color(TextColor.color(0xaabbcc)) + .clickEvent(ClickEvent.copyToClipboard("some text")) + .append( + Component.text("adipiscing ") + .font(Key.key("default")) + ) + .append( + Component.text("elit. ") + .hoverEvent(HoverEvent.showText(Component.text("hello world"))) + ) + ) + .append( + Component.text("Maecenas imperdiet ") + .color(NamedTextColor.AQUA) + .append( + Component.translatable( + "some.key", + Component.text("arg 1") + ) + ) + ); + + LegacyParser.SerializedComponent serializedComponent = new LegacyParser.SerializedComponent(component); + + assertEquals(1, serializedComponent.getClickEvents().size()); + assertEquals(1, serializedComponent.getHoverEvents().size()); + assertEquals(1, serializedComponent.getTranslatableComponents().size()); + assertEquals( + "§rLorem §0§lipsum dolor §r§lsit amet,§x§a§a§b§b§c§c\uE4005" + + serializedComponent.getClickEvents().keySet().iterator().next() + + " consectetur §x§a§a§b§b§c§c\uE800minecraft:default\uE802adipiscing \uE801§x§a§a§b§b§c§c\uE500" + + serializedComponent.getHoverEvents().keySet().iterator().next() + + "elit. \uE501\uE401§bMaecenas imperdiet §b\uE600some.key\uE600" + + serializedComponent.getTranslatableComponents().keySet().iterator().next() + + "\uE600", + serializedComponent.getText() + ); + } +} From 7c7f7d5409551972c628433cd9fe45cd06adc06f Mon Sep 17 00:00:00 2001 From: Diogo Correia Date: Thu, 22 Aug 2024 17:20:57 +0100 Subject: [PATCH 2/8] feat: add deserialize to adventure Component --- .../triton/language/parser/LegacyParser.java | 200 +++++++++++++++++- 1 file changed, 191 insertions(+), 9 deletions(-) diff --git a/core/src/main/java/com/rexcantor64/triton/language/parser/LegacyParser.java b/core/src/main/java/com/rexcantor64/triton/language/parser/LegacyParser.java index 5e177e50..f2caa7e8 100644 --- a/core/src/main/java/com/rexcantor64/triton/language/parser/LegacyParser.java +++ b/core/src/main/java/com/rexcantor64/triton/language/parser/LegacyParser.java @@ -6,6 +6,7 @@ import com.rexcantor64.triton.api.language.TranslationResult; import lombok.Getter; import lombok.val; +import net.kyori.adventure.key.Key; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.KeybindComponent; import net.kyori.adventure.text.TextComponent; @@ -20,32 +21,38 @@ import net.kyori.adventure.text.format.TextDecoration; import net.kyori.adventure.text.format.TextFormat; import net.kyori.adventure.text.serializer.legacy.CharacterAndFormat; -import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer; import net.kyori.adventure.text.serializer.legacy.Reset; +import org.intellij.lang.annotations.Subst; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.VisibleForTesting; +import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; +import java.util.List; import java.util.UUID; /** - * A message parser that has the same behaviour as the parser on Triton v3 and below. + * A message parser that has the same behaviour as the "AdvancedComponent" parser on Triton v3 and below. * For backwards compatibility purposes. * * @since 4.0.0 */ public class LegacyParser implements MessageParser { private static final char CLICK_DELIM = '\uE400'; - private static final char CLICK_END_DELIM = '\uE401'; + private static final char CLICK_END_DELIM = CLICK_DELIM + 1; private static final char HOVER_DELIM = '\uE500'; - private static final char HOVER_END_DELIM = '\uE501'; + private static final char HOVER_END_DELIM = HOVER_DELIM + 1; private static final char TRANSLATABLE_DELIM = '\uE600'; private static final char KEYBIND_DELIM = '\uE700'; private static final char FONT_START_DELIM = '\uE800'; - private static final char FONT_MID_DELIM = '\uE802'; - private static final char FONT_END_DELIM = '\uE801'; + private static final char FONT_MID_DELIM = FONT_START_DELIM + 2; + private static final char FONT_END_DELIM = FONT_START_DELIM + 1; + + private static final char SECTION_CHAR = '§'; + private static final char HEX_PREFIX = '#'; + private static final char HEX_CODE = 'x'; @Override public @NotNull TranslationResult translateString(String text, Localized language, FeatureSyntax syntax) { @@ -91,6 +98,181 @@ public SerializedComponent(Component component) { this.text = flattenerListener.toString(); } + /** + * Converts this object back to an Adventure {@link Component}. + * + * @return The corresponding {@link Component}. + */ + public @NotNull Component toComponent() { + val list = toComponentList(this.text); + if (list.isEmpty()) { + return Component.empty(); + } else if (list.size() == 1) { + return list.get(0); + } else { + return Component.text().append(list).build(); + } + } + + private @NotNull List<@NotNull Component> toComponentList(@NotNull String text) { + val list = new ArrayList(); + StringBuilder builder = new StringBuilder(); + TextComponent.Builder componentBuilder = Component.text(); + Style currentStyle = Style.empty(); // keep track of style of current component + for (int i = 0; i < text.length(); i++) { + val c = text.charAt(i); + if (c == SECTION_CHAR) { + i++; + if (i >= text.length()) { + // last character in string, ignore + builder.append(c); + continue; + } + + val lowercaseChar = Character.toLowerCase(text.charAt(i)); + TextFormat format; + if (lowercaseChar == HEX_CODE && i + 12 < text.length()) { + val color = text.substring(i + 1, i + 13); + format = TextColor.fromHexString(HEX_PREFIX + color.replace(String.valueOf(SECTION_CHAR), "")); + i += 12; + } else { + format = CharacterAndFormat.defaults().stream() + .filter(characterAndFormat -> characterAndFormat.character() == lowercaseChar) + .findFirst() + .map(CharacterAndFormat::format) + .orElse(null); + } + if (format == null) { + // unknown code, append color code as-is + builder.append(c); + i--; + continue; + } + + if (builder.length() != 0) { + // there's already some text in the builder, flush the component before changing styles + componentBuilder.content(builder.toString()); + builder = new StringBuilder(); + list.add(componentBuilder.build()); + } + componentBuilder = Component.text(); + if (format instanceof TextColor) { + currentStyle = Style.style((TextColor) format); + } else if (format instanceof TextDecoration) { + currentStyle = currentStyle.decorate((TextDecoration) format); + } else if (format instanceof Reset) { + currentStyle = Style.empty(); + } + componentBuilder.style(currentStyle); + } else if (c == CLICK_DELIM || c == HOVER_DELIM || c == FONT_START_DELIM) { + if (builder.length() != 0) { + // there's already some text in the builder, flush the component before changing styles + componentBuilder.content(builder.toString()); + builder = new StringBuilder(); + list.add(componentBuilder.build()); + } + componentBuilder = Component.text(); + componentBuilder.style(currentStyle); + + // handle styles/events based on delimiter found + switch (c) { + case CLICK_DELIM: + // action code is still present for compability only, but not used + val clickUuid = UUID.fromString(text.substring(i + 2, i + 2 + 36)); + val click = this.clickEvents.get(clickUuid); + componentBuilder.clickEvent(click); + i += 2 + 36; + break; + case HOVER_DELIM: + val hoverUuid = UUID.fromString(text.substring(i + 1, i + 1 + 36)); + val hover = this.hoverEvents.get(hoverUuid); + componentBuilder.hoverEvent(hover); + i += 1 + 36; + break; + case FONT_START_DELIM: + i++; + val font = new StringBuilder(); + while (text.charAt(i) != FONT_MID_DELIM) { + font.append(text.charAt(i)); + i++; + } + i++; + @Subst("minecraft:default") val fontName = font.toString(); + componentBuilder.font(Key.key(fontName)); + break; + } + + // get the content until the corresponding delimiter + int deep = 0; + StringBuilder content = new StringBuilder(); + while (text.charAt(i) != c + 1 || deep != 0) { + char c1 = text.charAt(i); + if (c1 == c) deep++; // c == \uE400 || c == \uE500 || c == \uE800 + if (c1 == c + 1) deep--; // c + 1 == \uE401 || c + 1 == \uE501 || c + 1 == \uE801 + content.append(c1); + i++; + } + List extra = toComponentList(content.toString()); + if (!extra.isEmpty()) { + componentBuilder.append(extra); + } + list.add(componentBuilder.build()); + componentBuilder = Component.text(); + // TODO check if this works if style changes inside the children + componentBuilder.style(currentStyle); + } else if (c == TRANSLATABLE_DELIM) { + i++; + while (text.charAt(i) != TRANSLATABLE_DELIM) { + // ignore key (still here for backwards compatibility) + i++; + } + val uuid = new StringBuilder(); + while (text.charAt(i) != TRANSLATABLE_DELIM) { + uuid.append(text.charAt(i)); + i++; + } + if (builder.length() != 0) { + // there's already some text in the builder, flush the component before adding the component + componentBuilder.content(builder.toString()); + builder = new StringBuilder(); + list.add(componentBuilder.build()); + componentBuilder = Component.text(); + componentBuilder.style(currentStyle); + } + val translatableComponent = this.translatableComponents.get(UUID.fromString(uuid.toString())); + if (translatableComponent != null) { + list.add(translatableComponent.style(currentStyle)); + } + } else if (c == KEYBIND_DELIM) { + i++; + val key = new StringBuilder(); + while (text.charAt(i) != KEYBIND_DELIM) { + key.append(text.charAt(i)); + i++; + } + if (builder.length() != 0) { + // there's already some text in the builder, flush the component before adding the component + componentBuilder.content(builder.toString()); + builder = new StringBuilder(); + list.add(componentBuilder.build()); + componentBuilder = Component.text(); + componentBuilder.style(currentStyle); + } + val keybindComponent = Component.keybind().keybind(key.toString()).style(currentStyle).build(); + list.add(keybindComponent); + } else { + // just a normal character + builder.append(c); + } + } + if (builder.length() != 0) { + // flush remaining text to a new component + componentBuilder.content(builder.toString()); + list.add(componentBuilder.build()); + } + return list; + } + /** * Uses reserved unicode characters to delimit components/styles that cannot be represented with * legacy color codes, such as click, hover, translatable components, fonts, keybinds, etc. @@ -194,9 +376,9 @@ public String toString() { // this is a hex color final TextColor color = (TextColor) format; String hexCode = String.format("%06x", color.value()); - final StringBuilder legacy = new StringBuilder("§x"); + final StringBuilder legacy = new StringBuilder("" + SECTION_CHAR + HEX_CODE); for (int i = 0, length = hexCode.length(); i < length; i++) { - legacy.append(LegacyComponentSerializer.SECTION_CHAR).append(hexCode.charAt(i)); + legacy.append(SECTION_CHAR).append(hexCode.charAt(i)); } return legacy.toString(); } @@ -204,7 +386,7 @@ public String toString() { .filter(characterAndFormat -> characterAndFormat.format().equals(format)) .findFirst() .map(CharacterAndFormat::character) - .map(c -> "§" + c) + .map(c -> "" + SECTION_CHAR + c) .orElse(""); } } From 1f38c5a6502b1859e30c9d9a2a1055e7fe27ff7f Mon Sep 17 00:00:00 2001 From: Diogo Correia Date: Sat, 24 Aug 2024 23:16:13 +0200 Subject: [PATCH 3/8] fix: error while deserializing translatable component Also add tests. --- .../triton/language/parser/LegacyParser.java | 2 +- .../language/parser/LegacyParserTest.java | 108 +++++++++++------- 2 files changed, 66 insertions(+), 44 deletions(-) diff --git a/core/src/main/java/com/rexcantor64/triton/language/parser/LegacyParser.java b/core/src/main/java/com/rexcantor64/triton/language/parser/LegacyParser.java index f2caa7e8..80e80c32 100644 --- a/core/src/main/java/com/rexcantor64/triton/language/parser/LegacyParser.java +++ b/core/src/main/java/com/rexcantor64/triton/language/parser/LegacyParser.java @@ -218,7 +218,6 @@ public SerializedComponent(Component component) { } list.add(componentBuilder.build()); componentBuilder = Component.text(); - // TODO check if this works if style changes inside the children componentBuilder.style(currentStyle); } else if (c == TRANSLATABLE_DELIM) { i++; @@ -226,6 +225,7 @@ public SerializedComponent(Component component) { // ignore key (still here for backwards compatibility) i++; } + i++; val uuid = new StringBuilder(); while (text.charAt(i) != TRANSLATABLE_DELIM) { uuid.append(text.charAt(i)); diff --git a/core/src/test/java/com/rexcantor64/triton/language/parser/LegacyParserTest.java b/core/src/test/java/com/rexcantor64/triton/language/parser/LegacyParserTest.java index 22cd7eae..1126623a 100644 --- a/core/src/test/java/com/rexcantor64/triton/language/parser/LegacyParserTest.java +++ b/core/src/test/java/com/rexcantor64/triton/language/parser/LegacyParserTest.java @@ -9,62 +9,84 @@ import net.kyori.adventure.text.format.TextDecoration; import org.junit.jupiter.api.Test; +import java.util.ArrayList; +import java.util.List; + import static org.junit.jupiter.api.Assertions.assertEquals; public class LegacyParserTest { - - private final AdventureParser parser = new AdventureParser(); + private final Component ALL_IN_ONE_COMPONENT = Component.text("Lorem ") + .append( + Component.text("ipsum dolor ") + .color(NamedTextColor.BLACK) + .decorate(TextDecoration.BOLD) + ) + .append( + Component.text("sit amet,") + .decorate(TextDecoration.BOLD) + ) + .append( + Component.text(" consectetur ") + .color(TextColor.color(0xaabbcc)) + .clickEvent(ClickEvent.copyToClipboard("some text")) + .append( + Component.text("adipiscing ") + .font(Key.key("default")) + ) + .append( + Component.text("elit. ") + .hoverEvent(HoverEvent.showText(Component.text("hello world"))) + ) + ) + .append( + Component.text("Maecenas imperdiet ") + .color(NamedTextColor.AQUA) + .append( + Component.translatable( + "some.key", + Component.text("arg 1") + ) + ) + ); @Test public void testSerializingComponent() { - Component component = Component.text("Lorem ") - .append( - Component.text("ipsum dolor ") - .color(NamedTextColor.BLACK) - .decorate(TextDecoration.BOLD) - ) - .append( - Component.text("sit amet,") - .decorate(TextDecoration.BOLD) - ) - .append( - Component.text(" consectetur ") - .color(TextColor.color(0xaabbcc)) - .clickEvent(ClickEvent.copyToClipboard("some text")) - .append( - Component.text("adipiscing ") - .font(Key.key("default")) - ) - .append( - Component.text("elit. ") - .hoverEvent(HoverEvent.showText(Component.text("hello world"))) - ) - ) - .append( - Component.text("Maecenas imperdiet ") - .color(NamedTextColor.AQUA) - .append( - Component.translatable( - "some.key", - Component.text("arg 1") - ) - ) - ); - - LegacyParser.SerializedComponent serializedComponent = new LegacyParser.SerializedComponent(component); + LegacyParser.SerializedComponent serializedComponent = new LegacyParser.SerializedComponent(ALL_IN_ONE_COMPONENT); assertEquals(1, serializedComponent.getClickEvents().size()); assertEquals(1, serializedComponent.getHoverEvents().size()); assertEquals(1, serializedComponent.getTranslatableComponents().size()); assertEquals( "§rLorem §0§lipsum dolor §r§lsit amet,§x§a§a§b§b§c§c\uE4005" - + serializedComponent.getClickEvents().keySet().iterator().next() - + " consectetur §x§a§a§b§b§c§c\uE800minecraft:default\uE802adipiscing \uE801§x§a§a§b§b§c§c\uE500" - + serializedComponent.getHoverEvents().keySet().iterator().next() - + "elit. \uE501\uE401§bMaecenas imperdiet §b\uE600some.key\uE600" - + serializedComponent.getTranslatableComponents().keySet().iterator().next() - + "\uE600", + + serializedComponent.getClickEvents().keySet().iterator().next() + + " consectetur §x§a§a§b§b§c§c\uE800minecraft:default\uE802adipiscing \uE801§x§a§a§b§b§c§c\uE500" + + serializedComponent.getHoverEvents().keySet().iterator().next() + + "elit. \uE501\uE401§bMaecenas imperdiet §b\uE600some.key\uE600" + + serializedComponent.getTranslatableComponents().keySet().iterator().next() + + "\uE600", serializedComponent.getText() ); } + + @Test + public void testSerializeDeserializeComponent() { + Component result = new LegacyParser.SerializedComponent(ALL_IN_ONE_COMPONENT).toComponent(); + + // slightly modify input to equivalent component, due to behaviour of the deserializer + List expectedChildren = new ArrayList<>(ALL_IN_ONE_COMPONENT.children()); + expectedChildren.remove(expectedChildren.size() - 1); // remove last child + Component expected = ALL_IN_ONE_COMPONENT.children(expectedChildren) + .append( + Component.text("Maecenas imperdiet ") + .color(NamedTextColor.AQUA) + ) + .append( + Component.translatable( + "some.key", + Component.text("arg 1") + ).color(NamedTextColor.AQUA) + ); + + assertEquals(expected.compact(), result.compact()); + } } From c13963c18e68a43862f55fd791241a51ac8d4aa0 Mon Sep 17 00:00:00 2001 From: Diogo Correia Date: Sun, 25 Aug 2024 22:07:29 +0200 Subject: [PATCH 4/8] wip: feat: start translate methods of legacy parser --- .../triton/api/language/MessageParser.java | 4 +- .../triton/language/TranslationManager.java | 7 +- .../language/parser/AdventureParser.java | 16 +- .../triton/language/parser/LegacyParser.java | 143 +++++++++++++++++- .../parser/TranslationConfiguration.java | 5 +- .../language/parser/AdventureParserTest.java | 2 +- 6 files changed, 157 insertions(+), 20 deletions(-) diff --git a/api/src/main/java/com/rexcantor64/triton/api/language/MessageParser.java b/api/src/main/java/com/rexcantor64/triton/api/language/MessageParser.java index 10a904ca..38be6f2a 100644 --- a/api/src/main/java/com/rexcantor64/triton/api/language/MessageParser.java +++ b/api/src/main/java/com/rexcantor64/triton/api/language/MessageParser.java @@ -27,7 +27,7 @@ public interface MessageParser { * @return The result of the translation * @since 4.0.0 */ - @NotNull TranslationResult translateString(String text, Localized language, FeatureSyntax syntax); + @NotNull TranslationResult translateString(@NotNull String text, @NotNull Localized language, @NotNull FeatureSyntax syntax); /** * Find and replace Triton placeholders in a Component. @@ -46,6 +46,6 @@ public interface MessageParser { * @return The result of the translation * @since 4.0.0 */ - @NotNull TranslationResult translateComponent(Component component, Localized language, FeatureSyntax syntax); + @NotNull TranslationResult translateComponent(@NotNull Component component, @NotNull Localized language, @NotNull FeatureSyntax syntax); } diff --git a/core/src/main/java/com/rexcantor64/triton/language/TranslationManager.java b/core/src/main/java/com/rexcantor64/triton/language/TranslationManager.java index 15e7cde6..27078224 100644 --- a/core/src/main/java/com/rexcantor64/triton/language/TranslationManager.java +++ b/core/src/main/java/com/rexcantor64/triton/language/TranslationManager.java @@ -41,8 +41,8 @@ @RequiredArgsConstructor public class TranslationManager implements com.rexcantor64.triton.api.language.TranslationManager { - private static final String MINIMESSAGE_TYPE_TAG = "[minimsg]"; - private static final String JSON_TYPE_TAG = "[triton_json]"; + public static final String MINIMESSAGE_TYPE_TAG = "[minimsg]"; + public static final String JSON_TYPE_TAG = "[triton_json]"; private final Triton triton; @@ -59,6 +59,7 @@ public class TranslationManager implements com.rexcantor64.triton.api.language.T @Getter private int signTranslationCount = 0; + @Getter private Component translationNotFoundComponent = Component.empty(); private final Map miniMessageInstances = new HashMap<>(); @@ -204,7 +205,7 @@ private synchronized void setupMiniMessage() { * @return The instance for the given language. * @since 4.0.0 */ - private @NotNull MiniMessage getMiniMessageInstanceForLanguage(@NotNull Language language) { + public @NotNull MiniMessage getMiniMessageInstanceForLanguage(@NotNull Language language) { return Streams.concat( Stream.of(language), language.getFallbackLanguages() diff --git a/core/src/main/java/com/rexcantor64/triton/language/parser/AdventureParser.java b/core/src/main/java/com/rexcantor64/triton/language/parser/AdventureParser.java index acfdceda..cc0b4256 100644 --- a/core/src/main/java/com/rexcantor64/triton/language/parser/AdventureParser.java +++ b/core/src/main/java/com/rexcantor64/triton/language/parser/AdventureParser.java @@ -55,7 +55,7 @@ public class AdventureParser implements MessageParser { * @see MessageParser#translateString(String, Localized, FeatureSyntax) */ @Override - public @NotNull TranslationResult translateString(String text, Localized language, FeatureSyntax syntax) { + public @NotNull TranslationResult translateString(@NotNull String text, @NotNull Localized language, @NotNull FeatureSyntax syntax) { return translateComponent( ComponentUtils.deserializeFromLegacy(text), language, @@ -81,8 +81,8 @@ public class AdventureParser implements MessageParser { * @since 4.0.0 */ @Override - public @NotNull TranslationResult translateComponent(Component component, Localized language, FeatureSyntax syntax) { - TranslationConfiguration configuration = new TranslationConfiguration( + public @NotNull TranslationResult translateComponent(@NotNull Component component, @NotNull Localized language, @NotNull FeatureSyntax syntax) { + val configuration = new TranslationConfiguration( syntax, Triton.get().getConfig().getDisabledLine(), // TODO properly integrate this @@ -102,7 +102,7 @@ public class AdventureParser implements MessageParser { * @since 4.0.0 */ @VisibleForTesting - TranslationResult translateComponent(Component component, TranslationConfiguration configuration) { + TranslationResult translateComponent(@NotNull Component component, @NotNull TranslationConfiguration configuration) { String plainText = componentToString(component); if (ComponentUtils.hasLegacyFormatting(plainText)) { @@ -112,7 +112,7 @@ TranslationResult translateComponent(Component component, Translation val indexes = this.getPatternIndexArray(plainText, configuration.getFeatureSyntax().getLang()); - if (indexes.size() == 0) { + if (indexes.isEmpty()) { return handleNonContentText(component, configuration); } @@ -165,7 +165,7 @@ TranslationResult translateComponent(Component component, Translation * @return The translation of this placeholder. Empty optional if the translation is "disabled line". * @since 4.0.0 */ - private Optional handlePlaceholder(Component placeholder, TranslationConfiguration configuration) { + private Optional handlePlaceholder(Component placeholder, TranslationConfiguration configuration) { Style defaultStyle = getStyleOfFirstCharacterOrEmpty(placeholder); placeholder = stripStyleOfFirstCharacter(placeholder); @@ -228,7 +228,7 @@ private Optional handlePlaceholder(Component placeholder, Translation * @since 4.0.0 */ @SuppressWarnings("unchecked") - private TranslationResult handleNonContentText(Component component, TranslationConfiguration configuration) { + private TranslationResult handleNonContentText(Component component, TranslationConfiguration configuration) { boolean changed = false; HoverEvent hoverEvent = component.hoverEvent(); if (hoverEvent != null) { @@ -536,7 +536,7 @@ private List splitComponent(List comps, SplitState state) * @since 4.0.0 */ private List flushAccumulator(List accumulator, List splits) { - if (accumulator.size() == 0) { + if (accumulator.isEmpty()) { return accumulator; } diff --git a/core/src/main/java/com/rexcantor64/triton/language/parser/LegacyParser.java b/core/src/main/java/com/rexcantor64/triton/language/parser/LegacyParser.java index 80e80c32..761decaf 100644 --- a/core/src/main/java/com/rexcantor64/triton/language/parser/LegacyParser.java +++ b/core/src/main/java/com/rexcantor64/triton/language/parser/LegacyParser.java @@ -1,10 +1,14 @@ package com.rexcantor64.triton.language.parser; +import com.rexcantor64.triton.Triton; import com.rexcantor64.triton.api.config.FeatureSyntax; import com.rexcantor64.triton.api.language.Localized; import com.rexcantor64.triton.api.language.MessageParser; import com.rexcantor64.triton.api.language.TranslationResult; +import com.rexcantor64.triton.utils.ComponentUtils; +import lombok.AccessLevel; import lombok.Getter; +import lombok.Setter; import lombok.val; import net.kyori.adventure.key.Key; import net.kyori.adventure.text.Component; @@ -20,6 +24,8 @@ import net.kyori.adventure.text.format.TextColor; import net.kyori.adventure.text.format.TextDecoration; import net.kyori.adventure.text.format.TextFormat; +import net.kyori.adventure.text.minimessage.MiniMessage; +import net.kyori.adventure.text.serializer.gson.GsonComponentSerializer; import net.kyori.adventure.text.serializer.legacy.CharacterAndFormat; import net.kyori.adventure.text.serializer.legacy.Reset; import org.intellij.lang.annotations.Subst; @@ -32,6 +38,10 @@ import java.util.HashMap; import java.util.List; import java.util.UUID; +import java.util.stream.Collectors; + +import static com.rexcantor64.triton.language.TranslationManager.JSON_TYPE_TAG; +import static com.rexcantor64.triton.language.TranslationManager.MINIMESSAGE_TYPE_TAG; /** * A message parser that has the same behaviour as the "AdvancedComponent" parser on Triton v3 and below. @@ -50,20 +60,129 @@ public class LegacyParser implements MessageParser { private static final char FONT_MID_DELIM = FONT_START_DELIM + 2; private static final char FONT_END_DELIM = FONT_START_DELIM + 1; + private static final char AMPERSAND_CHAR = '&'; private static final char SECTION_CHAR = '§'; private static final char HEX_PREFIX = '#'; private static final char HEX_CODE = 'x'; + private static final String VALID_COLOR_CODES = "0123456789AaBbCcDdEeFfKkLlMmNnOoRrXx"; + /** + * @see MessageParser#translateString(String, Localized, FeatureSyntax) + * @since 4.0.0 + */ @Override - public @NotNull TranslationResult translateString(String text, Localized language, FeatureSyntax syntax) { - return null; + public @NotNull TranslationResult translateString(@NotNull String text, @NotNull Localized language, @NotNull FeatureSyntax syntax) { + Triton.get().getDumpManager().dump(Component.text(text), language, syntax); + + return translateComponent( + new SerializedComponent(text), + language, + syntax + ) + .map(SerializedComponent::toComponent) + .map(ComponentUtils::serializeToLegacy); } + /** + * @see MessageParser#translateComponent(Component, Localized, FeatureSyntax) + * @since 4.0.0 + */ @Override - public @NotNull TranslationResult translateComponent(Component component, Localized language, FeatureSyntax syntax) { + public @NotNull TranslationResult translateComponent(@NotNull Component component, @NotNull Localized language, @NotNull FeatureSyntax syntax) { + Triton.get().getDumpManager().dump(component, language, syntax); + + return translateComponent( + new SerializedComponent(component), + language, + syntax + ).map(SerializedComponent::toComponent); + } + + private @NotNull TranslationResult translateComponent( + @NotNull SerializedComponent component, + @NotNull Localized language, + @NotNull FeatureSyntax syntax + ) { + val configuration = new TranslationConfiguration( + syntax, + Triton.get().getConfig().getDisabledLine(), + (key, arguments) -> Triton.get().getTranslationManager().getTextString(language, key) + .map(text -> this.handleTranslationType(text, language)) + .map(comp -> replaceArguments(comp, arguments)) + .orElseGet(() -> { + val notFoundComponent = new SerializedComponent(Triton.get().getTranslationManager().getTranslationNotFoundComponent()); + val argsConcatenation = Arrays.stream(arguments).map(SerializedComponent::getText).collect(Collectors.joining(", ")); + val argsContatenationComp = new SerializedComponent("[" + argsConcatenation + "]"); + for (SerializedComponent argument : arguments) { + argsContatenationComp.importFromComponent(argument); + } + + return replaceArguments(notFoundComponent, new SerializedComponent(key), argsContatenationComp); + }) + ); + + return translateComponent(component, configuration); + } + + @VisibleForTesting + @NotNull + TranslationResult translateComponent( + @NotNull SerializedComponent component, + @NotNull TranslationConfiguration configuration + ) { + // TODO! return null; } + public @NotNull String replaceArguments(@NotNull String text, @Nullable String @NotNull [] arguments) { + for (int i = arguments.length - 1; i >= 0; --i) { + text = text.replace("%" + (i + 1), String.valueOf(arguments[i])); + } + return text; + } + + private @NotNull SerializedComponent replaceArguments(@NotNull SerializedComponent comp, @NotNull SerializedComponent @NotNull ... arguments) { + // Replace args in text + String[] args = Arrays.stream(arguments).map(SerializedComponent::getText).toArray(String[]::new); + comp.setText(replaceArguments(comp.getText(), args)); + + // Merge non-text parts (click, hover, etc.) + for (SerializedComponent argument : arguments) { + comp.importFromComponent(argument); + } + return comp; + } + + private @NotNull SerializedComponent handleTranslationType(@NotNull String message, @NotNull Localized language) { + // TODO make minimsg the default (?) + if (message.startsWith(MINIMESSAGE_TYPE_TAG)) { + MiniMessage miniMessage = Triton.get().getTranslationManager().getMiniMessageInstanceForLanguage(language.getLanguage()); + return new SerializedComponent(miniMessage.deserialize(message.substring(MINIMESSAGE_TYPE_TAG.length()))); + } else if (message.startsWith(JSON_TYPE_TAG)) { + return new SerializedComponent(GsonComponentSerializer.gson().deserialize(message.substring(JSON_TYPE_TAG.length()))); + } else { + return new SerializedComponent(translateAlternateColorCodes(message)); + } + } + + /** + * Convert color codes with ampersand (&) into section characters (§). + * The character is only converted if followed by a valid color code. + * Inspired by md5's ChatColor#translateAlternateColorCodes. + * + * @param text The text to convert the color code characters from. + * @return The input text with the color codes replaced. + */ + private String translateAlternateColorCodes(String text) { + char[] chars = text.toCharArray(); + for (int i = 0; i < chars.length - 1; i++) { + if (chars[i] == AMPERSAND_CHAR && VALID_COLOR_CODES.indexOf(chars[i + 1]) != -1) { + chars[i] = SECTION_CHAR; + } + } + return new String(chars); + } + /** * Represents a {@link Component} but as a String and with additional storage for the * values of click/hover events and translatable components. @@ -90,6 +209,7 @@ static class SerializedComponent { @Getter private final HashMap> hoverEvents = new HashMap<>(); @Getter + @Setter(AccessLevel.PRIVATE) private String text; public SerializedComponent(Component component) { @@ -98,6 +218,10 @@ public SerializedComponent(Component component) { this.text = flattenerListener.toString(); } + public SerializedComponent(String legacyText) { + this.text = legacyText; + } + /** * Converts this object back to an Adventure {@link Component}. * @@ -273,6 +397,19 @@ public SerializedComponent(Component component) { return list; } + /** + * Import non-text parts (click, hover, translatable) from the given commponent + * into this component. + * Useful for merging components while preserving their non-text parts. + * + * @param other The component to import non-text parts from. + */ + public void importFromComponent(SerializedComponent other) { + this.clickEvents.putAll(other.getClickEvents()); + this.hoverEvents.putAll(other.getHoverEvents()); + this.translatableComponents.putAll(other.getTranslatableComponents()); + } + /** * Uses reserved unicode characters to delimit components/styles that cannot be represented with * legacy color codes, such as click, hover, translatable components, fonts, keybinds, etc. diff --git a/core/src/main/java/com/rexcantor64/triton/language/parser/TranslationConfiguration.java b/core/src/main/java/com/rexcantor64/triton/language/parser/TranslationConfiguration.java index 4ce3cd9a..78a7f905 100644 --- a/core/src/main/java/com/rexcantor64/triton/language/parser/TranslationConfiguration.java +++ b/core/src/main/java/com/rexcantor64/triton/language/parser/TranslationConfiguration.java @@ -2,13 +2,12 @@ import com.rexcantor64.triton.api.config.FeatureSyntax; import lombok.Data; -import net.kyori.adventure.text.Component; import java.util.function.BiFunction; @Data -public class TranslationConfiguration { +public class TranslationConfiguration { final FeatureSyntax featureSyntax; final String disabledLine; - final BiFunction translationSupplier; + final BiFunction translationSupplier; } diff --git a/core/src/test/java/com/rexcantor64/triton/language/parser/AdventureParserTest.java b/core/src/test/java/com/rexcantor64/triton/language/parser/AdventureParserTest.java index 0502a374..598b4690 100644 --- a/core/src/test/java/com/rexcantor64/triton/language/parser/AdventureParserTest.java +++ b/core/src/test/java/com/rexcantor64/triton/language/parser/AdventureParserTest.java @@ -76,7 +76,7 @@ public class AdventureParserTest { return Component.text("unknown placeholder"); }; - private final TranslationConfiguration configuration = new TranslationConfiguration( + private final TranslationConfiguration configuration = new TranslationConfiguration<>( defaultSyntax, "disabled.line", (key, args) -> parser.replaceArguments(messageResolver.apply(key), Arrays.asList(args)) From 9e2f59c22b614cc4d8e29389fda88ed162ddf3ad Mon Sep 17 00:00:00 2001 From: Diogo Correia Date: Tue, 27 Aug 2024 17:22:20 +0200 Subject: [PATCH 5/8] wip: feat: continue translate methods of legacy parser --- .../language/parser/AdventureParser.java | 58 ++----------- .../triton/language/parser/LegacyParser.java | 77 +++++++++++++++++- .../rexcantor64/triton/utils/ParserUtils.java | 81 +++++++++++++++++++ .../language/parser/AdventureParserTest.java | 17 ---- .../triton/utils/ParserUtilsTest.java | 29 +++++++ .../spigot/listeners/BukkitListener.java | 3 +- 6 files changed, 192 insertions(+), 73 deletions(-) create mode 100644 core/src/main/java/com/rexcantor64/triton/utils/ParserUtils.java create mode 100644 core/src/test/java/com/rexcantor64/triton/utils/ParserUtilsTest.java diff --git a/core/src/main/java/com/rexcantor64/triton/language/parser/AdventureParser.java b/core/src/main/java/com/rexcantor64/triton/language/parser/AdventureParser.java index cc0b4256..1d9380bc 100644 --- a/core/src/main/java/com/rexcantor64/triton/language/parser/AdventureParser.java +++ b/core/src/main/java/com/rexcantor64/triton/language/parser/AdventureParser.java @@ -5,6 +5,7 @@ import com.rexcantor64.triton.api.language.Localized; import com.rexcantor64.triton.api.language.MessageParser; import com.rexcantor64.triton.utils.ComponentUtils; +import com.rexcantor64.triton.utils.ParserUtils; import com.rexcantor64.triton.utils.StringUtils; import lombok.AccessLevel; import lombok.Getter; @@ -110,7 +111,7 @@ TranslationResult translateComponent(@NotNull Component component, @N plainText = componentToString(component); } - val indexes = this.getPatternIndexArray(plainText, configuration.getFeatureSyntax().getLang()); + val indexes = ParserUtils.getPatternIndexArray(plainText, configuration.getFeatureSyntax().getLang()); if (indexes.isEmpty()) { return handleNonContentText(component, configuration); @@ -170,7 +171,7 @@ private Optional handlePlaceholder(Component placeholder, Translation placeholder = stripStyleOfFirstCharacter(placeholder); String placeholderStr = componentToString(placeholder); - val indexes = this.getPatternIndexArray(placeholderStr, configuration.getFeatureSyntax().getArg()); + val indexes = ParserUtils.getPatternIndexArray(placeholderStr, configuration.getFeatureSyntax().getArg()); Queue indexesToSplitAt = indexes.stream() .flatMap(Arrays::stream) .sorted() @@ -186,10 +187,7 @@ private Optional handlePlaceholder(Component placeholder, Translation Component part = splitComponents.get(i); if (i == 0) { key = PlainTextComponentSerializer.plainText().serialize(part); - // The [args] tag is optional since v4.0.0, so strip it if it's present - if (key.endsWith("[" + configuration.getFeatureSyntax().getArgs() + "]")) { - key = key.substring(0, key.length() - configuration.getFeatureSyntax().getArgs().length() - 2); - } + key = ParserUtils.normalizeTranslationKey(key, configuration); if (!StringUtils.isEmptyOrNull(configuration.getDisabledLine()) && configuration.getDisabledLine() .equals(key)) { return Optional.empty(); @@ -352,7 +350,8 @@ private Style getStyleOfFirstCharacterOrEmpty(Component component) { */ @VisibleForTesting @Contract("_ -> new") - @NotNull Component stripStyleOfFirstCharacter(@NotNull Component component) { + @NotNull + Component stripStyleOfFirstCharacter(@NotNull Component component) { if (component instanceof TextComponent) { TextComponent textComponent = (TextComponent) component; if (!textComponent.content().isEmpty()) { @@ -584,51 +583,6 @@ private List handleChildren(Component parent, List childre return accumulator; } - /** - * Find the indexes of all root "[pattern][/pattern]" tags in the given string. - *

- * Only the root tags are included, that is, nested tags are ignored. - * For example, [pattern][pattern][/pattern][/pattern] would only - * return the indexes for the outer tags. - *

- * Each array in the returned list corresponds to a different set of opening and closing tags, - * and has size 4. - * Indexes have the following meaning: - *

    - *
  • 0: the first character of the opening tag
  • - *
  • 1: the character after the last character of the closing tag
  • - *
  • 2: the character after the last character of the opening tag
  • - *
  • 3: the first character of the closing tag
  • - *
- * - * @param input The string to search for opening and closing tags. - * @param pattern The tags to search for (i.e. "lang" will search for "[lang]" and "[/lang]"). - * @return A list of indexes of all the found tags, as specified by the method description. - */ - public List getPatternIndexArray(String input, String pattern) { - List result = new ArrayList<>(); - int start = -1; - int openedAmount = 0; - - for (int i = 0; i < input.length(); i++) { - char currentChar = input.charAt(i); - if (currentChar == '[' && input.length() > i + pattern.length() + 1 && input.substring(i + 1, - i + 2 + pattern.length()).equals(pattern + "]")) { - if (start == -1) start = i; - openedAmount++; - i += 1 + pattern.length(); - } else if (currentChar == '[' && input.length() > i + pattern.length() + 2 && input.substring(i + 1, - i + 3 + pattern.length()).equals("/" + pattern + "]")) { - openedAmount--; - if (openedAmount == 0) { - result.add(new Integer[]{start, i + 3 + pattern.length(), start + pattern.length() + 2, i}); - start = -1; - } - } - } - return result; - } - /** * Adventure has an issue where some components might not become empty * components, even though they should be. This is a fix for that, while diff --git a/core/src/main/java/com/rexcantor64/triton/language/parser/LegacyParser.java b/core/src/main/java/com/rexcantor64/triton/language/parser/LegacyParser.java index 761decaf..53f4a6d3 100644 --- a/core/src/main/java/com/rexcantor64/triton/language/parser/LegacyParser.java +++ b/core/src/main/java/com/rexcantor64/triton/language/parser/LegacyParser.java @@ -4,8 +4,8 @@ import com.rexcantor64.triton.api.config.FeatureSyntax; import com.rexcantor64.triton.api.language.Localized; import com.rexcantor64.triton.api.language.MessageParser; -import com.rexcantor64.triton.api.language.TranslationResult; import com.rexcantor64.triton.utils.ComponentUtils; +import com.rexcantor64.triton.utils.ParserUtils; import lombok.AccessLevel; import lombok.Getter; import lombok.Setter; @@ -37,6 +37,7 @@ import java.util.Arrays; import java.util.HashMap; import java.util.List; +import java.util.Optional; import java.util.UUID; import java.util.stream.Collectors; @@ -130,8 +131,78 @@ TranslationResult translateComponent( @NotNull SerializedComponent component, @NotNull TranslationConfiguration configuration ) { - // TODO! - return null; + String text = component.getText(); + val indexes = ParserUtils.getPatternIndexArray(text, configuration.getFeatureSyntax().getLang()); + + if (indexes.isEmpty()) { + // TODO handle non text components + return TranslationResult.unchanged(); + } + + val builder = new StringBuilder(); + // keep track of last index added to the string builder + int lastCharacter = 0; + + for (val index : indexes) { + builder.append(text, lastCharacter, index[0]); + lastCharacter = index[1]; + + val placeholder = text.substring(index[2], index[3]); + + val resultComponent = handlePlaceholder(placeholder, configuration); + if (!resultComponent.isPresent()) { + return TranslationResult.remove(); + } + + builder.append(resultComponent.get().getText()); + component.importFromComponent(resultComponent.get()); + } + + builder.append(text, lastCharacter, text.length()); + component.setText(builder.toString()); + + // TODO handle non text components + + return TranslationResult.changed(component); + } + + /** + * An auxiliary method to {@link LegacyParser#translateComponent(SerializedComponent, TranslationConfiguration)} + * that handles translating the component inside the [lang][/lang] tags. + * The [args][/args] tags are optional since Triton v4.0.0. + *

+ * This method gets the translation for the key and replaces its arguments, if any. + * + * @param placeholder The text inside the [lang][/lang] tags. + * @param configuration The settings to apply to this translation. + * @return The translation of this placeholder. Empty optional if the translation is "disabled line". + * @since 4.0.0 + */ + private @NotNull Optional handlePlaceholder( + @NotNull String placeholder, + @NotNull TranslationConfiguration configuration + ) { + val indexes = ParserUtils.getPatternIndexArray(placeholder, configuration.getFeatureSyntax().getArg()); + + SerializedComponent[] arguments = indexes.stream() + .map(index -> placeholder.substring(index[2], index[3])) + .map(SerializedComponent::new) + .toArray(SerializedComponent[]::new); + + String key = placeholder; + if (!indexes.isEmpty()) { + key = key.substring(0, indexes.get(0)[0]); + } + key = ParserUtils.normalizeTranslationKey(key, configuration); + + val result = configuration.translationSupplier.apply(key, arguments); + + TranslationResult translationResult = translateComponent(result, configuration); + if (translationResult.getState() == TranslationResult.ResultState.TO_REMOVE) { + return Optional.empty(); + } + + return Optional.of(translationResult.getResult().orElse(result)); } public @NotNull String replaceArguments(@NotNull String text, @Nullable String @NotNull [] arguments) { diff --git a/core/src/main/java/com/rexcantor64/triton/utils/ParserUtils.java b/core/src/main/java/com/rexcantor64/triton/utils/ParserUtils.java new file mode 100644 index 00000000..828c121e --- /dev/null +++ b/core/src/main/java/com/rexcantor64/triton/utils/ParserUtils.java @@ -0,0 +1,81 @@ +package com.rexcantor64.triton.utils; + +import com.rexcantor64.triton.language.parser.TranslationConfiguration; +import lombok.val; + +import java.util.ArrayList; +import java.util.List; + +/** + * Methods used in implementations of {@link com.rexcantor64.triton.api.language.MessageParser}. + * + * @since 4.0.0 + */ +public class ParserUtils { + + /** + * Find the indexes of all root "[pattern][/pattern]" tags in the given string. + *

+ * Only the root tags are included, that is, nested tags are ignored. + * For example, [pattern][pattern][/pattern][/pattern] would only + * return the indexes for the outer tags. + *

+ * Each array in the returned list corresponds to a different set of opening and closing tags, + * and has size 4. + * Indexes have the following meaning: + *

    + *
  • 0: the first character of the opening tag
  • + *
  • 1: the character after the last character of the closing tag
  • + *
  • 2: the character after the last character of the opening tag
  • + *
  • 3: the first character of the closing tag
  • + *
+ * + * @param input The string to search for opening and closing tags. + * @param pattern The tags to search for (i.e. "lang" will search for "[lang]" and "[/lang]"). + * @return A list of indexes of all the found tags, as specified by the method description. + */ + public static List getPatternIndexArray(String input, String pattern) { + List result = new ArrayList<>(); + int start = -1; + int openedAmount = 0; + + for (int i = 0; i < input.length(); i++) { + char currentChar = input.charAt(i); + if (currentChar == '[' && input.length() > i + pattern.length() + 1 && input.substring(i + 1, + i + 2 + pattern.length()).equals(pattern + "]")) { + if (start == -1) start = i; + openedAmount++; + i += 1 + pattern.length(); + } else if (currentChar == '[' && input.length() > i + pattern.length() + 2 && input.substring(i + 1, + i + 3 + pattern.length()).equals("/" + pattern + "]")) { + openedAmount--; + if (openedAmount == 0) { + result.add(new Integer[]{start, i + 3 + pattern.length(), start + pattern.length() + 2, i}); + start = -1; + } + } + } + return result; + } + + /** + * Removes legacy [args][/args] tags from (the end of) translation keys. + * Since v4.0.0, these tags are no longer needed and are therefore deprecated. + * For backwards compatibility, ignore them. + * + * @param key The key, potentially ending in [args], [/args], or both. + * @param configuration The settings being applied while translating the placeholder with this key. + * @return The key with the [args][/args] removed. + */ + public static String normalizeTranslationKey(String key, TranslationConfiguration configuration) { + val syntax = configuration.getFeatureSyntax().getArgs(); + // The [args] tag is optional since v4.0.0, so strip it if it's present + if (key.endsWith("[/" + syntax + "]")) { + key = key.substring(0, key.length() - syntax.length() - 3); + } + if (key.endsWith("[" + configuration.getFeatureSyntax().getArgs() + "]")) { + key = key.substring(0, key.length() - syntax.length() - 2); + } + return key; + } +} diff --git a/core/src/test/java/com/rexcantor64/triton/language/parser/AdventureParserTest.java b/core/src/test/java/com/rexcantor64/triton/language/parser/AdventureParserTest.java index 598b4690..06dfc75a 100644 --- a/core/src/test/java/com/rexcantor64/triton/language/parser/AdventureParserTest.java +++ b/core/src/test/java/com/rexcantor64/triton/language/parser/AdventureParserTest.java @@ -1182,23 +1182,6 @@ public void testSplitComponentWithSingleNonTextComponentWithRepeatedIndexes() { } } - @Test - public void testGetPatternIndexArray() { - String input = "Lorem ipsum [tag]dolor [tag]sit[/tag] amet[/tag], [tag2]consectetur[/tag2] [tag]adipiscing elit[/tag]. Nullam posuere."; - - List result = parser.getPatternIndexArray(input, "tag"); - - List expected = Arrays.asList( - new Integer[]{12, 48, 17, 42}, - new Integer[]{75, 101, 80, 95} - ); - - assertEquals(expected.size(), result.size()); - for (int i = 0; i < expected.size(); i++) { - assertArrayEquals(expected.get(i), result.get(i)); - } - } - @Test public void testGetStyleOfFirstStyle() { Component comp = Component.text() diff --git a/core/src/test/java/com/rexcantor64/triton/utils/ParserUtilsTest.java b/core/src/test/java/com/rexcantor64/triton/utils/ParserUtilsTest.java new file mode 100644 index 00000000..293cd6ad --- /dev/null +++ b/core/src/test/java/com/rexcantor64/triton/utils/ParserUtilsTest.java @@ -0,0 +1,29 @@ +package com.rexcantor64.triton.utils; + +import org.junit.jupiter.api.Test; + +import java.util.Arrays; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class ParserUtilsTest { + + @Test + public void testGetPatternIndexArray() { + String input = "Lorem ipsum [tag]dolor [tag]sit[/tag] amet[/tag], [tag2]consectetur[/tag2] [tag]adipiscing elit[/tag]. Nullam posuere."; + + List result = ParserUtils.getPatternIndexArray(input, "tag"); + + List expected = Arrays.asList( + new Integer[]{12, 48, 17, 42}, + new Integer[]{75, 101, 80, 95} + ); + + assertEquals(expected.size(), result.size()); + for (int i = 0; i < expected.size(); i++) { + assertArrayEquals(expected.get(i), result.get(i)); + } + } +} diff --git a/triton-spigot/src/main/java/com/rexcantor64/triton/spigot/listeners/BukkitListener.java b/triton-spigot/src/main/java/com/rexcantor64/triton/spigot/listeners/BukkitListener.java index d98d5bc9..069801ae 100644 --- a/triton-spigot/src/main/java/com/rexcantor64/triton/spigot/listeners/BukkitListener.java +++ b/triton-spigot/src/main/java/com/rexcantor64/triton/spigot/listeners/BukkitListener.java @@ -3,6 +3,7 @@ import com.rexcantor64.triton.Triton; import com.rexcantor64.triton.language.parser.AdventureParser; import com.rexcantor64.triton.spigot.SpigotTriton; +import com.rexcantor64.triton.utils.ParserUtils; import lombok.val; import org.bukkit.event.EventHandler; import org.bukkit.event.EventPriority; @@ -57,7 +58,7 @@ public void onChat(AsyncPlayerChatEvent e) { } String msg = e.getMessage(); - val indexes = parser().getPatternIndexArray(msg, Triton.get().getConfig().getChatSyntax().getLang()); + val indexes = ParserUtils.getPatternIndexArray(msg, Triton.get().getConfig().getChatSyntax().getLang()); for (int i = 0; i < indexes.size(); ++i) { val index = indexes.get(i); // add a zero width space to prevent the parser from finding this placeholder From e0a101c41692f568523cb921c960dbb2f4b89c69 Mon Sep 17 00:00:00 2001 From: Diogo Correia Date: Wed, 28 Aug 2024 11:41:28 +0200 Subject: [PATCH 6/8] wip: test: add tests for legacy parser --- .../triton/language/parser/LegacyParser.java | 35 ++++- .../language/parser/LegacyParserTest.java | 146 +++++++++++++++++- 2 files changed, 177 insertions(+), 4 deletions(-) diff --git a/core/src/main/java/com/rexcantor64/triton/language/parser/LegacyParser.java b/core/src/main/java/com/rexcantor64/triton/language/parser/LegacyParser.java index 53f4a6d3..5a7e763c 100644 --- a/core/src/main/java/com/rexcantor64/triton/language/parser/LegacyParser.java +++ b/core/src/main/java/com/rexcantor64/triton/language/parser/LegacyParser.java @@ -29,6 +29,7 @@ import net.kyori.adventure.text.serializer.legacy.CharacterAndFormat; import net.kyori.adventure.text.serializer.legacy.Reset; import org.intellij.lang.annotations.Subst; +import org.jetbrains.annotations.Contract; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.VisibleForTesting; @@ -39,6 +40,7 @@ import java.util.List; import java.util.Optional; import java.util.UUID; +import java.util.regex.Pattern; import java.util.stream.Collectors; import static com.rexcantor64.triton.language.TranslationManager.JSON_TYPE_TAG; @@ -66,6 +68,10 @@ public class LegacyParser implements MessageParser { private static final char HEX_PREFIX = '#'; private static final char HEX_CODE = 'x'; private static final String VALID_COLOR_CODES = "0123456789AaBbCcDdEeFfKkLlMmNnOoRrXx"; + private static final Pattern FORMATTING_STRIP_PATTERN = Pattern.compile( + CLICK_DELIM + "\\d[\\w-]{36}|" + HOVER_DELIM + "[\\w-]{36}|" + CLICK_END_DELIM + "|" + HOVER_END_DELIM + + "|" + SECTION_CHAR + "[0-9A-Fa-fK-Ok-oRrXx]" + ); /** * @see MessageParser#translateString(String, Localized, FeatureSyntax) @@ -194,6 +200,7 @@ TranslationResult translateComponent( key = key.substring(0, indexes.get(0)[0]); } key = ParserUtils.normalizeTranslationKey(key, configuration); + key = stripFormatting(key); val result = configuration.translationSupplier.apply(key, arguments); @@ -212,7 +219,9 @@ TranslationResult translateComponent( return text; } - private @NotNull SerializedComponent replaceArguments(@NotNull SerializedComponent comp, @NotNull SerializedComponent @NotNull ... arguments) { + @VisibleForTesting + @NotNull + SerializedComponent replaceArguments(@NotNull SerializedComponent comp, @NotNull SerializedComponent @NotNull ... arguments) { // Replace args in text String[] args = Arrays.stream(arguments).map(SerializedComponent::getText).toArray(String[]::new); comp.setText(replaceArguments(comp.getText(), args)); @@ -244,7 +253,12 @@ TranslationResult translateComponent( * @param text The text to convert the color code characters from. * @return The input text with the color codes replaced. */ - private String translateAlternateColorCodes(String text) { + @Contract("null -> null; !null -> !null") + private @Nullable String translateAlternateColorCodes(@Nullable String text) { + if (text == null) { + return null; + } + char[] chars = text.toCharArray(); for (int i = 0; i < chars.length - 1; i++) { if (chars[i] == AMPERSAND_CHAR && VALID_COLOR_CODES.indexOf(chars[i + 1]) != -1) { @@ -254,6 +268,23 @@ private String translateAlternateColorCodes(String text) { return new String(chars); } + /** + * Remove color code and click/hover event formatting in the text of {@link SerializedComponent}. + * + * @param text The text to remove the formatting from. + * @return The text without the associated formatting. + */ + @Contract("null -> null; !null -> !null") + @VisibleForTesting + @Nullable + String stripFormatting(@Nullable String text) { + if (text == null) { + return null; + } + + return FORMATTING_STRIP_PATTERN.matcher(text).replaceAll(""); + } + /** * Represents a {@link Component} but as a String and with additional storage for the * values of click/hover events and translatable components. diff --git a/core/src/test/java/com/rexcantor64/triton/language/parser/LegacyParserTest.java b/core/src/test/java/com/rexcantor64/triton/language/parser/LegacyParserTest.java index 1126623a..91784398 100644 --- a/core/src/test/java/com/rexcantor64/triton/language/parser/LegacyParserTest.java +++ b/core/src/test/java/com/rexcantor64/triton/language/parser/LegacyParserTest.java @@ -1,5 +1,8 @@ package com.rexcantor64.triton.language.parser; +import com.rexcantor64.triton.api.config.FeatureSyntax; +import com.rexcantor64.triton.language.parser.LegacyParser.SerializedComponent; +import com.rexcantor64.triton.utils.DefaultFeatureSyntax; import net.kyori.adventure.key.Key; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.event.ClickEvent; @@ -11,10 +14,43 @@ import java.util.ArrayList; import java.util.List; +import java.util.function.Function; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; public class LegacyParserTest { + private final LegacyParser parser = new LegacyParser(); + private final FeatureSyntax defaultSyntax = new DefaultFeatureSyntax(); + + private final Function messageResolver = (key) -> { + switch (key) { + case "without.formatting": + return new SerializedComponent("This is text without formatting"); + case "without.formatting.with.args": + return new SerializedComponent("This is text without formatting but with arguments (%1)"); + case "with.colors": + return new SerializedComponent("§aThis text is green"); + case "with.colors.two.args": + return new SerializedComponent("§dThis text is pink and has two arguments (%1 and %2)"); + case "with.colors.repeated.args": + return new SerializedComponent("§dThis text is pink and has three arguments (%1 and %2 and %1)"); + case "nested": + return new SerializedComponent("some text [lang]without.formatting[/lang]"); + case "with.placeholder.colors": + return new SerializedComponent("§d%1 §ais a very cool guy"); + case "change.colors.on.args": + return new SerializedComponent("§cSome text §9%1 more text"); + } + return new SerializedComponent("unknown placeholder"); + }; + + private final TranslationConfiguration configuration = new TranslationConfiguration<>( + defaultSyntax, + "disabled.line", + (key, args) -> parser.replaceArguments(messageResolver.apply(key), args) + ); + private final Component ALL_IN_ONE_COMPONENT = Component.text("Lorem ") .append( Component.text("ipsum dolor ") @@ -51,7 +87,7 @@ public class LegacyParserTest { @Test public void testSerializingComponent() { - LegacyParser.SerializedComponent serializedComponent = new LegacyParser.SerializedComponent(ALL_IN_ONE_COMPONENT); + SerializedComponent serializedComponent = new SerializedComponent(ALL_IN_ONE_COMPONENT); assertEquals(1, serializedComponent.getClickEvents().size()); assertEquals(1, serializedComponent.getHoverEvents().size()); @@ -70,7 +106,7 @@ public void testSerializingComponent() { @Test public void testSerializeDeserializeComponent() { - Component result = new LegacyParser.SerializedComponent(ALL_IN_ONE_COMPONENT).toComponent(); + Component result = new SerializedComponent(ALL_IN_ONE_COMPONENT).toComponent(); // slightly modify input to equivalent component, due to behaviour of the deserializer List expectedChildren = new ArrayList<>(ALL_IN_ONE_COMPONENT.children()); @@ -89,4 +125,110 @@ public void testSerializeDeserializeComponent() { assertEquals(expected.compact(), result.compact()); } + + @Test + public void testStripFormatting() { + Component component = Component.text() + .content("abc") + .append( + Component.text() + .content("def") + .color(NamedTextColor.AQUA) + ) + .append(Component.text("ghi")) + .color(NamedTextColor.GOLD) + .clickEvent(ClickEvent.copyToClipboard("Lorem Ipsum")) + .hoverEvent(HoverEvent.showText(Component.text("Lorem Ipsum"))) + .build(); + + SerializedComponent serializedComponent = new SerializedComponent(component); + + String result = parser.stripFormatting(serializedComponent.getText()); + + assertEquals("abcdefghi", result); + } + + @Test + public void testTranslateComponentWithoutPlaceholders() { + SerializedComponent comp = new SerializedComponent("Text without any placeholders whatsoever"); + + TranslationResult result = parser.translateComponent(comp, configuration); + + assertEquals(TranslationResult.ResultState.UNCHANGED, result.getState()); + } + + @Test + public void testParseComponentWithoutFormatting() { + SerializedComponent comp = new SerializedComponent("Text [lang]without.formatting[/lang] more text"); + + TranslationResult result = parser.translateComponent(comp, configuration) + .map(SerializedComponent::toComponent); + + Component expected = Component.text("Text This is text without formatting more text"); + + assertEquals(TranslationResult.ResultState.CHANGED, result.getState()); + assertNotNull(result.getResultRaw()); + assertEquals(expected.compact(), result.getResultRaw().compact()); + } + + @Test + public void testParseComponentStripFormattingFromKey() { + SerializedComponent comp = new SerializedComponent("Text [lang]with§bout.formatting[/lang] more text"); + + TranslationResult result = parser.translateComponent(comp, configuration) + .map(SerializedComponent::toComponent); + + Component expected = Component.text("Text This is text without formatting more text"); + + assertEquals(TranslationResult.ResultState.CHANGED, result.getState()); + assertNotNull(result.getResultRaw()); + assertEquals(expected.compact(), result.getResultRaw().compact()); + } + + @Test + public void testParseComponentStyleSpillFromPlaceholder() { + SerializedComponent comp = new SerializedComponent("Text [lang]with.colors[/lang] lorem ipsum [lang]without.formatting[/lang] more text"); + + TranslationResult result = parser.translateComponent(comp, configuration) + .map(SerializedComponent::toComponent); + + Component expected = Component.text() + .content("Text ") + .append( + Component.text() + .content("This text is green lorem ipsum This is text without formatting more text") + .color(NamedTextColor.GREEN) + ) + .build(); + + assertEquals(TranslationResult.ResultState.CHANGED, result.getState()); + assertNotNull(result.getResultRaw()); + assertEquals(expected.compact(), result.getResultRaw().compact()); + } + + @Test + public void testParseComponentStyleSpillFromArgument() { + SerializedComponent comp = new SerializedComponent("Text §4[lang]without.formatting.with.args[args][arg]§agreen text[/arg][/args][/lang] more text"); + + TranslationResult result = parser.translateComponent(comp, configuration) + .map(SerializedComponent::toComponent); + + Component expected = Component.text() + .content("Text ") + .append( + Component.text() + .content("This is text without formatting but with arguments (") + .color(NamedTextColor.DARK_RED) + ) + .append( + Component.text() + .content("green text) more text") + .color(NamedTextColor.GREEN) + ) + .build(); + + assertEquals(TranslationResult.ResultState.CHANGED, result.getState()); + assertNotNull(result.getResultRaw()); + assertEquals(expected.compact(), result.getResultRaw().compact()); + } } From 032a9f98550d7ef36a11875559545f1a58b0b518 Mon Sep 17 00:00:00 2001 From: Diogo Correia Date: Thu, 23 Jan 2025 17:38:37 +0100 Subject: [PATCH 7/8] chore: bump adventure to 4.18.0 See https://github.com/KyoriPowered/adventure/releases/tag/v4.18.0 --- core/build.gradle | 16 +++++------ .../triton/dependencies/Dependency.java | 28 +++++++++---------- triton-spigot/build.gradle | 4 +-- 3 files changed, 24 insertions(+), 24 deletions(-) diff --git a/core/build.gradle b/core/build.gradle index a3b51c3c..92d2597b 100644 --- a/core/build.gradle +++ b/core/build.gradle @@ -14,10 +14,10 @@ dependencies { compileOnly project(":api") // Adventure / MiniMessage - compileOnly 'net.kyori:adventure-text-serializer-gson:4.17.0' - compileOnly 'net.kyori:adventure-text-serializer-legacy:4.17.0' - compileOnly 'net.kyori:adventure-text-serializer-plain:4.17.0' - compileOnly('net.kyori:adventure-text-minimessage:4.17.0') { + compileOnly 'net.kyori:adventure-text-serializer-gson:4.18.0' + compileOnly 'net.kyori:adventure-text-serializer-legacy:4.18.0' + compileOnly 'net.kyori:adventure-text-serializer-plain:4.18.0' + compileOnly('net.kyori:adventure-text-minimessage:4.18.0') { exclude group: 'net.kyori', module: 'adventure-api' exclude group: 'net.kyori', module: 'adventure-key' exclude group: 'net.kyori', module: 'adventure-text-serialize-gson' @@ -48,10 +48,10 @@ dependencies { testImplementation 'org.junit.jupiter:junit-jupiter-api:5.9.0' testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.9.0' - testImplementation 'net.kyori:adventure-api:4.17.0' - testImplementation 'net.kyori:adventure-text-serializer-gson:4.17.0' - testImplementation 'net.kyori:adventure-text-serializer-legacy:4.17.0' - testImplementation 'net.kyori:adventure-text-serializer-plain:4.17.0' + testImplementation 'net.kyori:adventure-api:4.18.0' + testImplementation 'net.kyori:adventure-text-serializer-gson:4.18.0' + testImplementation 'net.kyori:adventure-text-serializer-legacy:4.18.0' + testImplementation 'net.kyori:adventure-text-serializer-plain:4.18.0' } shadowJar { diff --git a/core/src/main/java/com/rexcantor64/triton/dependencies/Dependency.java b/core/src/main/java/com/rexcantor64/triton/dependencies/Dependency.java index dcbe9968..82c859af 100644 --- a/core/src/main/java/com/rexcantor64/triton/dependencies/Dependency.java +++ b/core/src/main/java/com/rexcantor64/triton/dependencies/Dependency.java @@ -16,16 +16,16 @@ public enum Dependency { ADVENTURE( "net{}kyori", "adventure-api", - "4.17.0", - "FcjC6xpp2LG8kU9VQ1PajufPB0wFyAdNqYmK7lxw0Ng=", + "4.18.0", + "B5+PYDDzIMNxWau11V0Qaxc2tEyQ6fekKIaoEYPaw/w=", relocate("net{}kyori{}adventure", "adventure"), relocate("net{}kyori{}examination", "kyori{}examination") ), ADVENTURE_TEXT_SERIALIZER_GSON( "net{}kyori", "adventure-text-serializer-gson", - "4.17.0", - "YifWv9LWrm0SHT3NpZDTP/g3POpliUhd8/e3Wwnn0hA=", + "4.18.0", + "zPaBB6/mWl9vjYImmLAC5G4F4QrKb7WAGn+GoO1x+T0=", relocate("net{}kyori{}option", "kyori{}option"), relocateIf("net{}kyori{}adventure", "adventure", LoaderFlag.RELOCATE_ADVENTURE), relocateIf("net{}kyori{}examination", "kyori{}examination", LoaderFlag.RELOCATE_ADVENTURE), @@ -35,8 +35,8 @@ public enum Dependency { ADVENTURE_TEXT_SERIALIZER_LEGACY( "net{}kyori", "adventure-text-serializer-legacy", - "4.17.0", - "nUlVVC5A61tB2+0XKygmrPHL40BeAC8zYOsupXjVNro=", + "4.18.0", + "NuCPIYknxH3KHJAwavsx8fC7v8cePqWLD7/h1wIMaWU=", relocateIf("net{}kyori{}adventure", "adventure", LoaderFlag.RELOCATE_ADVENTURE), relocateIf("net{}kyori{}examination", "kyori{}examination", LoaderFlag.RELOCATE_ADVENTURE), relocateIfNot("net{}kyori{}adventure{}text{}serializer{}legacy", "adventure{}text{}serializer{}legacy", LoaderFlag.RELOCATE_ADVENTURE) @@ -44,8 +44,8 @@ public enum Dependency { ADVENTURE_TEXT_SERIALIZER_PLAIN( "net{}kyori", "adventure-text-serializer-plain", - "4.17.0", - "Im1KPSxCt3J98RoCppjGxkR1OEcF3RK/Asf2l0R5nC4=", + "4.18.0", + "Ff+eI31b8gDsrOtX6DBJ0jdlaQu4zjpTIKo31LXLo9o=", relocateIf("net{}kyori{}adventure", "adventure", LoaderFlag.RELOCATE_ADVENTURE), relocateIf("net{}kyori{}examination", "kyori{}examination", LoaderFlag.RELOCATE_ADVENTURE), relocateIfNot("net{}kyori{}adventure{}text{}serializer{}plain", "adventure{}text{}serializer{}plain", LoaderFlag.RELOCATE_ADVENTURE) @@ -66,8 +66,8 @@ public enum Dependency { ADVENTURE_MINI_MESSAGE( "net{}kyori", "adventure-text-minimessage", - "4.17.0", - "g3Kp6bwMCuIdYObObihKgcbJbR7DmPgIglAVxvYCUvU=", + "4.18.0", + "RE5xCOKwMNAtEtNkOVYzUMz88s9qj1DbYFf8dqFtCr8=", relocate("net{}kyori{}option", "kyori{}option"), relocateIf("net{}kyori{}adventure", "adventure", LoaderFlag.RELOCATE_ADVENTURE), relocateIf("net{}kyori{}examination", "kyori{}examination", LoaderFlag.RELOCATE_ADVENTURE), @@ -81,16 +81,16 @@ public enum Dependency { ADVENTURE_KEY( "net{}kyori", "adventure-key", - "4.17.0", - "jlz1cGEtbM7flDrBcWpEneLdWpAge33m9zwCNpNbdm4=", + "4.18.0", + "KzLdH5mHr43NC/Wrjpb8Pe6sA/hdUym1iNmLgGIw5S0=", relocate("net{}kyori{}adventure", "adventure"), relocateIf("net{}kyori{}examination", "kyori{}examination", LoaderFlag.RELOCATE_ADVENTURE) ), ADVENTURE_TEXT_SERIALIZER_JSON( "net{}kyori", "adventure-text-serializer-json", - "4.17.0", - "bJHT4jsGTXarfBVOvpQPVSIyrheLldIbUN/c/sKSXZY=", + "4.18.0", + "ZFBB/KVQs78by4UItylCzBXqGDd7Uc65bQc+30Mutm8=", relocate("net{}kyori{}option", "kyori{}option"), relocateIf("net{}kyori{}adventure", "adventure", LoaderFlag.RELOCATE_ADVENTURE), relocateIf("net{}kyori{}examination", "kyori{}examination", LoaderFlag.RELOCATE_ADVENTURE), diff --git a/triton-spigot/build.gradle b/triton-spigot/build.gradle index 9041be94..64a1ce4a 100644 --- a/triton-spigot/build.gradle +++ b/triton-spigot/build.gradle @@ -17,8 +17,8 @@ dependencies { compileOnly 'org.spigotmc:spigot-api:1.21.1-R0.1-SNAPSHOT' - compileOnly 'net.kyori:adventure-text-serializer-gson:4.17.0' - compileOnly 'net.kyori:adventure-text-serializer-legacy:4.17.0' + compileOnly 'net.kyori:adventure-text-serializer-gson:4.18.0' + compileOnly 'net.kyori:adventure-text-serializer-legacy:4.18.0' compileOnly 'net.kyori:adventure-text-serializer-bungeecord:4.3.4' compileOnly 'com.comphenix.protocol:ProtocolLib:5.4.0-SNAPSHOT' From a9c665e0660b101d869824aa2bbda009034532e9 Mon Sep 17 00:00:00 2001 From: Diogo Correia Date: Thu, 23 Jan 2025 19:57:39 +0100 Subject: [PATCH 8/8] feat: support 1.21.4+ shadowColor in legacy parser See #470 --- .../triton/language/parser/LegacyParser.java | 16 ++++++++++++++++ .../triton/language/parser/LegacyParserTest.java | 10 ++++++++-- 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/core/src/main/java/com/rexcantor64/triton/language/parser/LegacyParser.java b/core/src/main/java/com/rexcantor64/triton/language/parser/LegacyParser.java index 5a7e763c..0f2bfadf 100644 --- a/core/src/main/java/com/rexcantor64/triton/language/parser/LegacyParser.java +++ b/core/src/main/java/com/rexcantor64/triton/language/parser/LegacyParser.java @@ -20,6 +20,7 @@ import net.kyori.adventure.text.flattener.ComponentFlattener; import net.kyori.adventure.text.flattener.FlattenerListener; import net.kyori.adventure.text.format.NamedTextColor; +import net.kyori.adventure.text.format.ShadowColor; import net.kyori.adventure.text.format.Style; import net.kyori.adventure.text.format.TextColor; import net.kyori.adventure.text.format.TextDecoration; @@ -67,6 +68,7 @@ public class LegacyParser implements MessageParser { private static final char SECTION_CHAR = '§'; private static final char HEX_PREFIX = '#'; private static final char HEX_CODE = 'x'; + private static final char SHADOW_COLOR_CODE = 's'; private static final String VALID_COLOR_CODES = "0123456789AaBbCcDdEeFfKkLlMmNnOoRrXx"; private static final Pattern FORMATTING_STRIP_PATTERN = Pattern.compile( CLICK_DELIM + "\\d[\\w-]{36}|" + HOVER_DELIM + "[\\w-]{36}|" + CLICK_END_DELIM + "|" + HOVER_END_DELIM @@ -361,6 +363,13 @@ public SerializedComponent(String legacyText) { val color = text.substring(i + 1, i + 13); format = TextColor.fromHexString(HEX_PREFIX + color.replace(String.valueOf(SECTION_CHAR), "")); i += 12; + } else if (lowercaseChar == SHADOW_COLOR_CODE && i + 9 < text.length()) { + @Subst("#ffffffff") val color = text.substring(i + 1, i + 10); + val shadowColor = ShadowColor.fromHexString(color); + currentStyle = currentStyle.shadowColor(shadowColor); + componentBuilder.style(currentStyle); + i += 9; + continue; } else { format = CharacterAndFormat.defaults().stream() .filter(characterAndFormat -> characterAndFormat.character() == lowercaseChar) @@ -552,6 +561,13 @@ public void pushStyle(@NotNull Style style) { this.stringBuilder.append(formatToString(color)); } + @Nullable val shadowColor = style.shadowColor(); + if (shadowColor != null) { + this.stringBuilder.append(SECTION_CHAR).append('s'); + // format: #RRGGBBAA + this.stringBuilder.append(shadowColor.asHexString()); + } + style.decorations().entrySet().stream() .filter(entry -> entry.getValue() == TextDecoration.State.TRUE) .forEach(entry -> this.stringBuilder.append(formatToString(entry.getKey()))); diff --git a/core/src/test/java/com/rexcantor64/triton/language/parser/LegacyParserTest.java b/core/src/test/java/com/rexcantor64/triton/language/parser/LegacyParserTest.java index 91784398..198eba97 100644 --- a/core/src/test/java/com/rexcantor64/triton/language/parser/LegacyParserTest.java +++ b/core/src/test/java/com/rexcantor64/triton/language/parser/LegacyParserTest.java @@ -8,6 +8,7 @@ import net.kyori.adventure.text.event.ClickEvent; import net.kyori.adventure.text.event.HoverEvent; import net.kyori.adventure.text.format.NamedTextColor; +import net.kyori.adventure.text.format.ShadowColor; import net.kyori.adventure.text.format.TextColor; import net.kyori.adventure.text.format.TextDecoration; import org.junit.jupiter.api.Test; @@ -56,6 +57,7 @@ public class LegacyParserTest { Component.text("ipsum dolor ") .color(NamedTextColor.BLACK) .decorate(TextDecoration.BOLD) + .shadowColor(ShadowColor.shadowColor(0xdd, 0xee, 0xff, 0x44)) ) .append( Component.text("sit amet,") @@ -74,6 +76,10 @@ public class LegacyParserTest { .hoverEvent(HoverEvent.showText(Component.text("hello world"))) ) ) + .append( + Component.text("Aenean tempor urna ac consequat sodales. ") + .shadowColor(ShadowColor.shadowColor(0xaa, 0xbb, 0xcc, 0x88)) + ) .append( Component.text("Maecenas imperdiet ") .color(NamedTextColor.AQUA) @@ -93,11 +99,11 @@ public void testSerializingComponent() { assertEquals(1, serializedComponent.getHoverEvents().size()); assertEquals(1, serializedComponent.getTranslatableComponents().size()); assertEquals( - "§rLorem §0§lipsum dolor §r§lsit amet,§x§a§a§b§b§c§c\uE4005" + "§rLorem §0§s#DDEEFF44§lipsum dolor §r§lsit amet,§x§a§a§b§b§c§c\uE4005" + serializedComponent.getClickEvents().keySet().iterator().next() + " consectetur §x§a§a§b§b§c§c\uE800minecraft:default\uE802adipiscing \uE801§x§a§a§b§b§c§c\uE500" + serializedComponent.getHoverEvents().keySet().iterator().next() - + "elit. \uE501\uE401§bMaecenas imperdiet §b\uE600some.key\uE600" + + "elit. \uE501\uE401§r§s#AABBCC88Aenean tempor urna ac consequat sodales. §bMaecenas imperdiet §b\uE600some.key\uE600" + serializedComponent.getTranslatableComponents().keySet().iterator().next() + "\uE600", serializedComponent.getText()