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