From 683dc6e8e3f0315514fbcda3b7ab184e6473e2b8 Mon Sep 17 00:00:00 2001 From: PiggyPiglet Date: Sat, 25 Oct 2025 12:33:16 +0800 Subject: [PATCH] Uhh --- build.gradle.kts | 2 +- .../clip/placeholderapi/PlaceholderAPI.java | 8 + .../clip/placeholderapi/PlaceholderHook.java | 11 + .../replacer/ComponentCharsReplacer.java | 224 ++++++++++++++++++ 4 files changed, 244 insertions(+), 1 deletion(-) create mode 100644 src/main/java/me/clip/placeholderapi/replacer/ComponentCharsReplacer.java diff --git a/build.gradle.kts b/build.gradle.kts index ef92f98..c2702b1 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -25,7 +25,7 @@ repositories { dependencies { implementation("org.bstats:bstats-bukkit:3.0.1") - implementation("net.kyori:adventure-platform-bukkit:4.3.3") + implementation("net.kyori:adventure-platform-bukkit:4.4.1") //compileOnly("org.spigotmc:spigot-api:1.21-R0.1-SNAPSHOT") compileOnly("dev.folia:folia-api:1.20.1-R0.1-SNAPSHOT") diff --git a/src/main/java/me/clip/placeholderapi/PlaceholderAPI.java b/src/main/java/me/clip/placeholderapi/PlaceholderAPI.java index 97b6c48..a138c9a 100644 --- a/src/main/java/me/clip/placeholderapi/PlaceholderAPI.java +++ b/src/main/java/me/clip/placeholderapi/PlaceholderAPI.java @@ -35,6 +35,8 @@ import me.clip.placeholderapi.replacer.CharsReplacer; import me.clip.placeholderapi.replacer.Replacer; import me.clip.placeholderapi.replacer.Replacer.Closure; import me.clip.placeholderapi.util.Msg; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.TextReplacementConfig; import org.bukkit.OfflinePlayer; import org.bukkit.entity.Player; import org.bukkit.plugin.Plugin; @@ -57,6 +59,12 @@ public final class PlaceholderAPI { // === Current API === + @NotNull + public static Component setComponentPlaceholders(final OfflinePlayer player, @NotNull final Component component) { + // change charsreplacer to custom that returns component instead of string - this is going to suck mega + return component.replaceText(config -> config.match(PLACEHOLDER_PATTERN).replacement((result, builder) -> builder.content(REPLACER_PERCENT.apply(builder.content(), player, PlaceholderAPIPlugin.getInstance().getLocalExpansionManager()::getExpansion)))); + } + /** * Translates all placeholders into their corresponding values. *
The pattern of a valid placeholder is {@literal %_%}. diff --git a/src/main/java/me/clip/placeholderapi/PlaceholderHook.java b/src/main/java/me/clip/placeholderapi/PlaceholderHook.java index 9906b8a..773a575 100644 --- a/src/main/java/me/clip/placeholderapi/PlaceholderHook.java +++ b/src/main/java/me/clip/placeholderapi/PlaceholderHook.java @@ -20,11 +20,14 @@ package me.clip.placeholderapi; +import net.kyori.adventure.text.Component; import org.bukkit.OfflinePlayer; import org.bukkit.entity.Player; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; +import java.util.Optional; + public abstract class PlaceholderHook { @Nullable public String onRequest(final OfflinePlayer player, @NotNull final String params) { @@ -35,8 +38,16 @@ public abstract class PlaceholderHook { return onPlaceholderRequest(null, params); } + @Deprecated @Nullable public String onPlaceholderRequest(final Player player, @NotNull final String params) { return null; } + + @Nullable + public Component onPlaceholderComponentRequest(final Player player, @NotNull final String params) { + final String result = onPlaceholderRequest(player, params); + + return result == null ? null : Component.text(result); + } } diff --git a/src/main/java/me/clip/placeholderapi/replacer/ComponentCharsReplacer.java b/src/main/java/me/clip/placeholderapi/replacer/ComponentCharsReplacer.java new file mode 100644 index 0000000..e405ae2 --- /dev/null +++ b/src/main/java/me/clip/placeholderapi/replacer/ComponentCharsReplacer.java @@ -0,0 +1,224 @@ +/* + * This file is part of adventure, licensed under the MIT License. + * + * Copyright (c) 2017-2025 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 me.clip.placeholderapi.replacer; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.BiFunction; +import java.util.function.Function; +import java.util.regex.MatchResult; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import me.clip.placeholderapi.expansion.PlaceholderExpansion; +import net.kyori.adventure.text.*; +import net.kyori.adventure.text.event.HoverEvent; +import net.kyori.adventure.text.format.Style; +import net.kyori.adventure.text.renderer.ComponentRenderer; +import org.bukkit.OfflinePlayer; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +/** + * A renderer performing a replacement on every {@link TextComponent} element of a component tree. + */ +final class ComponentCharsReplacer implements ComponentRenderer { + //static final TextReplacementRenderer INSTANCE = new TextReplacementRenderer(); + private final OfflinePlayer player; + private final Function lookup; + + public ComponentCharsReplacer(@Nullable final OfflinePlayer player, + @NotNull final Function lookup) { + this.player = player; + this.lookup = lookup; + } + + @Override + public Component render(final Component component, final State state) { + if (!state.running) return component; + final boolean prevFirstMatch = state.firstMatch; + state.firstMatch = true; + + final List oldChildren = component.children(); + final int oldChildrenSize = oldChildren.size(); + Style oldStyle = component.style(); + List children = null; + Component modified = component; + // replace the component itself + if (component instanceof TextComponent) { + TextComponent tc = (TextComponent) component; + final String content = tc.content(); + + + + final Matcher matcher = state.pattern.matcher(content); + int replacedUntil = 0; // last index handled + while (matcher.find()) { + final PatternReplacementResult result = state.continuer.shouldReplace(matcher, ++state.matchCount, state.replaceCount); + + if (matcher.start() == 0) { + // if we're a full match, modify the component directly + if (matcher.end() == content.length()) { + final ComponentLike replacement = state.replacement.apply(matcher, Component.text().content(matcher.group()) + .style(component.style())); + + modified = replacement == null ? Component.empty() : replacement.asComponent(); + + if (modified.style().hoverEvent() != null) { + oldStyle = oldStyle.hoverEvent(null); // Remove original hover if it has been replaced completely + } + + // merge style of the match into this component to prevent unexpected loss of style + modified = modified.style(modified.style().merge(component.style(), Style.Merge.Strategy.IF_ABSENT_ON_TARGET)); + + if (children == null) { // Prepare children + children = new ArrayList<>(oldChildrenSize + modified.children().size()); + children.addAll(modified.children()); + } + } else { + // otherwise, work on a child of the root node + modified = Component.text("", component.style()); + final ComponentLike child = state.replacement.apply(matcher, Component.text().content(matcher.group())); + if (child != null) { + if (children == null) { + children = new ArrayList<>(oldChildrenSize + 1); + } + children.add(child.asComponent()); + } + } + } else { + if (children == null) { + children = new ArrayList<>(oldChildrenSize + 2); + } + if (state.firstMatch) { + // truncate parent to content before match + modified = ((TextComponent) component).content(content.substring(0, matcher.start())); + } else if (replacedUntil < matcher.start()) { + children.add(Component.text(content.substring(replacedUntil, matcher.start()))); + } + final ComponentLike builder = state.replacement.apply(matcher, Component.text().content(matcher.group())); + if (builder != null) { + children.add(builder.asComponent()); + } + } + state.replaceCount++; + state.firstMatch = false; + replacedUntil = matcher.end(); + } + if (replacedUntil < content.length()) { + // append trailing content + if (replacedUntil > 0) { + if (children == null) { + children = new ArrayList<>(oldChildrenSize); + } + children.add(Component.text(content.substring(replacedUntil))); + } + // otherwise, we haven't modified the component, so nothing to change + } + } else if (modified instanceof TranslatableComponent) { // get TranslatableComponent with() args + final List args = ((TranslatableComponent) modified).arguments(); + List newArgs = null; + for (int i = 0, size = args.size(); i < size; i++) { + final TranslationArgument original = args.get(i); + final TranslationArgument replaced = original.value() instanceof Component ? TranslationArgument.component(this.render((Component) original.value(), state)) : original; + if (replaced != original) { + if (newArgs == null) { + newArgs = new ArrayList<>(size); + if (i > 0) { + newArgs.addAll(args.subList(0, i)); + } + } + } + if (newArgs != null) { + newArgs.add(replaced); + } + } + if (newArgs != null) { + modified = ((TranslatableComponent) modified).arguments(newArgs); + } + } + // Only visit children if we're running + if (state.running) { + // hover event + if (state.replaceInsideHoverEvents) { + final HoverEvent event = oldStyle.hoverEvent(); + if (event != null) { + final HoverEvent rendered = event.withRenderedValue(this, state); + if (event != rendered) { + modified = modified.style(s -> s.hoverEvent(rendered)); + } + } + } + // Children + boolean first = true; + for (int i = 0; i < oldChildrenSize; i++) { + final Component child = oldChildren.get(i); + final Component replaced = this.render(child, state); + if (replaced != child) { + if (children == null) { + children = new ArrayList<>(oldChildrenSize); + } + if (first) { + children.addAll(oldChildren.subList(0, i)); + } + first = false; + } + if (children != null) { + children.add(replaced); + first = false; + } + } + } else { + // we're not visiting children, re-add original children if necessary + if (children != null) { + children.addAll(oldChildren); + } + } + + state.firstMatch = prevFirstMatch; + // Update the modified component with new children + if (children != null) { + return modified.children(children); + } + return modified; + } + + static final class State { + final Pattern pattern; + final BiFunction replacement; + final TextReplacementConfig.Condition continuer; + final boolean replaceInsideHoverEvents; + boolean running = true; + int matchCount = 0; + int replaceCount = 0; + boolean firstMatch = true; + + State(final Pattern pattern, final BiFunction replacement, final TextReplacementConfig.Condition continuer, final boolean replaceInsideHoverEvents) { + this.pattern = pattern; + this.replacement = replacement; + this.continuer = continuer; + this.replaceInsideHoverEvents = replaceInsideHoverEvents; + } + } +} \ No newline at end of file