From 3c24c3bf923dd8ec8c527e0dc4c39bbba1f089f0 Mon Sep 17 00:00:00 2001 From: Zach Levis Date: Sat, 14 Nov 2020 20:09:30 -0800 Subject: [PATCH] api: Add a builder for replacement config This allows accessing the MatchResult directly when doing a replacement. --- .../adventure/text/AbstractComponent.java | 22 +- .../net/kyori/adventure/text/Component.java | 52 ++++- .../adventure/text/TextReplacementConfig.java | 200 ++++++++++++++++++ .../text/TextReplacementConfigImpl.java | 131 ++++++++++++ .../adventure/text/TextComponentTest.java | 21 +- .../legacy/LegacyComponentSerializerImpl.java | 42 ++-- 6 files changed, 419 insertions(+), 49 deletions(-) create mode 100644 api/src/main/java/net/kyori/adventure/text/TextReplacementConfig.java create mode 100644 api/src/main/java/net/kyori/adventure/text/TextReplacementConfigImpl.java diff --git a/api/src/main/java/net/kyori/adventure/text/AbstractComponent.java b/api/src/main/java/net/kyori/adventure/text/AbstractComponent.java index 7cf68ce48..e54c87b26 100644 --- a/api/src/main/java/net/kyori/adventure/text/AbstractComponent.java +++ b/api/src/main/java/net/kyori/adventure/text/AbstractComponent.java @@ -27,16 +27,16 @@ import java.util.Collections; import java.util.List; import java.util.Objects; -import java.util.function.Function; -import java.util.regex.Pattern; +import java.util.function.Consumer; import java.util.stream.Stream; import net.kyori.adventure.text.format.Style; -import net.kyori.adventure.util.IntFunction2; import net.kyori.examination.ExaminableProperty; import net.kyori.examination.string.StringExaminer; import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.Nullable; +import static java.util.Objects.requireNonNull; + /** * An abstract implementation of a text component. * @@ -93,8 +93,20 @@ protected AbstractComponent(final @NonNull List childre } @Override - public @NonNull Component replaceText(final @NonNull Pattern pattern, final @NonNull Function replacement, final @NonNull IntFunction2 fn) { - return TextReplacementRenderer.INSTANCE.render(this, new TextReplacementRenderer.State(pattern, (result, builder) -> replacement.apply(builder), fn)); + public @NonNull Component replaceText(final @NonNull Consumer configurer) { + requireNonNull(configurer, "configurer"); + final TextReplacementConfigImpl.Builder builder = new TextReplacementConfigImpl.Builder(); + configurer.accept(builder); + return TextReplacementRenderer.INSTANCE.render(this, builder.toState()); + } + + @Override + public @NonNull Component replaceText(final @NonNull TextReplacementConfig config) { + requireNonNull(config, "replacement"); + if(!(config instanceof TextReplacementConfigImpl)) { + throw new IllegalArgumentException("Provided replacement was a custom TextReplacementConfig implementation, which is not supported."); + } + return TextReplacementRenderer.INSTANCE.render(this, ((TextReplacementConfigImpl) config).toState()); } @Override diff --git a/api/src/main/java/net/kyori/adventure/text/Component.java b/api/src/main/java/net/kyori/adventure/text/Component.java index 61526aa50..6e803b04d 100644 --- a/api/src/main/java/net/kyori/adventure/text/Component.java +++ b/api/src/main/java/net/kyori/adventure/text/Component.java @@ -1469,6 +1469,24 @@ default boolean hasStyling() { return !this.style().isEmpty(); } + /** + * Finds and replaces any text with this or child {@link Component}s using the configured options. + * + * @param configurer the configurer + * @return a modified copy of this component + * @since 4.2.0 + */ + @NonNull Component replaceText(final @NonNull Consumer configurer); + + /** + * Finds and replaces any text with this or child {@link Component}s using the provided options. + * + * @param config the replacement config + * @return a modified copy of this component + * @since 4.2.0 + */ + @NonNull Component replaceText(final @NonNull TextReplacementConfig config); + /** * Finds and replaces text within any {@link Component}s using a string literal. * @@ -1476,9 +1494,11 @@ default boolean hasStyling() { * @param replacement a {@link ComponentLike} to replace each match * @return a modified copy of this component * @since 4.0.0 + * @deprecated for removal since 4.2.0, use {@link #replaceText(Consumer)} or {@link #replaceText(TextReplacementConfig)} instead. */ + @Deprecated default @NonNull Component replaceText(final @NonNull String search, final @Nullable ComponentLike replacement) { - return this.replaceText(Pattern.compile(search, Pattern.LITERAL), old -> replacement); + return this.replaceText(b -> b.matchLiteral(search).replacement(replacement)); } /** @@ -1488,9 +1508,11 @@ default boolean hasStyling() { * @param replacement a function to replace each match * @return a modified copy of this component * @since 4.0.0 + * @deprecated for removal since 4.2.0, use {@link #replaceText(Consumer)} or {@link #replaceText(TextReplacementConfig)} instead. */ + @Deprecated default @NonNull Component replaceText(final @NonNull Pattern pattern, final @NonNull Function replacement) { - return this.replaceText(pattern, replacement, (index, replaced) -> PatternReplacementResult.REPLACE); + return this.replaceText(b -> b.match(pattern).replacement(replacement)); } /** @@ -1500,9 +1522,11 @@ default boolean hasStyling() { * @param replacement a {@link ComponentLike} to replace the first match * @return a modified copy of this component * @since 4.0.0 + * @deprecated for removal since 4.2.0, use {@link #replaceText(Consumer)} or {@link #replaceText(TextReplacementConfig)} instead. */ + @Deprecated default @NonNull Component replaceFirstText(final @NonNull String search, final @Nullable ComponentLike replacement) { - return this.replaceText(search, replacement, 1); + return this.replaceText(b -> b.matchLiteral(search).once().replacement(replacement)); } /** @@ -1512,9 +1536,11 @@ default boolean hasStyling() { * @param replacement a function to replace the first match * @return a modified copy of this component * @since 4.0.0 + * @deprecated for removal since 4.2.0, use {@link #replaceText(Consumer)} or {@link #replaceText(TextReplacementConfig)} instead. */ + @Deprecated default @NonNull Component replaceFirstText(final @NonNull Pattern pattern, final @NonNull Function replacement) { - return this.replaceText(pattern, replacement, 1); + return this.replaceText(b -> b.match(pattern).once().replacement(replacement)); } /** @@ -1525,9 +1551,11 @@ default boolean hasStyling() { * @param numberOfReplacements the amount of matches that should be replaced * @return a modified copy of this component * @since 4.0.0 + * @deprecated for removal since 4.2.0, use {@link #replaceText(Consumer)} or {@link #replaceText(TextReplacementConfig)} instead. */ + @Deprecated default @NonNull Component replaceText(final @NonNull String search, final @Nullable ComponentLike replacement, final int numberOfReplacements) { - return this.replaceText(search, replacement, (index, replaced) -> replaced < numberOfReplacements ? PatternReplacementResult.REPLACE : PatternReplacementResult.STOP); + return this.replaceText(b -> b.matchLiteral(search).times(numberOfReplacements).replacement(replacement)); } /** @@ -1538,9 +1566,11 @@ default boolean hasStyling() { * @param numberOfReplacements the amount of matches that should be replaced * @return a modified copy of this component * @since 4.0.0 + * @deprecated for removal since 4.2.0, use {@link #replaceText(Consumer)} or {@link #replaceText(TextReplacementConfig)} instead. */ + @Deprecated default @NonNull Component replaceText(final @NonNull Pattern pattern, final @NonNull Function replacement, final int numberOfReplacements) { - return this.replaceText(pattern, replacement, (index, replaced) -> replaced < numberOfReplacements ? PatternReplacementResult.REPLACE : PatternReplacementResult.STOP); + return this.replaceText(b -> b.match(pattern).times(numberOfReplacements).replacement(replacement)); } /** @@ -1553,9 +1583,11 @@ default boolean hasStyling() { * @param fn a function of (index, replaced) used to determine if matches should be replaced, where "replaced" is the number of successful replacements * @return a modified copy of this component * @since 4.0.0 + * @deprecated for removal since 4.2.0, use {@link #replaceText(Consumer)} or {@link #replaceText(TextReplacementConfig)} instead. */ + @Deprecated default @NonNull Component replaceText(final @NonNull String search, final @Nullable ComponentLike replacement, final @NonNull IntFunction2 fn) { - return this.replaceText(Pattern.compile(search, Pattern.LITERAL), old -> replacement, fn); + return this.replaceText(b -> b.matchLiteral(search).replacement(replacement).condition(fn)); } /** @@ -1568,8 +1600,12 @@ default boolean hasStyling() { * @param fn a function of (index, replaced) used to determine if matches should be replaced, where "replaced" is the number of successful replacements * @return a modified copy of this component * @since 4.0.0 + * @deprecated for removal since 4.2.0, use {@link #replaceText(Consumer)} or {@link #replaceText(TextReplacementConfig)} instead. */ - @NonNull Component replaceText(final @NonNull Pattern pattern, final @NonNull Function replacement, final @NonNull IntFunction2 fn); + @Deprecated + default @NonNull Component replaceText(final @NonNull Pattern pattern, final @NonNull Function replacement, final @NonNull IntFunction2 fn) { + return this.replaceText(b -> b.match(pattern).replacement(replacement).condition(fn)); + } @Override default void componentBuilderApply(final @NonNull ComponentBuilder component) { diff --git a/api/src/main/java/net/kyori/adventure/text/TextReplacementConfig.java b/api/src/main/java/net/kyori/adventure/text/TextReplacementConfig.java new file mode 100644 index 000000000..8681737e4 --- /dev/null +++ b/api/src/main/java/net/kyori/adventure/text/TextReplacementConfig.java @@ -0,0 +1,200 @@ +/* + * This file is part of adventure, licensed under the MIT License. + * + * Copyright (c) 2017-2020 KyoriPowered + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package net.kyori.adventure.text; + +import java.util.function.BiFunction; +import java.util.function.Function; +import java.util.regex.MatchResult; +import java.util.regex.Pattern; +import net.kyori.adventure.util.Buildable; +import net.kyori.adventure.util.IntFunction2; +import net.kyori.examination.Examinable; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.checkerframework.checker.regex.qual.Regex; + +import static java.util.Objects.requireNonNull; + +/** + * A configuration for how text can be replaced in a component. + * + *

The exact structure for a replacement specification is an implementation detail and therefore not exposed. + * Custom implementations of {@code TextReplacementConfig} are not supported.

+ * + * @since 4.2.0 + */ +public interface TextReplacementConfig extends Buildable, Examinable { + /** + * Create a new builder. + * + * @return a new builder + * @since 4.2.0 + */ + static @NonNull Builder builder() { + return new TextReplacementConfigImpl.Builder(); + } + + /** + * Get the pattern that will be searched for. + * + * @return the match pattern + * @since 4.2.0 + */ + @NonNull Pattern matchPattern(); + + /** + * A builder for replacement configurations. + * + * @since 4.2.0 + */ + interface Builder extends Buildable.Builder { + /* + * ------------------- + * ---- Patterns ----- + * ------------------- + */ + + /** + * Match against the literal string provided. + * + *

This will NOT be parsed as a regular expression.

+ * + * @param literal the literal string to match + * @return this builder + * @since 4.2.0 + */ + default Builder matchLiteral(final String literal) { + return this.match(Pattern.compile(literal, Pattern.LITERAL)); + } + + /** + * Compile the provided input as a {@link Pattern} and match against it. + * + * @param pattern the regex pattern to match + * @return this builder + * @since 4.2.0 + */ + default @NonNull Builder match(final @NonNull @Regex String pattern) { + return this.match(Pattern.compile(pattern)); + } + + /** + * Match the provided {@link Pattern}. + * + * @param pattern pattern to find in any searched components + * @return this builder + * @since 4.2.0 + */ + @NonNull Builder match(final @NonNull Pattern pattern); + + /* + * --------------------------- + * ---- Number of matches ---- + * --------------------------- + */ + + /** + * Only replace the first occurrence of the matched pattern. + * + * @return this builder + * @since 4.2.0 + */ + default @NonNull Builder once() { + return this.times(1); + } + + /** + * Only replace the first {@code times} matches of the pattern. + * + * @param times maximum amount of matches to process + * @return this builder + * @since 4.2.0 + */ + default @NonNull Builder times(int times) { + return this.condition((index, replaced) -> replaced < times ? PatternReplacementResult.REPLACE : PatternReplacementResult.STOP); + } + + /** + * Set the function to determine how an individual match should be processed. + * + * @param condition a function of {@code (index, replaced)} used to determine if matches should be replaced, where "replaced" is the number of successful replacements. + * @return this builder + * @since 4.2.0 + */ + @NonNull Builder condition(final @NonNull IntFunction2 condition); + + /* + * ------------------------- + * ---- Action on match ---- + * ------------------------- + */ + + /** + * Supply a literal replacement for the matched pattern. + * + * @param replacement the replacement + * @return this builder + * @since 4.2.0 + */ + default @NonNull Builder replacement(final @NonNull String replacement) { + requireNonNull(replacement, "replacement"); + return this.replacement(builder -> builder.content(replacement)); + } + + /** + * Supply a literal replacement for the matched pattern. + * + * @param replacement the replacement + * @return this builder + * @since 4.2.0 + */ + default @NonNull Builder replacement(final @NonNull ComponentLike replacement) { + requireNonNull(replacement, "replacement"); + final Component baked = replacement.asComponent(); + return this.replacement((result, input) -> baked); + } + + /** + * Supply a function that provides replacements for each match. + * + * @param replacement the replacement function + * @return this builder + * @since 4.2.0 + */ + default @NonNull Builder replacement(final @NonNull Function replacement) { + requireNonNull(replacement, "replacement"); + return this.replacement((result, input) -> replacement.apply(input)); + + } + + /** + * Supply a function that provides replacements for each match, with access to group information. + * + * @param replacement the replacement function, taking a match result and a text component pre-populated with + * @return this builder + * @since 4.2.0 + */ + @NonNull Builder replacement(final @NonNull BiFunction replacement); + } +} diff --git a/api/src/main/java/net/kyori/adventure/text/TextReplacementConfigImpl.java b/api/src/main/java/net/kyori/adventure/text/TextReplacementConfigImpl.java new file mode 100644 index 000000000..3d43244f1 --- /dev/null +++ b/api/src/main/java/net/kyori/adventure/text/TextReplacementConfigImpl.java @@ -0,0 +1,131 @@ +/* + * This file is part of adventure, licensed under the MIT License. + * + * Copyright (c) 2017-2020 KyoriPowered + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package net.kyori.adventure.text; + +import java.util.function.BiFunction; +import java.util.regex.MatchResult; +import java.util.regex.Pattern; +import java.util.stream.Stream; +import net.kyori.adventure.util.IntFunction2; +import net.kyori.examination.ExaminableProperty; +import net.kyori.examination.string.StringExaminer; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; + +import static java.util.Objects.requireNonNull; + +final class TextReplacementConfigImpl implements TextReplacementConfig { + private final Pattern matchPattern; + private final BiFunction replacement; + private final IntFunction2 continuer; + + TextReplacementConfigImpl(final Builder builder) { + this.matchPattern = builder.matchPattern; + this.replacement = builder.replacement; + this.continuer = builder.continuer; + } + + @Override + public @NonNull Pattern matchPattern() { + return this.matchPattern; + } + + TextReplacementRenderer.State toState() { + return new TextReplacementRenderer.State(this.matchPattern, this.replacement, this.continuer); + } + + @Override + public TextReplacementConfig.@NonNull Builder toBuilder() { + return new Builder(this); + } + + @Override + public @NonNull Stream examinableProperties() { + return Stream.of( + ExaminableProperty.of("matchPattern", this.matchPattern), + ExaminableProperty.of("replacement", this.replacement), + ExaminableProperty.of("continuer", this.continuer) + ); + } + + @Override + public String toString() { + return this.examine(StringExaminer.simpleEscaping()); + } + + static final class Builder implements TextReplacementConfig.Builder { + @MonotonicNonNull Pattern matchPattern; + BiFunction replacement; + IntFunction2 continuer = (index, replacement) -> PatternReplacementResult.REPLACE; + + Builder() { + } + + Builder(final TextReplacementConfigImpl instance) { + this.matchPattern = instance.matchPattern; + this.replacement = instance.replacement; + this.continuer = instance.continuer; + } + + @Override + public @NonNull Builder match(final @NonNull Pattern pattern) { + this.matchPattern = requireNonNull(pattern, "pattern"); + return this; + } + + @Override + public @NonNull Builder condition(final @NonNull IntFunction2 condition) { + this.continuer = requireNonNull(condition, "continuation"); + return this; + } + + @Override + public @NonNull Builder replacement(final @NonNull BiFunction replacement) { + this.replacement = requireNonNull(replacement, "replacement"); + return this; + } + + private void validate() { + if(this.matchPattern == null) { + throw new IllegalStateException("A pattern must be provided to match against"); + } + if(this.replacement == null) { + throw new IllegalStateException("A replacement action must be provided"); + } + } + + TextReplacementRenderer.State toState() { + this.validate(); + return new TextReplacementRenderer.State(this.matchPattern, this.replacement, this.continuer); + } + + @Override + public TextReplacementConfig build() { + this.validate(); + + return new TextReplacementConfigImpl(this); + } + } +} diff --git a/api/src/test/java/net/kyori/adventure/text/TextComponentTest.java b/api/src/test/java/net/kyori/adventure/text/TextComponentTest.java index 02a6dbc72..14270924e 100644 --- a/api/src/test/java/net/kyori/adventure/text/TextComponentTest.java +++ b/api/src/test/java/net/kyori/adventure/text/TextComponentTest.java @@ -141,7 +141,7 @@ void testReplace() { .content("cat says ") .append(Component.translatable("cat.meow")) // or any non-text component .build(); - final Component replaced = component.replaceText(Pattern.compile("says"), match -> match.color(NamedTextColor.DARK_PURPLE)); + final Component replaced = component.replaceText(b -> b.match("says").replacement(match -> match.color(NamedTextColor.DARK_PURPLE))); assertEquals(Component.text() .content("cat ") .append(Component.text("says", NamedTextColor.DARK_PURPLE)) @@ -156,7 +156,7 @@ void testReplaceFirst() { .content("Buffalo buffalo Buffalo buffalo buffalo buffalo Buffalo buffalo ") .append(Component.translatable("buffalo.buffalo")) // or any non-text component .build(); - final Component replaced = component.replaceFirstText(Pattern.compile("buffalo"), match -> match.color(NamedTextColor.DARK_PURPLE)); + final Component replaced = component.replaceText(b -> b.match("buffalo").once().replacement(match -> match.color(NamedTextColor.DARK_PURPLE))); assertEquals(Component.text() .content("Buffalo ") .append(Component.text("buffalo", NamedTextColor.DARK_PURPLE)) @@ -171,7 +171,7 @@ void testReplaceN() { .content("Buffalo buffalo Buffalo buffalo buffalo buffalo Buffalo buffalo ") .append(Component.translatable("buffalo.buffalo")) // or any non-text component .build(); - final Component replaced = component.replaceText(Pattern.compile("buffalo"), match -> match.color(NamedTextColor.DARK_PURPLE), 2); + final Component replaced = component.replaceText(b -> b.match("buffalo").replacement(match -> match.color(NamedTextColor.DARK_PURPLE)).times(2)); assertEquals(Component.text() .content("Buffalo ") .append(Component.text("buffalo", NamedTextColor.DARK_PURPLE)) @@ -189,8 +189,9 @@ void testReplaceEveryOther() { .append(Component.translatable("purple.purple")) // or any non-text component .build(); - final Component replaced = component.replaceText(Pattern.compile("purple"), match -> match.color(NamedTextColor.DARK_PURPLE), - (index, replace) -> index % 2 == 0 ? PatternReplacementResult.REPLACE : PatternReplacementResult.CONTINUE); + final Component replaced = component.replaceText(b -> b.match("purple") + .replacement(match -> match.color(NamedTextColor.DARK_PURPLE)) + .condition((index, replace) -> index % 2 == 0 ? PatternReplacementResult.REPLACE : PatternReplacementResult.CONTINUE)); assertEquals(Component.text() .content("purple ") @@ -215,7 +216,7 @@ void testReplaceNonStringComponents() { .append(Component.text().append(Component.text("Parent")).append(Component.text(" 1")), Component.keybind("key.adventure.purr"), Component.text().append(Component.text("Parent")).append(Component.text(" 3"))) .build(); - assertEquals(expectedReplacement, original.replaceText(Pattern.compile("Child"), match -> match.content("Parent"))); + assertEquals(expectedReplacement, original.replaceText(b -> b.match("Child").replacement(match -> match.content("Parent")))); } // https://github.com/KyoriPowered/adventure/issues/129 @@ -236,7 +237,7 @@ void testReplaceWithChildren() { .append(Component.text(" under ").color(NamedTextColor.DARK_AQUA).append(Component.text("me"))) .build(); - assertEquals(expectedReplacement, component.replaceText(Pattern.compile("test"), match -> match.content("me"))); + assertEquals(expectedReplacement, component.replaceText(b -> b.match("test").replacement("me"))); } @Test @@ -248,7 +249,7 @@ void testReplaceInStyle() { .hoverEvent(HoverEvent.showText(Component.text("meow", NamedTextColor.DARK_RED))) .build(); - assertEquals(expectedReplacement, component.replaceText(Pattern.compile("meep"), builder -> builder.content("meow").color(NamedTextColor.DARK_RED))); + assertEquals(expectedReplacement, component.replaceText(b -> b.match("meep").replacement(builder -> builder.content("meow").color(NamedTextColor.DARK_RED)))); } @Test @@ -260,7 +261,7 @@ void testReplaceTranslatableArgs() { .append(Component.translatable("my.translation", Component.text().content("cats ").append(Component.text("good")))) .build(); - assertEquals(expectedReplacement, component.replaceText(Pattern.compile("bad"), builder -> builder.content("good"))); + assertEquals(expectedReplacement, component.replaceText(b -> b.match(Pattern.compile("bad")).replacement(builder -> builder.content("good")))); } @Test @@ -272,7 +273,7 @@ void testPartialReplaceHasIsolatedStyle() { .append(Component.text(" world")); }); - assertEquals(expectedReplacement, component.replaceText(Pattern.compile("Hello"), builder -> builder.content("Goodbye").color(NamedTextColor.LIGHT_PURPLE))); + assertEquals(expectedReplacement, component.replaceText(b -> b.match(Pattern.compile("Hello")).replacement(builder -> builder.content("Goodbye").color(NamedTextColor.LIGHT_PURPLE)))); } // https://github.com/KyoriPowered/adventure/issues/197 diff --git a/text-serializer-legacy/src/main/java/net/kyori/adventure/text/serializer/legacy/LegacyComponentSerializerImpl.java b/text-serializer-legacy/src/main/java/net/kyori/adventure/text/serializer/legacy/LegacyComponentSerializerImpl.java index 4faca9256..dc767b382 100644 --- a/text-serializer-legacy/src/main/java/net/kyori/adventure/text/serializer/legacy/LegacyComponentSerializerImpl.java +++ b/text-serializer-legacy/src/main/java/net/kyori/adventure/text/serializer/legacy/LegacyComponentSerializerImpl.java @@ -33,6 +33,7 @@ import java.util.regex.Pattern; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.TextComponent; +import net.kyori.adventure.text.TextReplacementConfig; import net.kyori.adventure.text.event.ClickEvent; import net.kyori.adventure.text.format.NamedTextColor; import net.kyori.adventure.text.format.Style; @@ -90,24 +91,19 @@ final class LegacyComponentSerializerImpl implements LegacyComponentSerializer { } } - static final LegacyComponentSerializer SECTION_SERIALIZER = new LegacyComponentSerializerImpl(SECTION_CHAR, HEX_CHAR, false, null, null, false, false); - static final LegacyComponentSerializer AMPERSAND_SERIALIZER = new LegacyComponentSerializerImpl(AMPERSAND_CHAR, HEX_CHAR, false, null, null, false, false); + static final LegacyComponentSerializer SECTION_SERIALIZER = new LegacyComponentSerializerImpl(SECTION_CHAR, HEX_CHAR, null, false, false); + static final LegacyComponentSerializer AMPERSAND_SERIALIZER = new LegacyComponentSerializerImpl(AMPERSAND_CHAR, HEX_CHAR, null, false, false); private final char character; private final char hexCharacter; - private final boolean urlLink; - private final Pattern urlPattern; - private final Style urlStyle; + private final @Nullable TextReplacementConfig urlReplacementConfig; private final boolean hexColours; private final boolean useTerriblyStupidHexFormat; // (╯°□°)╯︵ ┻━┻ - LegacyComponentSerializerImpl(final char character, final char hexCharacter, final boolean urlLink, final @Nullable Pattern urlPattern, final @Nullable Style urlStyle, final boolean hexColours, final boolean useTerriblyStupidHexFormat) { - if(urlLink) requireNonNull(urlPattern, "url pattern must be non-null when linking"); + LegacyComponentSerializerImpl(final char character, final char hexCharacter, final @Nullable TextReplacementConfig urlReplacementConfig, final boolean hexColours, final boolean useTerriblyStupidHexFormat) { this.character = character; this.hexCharacter = hexCharacter; - this.urlLink = urlLink; - this.urlPattern = urlPattern; - this.urlStyle = urlStyle; + this.urlReplacementConfig = urlReplacementConfig; this.hexColours = hexColours; this.useTerriblyStupidHexFormat = useTerriblyStupidHexFormat; } @@ -198,8 +194,8 @@ private String toLegacyCode(TextFormat format) { } private TextComponent extractUrl(final TextComponent component) { - if(!this.urlLink) return component; - final Component newComponent = component.replaceText(this.urlPattern, url -> (this.urlStyle == null ? url : url.style(this.urlStyle)).clickEvent(ClickEvent.openUrl(url.content()))); + if(this.urlReplacementConfig == null) return component; + final Component newComponent = component.replaceText(this.urlReplacementConfig); if(newComponent instanceof TextComponent) return (TextComponent) newComponent; return TextComponent.ofChildren(newComponent); } @@ -429,9 +425,7 @@ private void applyFullFormat() { static final class BuilderImpl implements Builder { private char character = LegacyComponentSerializer.SECTION_CHAR; private char hexCharacter = LegacyComponentSerializer.HEX_CHAR; - private boolean urlLink = false; - private Pattern urlPattern = DEFAULT_URL_PATTERN; - private Style urlStyle = null; + private TextReplacementConfig urlReplacementConfig = null; private boolean hexColours = false; private boolean useTerriblyStupidHexFormat = false; @@ -441,8 +435,7 @@ static final class BuilderImpl implements Builder { BuilderImpl(final @NonNull LegacyComponentSerializerImpl serializer) { this.character = serializer.character; this.hexCharacter = serializer.hexCharacter; - this.urlStyle = serializer.urlStyle; - this.urlLink = serializer.urlLink; + this.urlReplacementConfig = serializer.urlReplacementConfig; this.hexColours = serializer.hexColours; this.useTerriblyStupidHexFormat = serializer.useTerriblyStupidHexFormat; } @@ -476,9 +469,11 @@ static final class BuilderImpl implements Builder { @Override public @NonNull Builder extractUrls(final @NonNull Pattern pattern, final @Nullable Style style) { - this.urlLink = true; - this.urlPattern = pattern; - this.urlStyle = style; + requireNonNull(pattern, "pattern"); + this.urlReplacementConfig = TextReplacementConfig.builder() + .match(pattern) + .replacement(url -> (style == null ? url : url.style(style)).clickEvent(ClickEvent.openUrl(url.content()))) + .build(); return this; } @@ -496,12 +491,7 @@ static final class BuilderImpl implements Builder { @Override public @NonNull LegacyComponentSerializer build() { - if(this.urlLink) { - if(this.urlPattern == null) { - throw new IllegalStateException("url pattern must be non-null when creating a linking serializer"); - } - } - return new LegacyComponentSerializerImpl(this.character, this.hexCharacter, this.urlLink, this.urlPattern, this.urlStyle, this.hexColours, this.useTerriblyStupidHexFormat); + return new LegacyComponentSerializerImpl(this.character, this.hexCharacter, this.urlReplacementConfig, this.hexColours, this.useTerriblyStupidHexFormat); } }