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