diff --git a/src/main/java/gg/gemstone/component/ComponentParser.java b/src/main/java/gg/gemstone/component/ComponentParser.java
index 30d2598..e37a95b 100644
--- a/src/main/java/gg/gemstone/component/ComponentParser.java
+++ b/src/main/java/gg/gemstone/component/ComponentParser.java
@@ -78,6 +78,24 @@ static ComponentParser componentParserAmpersand() {
*/
@NotNull Component parse(@NotNull String input);
+ /**
+ * Escapes {@code input} so that no token recognized by this parser's translator chain or by
+ * {@link MiniMessage} will be interpreted on a subsequent {@link #parse(String)} call.
+ *
+ * @param input the raw input string
+ * @return the escaped string
+ */
+ @NotNull String escape(@NotNull String input);
+
+ /**
+ * Removes every token recognized by this parser's translator chain and by {@link MiniMessage}
+ * from {@code input}, leaving only plain text.
+ *
+ * @param input the raw input string
+ * @return the stripped string
+ */
+ @NotNull String strip(@NotNull String input);
+
/**
* Returns a new {@link Builder} pre-populated with this parser's translator chain
* and {@link MiniMessage} instance, allowing a modified copy to be built without
diff --git a/src/main/java/gg/gemstone/component/ComponentParserImpl.java b/src/main/java/gg/gemstone/component/ComponentParserImpl.java
index 9ac9f53..dac1f49 100644
--- a/src/main/java/gg/gemstone/component/ComponentParserImpl.java
+++ b/src/main/java/gg/gemstone/component/ComponentParserImpl.java
@@ -42,9 +42,35 @@ class ComponentParserImpl implements ComponentParser {
@Override
public @NotNull Component parse(@NotNull String input) {
+ requireNonNull(input, "input");
+
return miniMessage.deserialize(translate(input));
}
+ @Override
+ public @NotNull String escape(@NotNull String input) {
+ requireNonNull(input, "input");
+
+ String result = input;
+ for (MiniMessageTranslator translator : translators) {
+ result = translator.escape(result);
+ }
+
+ return miniMessage.escapeTags(result);
+ }
+
+ @Override
+ public @NotNull String strip(@NotNull String input) {
+ requireNonNull(input, "input");
+
+ String result = input;
+ for (MiniMessageTranslator translator : translators) {
+ result = translator.strip(result);
+ }
+
+ return miniMessage.stripTags(result);
+ }
+
@Override
public boolean equals(Object o) {
if (this == o) return true;
@@ -64,8 +90,6 @@ public ComponentParser.Builder toBuilder() {
@VisibleForTesting
@NotNull String translate(@NotNull String input) {
- requireNonNull(input, "input");
-
String result = input;
for (MiniMessageTranslator translator : translators) {
result = translator.translate(result);
diff --git a/src/main/java/gg/gemstone/component/translator/LegacyFormattingCodeTranslator.java b/src/main/java/gg/gemstone/component/translator/LegacyFormattingCodeTranslator.java
index f5addda..95a9e45 100644
--- a/src/main/java/gg/gemstone/component/translator/LegacyFormattingCodeTranslator.java
+++ b/src/main/java/gg/gemstone/component/translator/LegacyFormattingCodeTranslator.java
@@ -94,6 +94,46 @@ class LegacyFormattingCodeTranslator implements MiniMessageTranslator {
this.sectionChar = sectionChar;
}
+ @Override
+ public @NotNull String escape(@NotNull String input) {
+ return rewriteLegacyCodes(input, true);
+ }
+
+ @Override
+ public @NotNull String strip(@NotNull String input) {
+ return rewriteLegacyCodes(input, false);
+ }
+
+ private String rewriteLegacyCodes(String input, boolean escape) {
+ if (input.isEmpty()) {
+ return input;
+ }
+
+ StringBuilder output = new StringBuilder(input.length());
+
+ int i = 0;
+ while (i < input.length()) {
+ char c = input.charAt(i);
+
+ if (c == sectionChar && i + 1 < input.length()) {
+ char code = Character.toLowerCase(input.charAt(i + 1));
+
+ if (code == RESET_CODE || COLOR_CODES.containsKey(code) || FORMAT_CODES.containsKey(code)) {
+ if (escape) {
+ output.append(sectionChar).append('\\').append(input.charAt(i + 1));
+ }
+ i += 2;
+ continue;
+ }
+ }
+
+ output.append(c);
+ i++;
+ }
+
+ return output.toString();
+ }
+
@Override
public @NotNull String translate(@NotNull String input) {
if (input.isEmpty()) {
diff --git a/src/main/java/gg/gemstone/component/translator/MiniMessageTranslator.java b/src/main/java/gg/gemstone/component/translator/MiniMessageTranslator.java
index f48fdab..8f6d240 100644
--- a/src/main/java/gg/gemstone/component/translator/MiniMessageTranslator.java
+++ b/src/main/java/gg/gemstone/component/translator/MiniMessageTranslator.java
@@ -35,4 +35,25 @@ public interface MiniMessageTranslator {
* @return the translated string, or {@code input} unchanged if no recognized patterns were found
*/
@NotNull String translate(@NotNull String input);
+
+ /**
+ * Escapes every pattern recognized by this translator so it will not be picked up by a
+ * subsequent {@link #translate(String)} call. The convention is to insert a backslash
+ * inside the recognized token.
+ *
+ *
Implementations must not re-escape tokens that have already been escaped by another
+ * translator earlier in the chain.
+ *
+ * @param input the string to escape
+ * @return the escaped string, or {@code input} unchanged if no recognized patterns were found
+ */
+ @NotNull String escape(@NotNull String input);
+
+ /**
+ * Removes every pattern recognized by this translator from {@code input}.
+ *
+ * @param input the string to strip
+ * @return the stripped string, or {@code input} unchanged if no recognized patterns were found
+ */
+ @NotNull String strip(@NotNull String input);
}
diff --git a/src/main/java/gg/gemstone/component/translator/MojangBoxedHexPatternTranslator.java b/src/main/java/gg/gemstone/component/translator/MojangBoxedHexPatternTranslator.java
index a4424fe..c4259b7 100644
--- a/src/main/java/gg/gemstone/component/translator/MojangBoxedHexPatternTranslator.java
+++ b/src/main/java/gg/gemstone/component/translator/MojangBoxedHexPatternTranslator.java
@@ -38,6 +38,12 @@ class MojangBoxedHexPatternTranslator implements MiniMessageTranslator {
*/
private static final String BOXED_HEX_REPLACEMENT = "<#$1>";
+ /**
+ * Escape replacement: {@code <RRGGBB>} becomes {@code <&\#RRGGBB>}, neutralising the pattern
+ * for both this translator and {@link MojangUnboxedHexPatternTranslator}.
+ */
+ private static final String BOXED_HEX_ESCAPE_REPLACEMENT = "<&\\\\#$1>";
+
/**
* Use {@link MiniMessageTranslators#MOJANG_BOXED_HEX}.
*/
@@ -48,4 +54,14 @@ class MojangBoxedHexPatternTranslator implements MiniMessageTranslator {
public @NotNull String translate(final @NotNull String input) {
return BOXED_MOJANG_PATTERN.matcher(input).replaceAll(BOXED_HEX_REPLACEMENT);
}
+
+ @Override
+ public @NotNull String escape(final @NotNull String input) {
+ return BOXED_MOJANG_PATTERN.matcher(input).replaceAll(BOXED_HEX_ESCAPE_REPLACEMENT);
+ }
+
+ @Override
+ public @NotNull String strip(final @NotNull String input) {
+ return BOXED_MOJANG_PATTERN.matcher(input).replaceAll("");
+ }
}
diff --git a/src/main/java/gg/gemstone/component/translator/MojangUnboxedHexPatternTranslator.java b/src/main/java/gg/gemstone/component/translator/MojangUnboxedHexPatternTranslator.java
index b511197..27b656f 100644
--- a/src/main/java/gg/gemstone/component/translator/MojangUnboxedHexPatternTranslator.java
+++ b/src/main/java/gg/gemstone/component/translator/MojangUnboxedHexPatternTranslator.java
@@ -40,6 +40,12 @@ class MojangUnboxedHexPatternTranslator implements MiniMessageTranslator {
*/
private static final String BOXED_HEX_REPLACEMENT = "<#$1>";
+ /**
+ * Escape replacement: {@code RRGGBB} becomes {@code &\#RRGGBB}, neutralising the pattern
+ * for this translator.
+ */
+ private static final String UNBOXED_HEX_ESCAPE_REPLACEMENT = "&\\\\#$1";
+
/**
* Use {@link MiniMessageTranslators#MOJANG_UNBOXED_HEX}.
*/
@@ -50,4 +56,14 @@ class MojangUnboxedHexPatternTranslator implements MiniMessageTranslator {
public @NotNull String translate(final @NotNull String input) {
return UNBOXED_MOJANG_PATTERN.matcher(input).replaceAll(BOXED_HEX_REPLACEMENT);
}
+
+ @Override
+ public @NotNull String escape(final @NotNull String input) {
+ return UNBOXED_MOJANG_PATTERN.matcher(input).replaceAll(UNBOXED_HEX_ESCAPE_REPLACEMENT);
+ }
+
+ @Override
+ public @NotNull String strip(final @NotNull String input) {
+ return UNBOXED_MOJANG_PATTERN.matcher(input).replaceAll("");
+ }
}
diff --git a/src/main/java/gg/gemstone/component/translator/UnboxedHexPatternTranslator.java b/src/main/java/gg/gemstone/component/translator/UnboxedHexPatternTranslator.java
index b59950c..1453c99 100644
--- a/src/main/java/gg/gemstone/component/translator/UnboxedHexPatternTranslator.java
+++ b/src/main/java/gg/gemstone/component/translator/UnboxedHexPatternTranslator.java
@@ -33,16 +33,23 @@ class UnboxedHexPatternTranslator implements MiniMessageTranslator {
/**
* Matches unboxed hex codes (e.g. {@code #FFFFFF}), excluding those already boxed in
- * MiniMessage format (e.g. {@code <#FFFFFF>}) or preceded by {@code &} (Mojang-style,
- * e.g. {@code FFFFFF}) via a negative lookbehind for {@code <} and {@code &}.
+ * MiniMessage format (e.g. {@code <#FFFFFF>}), preceded by {@code &} (Mojang-style,
+ * e.g. {@code FFFFFF}), or preceded by a backslash (escape marker, e.g. {@code \#FFFFFF})
+ * via a negative lookbehind for {@code <}, {@code &}, and {@code \}.
*/
- private static final Pattern UNBOXED_HEX_PATTERN = Pattern.compile("(?}).
*/
private static final String BOXED_HEX_REPLACEMENT = "<#$1>";
+ /**
+ * Escape replacement: {@code #RRGGBB} becomes {@code #\RRGGBB}, neutralising the pattern
+ * for this translator.
+ */
+ private static final String UNBOXED_HEX_ESCAPE_REPLACEMENT = "#\\\\$1";
+
/**
* Use {@link MiniMessageTranslators#UNBOXED_HEX}.
*/
@@ -53,4 +60,14 @@ class UnboxedHexPatternTranslator implements MiniMessageTranslator {
public @NotNull String translate(final @NotNull String input) {
return UNBOXED_HEX_PATTERN.matcher(input).replaceAll(BOXED_HEX_REPLACEMENT);
}
+
+ @Override
+ public @NotNull String escape(final @NotNull String input) {
+ return UNBOXED_HEX_PATTERN.matcher(input).replaceAll(UNBOXED_HEX_ESCAPE_REPLACEMENT);
+ }
+
+ @Override
+ public @NotNull String strip(final @NotNull String input) {
+ return UNBOXED_HEX_PATTERN.matcher(input).replaceAll("");
+ }
}
diff --git a/src/test/java/gg/gemstone/component/ComponentParserImplTest.java b/src/test/java/gg/gemstone/component/ComponentParserImplTest.java
index 86c9560..bc242c4 100644
--- a/src/test/java/gg/gemstone/component/ComponentParserImplTest.java
+++ b/src/test/java/gg/gemstone/component/ComponentParserImplTest.java
@@ -332,4 +332,83 @@ void explicitEmptyTranslatorArrayReturnsInputUnchanged() {
assertEquals(input, parser.translate(input));
}
}
+
+ @Nested
+ class Escape {
+
+ @Test
+ void escapeNeutralisesAllTranslatorPatterns() {
+ // Each translator inserts its own backslash; no translator re-escapes another's output.
+ // MM.escapeTags leaves the boxed token alone because <&\#AABBCC> is not a known MM tag.
+ String input = "§cRed <AABBCC> #445566 done";
+ assertEquals("§\\cRed <&\\#AABBCC> &\\#112233 #\\445566 done", parser.escape(input));
+ }
+
+ @Test
+ void escapedOutputIsNotRetranslated() {
+ // Re-running translate over escaped text must be a no-op for each translator.
+ String escaped = parser.escape("§cHello <AABBCC> #445566 done");
+ assertEquals(escaped, parser.translate(escaped));
+ }
+
+ @Test
+ void miniMessageTagsAreEscapedByMiniMessage() {
+ // is not a translator pattern; MM.escapeTags must escape it (both open and close).
+ assertEquals("\\Hi\\", parser.escape("Hi"));
+ }
+
+ @Test
+ void emptyStringReturnsEmpty() {
+ assertEquals("", parser.escape(""));
+ }
+
+ @Test
+ void plainTextReturnsUnchanged() {
+ assertEquals("Hello, world!", parser.escape("Hello, world!"));
+ }
+ }
+
+ @Nested
+ class Strip {
+
+ @Test
+ void stripRemovesAllTranslatorPatternsAndMmTags() {
+ // §c, <AABBCC>, , and #445566 are each stripped, leaving their surrounding
+ // spaces intact - four single-space gaps between "Red" and "done".
+ assertEquals("Red done",
+ parser.strip("§cRed <AABBCC> #445566 done"));
+ }
+
+ @Test
+ void stripRemovesMiniMessageTags() {
+ assertEquals("Hi", parser.strip("Hi"));
+ }
+
+ @Test
+ void stripRemovesMixedLegacyAndMmTags() {
+ assertEquals("Hello world",
+ parser.strip("§cHello §rworld"));
+ }
+
+ @Test
+ void emptyStringReturnsEmpty() {
+ assertEquals("", parser.strip(""));
+ }
+
+ @Test
+ void plainTextReturnsUnchanged() {
+ assertEquals("Hello, world!", parser.strip("Hello, world!"));
+ }
+
+ @Test
+ void stripWithAmpersandLegacyTranslator() {
+ ComponentParserImpl parserAmpersand = new ComponentParserImpl(
+ List.of(MiniMessageTranslators.MOJANG_BOXED_HEX, MiniMessageTranslators.MOJANG_UNBOXED_HEX,
+ MiniMessageTranslators.UNBOXED_HEX, MiniMessageTranslators.LEGACY_CODE_AMPERSAND),
+ MiniMessage.miniMessage()
+ );
+
+ assertEquals("Hello world", parserAmpersand.strip("&cHello &rworld"));
+ }
+ }
}
diff --git a/src/test/java/gg/gemstone/component/translator/LegacyFormattingCodeTranslatorTest.java b/src/test/java/gg/gemstone/component/translator/LegacyFormattingCodeTranslatorTest.java
index 370c7b1..3349b10 100644
--- a/src/test/java/gg/gemstone/component/translator/LegacyFormattingCodeTranslatorTest.java
+++ b/src/test/java/gg/gemstone/component/translator/LegacyFormattingCodeTranslatorTest.java
@@ -651,4 +651,130 @@ void mixedExample_legacyCodeInsideUnclosedMmScopes_treatedAsInner() {
"§cI'm red. I'm green. I'm blue! §fWill I be white?"));
}
}
+
+ @Nested
+ class Escape {
+
+ @Test
+ void emptyStringIsReturnedUnchanged() {
+ assertEquals("", translator.escape(""));
+ }
+
+ @Test
+ void plainTextWithNoCodesIsReturnedUnchanged() {
+ assertEquals("Hello, world!", translator.escape("Hello, world!"));
+ }
+
+ @Test
+ void escapesColorCode() {
+ assertEquals("§\\cHello", translator.escape("§cHello"));
+ }
+
+ @Test
+ void escapesFormattingCode() {
+ assertEquals("§\\lBold", translator.escape("§lBold"));
+ }
+
+ @Test
+ void escapesResetCode() {
+ assertEquals("a§\\rb", translator.escape("a§rb"));
+ }
+
+ @Test
+ void preservesOriginalCaseOfCode() {
+ // §C is a valid code (case-insensitive lookup), but the original 'C' must be preserved.
+ assertEquals("§\\CHello", translator.escape("§CHello"));
+ }
+
+ @Test
+ void escapesEveryCodeInString() {
+ assertEquals("§\\cA§\\lB§\\r", translator.escape("§cA§lB§r"));
+ }
+
+ @Test
+ void unknownCodeCharIsLeftAlone() {
+ assertEquals("§zHello", translator.escape("§zHello"));
+ }
+
+ @Test
+ void sectionCharAtEndOfStringIsLeftAlone() {
+ assertEquals("Hello§", translator.escape("Hello§"));
+ }
+
+ @Test
+ void minimessageTagsArePassedThroughVerbatim() {
+ assertEquals("Hi", translator.escape("Hi"));
+ }
+
+ @Test
+ void escapedOutputIsNoLongerTranslated() {
+ String escaped = translator.escape("§cHello");
+ assertEquals("§\\cHello", escaped);
+ assertEquals(escaped, translator.translate(escaped));
+ }
+
+ @Test
+ void doesNotReEscapeAlreadyEscapedCode() {
+ // §\c - the char after § is '\', not a valid legacy code char, so we leave it alone.
+ assertEquals("§\\cHello", translator.escape("§\\cHello"));
+ }
+
+ @Test
+ void customSectionChar() {
+ LegacyFormattingCodeTranslator ampersand = new LegacyFormattingCodeTranslator('&');
+ assertEquals("&\\cHello", ampersand.escape("&cHello"));
+ assertEquals("§cHello", ampersand.escape("§cHello"));
+ }
+ }
+
+ @Nested
+ class Strip {
+
+ @Test
+ void emptyStringIsReturnedUnchanged() {
+ assertEquals("", translator.strip(""));
+ }
+
+ @Test
+ void plainTextWithNoCodesIsReturnedUnchanged() {
+ assertEquals("Hello, world!", translator.strip("Hello, world!"));
+ }
+
+ @Test
+ void stripsSingleColorCode() {
+ assertEquals("Hello", translator.strip("§cHello"));
+ }
+
+ @Test
+ void stripsAllCodesInString() {
+ assertEquals("AB", translator.strip("§cA§lB§r"));
+ }
+
+ @Test
+ void stripsAdjacentCodes() {
+ assertEquals("Hello", translator.strip("§c§l§nHello"));
+ }
+
+ @Test
+ void preservesUnknownCodes() {
+ assertEquals("§zHello", translator.strip("§zHello"));
+ }
+
+ @Test
+ void sectionCharAtEndOfStringIsPreserved() {
+ assertEquals("Hello§", translator.strip("Hello§"));
+ }
+
+ @Test
+ void minimessageTagsArePassedThroughVerbatim() {
+ assertEquals("Hi", translator.strip("Hi"));
+ }
+
+ @Test
+ void customSectionChar() {
+ LegacyFormattingCodeTranslator ampersand = new LegacyFormattingCodeTranslator('&');
+ assertEquals("Hello", ampersand.strip("&cHello"));
+ assertEquals("§cHello", ampersand.strip("§cHello"));
+ }
+ }
}
diff --git a/src/test/java/gg/gemstone/component/translator/MojangBoxedHexPatternTranslatorTest.java b/src/test/java/gg/gemstone/component/translator/MojangBoxedHexPatternTranslatorTest.java
index 04a270f..29b2038 100644
--- a/src/test/java/gg/gemstone/component/translator/MojangBoxedHexPatternTranslatorTest.java
+++ b/src/test/java/gg/gemstone/component/translator/MojangBoxedHexPatternTranslatorTest.java
@@ -19,6 +19,7 @@
import static org.junit.jupiter.api.Assertions.assertEquals;
+import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
class MojangBoxedHexPatternTranslatorTest {
@@ -74,4 +75,84 @@ void shouldNotMatchSevenDigitHex() {
void shouldPreservesSurroundingText() {
assertEquals("Hello <#FFFFFF> world", translator.translate("Hello <FFFFFF> world"));
}
+
+ @Nested
+ class Escape {
+
+ @Test
+ void shouldEscapeSingle() {
+ assertEquals("<&\\#FFFFFF>", translator.escape("<FFFFFF>"));
+ }
+
+ @Test
+ void shouldEscapeMultiple() {
+ assertEquals("<&\\#FFFFFF> <&\\#000000>", translator.escape("<FFFFFF> <>"));
+ }
+
+ @Test
+ void shouldNotEscapeUnboxedMojang() {
+ assertEquals("FFFFFF", translator.escape("FFFFFF"));
+ }
+
+ @Test
+ void shouldNotEscapeAlreadyConverted() {
+ assertEquals("<#FFFFFF>", translator.escape("<#FFFFFF>"));
+ }
+
+ @Test
+ void shouldNotEscapeInvalidHex() {
+ assertEquals("<GGGGGG>", translator.escape("<GGGGGG>"));
+ }
+
+ @Test
+ void shouldNotEscapeFiveDigitHex() {
+ assertEquals("<FFFFF>", translator.escape("<FFFFF>"));
+ }
+
+ @Test
+ void shouldNotEscapeSevenDigitHex() {
+ assertEquals("<FFFFFFF>", translator.escape("<FFFFFFF>"));
+ }
+
+ @Test
+ void escapedOutputIsNoLongerTranslated() {
+ String escaped = translator.escape("<AABBCC>");
+ assertEquals("<&\\#AABBCC>", escaped);
+ assertEquals(escaped, translator.translate(escaped));
+ }
+ }
+
+ @Nested
+ class Strip {
+
+ @Test
+ void shouldStripSingle() {
+ assertEquals("", translator.strip("<FFFFFF>"));
+ }
+
+ @Test
+ void shouldStripMultiple() {
+ assertEquals(" ", translator.strip("<FFFFFF> <>"));
+ }
+
+ @Test
+ void shouldNotStripUnboxedMojang() {
+ assertEquals("FFFFFF", translator.strip("FFFFFF"));
+ }
+
+ @Test
+ void shouldNotStripAlreadyConverted() {
+ assertEquals("<#FFFFFF>", translator.strip("<#FFFFFF>"));
+ }
+
+ @Test
+ void shouldNotStripInvalidHex() {
+ assertEquals("<GGGGGG>", translator.strip("<GGGGGG>"));
+ }
+
+ @Test
+ void shouldPreserveSurroundingText() {
+ assertEquals("Hello world", translator.strip("Hello <FFFFFF> world"));
+ }
+ }
}
diff --git a/src/test/java/gg/gemstone/component/translator/MojangUnboxedHexPatternTranslatorTest.java b/src/test/java/gg/gemstone/component/translator/MojangUnboxedHexPatternTranslatorTest.java
index 82423ce..dffe5a9 100644
--- a/src/test/java/gg/gemstone/component/translator/MojangUnboxedHexPatternTranslatorTest.java
+++ b/src/test/java/gg/gemstone/component/translator/MojangUnboxedHexPatternTranslatorTest.java
@@ -19,6 +19,7 @@
import static org.junit.jupiter.api.Assertions.assertEquals;
+import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
class MojangUnboxedHexPatternTranslatorTest {
@@ -74,4 +75,79 @@ void shouldNotMatchSevenDigitHex() {
void shouldPreserveSurroundingText() {
assertEquals("Hello <#FFFFFF> world", translator.translate("Hello FFFFFF world"));
}
+
+ @Nested
+ class Escape {
+
+ @Test
+ void shouldEscapeSingle() {
+ assertEquals("&\\#FFFFFF", translator.escape("FFFFFF"));
+ }
+
+ @Test
+ void shouldEscapeMultiple() {
+ assertEquals("&\\#FFFFFF &\\#000000", translator.escape("FFFFFF "));
+ }
+
+ @Test
+ void shouldNotEscapeBoxedMojang() {
+ assertEquals("<FFFFFF>", translator.escape("<FFFFFF>"));
+ }
+
+ @Test
+ void shouldNotEscapeAlreadyConverted() {
+ assertEquals("<#FFFFFF>", translator.escape("<#FFFFFF>"));
+ }
+
+ @Test
+ void shouldNotEscapeInvalidHex() {
+ assertEquals("GGGGGG", translator.escape("GGGGGG"));
+ }
+
+ @Test
+ void shouldNotEscapeSevenDigitHex() {
+ assertEquals("FFFFFFF", translator.escape("FFFFFFF"));
+ }
+
+ @Test
+ void escapedOutputIsNoLongerTranslated() {
+ String escaped = translator.escape("AABBCC");
+ assertEquals("&\\#AABBCC", escaped);
+ assertEquals(escaped, translator.translate(escaped));
+ }
+ }
+
+ @Nested
+ class Strip {
+
+ @Test
+ void shouldStripSingle() {
+ assertEquals("", translator.strip("FFFFFF"));
+ }
+
+ @Test
+ void shouldStripMultiple() {
+ assertEquals(" ", translator.strip("FFFFFF "));
+ }
+
+ @Test
+ void shouldNotStripBoxedMojang() {
+ assertEquals("<FFFFFF>", translator.strip("<FFFFFF>"));
+ }
+
+ @Test
+ void shouldNotStripAlreadyConverted() {
+ assertEquals("<#FFFFFF>", translator.strip("<#FFFFFF>"));
+ }
+
+ @Test
+ void shouldNotStripInvalidHex() {
+ assertEquals("GGGGGG", translator.strip("GGGGGG"));
+ }
+
+ @Test
+ void shouldPreserveSurroundingText() {
+ assertEquals("Hello world", translator.strip("Hello FFFFFF world"));
+ }
+ }
}
diff --git a/src/test/java/gg/gemstone/component/translator/UnboxedHexPatternTranslatorTest.java b/src/test/java/gg/gemstone/component/translator/UnboxedHexPatternTranslatorTest.java
index 4e104b1..3a6587d 100644
--- a/src/test/java/gg/gemstone/component/translator/UnboxedHexPatternTranslatorTest.java
+++ b/src/test/java/gg/gemstone/component/translator/UnboxedHexPatternTranslatorTest.java
@@ -19,6 +19,7 @@
import static org.junit.jupiter.api.Assertions.assertEquals;
+import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
class UnboxedHexPatternTranslatorTest {
@@ -75,4 +76,87 @@ void shouldNotCorruptMojangBoxedAfterConversion() {
// Simulates output from MojangBoxedHexPatternTranslator being passed in
assertEquals("<#FFFFFF>", translator.translate("<#FFFFFF>"));
}
+
+ @Nested
+ class Escape {
+
+ @Test
+ void shouldEscapeSingle() {
+ assertEquals("#\\FFFFFF", translator.escape("#FFFFFF"));
+ }
+
+ @Test
+ void shouldEscapeMultiple() {
+ assertEquals("#\\FFFFFF #\\000000", translator.escape("#FFFFFF #000000"));
+ }
+
+ @Test
+ void shouldNotEscapeAlreadyConverted() {
+ assertEquals("<#FFFFFF>", translator.escape("<#FFFFFF>"));
+ }
+
+ @Test
+ void shouldNotEscapeMojangUnboxed() {
+ assertEquals("FFFFFF", translator.escape("FFFFFF"));
+ }
+
+ @Test
+ void shouldNotEscapeInvalidHex() {
+ assertEquals("#GGGGGG", translator.escape("#GGGGGG"));
+ }
+
+ @Test
+ void shouldNotEscapeSevenDigitHex() {
+ assertEquals("#FFFFFFF", translator.escape("#FFFFFFF"));
+ }
+
+ @Test
+ void shouldNotReEscapeBackslashEscapedHex() {
+ // Simulates output of MojangBoxed/MojangUnboxed escape (e.g. "&\#FFFFFF").
+ // The leading backslash must keep us from re-escaping the inner #FFFFFF.
+ assertEquals("&\\#FFFFFF", translator.escape("&\\#FFFFFF"));
+ assertEquals("<&\\#FFFFFF>", translator.escape("<&\\#FFFFFF>"));
+ }
+
+ @Test
+ void escapedOutputIsNoLongerTranslated() {
+ String escaped = translator.escape("#AABBCC");
+ assertEquals("#\\AABBCC", escaped);
+ assertEquals(escaped, translator.translate(escaped));
+ }
+ }
+
+ @Nested
+ class Strip {
+
+ @Test
+ void shouldStripSingle() {
+ assertEquals("", translator.strip("#FFFFFF"));
+ }
+
+ @Test
+ void shouldStripMultiple() {
+ assertEquals(" ", translator.strip("#FFFFFF #000000"));
+ }
+
+ @Test
+ void shouldNotStripAlreadyConverted() {
+ assertEquals("<#FFFFFF>", translator.strip("<#FFFFFF>"));
+ }
+
+ @Test
+ void shouldNotStripMojangUnboxed() {
+ assertEquals("FFFFFF", translator.strip("FFFFFF"));
+ }
+
+ @Test
+ void shouldNotStripInvalidHex() {
+ assertEquals("#GGGGGG", translator.strip("#GGGGGG"));
+ }
+
+ @Test
+ void shouldPreserveSurroundingText() {
+ assertEquals("Hello world", translator.strip("Hello #FFFFFF world"));
+ }
+ }
}