Skip to content

Commit

Permalink
api: Add a builder for replacement config
Browse files Browse the repository at this point in the history
This allows accessing the MatchResult directly when doing a replacement.
  • Loading branch information
zml2008 committed Nov 15, 2020
1 parent ae2005b commit a7eb5f2
Show file tree
Hide file tree
Showing 6 changed files with 418 additions and 39 deletions.
22 changes: 17 additions & 5 deletions api/src/main/java/net/kyori/adventure/text/AbstractComponent.java
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand Down Expand Up @@ -93,8 +93,20 @@ protected AbstractComponent(final @NonNull List<? extends ComponentLike> childre
}

@Override
public @NonNull Component replaceText(final @NonNull Pattern pattern, final @NonNull Function<TextComponent.Builder, @Nullable ComponentLike> replacement, final @NonNull IntFunction2<PatternReplacementResult> fn) {
return TextReplacementRenderer.INSTANCE.render(this, new TextReplacementRenderer.State(pattern, (result, builder) -> replacement.apply(builder), fn));
public @NonNull Component replaceText(final @NonNull Consumer<TextReplacementConfig.Builder> 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
Expand Down
52 changes: 44 additions & 8 deletions api/src/main/java/net/kyori/adventure/text/Component.java
Original file line number Diff line number Diff line change
Expand Up @@ -1469,16 +1469,36 @@ 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<TextReplacementConfig.Builder> 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.
*
* @param search a string literal
* @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));
}

/**
Expand All @@ -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<TextComponent.Builder, @Nullable ComponentLike> replacement) {
return this.replaceText(pattern, replacement, (index, replaced) -> PatternReplacementResult.REPLACE);
return this.replaceText(b -> b.match(pattern).replacement(replacement));
}

/**
Expand All @@ -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));
}

/**
Expand All @@ -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<TextComponent.Builder, @Nullable ComponentLike> replacement) {
return this.replaceText(pattern, replacement, 1);
return this.replaceText(b -> b.match(pattern).once().replacement(replacement));
}

/**
Expand All @@ -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));
}

/**
Expand All @@ -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<TextComponent.Builder, @Nullable ComponentLike> 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));
}

/**
Expand All @@ -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<PatternReplacementResult> fn) {
return this.replaceText(Pattern.compile(search, Pattern.LITERAL), old -> replacement, fn);
return this.replaceText(b -> b.matchLiteral(search).replacement(replacement).condition(fn));
}

/**
Expand All @@ -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<TextComponent.Builder, @Nullable ComponentLike> replacement, final @NonNull IntFunction2<PatternReplacementResult> fn);
@Deprecated
default @NonNull Component replaceText(final @NonNull Pattern pattern, final @NonNull Function<TextComponent.Builder, @Nullable ComponentLike> replacement, final @NonNull IntFunction2<PatternReplacementResult> fn) {
return this.replaceText(b -> b.match(pattern).replacement(replacement).condition(fn));
}

@Override
default void componentBuilderApply(final @NonNull ComponentBuilder<?, ?> component) {
Expand Down
200 changes: 200 additions & 0 deletions api/src/main/java/net/kyori/adventure/text/TextReplacementConfig.java
Original file line number Diff line number Diff line change
@@ -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.
*
* <p>The exact structure for a replacement specification is an implementation detail and therefore not exposed.
* Custom implementations of {@code TextReplacementConfig} are not supported.</p>
*
* @since 4.2.0
*/
public interface TextReplacementConfig extends Buildable<TextReplacementConfig, TextReplacementConfig.Builder>, 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<TextReplacementConfig> {
/*
* -------------------
* ---- Patterns -----
* -------------------
*/

/**
* Match against the literal string provided.
*
* <p>This will <b>NOT</b> be parsed as a regular expression.</p>
*
* @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<PatternReplacementResult> 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<TextComponent.Builder, @Nullable ComponentLike> 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<MatchResult, TextComponent.Builder, @Nullable ComponentLike> replacement);
}
}
Loading

0 comments on commit a7eb5f2

Please sign in to comment.