Compare commits

...

30 Commits

Author SHA1 Message Date
PiggyPiglet
feb4eff237 Merge pull request #1205 from PlaceholderAPI/feat/new-chars-replacer
Feat/new chars replacer
2026-02-25 21:13:49 +08:00
PiggyPiglet
7286d8849b only flag spaces if we haven't met an underscore 2026-02-25 21:13:05 +08:00
PiggyPiglet
1807b92505 choco chars replacer 2026-02-20 21:19:25 +08:00
PiggyPiglet
3c82c6cdd7 revert #1163 2026-02-20 21:18:22 +08:00
PiggyPiglet
17ad663257 fix papi version header 2026-02-14 18:07:15 +08:00
PiggyPiglet
984ec7f7b9 add papi version to user agent 2026-02-14 17:24:31 +08:00
PiggyPiglet
c8f73a5940 add custom user agent for http requests 2026-02-14 17:19:45 +08:00
PiggyPiglet
81675c1fd6 Merge pull request #1188 from VaultedMC/master
PAPIComponents Deserializer Support
2026-02-13 09:56:30 +08:00
Justin
69cef6f72b Renamed serializer to deserializer 2026-02-12 20:38:31 -05:00
Justin
ca2bd9f19a Merge remote-tracking branch 'upstream/master' 2026-02-12 20:32:37 -05:00
Justin
b331145c15 Added serializer support to placeholder replacement methods. 2026-02-12 20:30:30 -05:00
PiggyPiglet
b085e49062 Reimplement Blitz' replacer change for new replacer 2026-02-12 21:00:57 +08:00
PiggyPiglet
43f6a517af Merge pull request #1163 from ichoco-milk/master
Refactor CharsReplacer & PlaceholderAPI for better performance and reduced memory allocation
2026-02-12 20:35:41 +08:00
PiggyPiglet
59769c2d93 Merge branch 'master' into master 2026-02-12 20:33:19 +08:00
PiggyPiglet
354aebe8df Merge pull request #1178 from PlaceholderAPI/fix/argumentless-expansions
Make setPlaceholders behavior consistent with setRelationalPlaceholders
2026-02-10 11:21:25 +08:00
BlitzOffline
566beb48c5 Fix copyright 2026-02-09 17:06:35 +02:00
BlitzOffline
64b3d6fa68 Make setPlaceholders behavior consistent with setRelationalPlaceholders 2026-02-09 17:05:20 +02:00
PiggyPiglet
975b1ac3ca Merge pull request #1089 from PlaceholderAPI/feature/update-issue-template
Update Common Issues page link in issue-template
2026-02-09 22:42:17 +08:00
Andre_601
7b8550c3f1 Fix wrong install command and move comment step 2026-02-09 15:28:11 +01:00
Andre_601
f267887b5e Use wiki PR validation on main branch. 2026-02-09 15:20:59 +01:00
PiggyPiglet
e187eb6402 2.12.3-dev 2026-02-09 01:05:57 +08:00
PiggyPiglet
0d1a356e0f ExpansionSafety: Fix NPE & Only check files not directories 2026-02-09 01:02:32 +08:00
ichocomilk
407b9f5e54 Simplify getExpansion method by removing unnecessary locking 2026-02-04 20:22:01 -03:00
ichocomilk
469997e114 Refactor placeholder methods to use ArrayList for compatibility | I used immutable list by error :( 2026-02-04 20:17:44 -03:00
ichocomilk
8185b7bfe9 Merge remote-tracking branch 'origin/master' 2026-02-04 19:50:07 -03:00
ichocomilk
fe15e4ed7a Update Gradle dependencies and add benchmarks 2026-02-04 19:49:54 -03:00
iChocoMilk
0f35362a0c Fix PlaceholderAPI#containsBracketPlaceholders variable name 2026-02-04 01:50:19 -03:00
ichocomilk
9a4fa18304 Enhance placeholder translation performance and improve code clarity 2026-02-04 01:26:39 -03:00
ichocomilk
cb5d6c0895 Optimize containsPlaceholders 2026-02-04 00:47:58 -03:00
Andre_601
3684e32ac0 Update Common Issues page link in issue-template 2024-12-04 15:00:51 +01:00
18 changed files with 729 additions and 205 deletions

View File

@@ -18,13 +18,13 @@ body:
label: Confirmation
description: Please make sure to have followed the following checks.
options:
- label: My issue isn't already found on the Issue tracker.
- label: "My issue isn't already found on the Issue tracker."
required: true
- label: My issue is about **PlaceholderAPI** and not any expansion or external plugin
- label: "My issue is about **PlaceholderAPI** and not any expansion or external plugin."
required: true
- label: The issue isn't already fixed in a Spigot Release or Development Build.
- label: "The issue isn't already fixed in a Spigot Release or Development Build."
required: true
- label: The [Common Issues](https://github.com/PlaceholderAPI/PlaceholderAPI/wiki/Common-Issues) page doesn't mention this issue.
- label: "The [Common Issues](https://wiki.placeholderapi.com/common-issues/) page doesn't mention this issue."
required: true
- type: dropdown
attributes:
@@ -85,6 +85,8 @@ body:
description: |-
Get the latest content of your `latest.log` file an upload it to https://paste.helpch.at
Take the generated URL and paste it into this field.
**Always provide the full `latest.log` and not just parts of it or just the error (if any)!**
placeholder: "https://paste.helpch.at/latest.log"
- type: input
id: "error"
@@ -99,5 +101,5 @@ body:
description: |-
Add any extra info you think is nessesary for this Bug report.
- If you selected `API Bug` will you need to include code-examples here to reproduce the issue.
- If you selected `Plugin/Server Incompatability` should you include extra Server info such as a Timings or Spark-Report or info about the plugin in question.
- If you selected `Plugin/Server Incompatability` should you include extra Server info such as Spark-Report or info about the plugin in question.
placeholder: "Put any extra info you like into this field..."

View File

@@ -0,0 +1,55 @@
#
# Validates Pull requests targeting the wiki branch
# to ensure that the site can be build successfully
# without broken links, navigation, etc.
#
name: "Validate Wiki Build"
on:
pull_request_target:
types:
- opened
- reopened
- synchronize
paths-ignore:
- "README.md"
branches:
- wiki
workflow_dispatch:
permissions:
contents: read
issues: write
env:
RUN_URL: "${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_number }}"
jobs:
buildWiki:
runs-on: ubuntu-latest
steps:
- name: "Checkout Repository"
uses: actions/checkout@v6
with:
fetch-depth: 0
ref: "${{ github.event.pull_request.head.sha }}"
- name: "Setup Python 3.x"
uses: actions/setup-python@v6
with:
python-version: 3.x
- name: "Install dependencies"
run: "python -m pip install mkdocs-material"
- name: "Build Site"
run: "mkdocs build --strict"
- name: "Create Comment"
uses: peter-evans/create-or-update-comment@v5
if: ${{ failure() }}
with:
body: |-
## Wiki Build failure
Something went wrong while creating a test-build of the Wiki for this Pull request.
Please check the [Workflow Logs](${{ env.RUN_URL }}) for any errors.
issue-number: "${{ github.event.pull_request.number }}"
token: "${{ secrets.GITHUB_TOKEN }}"
edit-mode: replace

View File

@@ -4,11 +4,12 @@ plugins {
`java-library`
`maven-publish`
// id("com.github.hierynomus.license") version "0.16.1"
id("io.github.goooler.shadow") version "8.1.7"
id("com.gradleup.shadow") version "9.3.1"
id("me.champeau.jmh") version "0.7.2"
}
group = "me.clip"
version = "2.12.2"
version = "2.12.3-DEV-${System.getProperty("BUILD_NUMBER")}"
description = "An awesome placeholder provider!"
@@ -41,12 +42,13 @@ dependencies {
compileOnly("dev.folia:folia-api:1.21.11-R0.1-SNAPSHOT")
compileOnlyApi("org.jetbrains:annotations:23.0.0")
testImplementation("org.openjdk.jmh:jmh-core:1.32")
testImplementation("org.openjdk.jmh:jmh-generator-annprocess:1.32")
testImplementation("org.junit.jupiter:junit-jupiter-engine:5.8.2")
testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:5.8.1")
}
jmh("org.openjdk.jmh:jmh-core:1.37")
jmh("org.openjdk.jmh:jmh-generator-annprocess:1.37")
jmhAnnotationProcessor("org.openjdk.jmh:jmh-generator-annprocess:1.37")
testImplementation("org.junit.jupiter:junit-jupiter:6.0.2")
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
}
java {
sourceCompatibility = JavaVersion.VERSION_1_8

View File

@@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.8-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-9.3.0-bin.zip
networkTimeout=10000
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

View File

@@ -0,0 +1,94 @@
/*
* This file is part of PlaceholderAPI
*
* PlaceholderAPI
* Copyright (c) 2015 - 2026 PlaceholderAPI Team
*
* PlaceholderAPI free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* PlaceholderAPI is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package me.clip.placeholderapi;
import com.google.common.collect.ImmutableMap;
import me.clip.placeholderapi.expansion.PlaceholderExpansion;
import me.clip.placeholderapi.replacer.CharsReplacer;
import me.clip.placeholderapi.replacer.OldCharsReplacer;
import me.clip.placeholderapi.replacer.Replacer;
import org.bukkit.OfflinePlayer;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
public interface Values {
String SMALL_TEXT = "My name is %player_name%";
String LARGE_TEXT = "My name is %player_name% and my location is (%player_x%, %player_y%, %player_z%), this placeholder is invalid %server_name%";
ImmutableMap<String, PlaceholderExpansion> PLACEHOLDERS = ImmutableMap.<String, PlaceholderExpansion>builder()
.put("player", new MockPlayerPlaceholderExpansion())
.build();
Replacer CHARS_REPLACER = new CharsReplacer(Replacer.Closure.PERCENT);
Replacer OLD_CHARS_REPLACER = new OldCharsReplacer(Replacer.Closure.PERCENT);
final class MockPlayerPlaceholderExpansion extends PlaceholderExpansion {
public static final String PLAYER_X = "10";
public static final String PLAYER_Y = "20";
public static final String PLAYER_Z = "30";
public static final String PLAYER_NAME = "Sxtanna";
@NotNull
@Override
public String getIdentifier() {
return "player";
}
@NotNull
@Override
public String getAuthor() {
return "Sxtanna";
}
@NotNull
@Override
public String getVersion() {
return "1.0";
}
@Override
public String onRequest(@Nullable final OfflinePlayer player, @NotNull final String params) {
final String[] parts = params.split("_");
if (parts.length == 0) {
return null;
}
switch (parts[0]) {
case "name":
return PLAYER_NAME;
case "x":
return PLAYER_X;
case "y":
return PLAYER_Y;
case "z":
return PLAYER_Z;
}
return null;
}
}
}

View File

@@ -0,0 +1,136 @@
package me.clip.placeholderapi.replacer;
/*
* This file is part of PlaceholderAPI
*
* PlaceholderAPI
* Copyright (c) 2015 - 2026 PlaceholderAPI Team
*
* PlaceholderAPI free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* PlaceholderAPI is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import java.util.Locale;
import java.util.function.Function;
import me.clip.placeholderapi.expansion.PlaceholderExpansion;
import org.bukkit.OfflinePlayer;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
public final class OldCharsReplacer implements Replacer {
@NotNull
private final Closure closure;
public OldCharsReplacer(@NotNull final Closure closure) {
this.closure = closure;
}
@NotNull
@Override
public String apply(@NotNull final String text, @Nullable final OfflinePlayer player,
@NotNull final Function<String, @Nullable PlaceholderExpansion> lookup) {
final char[] chars = text.toCharArray();
final StringBuilder builder = new StringBuilder(text.length());
final StringBuilder identifier = new StringBuilder();
final StringBuilder parameters = new StringBuilder();
for (int i = 0; i < chars.length; i++) {
final char l = chars[i];
if (l != closure.head || i + 1 >= chars.length) {
builder.append(l);
continue;
}
boolean identified = false;
boolean invalid = true;
boolean hadSpace = false;
while (++i < chars.length) {
final char p = chars[i];
if (p == ' ' && !identified) {
hadSpace = true;
break;
}
if (p == closure.tail) {
invalid = false;
break;
}
if (p == '_' && !identified) {
identified = true;
continue;
}
if (identified) {
parameters.append(p);
} else {
identifier.append(p);
}
}
final String identifierString = identifier.toString();
final String lowercaseIdentifierString = identifierString.toLowerCase(Locale.ROOT);
final String parametersString = parameters.toString();
identifier.setLength(0);
parameters.setLength(0);
if (invalid) {
builder.append(closure.head).append(identifierString);
if (identified) {
builder.append('_').append(parametersString);
}
if (hadSpace) {
builder.append(' ');
}
continue;
}
final PlaceholderExpansion placeholder = lookup.apply(lowercaseIdentifierString);
if (placeholder == null) {
builder.append(closure.head).append(identifierString);
if (identified) {
builder.append('_');
}
builder.append(parametersString).append(closure.tail);
continue;
}
final String replacement = placeholder.onRequest(player, parametersString);
if (replacement == null) {
builder.append(closure.head).append(identifierString);
if (identified) {
builder.append('_');
}
builder.append(parametersString).append(closure.tail);
continue;
}
builder.append(replacement);
}
return builder.toString();
}
}

View File

@@ -0,0 +1,66 @@
/*
* This file is part of PlaceholderAPI
*
* PlaceholderAPI
* Copyright (c) 2015 - 2026 PlaceholderAPI Team
*
* PlaceholderAPI free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* PlaceholderAPI is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package me.clip.placeholderapi.replacer;
import me.clip.placeholderapi.Values;
import me.clip.placeholderapi.expansion.PlaceholderExpansion;
import org.jetbrains.annotations.Nullable;
import org.openjdk.jmh.annotations.*;
import org.openjdk.jmh.infra.Blackhole;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;
@State(Scope.Benchmark)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@BenchmarkMode({Mode.AverageTime, Mode.Throughput})
@Fork(value = 3, warmups = 1)
@Warmup(iterations = 5, time = 1)
@Measurement(iterations = 10, time = 1)
public class ReplacerBenchmarks {
private Function<String, @Nullable PlaceholderExpansion> expansionFunction;
@Setup
public void setup() {
this.expansionFunction = Values.PLACEHOLDERS::get;
}
@Benchmark
public void measureCharsReplacerSmallText(final Blackhole blackhole) {
blackhole.consume(Values.CHARS_REPLACER.apply(Values.SMALL_TEXT, null, expansionFunction));
}
@Benchmark
public void measureCharsReplacerLargeText(final Blackhole blackhole) {
blackhole.consume(Values.CHARS_REPLACER.apply(Values.LARGE_TEXT, null, expansionFunction));
}
@Benchmark
public void measureCharsReplacerSmallTextOld(final Blackhole blackhole) {
blackhole.consume(Values.OLD_CHARS_REPLACER.apply(Values.SMALL_TEXT, null, expansionFunction));
}
@Benchmark
public void measureCharsReplacerLargeTextOld(final Blackhole blackhole) {
blackhole.consume(Values.OLD_CHARS_REPLACER.apply(Values.LARGE_TEXT, null, expansionFunction));
}
}

View File

@@ -20,12 +20,7 @@
package me.clip.placeholderapi;
import com.google.common.collect.ImmutableSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
@@ -85,7 +80,11 @@ public final class PlaceholderAPI {
@NotNull
public static List<String> setPlaceholders(final OfflinePlayer player,
@NotNull final List<String> text) {
return text.stream().map(line -> setPlaceholders(player, line)).collect(Collectors.toList());
final List<String> result = new ArrayList<>(text.size());
for (final String line : text) {
result.add(setPlaceholders(player, line));
}
return result;
}
/**
@@ -140,8 +139,11 @@ public final class PlaceholderAPI {
@NotNull
public static List<@NotNull String> setBracketPlaceholders(final OfflinePlayer player,
@NotNull final List<@NotNull String> text) {
return text.stream().map(line -> setBracketPlaceholders(player, line))
.collect(Collectors.toList());
final List<String> result = new ArrayList<>(text.size());
for (final String line : text) {
result.add(setBracketPlaceholders(player, line));
}
return result;
}
/**
@@ -179,14 +181,14 @@ public final class PlaceholderAPI {
* @param text Text to parse the placeholders in
* @return The text containing the parsed relational placeholders
*/
public static String setRelationalPlaceholders(Player one, Player two, String text) {
public static String setRelationalPlaceholders(final Player one, final Player two, @NotNull String text) {
final Matcher matcher = RELATIONAL_PLACEHOLDER_PATTERN.matcher(text);
while (matcher.find()) {
final String format = matcher.group(2);
final int index = format.indexOf("_");
final int index = format.indexOf('_');
if (index <= 0 || index >= format.length()) {
if (index <= 0) {
continue;
}
@@ -218,9 +220,12 @@ public final class PlaceholderAPI {
* @param text text to parse the placeholder values to
* @return The text containing the parsed relational placeholders
*/
public static List<String> setRelationalPlaceholders(Player one, Player two, List<String> text) {
return text.stream().map(line -> setRelationalPlaceholders(one, two, line))
.collect(Collectors.toList());
public static List<String> setRelationalPlaceholders(final Player one, final Player two, final @NotNull List<String> text) {
final List<String> result = new ArrayList<>(text.size());
for (final String line : text) {
result.add(setRelationalPlaceholders(one, two, line));
}
return result;
}
/**
@@ -241,8 +246,7 @@ public final class PlaceholderAPI {
*/
@NotNull
public static Set<String> getRegisteredIdentifiers() {
return ImmutableSet
.copyOf(PlaceholderAPIPlugin.getInstance().getLocalExpansionManager().getIdentifiers());
return PlaceholderAPIPlugin.getInstance().getLocalExpansionManager().getIdentifiers();
}
/**
@@ -279,8 +283,15 @@ public final class PlaceholderAPI {
* @param text String to check
* @return true if String contains any matches to the normal placeholder pattern, false otherwise
*/
public static boolean containsPlaceholders(String text) {
return text != null && PLACEHOLDER_PATTERN.matcher(text).find();
public static boolean containsPlaceholders(final String text) {
if (text == null) {
return false;
}
final int firstPercent = text.indexOf('%');
if (firstPercent == -1) {
return false;
}
return text.indexOf('%', firstPercent + 1) != -1;
}
/**
@@ -290,8 +301,15 @@ public final class PlaceholderAPI {
* @param text String to check
* @return true if String contains any matches to the bracket placeholder pattern, false otherwise
*/
public static boolean containsBracketPlaceholders(String text) {
return text != null && BRACKET_PLACEHOLDER_PATTERN.matcher(text).find();
public static boolean containsBracketPlaceholders(final String text) {
if (text == null) {
return false;
}
final int openBracket = text.indexOf('{');
if (openBracket == -1) {
return false;
}
return text.indexOf('}', openBracket + 1) != -1;
}
// === Deprecated API ===

View File

@@ -26,12 +26,9 @@ import com.google.common.util.concurrent.ThreadFactoryBuilder;
import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.*;
import java.lang.reflect.Type;
import java.net.URL;
import java.net.UnknownHostException;
import java.net.*;
import java.nio.channels.Channels;
import java.nio.channels.ReadableByteChannel;
import java.nio.charset.StandardCharsets;
@@ -61,6 +58,24 @@ import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Unmodifiable;
public final class CloudExpansionManager {
@NotNull
public static final String USER_AGENT;
static {
String userAgent;
try (final InputStream in = PlaceholderAPIPlugin.class.getResourceAsStream("/user-agent.txt")) {
if (in == null) {
userAgent = "PlaceholderAPI-Bukkit-null";
} else {
userAgent = new BufferedReader(new InputStreamReader(in, StandardCharsets.UTF_8)).readLine();
}
} catch (IOException e) {
userAgent = "PlaceholderAPI-Bukkit-null";
}
USER_AGENT = userAgent;
}
@NotNull
private static final String API_URL = "https://ecloud.placeholderapi.com/api/v3/?platform=bukkit";
@@ -185,8 +200,16 @@ public final class CloudExpansionManager {
// a defence tactic! use ConcurrentHashMap instead of normal HashMap
Map<String, CloudExpansion> values = new ConcurrentHashMap<>();
try {
//noinspection UnstableApiUsage
String json = Resources.toString(new URL(API_URL), StandardCharsets.UTF_8);
final URI uri = new URI(API_URL);
final URLConnection connection = uri.toURL().openConnection();
connection.setRequestProperty("User-Agent", USER_AGENT);
final String json;
try (final InputStream in = connection.getInputStream()) {
final BufferedReader reader = new BufferedReader(new InputStreamReader(in, StandardCharsets.UTF_8));
json = reader.lines().collect(Collectors.joining(System.lineSeparator()));
}
values.putAll(GSON.fromJson(json, TYPE));
List<String> toRemove = new ArrayList<>();
@@ -260,10 +283,14 @@ public final class CloudExpansionManager {
"Expansion-" + toIndexName(expansion) + ".jar");
final CompletableFuture<File> download = CompletableFuture.supplyAsync(() -> {
try (final ReadableByteChannel source = Channels.newChannel(new URL(version.getUrl())
.openStream()); final FileOutputStream target = new FileOutputStream(file)) {
target.getChannel().transferFrom(source, 0, Long.MAX_VALUE);
} catch (final IOException ex) {
try {
final URLConnection connection = new URI(version.getUrl()).toURL().openConnection();
connection.setRequestProperty("User-Agent", USER_AGENT);
try (final ReadableByteChannel source = Channels.newChannel(connection.getInputStream()); final FileOutputStream target = new FileOutputStream(file)) {
target.getChannel().transferFrom(source, 0, Long.MAX_VALUE);
}
} catch (final IOException | URISyntaxException ex) {
throw new CompletionException(ex);
}
return file;

View File

@@ -114,7 +114,7 @@ public final class LocalExpansionManager implements Listener {
@NotNull
@Unmodifiable
public Collection<String> getIdentifiers() {
public Set<String> getIdentifiers() {
expansionsLock.lock();
try {
return ImmutableSet.copyOf(expansions.keySet());
@@ -136,12 +136,7 @@ public final class LocalExpansionManager implements Listener {
@Nullable
public PlaceholderExpansion getExpansion(@NotNull final String identifier) {
expansionsLock.lock();
try {
return expansions.get(identifier.toLowerCase(Locale.ROOT));
} finally {
expansionsLock.unlock();
}
return expansions.get(identifier.toLowerCase(Locale.ROOT));
}
@NotNull

View File

@@ -22,9 +22,7 @@ package me.clip.placeholderapi.replacer;
import java.util.Locale;
import java.util.function.Function;
import me.clip.placeholderapi.expansion.PlaceholderExpansion;
import org.bukkit.ChatColor;
import org.bukkit.OfflinePlayer;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@@ -38,101 +36,116 @@ public final class CharsReplacer implements Replacer {
this.closure = closure;
}
/**
* Translates placeholders within the provided text using a high-performance
* character-scanning approach.
* * <p>The method identifies placeholders delimited by the defined {@link Closure}
* (e.g., %identifier_params% or {identifier_params}). If a placeholder is
* successfully identified, the provided lookup function is used to fetch the
* corresponding {@link PlaceholderExpansion}.</p>
*
* @param text The raw text containing potential placeholders to be replaced.
* @param player The {@link OfflinePlayer} to contextually parse the placeholders against.
* May be {@code null} if no player context is available.
* @param lookup A function that maps a lowercase identifier string to a registered
* {@link PlaceholderExpansion}.
* @return A string with all valid placeholders replaced by their respective values.
* Returns the original text if no placeholders are found.
*/
@NotNull
@Override
public String apply(@NotNull final String text, @Nullable final OfflinePlayer player,
@NotNull final Function<String, @Nullable PlaceholderExpansion> lookup) {
final char[] chars = text.toCharArray();
final StringBuilder builder = new StringBuilder(text.length());
final char head = closure.head;
int startPlaceholder = text.indexOf(head);
final StringBuilder identifier = new StringBuilder();
final StringBuilder parameters = new StringBuilder();
if (startPlaceholder == -1) {
return text;
}
for (int i = 0; i < chars.length; i++) {
final char l = chars[i];
final int length = text.length();
final StringBuilder builder = new StringBuilder(length + (length >> 3));
int cursor = 0;
if (l != closure.head || i + 1 >= chars.length) {
builder.append(l);
final char tail = closure.tail;
loop: do {
// Append plain text preceding the placeholder
if (startPlaceholder > cursor) {
builder.append(text, cursor, startPlaceholder);
}
final int endPlaceholder = text.indexOf(tail, startPlaceholder + 1);
if (endPlaceholder == -1) {
builder.append(text, startPlaceholder, length);
return builder.toString();
}
int underscoreIndex = -1;
for (int i = startPlaceholder + 1; i < endPlaceholder; i++) {
final char current = text.charAt(i);
if (current == ' ' && underscoreIndex == -1) {
// Invalid placeholder (contains space before _).
// Treat the opening symbol as literal text and search for the next one.
builder.append(head);
cursor = startPlaceholder + 1;
startPlaceholder = text.indexOf(head, cursor);
// Safety check: If no more placeholders exist, break to finalize
if (startPlaceholder == -1) {
break loop;
}
continue loop;
}
if (current == '_' && underscoreIndex == -1) {
underscoreIndex = i;
}
}
if (underscoreIndex == -1) {
builder.append(text, startPlaceholder, endPlaceholder + 1);
cursor = endPlaceholder + 1;
startPlaceholder = text.indexOf(head, cursor);
continue;
}
boolean identified = false;
boolean invalid = true;
boolean hadSpace = false;
String identifier = text.substring(startPlaceholder + 1, underscoreIndex);
String parameters = "";
while (++i < chars.length) {
final char p = chars[i];
if (p == ' ' && !identified) {
hadSpace = true;
break;
}
if (p == closure.tail) {
invalid = false;
break;
}
if (p == '_' && !identified) {
identified = true;
continue;
}
if (identified) {
parameters.append(p);
} else {
identifier.append(p);
}
if (underscoreIndex + 1 < endPlaceholder) {
parameters = text.substring(underscoreIndex + 1, endPlaceholder);
}
final String identifierString = identifier.toString();
final String lowercaseIdentifierString = identifierString.toLowerCase(Locale.ROOT);
final String parametersString = parameters.toString();
final PlaceholderExpansion expansion = lookup.apply(identifier.toLowerCase(Locale.ROOT));
String replacement = null;
identifier.setLength(0);
parameters.setLength(0);
if (invalid) {
builder.append(closure.head).append(identifierString);
if (identified) {
builder.append('_').append(parametersString);
}
if (hadSpace) {
builder.append(' ');
}
continue;
if (expansion != null) {
replacement = expansion.onRequest(player, parameters);
}
final PlaceholderExpansion placeholder = lookup.apply(lowercaseIdentifierString);
if (placeholder == null) {
builder.append(closure.head).append(identifierString);
if (identified) {
builder.append('_');
}
builder.append(parametersString).append(closure.tail);
continue;
if (replacement != null) {
builder.append(replacement);
} else {
// Fallback: Restore original placeholder format
builder.append(head).append(identifier);
builder.append('_').append(parameters);
builder.append(tail);
}
final String replacement = placeholder.onRequest(player, parametersString);
if (replacement == null) {
builder.append(closure.head).append(identifierString);
cursor = endPlaceholder + 1;
startPlaceholder = text.indexOf(head, cursor);
if (identified) {
builder.append('_');
}
} while (startPlaceholder != -1);
builder.append(parametersString).append(closure.tail);
continue;
}
builder.append(replacement);
if (cursor < length) {
builder.append(text, cursor, length);
}
return builder.toString();
}
}
}

View File

@@ -4,10 +4,15 @@ import com.google.common.hash.Hashing;
import com.google.common.io.Files;
import com.google.common.io.Resources;
import me.clip.placeholderapi.PlaceholderAPIPlugin;
import me.clip.placeholderapi.expansion.manager.CloudExpansionManager;
import org.jetbrains.annotations.NotNull;
import java.io.BufferedReader;
import java.io.File;
import java.io.InputStreamReader;
import java.net.URI;
import java.net.URL;
import java.net.URLConnection;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.HashSet;
@@ -47,7 +52,10 @@ public final class ExpansionSafetyCheck {
final Set<String> knownMaliciousExpansions;
try {
final String hashes = Resources.toString(new URL("https://check.placeholderapi.com"), StandardCharsets.UTF_8);
final URLConnection connection = new URI("https://check.placeholderapi.com").toURL().openConnection();
connection.setRequestProperty("User-Agent", CloudExpansionManager.USER_AGENT);
final String hashes = new BufferedReader(new InputStreamReader(connection.getInputStream(), StandardCharsets.UTF_8))
.lines().collect(Collectors.joining(System.lineSeparator()));
knownMaliciousExpansions = Arrays.stream(hashes.split("\n")).collect(Collectors.toSet());
} catch (Exception e) {
main.getLogger().log(Level.SEVERE, "Failed to download anti malware hash check list from https://check.placeholderapi.com", e);
@@ -55,9 +63,18 @@ public final class ExpansionSafetyCheck {
}
final Set<String> maliciousPaths = new HashSet<>();
final File[] files = expansionsFolder.listFiles();
for (File file : expansionsFolder.listFiles()) {
if (files == null) {
return false;
}
for (File file : files) {
try {
if (!file.isFile()) {
continue;
}
final String hash = Hashing.sha256().hashBytes(Files.asByteSource(file).read()).toString();
if (knownMaliciousExpansions.contains(hash)) {

View File

@@ -0,0 +1 @@
PlaceholderAPI-Bukkit-${version}

View File

@@ -25,11 +25,14 @@ import me.clip.placeholderapi.replacer.ExactReplacer;
import me.clip.placeholderapi.replacer.RelationalExactReplacer;
import me.clip.placeholderapi.replacer.Replacer;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.ComponentLike;
import org.bukkit.OfflinePlayer;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.List;
import java.util.function.Function;
import java.util.stream.Collectors;
import static me.clip.placeholderapi.PlaceholderAPI.RELATIONAL_PLACEHOLDER_PATTERN;
@@ -43,122 +46,236 @@ public final class PAPIComponents {
* Translates all placeholders into their corresponding values.
* <br>The pattern of a valid placeholder is {@literal %<identifier>_<params>%}.
*
* @param player Player to parse the placeholders against
* @param player Player to parse the placeholders against
* @param component Component to set the placeholder values in
* @return Component containing all translated placeholders
*/
@NotNull
public static Component setPlaceholders(final OfflinePlayer player, @NotNull final Component component) {
if (PlaceholderAPIPlugin.getInstance().getPlaceholderAPIConfig().useAdventureProvidedReplacer()) {
return component.replaceText(config -> config.match(PlaceholderAPI.PLACEHOLDER_PATTERN).replacement((result, builder) ->
builder.content(PERCENT_EXACT_REPLACER.apply(result.group(), player, PlaceholderAPIPlugin.getInstance().getLocalExpansionManager()::getExpansion))));
}
return ComponentReplacer.replace(component, str -> PlaceholderAPI.setPlaceholders(player, str));
return setPlaceholders(player, component, null);
}
/**
* Translates all placeholders into their corresponding values.
* <br>The pattern of a valid placeholder is {@literal %<identifier>_<params>%}.
*
* @param player Player to parse the placeholders against
* @param player Player to parse the placeholders against
* @param component Component to set the placeholder values in
* @param deserializer Optional function to serialize parsed placeholder values into ComponentLike
* @return Component containing all translated placeholders
*/
@NotNull
public static Component setPlaceholders(final OfflinePlayer player, @NotNull final Component component, @Nullable Function<String, ComponentLike> deserializer) {
if (PlaceholderAPIPlugin.getInstance().getPlaceholderAPIConfig().useAdventureProvidedReplacer()) {
return component.replaceText(config -> config.match(PlaceholderAPI.PLACEHOLDER_PATTERN).replacement((result, builder) -> {
String parsed = PERCENT_EXACT_REPLACER.apply(result.group(), player, PlaceholderAPIPlugin.getInstance().getLocalExpansionManager()::getExpansion);
return deserializer == null ? builder.content(parsed) : deserializer.apply(parsed);
}));
}
return ComponentReplacer.replace(component, str -> PlaceholderAPI.setPlaceholders(player, str), deserializer == null ? null : s -> deserializer.apply(s).asComponent());
}
/**
* Translates all placeholders into their corresponding values.
* <br>The pattern of a valid placeholder is {@literal %<identifier>_<params>%}.
*
* @param player Player to parse the placeholders against
* @param components List of Components to set the placeholder values in
* @return List of Components containing all translated placeholders
*/
@NotNull
public static List<Component> setPlaceholders(final OfflinePlayer player, @NotNull final List<Component> components) {
return components.stream().map(component -> setPlaceholders(player, component)).collect(Collectors.toList());
return setPlaceholders(player, components, null);
}
/**
* Translates all placeholders into their corresponding values.
* <br>The pattern of a valid placeholder is {@literal %<identifier>_<params>%}.
*
* @param player Player to parse the placeholders against
* @param player Player to parse the placeholders against
* @param components List of Components to set the placeholder values in
* @param deserializer Optional function to serialize parsed placeholder values into ComponentLike
* @return List of Components containing all translated placeholders
*/
@NotNull
public static List<Component> setPlaceholders(final OfflinePlayer player, @NotNull final List<Component> components, @Nullable Function<String, ComponentLike> deserializer) {
return components.stream().map(component -> setPlaceholders(player, component, deserializer == null ? null : s -> deserializer.apply(s).asComponent())).collect(Collectors.toList());
}
/**
* Translates all placeholders into their corresponding values.
* <br>The pattern of a valid placeholder is {@literal %<identifier>_<params>%}.
*
* @param player Player to parse the placeholders against
* @param component Component to set the placeholder values in
* @return Component containing all translated placeholders
*/
@NotNull
public static Component setPlaceholders(final Player player, @NotNull final Component component) {
return setPlaceholders((OfflinePlayer) player, component);
return setPlaceholders(player, component, null);
}
/**
* Translates all placeholders into their corresponding values.
* <br>The pattern of a valid placeholder is {@literal %<identifier>_<params>%}.
*
* @param player Player to parse the placeholders against
* @param player Player to parse the placeholders against
* @param component Component to set the placeholder values in
* @param deserializer Optional function to serialize parsed placeholder values into ComponentLike
* @return Component containing all translated placeholders
*/
@NotNull
public static Component setPlaceholders(final Player player, @NotNull final Component component, @Nullable Function<String, ComponentLike> deserializer) {
return setPlaceholders((OfflinePlayer) player, component, deserializer);
}
/**
* Translates all placeholders into their corresponding values.
* <br>The pattern of a valid placeholder is {@literal %<identifier>_<params>%}.
*
* @param player Player to parse the placeholders against
* @param components List of Components to set the placeholder values in
* @return List of components containing all translated placeholders
*/
@NotNull
public static List<Component> setPlaceholders(final Player player, @NotNull final List<Component> components) {
return setPlaceholders((OfflinePlayer) player, components);
return setPlaceholders(player, components, null);
}
/**
* Translates all placeholders into their corresponding values.
* <br>The pattern of a valid placeholder is {@literal %<identifier>_<params>%}.
*
* @param player Player to parse the placeholders against
* @param components List of Components to set the placeholder values in
* @param deserializer Optional function to serialize parsed placeholder values into ComponentLike
* @return List of components containing all translated placeholders
*/
@NotNull
public static List<Component> setPlaceholders(final Player player, @NotNull final List<Component> components, @Nullable Function<String, ComponentLike> deserializer) {
return setPlaceholders((OfflinePlayer) player, components, deserializer);
}
/**
* Translates all placeholders into their corresponding values.
* <br>The pattern of a valid placeholder is {@literal {<identifier>_<params>}}.
*
* @param player Player to parse the placeholders against
* @param player Player to parse the placeholders against
* @param component Component to set the placeholder values in
* @return Component containing all translated placeholders
*/
@NotNull
public static Component setBracketPlaceholders(final OfflinePlayer player, @NotNull final Component component) {
return setBracketPlaceholders(player, component, null);
}
/**
* Translates all placeholders into their corresponding values.
* <br>The pattern of a valid placeholder is {@literal {<identifier>_<params>}}.
*
* @param player Player to parse the placeholders against
* @param component Component to set the placeholder values in
* @param deserializer Optional function to serialize parsed placeholder values into ComponentLike
* @return Component containing all translated placeholders
*/
@NotNull
public static Component setBracketPlaceholders(final OfflinePlayer player, @NotNull final Component component, @Nullable Function<String, ComponentLike> deserializer) {
if (PlaceholderAPIPlugin.getInstance().getPlaceholderAPIConfig().useAdventureReplacer()) {
return component.replaceText(config -> config.match(PlaceholderAPI.BRACKET_PLACEHOLDER_PATTERN).replacement((result, builder) ->
builder.content(BRACKET_EXACT_REPLACER.apply(result.group(), player, PlaceholderAPIPlugin.getInstance().getLocalExpansionManager()::getExpansion))));
}
return ComponentReplacer.replace(component, str -> PlaceholderAPI.setBracketPlaceholders(player, str));
return ComponentReplacer.replace(component, str -> PlaceholderAPI.setBracketPlaceholders(player, str), deserializer == null ? null : s -> deserializer.apply(s).asComponent());
}
/**
* Translates all placeholders into their corresponding values.
* <br>The pattern of a valid placeholder is {@literal {<identifier>_<params>}}.
*
* @param player Player to parse the placeholders against
* @param player Player to parse the placeholders against
* @param components List of Components to set the placeholder values in
* @return List of Components containing all translated placeholders
*/
@NotNull
public static List<Component> setBracketPlaceholders(final OfflinePlayer player, @NotNull final List<Component> components) {
return components.stream().map(component -> setBracketPlaceholders(player, component)).collect(Collectors.toList());
return setBracketPlaceholders(player, components, null);
}
/**
* Translates all placeholders into their corresponding values.
* <br>The pattern of a valid placeholder is {@literal {<identifier>_<params>}}.
*
* @param player Player to parse the placeholders against
* @param player Player to parse the placeholders against
* @param components List of Components to set the placeholder values in
* @param deserializer Optional function to serialize parsed placeholder values into ComponentLike
* @return List of Components containing all translated placeholders
*/
@NotNull
public static List<Component> setBracketPlaceholders(final OfflinePlayer player, @NotNull final List<Component> components, @Nullable Function<String, ComponentLike> deserializer) {
return components.stream().map(component -> setBracketPlaceholders(player, component, deserializer)).collect(Collectors.toList());
}
/**
* Translates all placeholders into their corresponding values.
* <br>The pattern of a valid placeholder is {@literal {<identifier>_<params>}}.
*
* @param player Player to parse the placeholders against
* @param component Component to set the placeholder values in
* @return Component containing all translated placeholders
*/
@NotNull
public static Component setBracketPlaceholders(final Player player, @NotNull final Component component) {
return setBracketPlaceholders((OfflinePlayer) player, component);
return setBracketPlaceholders(player, component, null);
}
/**
* Translates all placeholders into their corresponding values.
* <br>The pattern of a valid placeholder is {@literal {<identifier>_<params>}}.
*
* @param player Player to parse the placeholders against
* @param player Player to parse the placeholders against
* @param component Component to set the placeholder values in
* @param deserializer Optional function to serialize parsed placeholder values into ComponentLike
* @return Component containing all translated placeholders
*/
@NotNull
public static Component setBracketPlaceholders(final Player player, @NotNull final Component component, @Nullable Function<String, ComponentLike> deserializer) {
return setBracketPlaceholders((OfflinePlayer) player, component, deserializer);
}
/**
* Translates all placeholders into their corresponding values.
* <br>The pattern of a valid placeholder is {@literal {<identifier>_<params>}}.
*
* @param player Player to parse the placeholders against
* @param components List of Components to set the placeholder values in
* @return List of Components containing all translated placeholders
*/
@NotNull
public static List<Component> setBracketPlaceholders(final Player player, @NotNull final List<Component> components) {
return setBracketPlaceholders((OfflinePlayer) player, components);
return setBracketPlaceholders(player, components, null);
}
/**
* Translates all placeholders into their corresponding values.
* <br>The pattern of a valid placeholder is {@literal {<identifier>_<params>}}.
*
* @param player Player to parse the placeholders against
* @param components List of Components to set the placeholder values in
* @param deserializer Optional function to serialize parsed placeholder values into ComponentLike
* @return List of Components containing all translated placeholders
*/
@NotNull
public static List<Component> setBracketPlaceholders(final Player player, @NotNull final List<Component> components, @Nullable Function<String, ComponentLike> deserializer) {
return setBracketPlaceholders((OfflinePlayer) player, components, deserializer);
}
/**
* set relational placeholders in the text specified placeholders are matched with the pattern
* {@literal %<rel_(identifier)_(params)>%} when set with this method
*
* @param one First player to compare
* @param two Second player to compare
* @param one First player to compare
* @param two Second player to compare
* @param component Component to parse the placeholders in
* @return The Component containing the parsed relational placeholders
*/
@@ -172,8 +289,8 @@ public final class PAPIComponents {
* Translate placeholders in the provided List based on the relation of the two provided players.
* <br>The pattern of a valid placeholder is {@literal %rel_<identifier>_<param>%}.
*
* @param one Player to compare
* @param two Player to compare
* @param one Player to compare
* @param two Player to compare
* @param components List of Components to parse the placeholder values to
* @return The List of Components containing the parsed relational placeholders
*/
@@ -181,4 +298,4 @@ public final class PAPIComponents {
return components.stream().map(line -> setRelationalPlaceholders(one, two, line))
.collect(Collectors.toList());
}
}
}

View File

@@ -8,6 +8,7 @@ import net.kyori.adventure.text.event.DataComponentValue;
import net.kyori.adventure.text.event.HoverEvent;
import net.kyori.adventure.text.format.Style;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
import java.util.HashMap;
@@ -17,25 +18,25 @@ import java.util.function.Function;
public class ComponentReplacer {
@NotNull
public static Component replace(@NotNull final Component component, @NotNull final Function<String, String> replacer) {
return rebuild(component, replacer);
public static Component replace(@NotNull final Component component, @NotNull final Function<String, String> replacer, @Nullable final Function<String, Component> deserializer) {
return rebuild(component, replacer, deserializer);
}
@NotNull
private static Component rebuild(@NotNull final Component component, @NotNull final Function<String, String> replacer) {
private static Component rebuild(@NotNull final Component component, @NotNull final Function<String, String> replacer, @Nullable final Function<String, Component> deserializer) {
Component rebuilt;
if (component instanceof TextComponent) {
final TextComponent text = (TextComponent) component;
final String replaced = replacer.apply(text.content());
rebuilt = Component.text(replaced);
rebuilt = deserializer == null ? Component.text(replaced) : deserializer.apply(replaced);
} else if (component instanceof TranslatableComponent) {
final TranslatableComponent translatable = (TranslatableComponent) component;
final List<Component> arguments = new ArrayList<>();
for (final ComponentLike arg : translatable.arguments()) {
arguments.add(rebuild(arg.asComponent(), replacer));
arguments.add(rebuild(arg.asComponent(), replacer, deserializer));
}
rebuilt = Component.translatable(translatable.key(), arguments);
@@ -52,12 +53,12 @@ public class ComponentReplacer {
rebuilt = Component.empty();
}
rebuilt = rebuilt.style(rebuildStyle(component.style(), replacer));
rebuilt = rebuilt.style(rebuildStyle(component.style(), replacer, deserializer));
if (!component.children().isEmpty()) {
final List<Component> children = new ArrayList<>();
for (Component child : component.children()) {
children.add(rebuild(child, replacer));
children.add(rebuild(child, replacer, deserializer));
}
rebuilt = rebuilt.children(children);
}
@@ -66,7 +67,7 @@ public class ComponentReplacer {
}
@NotNull
private static Style rebuildStyle(@NotNull final Style style, @NotNull final Function<String, String> replacer) {
private static Style rebuildStyle(@NotNull final Style style, @NotNull final Function<String, String> replacer, @Nullable final Function<String, Component> deserializer) {
final Style.Builder builder = style.toBuilder();
final ClickEvent click = style.clickEvent();
@@ -77,7 +78,7 @@ public class ComponentReplacer {
final HoverEvent<?> hover = style.hoverEvent();
if (hover != null) {
builder.hoverEvent(rebuildHoverEvent(hover, replacer));
builder.hoverEvent(rebuildHoverEvent(hover, replacer, deserializer));
}
return builder.build();
@@ -113,11 +114,11 @@ public class ComponentReplacer {
}
@NotNull
private static HoverEvent<?> rebuildHoverEvent(@NotNull final HoverEvent<?> hover, @NotNull final Function<String, String> replacer) {
private static HoverEvent<?> rebuildHoverEvent(@NotNull final HoverEvent<?> hover, @NotNull final Function<String, String> replacer, @Nullable final Function<String, Component> deserializer) {
final Object value = hover.value();
if (value instanceof Component) {
final Component rebuilt = rebuild((Component) value, replacer);
final Component rebuilt = rebuild((Component) value, replacer, deserializer);
return HoverEvent.showText(rebuilt);
}
@@ -130,7 +131,7 @@ public class ComponentReplacer {
Component rebuiltName = null;
if (entity.name() != null) {
rebuiltName = rebuild(entity.name(), replacer);
rebuiltName = rebuild(entity.name(), replacer, deserializer);
}
return HoverEvent.showEntity(entity.type(), entity.id(), rebuiltName);

View File

@@ -30,6 +30,8 @@ import org.jetbrains.annotations.Nullable;
public interface Values {
String NO_ARGUMENTS_PLACEHOLDER = "%player%";
String EMPTY_ARGUMENT_PLACEHOLDER = "%player_%";
String SMALL_TEXT = "My name is %player_name%";
String LARGE_TEXT = "My name is %player_name% and my location is (%player_x%, %player_y%, %player_z%), this placeholder is invalid %server_name%";
@@ -43,6 +45,7 @@ public interface Values {
final class MockPlayerPlaceholderExpansion extends PlaceholderExpansion {
public static final String EMPTY_ARGUMENT = "Empty Argument";
public static final String PLAYER_X = "10";
public static final String PLAYER_Y = "20";
public static final String PLAYER_Z = "30";
@@ -83,6 +86,8 @@ public interface Values {
return PLAYER_Y;
case "z":
return PLAYER_Z;
case "":
return EMPTY_ARGUMENT;
}
return null;

View File

@@ -1,38 +0,0 @@
/*
* This file is part of PlaceholderAPI
*
* PlaceholderAPI
* Copyright (c) 2015 - 2026 PlaceholderAPI Team
*
* PlaceholderAPI free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* PlaceholderAPI is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package me.clip.placeholderapi.replacer;
import me.clip.placeholderapi.Values;
import org.openjdk.jmh.annotations.Benchmark;
public class ReplacerBenchmarks {
@Benchmark
public void measureCharsReplacerSmallText() {
Values.CHARS_REPLACER.apply(Values.SMALL_TEXT, null, Values.PLACEHOLDERS::get);
}
@Benchmark
public void measureCharsReplacerLargeText() {
Values.CHARS_REPLACER.apply(Values.LARGE_TEXT, null, Values.PLACEHOLDERS::get);
}
}

View File

@@ -24,6 +24,7 @@ import static me.clip.placeholderapi.Values.MockPlayerPlaceholderExpansion.PLAYE
import static me.clip.placeholderapi.Values.MockPlayerPlaceholderExpansion.PLAYER_X;
import static me.clip.placeholderapi.Values.MockPlayerPlaceholderExpansion.PLAYER_Y;
import static me.clip.placeholderapi.Values.MockPlayerPlaceholderExpansion.PLAYER_Z;
import static me.clip.placeholderapi.Values.MockPlayerPlaceholderExpansion.EMPTY_ARGUMENT;
import static org.junit.jupiter.api.Assertions.assertEquals;
import me.clip.placeholderapi.Values;
@@ -37,6 +38,18 @@ public final class ReplacerUnitTester {
Values.CHARS_REPLACER.apply("%player_name%", null, Values.PLACEHOLDERS::get));
}
@Test
void charsReplacersDoesNotParsePlaceholdersWithNoArguments() {
assertEquals(Values.NO_ARGUMENTS_PLACEHOLDER,
Values.CHARS_REPLACER.apply(Values.NO_ARGUMENTS_PLACEHOLDER, null, Values.PLACEHOLDERS::get));
}
@Test
void charsReplacersParsesPlaceholdersWithOneArgumentThatIsEmpty() {
assertEquals(EMPTY_ARGUMENT,
Values.CHARS_REPLACER.apply(Values.EMPTY_ARGUMENT_PLACEHOLDER, null, Values.PLACEHOLDERS::get));
}
@Test
void testCharsReplacerProducesExpectedSentence() {
assertEquals(String.format(