Compare commits

...

45 Commits

Author SHA1 Message Date
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
PiggyPiglet
0a53559ae1 2.12.2 release 2026-02-08 20:09:01 +08:00
PiggyPiglet
6c5f19b61d Fix #1167 2026-02-08 20:02:02 +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
PiggyPiglet
9022daf07f 2.12.2-dev 2026-02-03 17:17:36 +08:00
PiggyPiglet
675b305cac 2.12.1 release 2026-02-03 17:16:29 +08:00
PiggyPiglet
d49c76c560 exclamation marks are important 2026-02-03 17:15:46 +08:00
PiggyPiglet
13e492cf44 Show version in papi ecloud list hover 2026-02-03 16:55:12 +08:00
PiggyPiglet
d561afbb63 Use modrinth for update checker 2026-02-03 16:52:25 +08:00
PiggyPiglet
2a3f4482a0 check for perms on all tab complete 2026-02-03 16:28:42 +08:00
Funnycube
4ee2840a0a Update download and server usage statistics in README 2026-02-02 22:24:12 +11:00
PiggyPiglet
c52d117f12 2.12.1 dev 2026-02-02 18:28:07 +08:00
PiggyPiglet
e307aba414 2.12.0 release 2026-02-02 17:58:26 +08:00
PiggyPiglet
5ea5a18fe8 fix config option path 2026-02-02 17:43:41 +08:00
PiggyPiglet
9c1db4b48a fix newlines in ecloud command error & restrict api to bukkit platform 2026-02-02 17:29:15 +08:00
PiggyPiglet
b233c92ca1 Add warning message to failed ecloud download 2026-02-02 17:10:50 +08:00
PiggyPiglet
1b1d2e61b9 Add config option to use adventure replacer, add bracket {} support 2026-02-02 16:35:13 +08:00
PiggyPiglet
2dc5b93133 WIP custom component replacer no regex!! 2026-01-21 22:14:46 +08:00
Andre_601
3684e32ac0 Update Common Issues page link in issue-template 2024-12-04 15:00:51 +01:00
25 changed files with 950 additions and 217 deletions

View File

@@ -18,13 +18,13 @@ body:
label: Confirmation label: Confirmation
description: Please make sure to have followed the following checks. description: Please make sure to have followed the following checks.
options: 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 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 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 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 required: true
- type: dropdown - type: dropdown
attributes: attributes:
@@ -85,6 +85,8 @@ body:
description: |- description: |-
Get the latest content of your `latest.log` file an upload it to https://paste.helpch.at 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. 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" placeholder: "https://paste.helpch.at/latest.log"
- type: input - type: input
id: "error" id: "error"
@@ -99,5 +101,5 @@ body:
description: |- description: |-
Add any extra info you think is nessesary for this Bug report. 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 `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..." 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

@@ -32,7 +32,7 @@
Support for specific plugins are provided either by the plugin itself or through expansions. The expansions may be downloaded in-game through the PAPI Expansion Cloud. There are currently over 240+ expansions that support a wide variety of plugins, such as Essentials, Factions, LuckPerms, and Vault. Support for specific plugins are provided either by the plugin itself or through expansions. The expansions may be downloaded in-game through the PAPI Expansion Cloud. There are currently over 240+ expansions that support a wide variety of plugins, such as Essentials, Factions, LuckPerms, and Vault.
PlaceholderAPI has been downloaded over 1,700,000 times on Spigot and has been used concurrently on over 45,000 servers, which makes it a must-have for a server of any type or scale. PlaceholderAPI has been downloaded over 2,000,000 times on Spigot and has been used concurrently on over 50,000 servers, which makes it a must-have for a server of any type or scale.
## Contribute ## Contribute
If you would like to contribute towards PlaceholderAPI should you take a look at our [Contributing file][contributing] for the ins and outs on how you can do that and what you need to keep in mind. If you would like to contribute towards PlaceholderAPI should you take a look at our [Contributing file][contributing] for the ins and outs on how you can do that and what you need to keep in mind.

View File

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

View File

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

View File

@@ -32,6 +32,7 @@ import java.util.Locale;
import java.util.Map; import java.util.Map;
import java.util.stream.Stream; import java.util.stream.Stream;
import com.google.common.collect.Lists;
import me.clip.placeholderapi.PlaceholderAPIPlugin; import me.clip.placeholderapi.PlaceholderAPIPlugin;
import me.clip.placeholderapi.commands.impl.cloud.CommandECloud; import me.clip.placeholderapi.commands.impl.cloud.CommandECloud;
import me.clip.placeholderapi.commands.impl.local.CommandDump; import me.clip.placeholderapi.commands.impl.local.CommandDump;
@@ -119,14 +120,18 @@ public final class PlaceholderCommandRouter implements CommandExecutor, TabCompl
} }
@Override @Override
public List<String> onTabComplete(@NotNull final CommandSender sender, public List<String> onTabComplete(@NotNull final CommandSender sender, @NotNull final Command command,
@NotNull final Command command, @NotNull final String alias, @NotNull final String[] args) { @NotNull final String alias, @NotNull final String[] args) {
final List<String> suggestions = new ArrayList<>(); final List<String> suggestions = new ArrayList<>();
if (args.length > 1) { if (args.length > 1) {
final PlaceholderCommand target = this.commands.get(args[0].toLowerCase(Locale.ROOT)); final PlaceholderCommand target = this.commands.get(args[0].toLowerCase(Locale.ROOT));
if (target != null) { if (target != null) {
if (target.getPermission() != null && !target.getPermission().isEmpty() && !sender.hasPermission(target.getPermission())) {
return suggestions;
}
target.complete(plugin, sender, args[0].toLowerCase(Locale.ROOT), target.complete(plugin, sender, args[0].toLowerCase(Locale.ROOT),
Arrays.asList(Arrays.copyOfRange(args, 1, args.length)), suggestions); Arrays.asList(Arrays.copyOfRange(args, 1, args.length)), suggestions);
} }

View File

@@ -118,6 +118,11 @@ public final class CommandECloud extends PlaceholderCommand {
return; return;
} }
if (!target.getLabel().equalsIgnoreCase("refresh") && plugin.getCloudExpansionManager().isEmpty()) {
Msg.msg(sender, "&cThere is no available data from the eCloud. Please try running &f/papi ecloud refresh&c. If this does not resolve the issue, the eCloud may be blocked by your firewall, server host, or service provider.\n&r\n&cMore information: &fhttps://placeholderapi.com/ecloud-blocked");
return;
}
target.evaluate(plugin, sender, search, params.subList(1, params.size())); target.evaluate(plugin, sender, search, params.subList(1, params.size()));
} }

View File

@@ -170,6 +170,8 @@ public final class CommandECloudExpansionList extends PlaceholderCommand {
.append(newline()).append(newline()) .append(newline()).append(newline())
.append(text("Author: ", AQUA)).append(text(expansion.getAuthor(), WHITE)) .append(text("Author: ", AQUA)).append(text(expansion.getAuthor(), WHITE))
.append(newline()) .append(newline())
.append(text("Version: ", AQUA)).append(text(expansion.getVersion().getVersion(), WHITE))
.append(newline())
.append(text("Verified: ", AQUA)).append(text(expansion.getVersion().isVerified() ? "" : "", expansion.getVersion().isVerified() ? GREEN : RED, TextDecoration.BOLD)) .append(text("Verified: ", AQUA)).append(text(expansion.getVersion().isVerified() ? "" : "", expansion.getVersion().isVerified() ? GREEN : RED, TextDecoration.BOLD))
.append(newline()) .append(newline())
.append(text("Released: ", AQUA)).append(text(format.format(expansion.getLastUpdate()), WHITE)) .append(text("Released: ", AQUA)).append(text(format.format(expansion.getLastUpdate()), WHITE))

View File

@@ -54,6 +54,10 @@ public final class PlaceholderAPIConfig {
return plugin.getConfig().getBoolean("debug", false); return plugin.getConfig().getBoolean("debug", false);
} }
public boolean useAdventureReplacer() {
return plugin.getConfig().getBoolean("use_adventure_provided_replacer", false);
}
public Optional<ExpansionSort> getExpansionSort() { public Optional<ExpansionSort> getExpansionSort() {
final String option = plugin.getConfig() final String option = plugin.getConfig()
@@ -87,8 +91,11 @@ public final class PlaceholderAPIConfig {
return plugin.getConfig().getString("boolean.false", "false"); return plugin.getConfig().getString("boolean.false", "false");
} }
public boolean useAdventureProvidedReplacer() {
return plugin.getConfig().getBoolean("use_adventure_provided_replacer", false);
}
public boolean detectMaliciousExpansions() { public boolean detectMaliciousExpansions() {
return plugin.getConfig().getBoolean("detect_malicious_expansions", true); return plugin.getConfig().getBoolean("detect_malicious_expansions", true);
} }
} }

View File

@@ -26,11 +26,9 @@ import com.google.common.util.concurrent.ThreadFactoryBuilder;
import com.google.gson.Gson; import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken; import com.google.gson.reflect.TypeToken;
import java.io.File; import java.io.*;
import java.io.FileOutputStream;
import java.io.IOException;
import java.lang.reflect.Type; import java.lang.reflect.Type;
import java.net.URL; import java.net.*;
import java.nio.channels.Channels; import java.nio.channels.Channels;
import java.nio.channels.ReadableByteChannel; import java.nio.channels.ReadableByteChannel;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
@@ -60,9 +58,27 @@ import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Unmodifiable; import org.jetbrains.annotations.Unmodifiable;
public final class CloudExpansionManager { 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 @NotNull
private static final String API_URL = "https://ecloud.placeholderapi.com/api/v3/"; private static final String API_URL = "https://ecloud.placeholderapi.com/api/v3/?platform=bukkit";
@NotNull @NotNull
private static final Gson GSON = new Gson(); private static final Gson GSON = new Gson();
@@ -115,6 +131,10 @@ public final class CloudExpansionManager {
return ImmutableMap.copyOf(cache); return ImmutableMap.copyOf(cache);
} }
public boolean isEmpty() {
return cache.isEmpty();
}
@NotNull @NotNull
@Unmodifiable @Unmodifiable
public Map<String, CloudExpansion> getCloudExpansionsInstalled() { public Map<String, CloudExpansion> getCloudExpansionsInstalled() {
@@ -180,8 +200,16 @@ public final class CloudExpansionManager {
// a defence tactic! use ConcurrentHashMap instead of normal HashMap // a defence tactic! use ConcurrentHashMap instead of normal HashMap
Map<String, CloudExpansion> values = new ConcurrentHashMap<>(); Map<String, CloudExpansion> values = new ConcurrentHashMap<>();
try { try {
//noinspection UnstableApiUsage final URI uri = new URI(API_URL);
String json = Resources.toString(new URL(API_URL), StandardCharsets.UTF_8); 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)); values.putAll(GSON.fromJson(json, TYPE));
List<String> toRemove = new ArrayList<>(); List<String> toRemove = new ArrayList<>();
@@ -197,6 +225,8 @@ public final class CloudExpansionManager {
for (String name : toRemove) { for (String name : toRemove) {
values.remove(name); values.remove(name);
} }
} catch (UnknownHostException e) {
plugin.getLogger().log(Level.WARNING, "There is no data available from the eCloud. Please try running /papi refresh. If this does not resolve the issue, the eCloud may be blocked by your firewall, server host, or service provider.\n\nMore information: https://placeholderapi.com/ecloud-blocked", e);
} catch (Throwable e) { } catch (Throwable e) {
// ugly swallowing of every throwable, but we have to be defensive // ugly swallowing of every throwable, but we have to be defensive
plugin.getLogger().log(Level.WARNING, "Failed to download expansion information", e); plugin.getLogger().log(Level.WARNING, "Failed to download expansion information", e);
@@ -253,10 +283,14 @@ public final class CloudExpansionManager {
"Expansion-" + toIndexName(expansion) + ".jar"); "Expansion-" + toIndexName(expansion) + ".jar");
final CompletableFuture<File> download = CompletableFuture.supplyAsync(() -> { final CompletableFuture<File> download = CompletableFuture.supplyAsync(() -> {
try (final ReadableByteChannel source = Channels.newChannel(new URL(version.getUrl()) try {
.openStream()); final FileOutputStream target = new FileOutputStream(file)) { final URLConnection connection = new URI(version.getUrl()).toURL().openConnection();
target.getChannel().transferFrom(source, 0, Long.MAX_VALUE); connection.setRequestProperty("User-Agent", USER_AGENT);
} catch (final IOException ex) {
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); throw new CompletionException(ex);
} }
return file; return file;

View File

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

View File

@@ -22,9 +22,7 @@ package me.clip.placeholderapi.replacer;
import java.util.Locale; import java.util.Locale;
import java.util.function.Function; import java.util.function.Function;
import me.clip.placeholderapi.expansion.PlaceholderExpansion; import me.clip.placeholderapi.expansion.PlaceholderExpansion;
import org.bukkit.ChatColor;
import org.bukkit.OfflinePlayer; import org.bukkit.OfflinePlayer;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
@@ -38,101 +36,116 @@ public final class CharsReplacer implements Replacer {
this.closure = closure; 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 @NotNull
@Override @Override
public String apply(@NotNull final String text, @Nullable final OfflinePlayer player, public String apply(@NotNull final String text, @Nullable final OfflinePlayer player,
@NotNull final Function<String, @Nullable PlaceholderExpansion> lookup) { @NotNull final Function<String, @Nullable PlaceholderExpansion> lookup) {
final char[] chars = text.toCharArray(); final char head = closure.head;
final StringBuilder builder = new StringBuilder(text.length()); int startPlaceholder = text.indexOf(head);
final StringBuilder identifier = new StringBuilder(); if (startPlaceholder == -1) {
final StringBuilder parameters = new StringBuilder(); return text;
}
for (int i = 0; i < chars.length; i++) { final int length = text.length();
final char l = chars[i]; final StringBuilder builder = new StringBuilder(length + (length >> 3));
int cursor = 0;
if (l != closure.head || i + 1 >= chars.length) { final char tail = closure.tail;
builder.append(l);
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; continue;
} }
boolean identified = false; String identifier = text.substring(startPlaceholder + 1, underscoreIndex);
boolean invalid = true; String parameters = "";
boolean hadSpace = false;
while (++i < chars.length) { if (underscoreIndex + 1 < endPlaceholder) {
final char p = chars[i]; parameters = text.substring(underscoreIndex + 1, endPlaceholder);
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 PlaceholderExpansion expansion = lookup.apply(identifier.toLowerCase(Locale.ROOT));
final String lowercaseIdentifierString = identifierString.toLowerCase(Locale.ROOT); String replacement = null;
final String parametersString = parameters.toString();
identifier.setLength(0); if (expansion != null) {
parameters.setLength(0); replacement = expansion.onRequest(player, parameters);
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 (replacement != null) {
if (placeholder == null) { builder.append(replacement);
builder.append(closure.head).append(identifierString); } else {
// Fallback: Restore original placeholder format
if (identified) { builder.append(head).append(identifier);
builder.append('_'); builder.append('_').append(parameters);
} builder.append(tail);
builder.append(parametersString).append(closure.tail);
continue;
} }
final String replacement = placeholder.onRequest(player, parametersString); cursor = endPlaceholder + 1;
if (replacement == null) { startPlaceholder = text.indexOf(head, cursor);
builder.append(closure.head).append(identifierString);
if (identified) { } while (startPlaceholder != -1);
builder.append('_');
}
builder.append(parametersString).append(closure.tail); if (cursor < length) {
continue; builder.append(text, cursor, length);
}
builder.append(replacement);
} }
return builder.toString(); return builder.toString();
} }
} }

View File

@@ -26,6 +26,8 @@ import java.net.URL;
import java.util.Arrays; import java.util.Arrays;
import javax.net.ssl.HttpsURLConnection; import javax.net.ssl.HttpsURLConnection;
import com.google.gson.Gson;
import com.google.gson.JsonElement;
import me.clip.placeholderapi.PlaceholderAPIPlugin; import me.clip.placeholderapi.PlaceholderAPIPlugin;
import me.clip.placeholderapi.scheduler.scheduling.schedulers.TaskScheduler; import me.clip.placeholderapi.scheduler.scheduling.schedulers.TaskScheduler;
import me.clip.placeholderapi.util.Msg; import me.clip.placeholderapi.util.Msg;
@@ -36,12 +38,13 @@ import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerJoinEvent; import org.bukkit.event.player.PlayerJoinEvent;
public class UpdateChecker implements Listener { public class UpdateChecker implements Listener {
private static final String MODRINTH_URL = "https://api.modrinth.com/v2/project/lKEzGugV/version";
private static final int RESOURCE_ID = 6245; private static final int RESOURCE_ID = 6245;
private final PlaceholderAPIPlugin plugin; private final PlaceholderAPIPlugin plugin;
private final TaskScheduler scheduler; private final TaskScheduler scheduler;
private final String pluginVersion; private final String pluginVersion;
private String spigotVersion; private String modrinthVersion;
private boolean updateAvailable; private boolean updateAvailable;
public UpdateChecker(PlaceholderAPIPlugin plugin) { public UpdateChecker(PlaceholderAPIPlugin plugin) {
@@ -54,27 +57,27 @@ public class UpdateChecker implements Listener {
return updateAvailable; return updateAvailable;
} }
public String getSpigotVersion() { public String getModrinthVersion() {
return spigotVersion; return modrinthVersion;
} }
public void fetch() { public void fetch() {
scheduler.runTaskAsynchronously(() -> { scheduler.runTaskAsynchronously(() -> {
try { try {
HttpsURLConnection con = (HttpsURLConnection) new URL( HttpsURLConnection con = (HttpsURLConnection) new URL(MODRINTH_URL).openConnection();
"https://api.spigotmc.org/legacy/update.php?resource=" + RESOURCE_ID).openConnection();
con.setRequestMethod("GET"); con.setRequestMethod("GET");
spigotVersion = new BufferedReader(new InputStreamReader(con.getInputStream())).readLine(); final JsonElement json = new Gson().fromJson(new BufferedReader(new InputStreamReader(con.getInputStream())), JsonElement.class);
modrinthVersion = json.getAsJsonArray().get(0).getAsJsonObject().get("version_number").getAsString();
} catch (Exception ex) { } catch (Exception ex) {
plugin.getLogger().info("Failed to check for updates on spigot."); plugin.getLogger().info("Failed to check for updates on modrinth.");
return; return;
} }
if (spigotVersion == null || spigotVersion.isEmpty()) { if (modrinthVersion == null || modrinthVersion.isEmpty()) {
return; return;
} }
updateAvailable = spigotIsNewer(); updateAvailable = modrinthIsNewer();
if (!updateAvailable) { if (!updateAvailable) {
return; return;
@@ -82,21 +85,21 @@ public class UpdateChecker implements Listener {
scheduler.runTask(() -> { scheduler.runTask(() -> {
plugin.getLogger() plugin.getLogger()
.info("An update for PlaceholderAPI (v" + getSpigotVersion() + ") is available at:"); .info("An update for PlaceholderAPI (v" + getModrinthVersion() + ") is available at:");
plugin.getLogger() plugin.getLogger()
.info("https://www.spigotmc.org/resources/placeholderapi." + RESOURCE_ID + "/"); .info("https://modrinth.com/plugin/placeholderapi");
Bukkit.getPluginManager().registerEvents(this, plugin); Bukkit.getPluginManager().registerEvents(this, plugin);
}); });
}); });
} }
private boolean spigotIsNewer() { private boolean modrinthIsNewer() {
if (spigotVersion == null || spigotVersion.isEmpty()) { if (modrinthVersion == null || modrinthVersion.isEmpty()) {
return false; return false;
} }
int[] plV = toReadable(pluginVersion); int[] plV = toReadable(pluginVersion);
int[] spV = toReadable(spigotVersion); int[] spV = toReadable(modrinthVersion);
if (plV[0] < spV[0]) { if (plV[0] < spV[0]) {
return true; return true;
@@ -119,10 +122,9 @@ public class UpdateChecker implements Listener {
public void onJoin(PlayerJoinEvent e) { public void onJoin(PlayerJoinEvent e) {
if (e.getPlayer().hasPermission("placeholderapi.updatenotify")) { if (e.getPlayer().hasPermission("placeholderapi.updatenotify")) {
Msg.msg(e.getPlayer(), Msg.msg(e.getPlayer(),
"&bAn update for &fPlaceholder&7API &e(&fPlaceholder&7API &fv" + getSpigotVersion() "&bAn update for &fPlaceholder&7API &e(&fPlaceholder&7API &fv" + getModrinthVersion()
+ "&e)" + "&e)"
, "&bis available at &ehttps://www.spigotmc.org/resources/placeholderapi." + RESOURCE_ID , "&bis available at &ehttps://modrinth.com/plugin/placeholderapi");
+ "/");
} }
} }
} }

View File

@@ -4,10 +4,15 @@ import com.google.common.hash.Hashing;
import com.google.common.io.Files; import com.google.common.io.Files;
import com.google.common.io.Resources; import com.google.common.io.Resources;
import me.clip.placeholderapi.PlaceholderAPIPlugin; import me.clip.placeholderapi.PlaceholderAPIPlugin;
import me.clip.placeholderapi.expansion.manager.CloudExpansionManager;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import java.io.BufferedReader;
import java.io.File; import java.io.File;
import java.io.InputStreamReader;
import java.net.URI;
import java.net.URL; import java.net.URL;
import java.net.URLConnection;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.util.Arrays; import java.util.Arrays;
import java.util.HashSet; import java.util.HashSet;
@@ -47,7 +52,10 @@ public final class ExpansionSafetyCheck {
final Set<String> knownMaliciousExpansions; final Set<String> knownMaliciousExpansions;
try { 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()); knownMaliciousExpansions = Arrays.stream(hashes.split("\n")).collect(Collectors.toSet());
} catch (Exception e) { } catch (Exception e) {
main.getLogger().log(Level.SEVERE, "Failed to download anti malware hash check list from https://check.placeholderapi.com", 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 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 { try {
if (!file.isFile()) {
continue;
}
final String hash = Hashing.sha256().hashBytes(Files.asByteSource(file).read()).toString(); final String hash = Hashing.sha256().hashBytes(Files.asByteSource(file).read()).toString();
if (knownMaliciousExpansions.contains(hash)) { if (knownMaliciousExpansions.contains(hash)) {

View File

@@ -6,6 +6,8 @@
# Expansions: https://placeholderapi.com/ecloud # Expansions: https://placeholderapi.com/ecloud
# Wiki: https://wiki.placeholderapi.com/ # Wiki: https://wiki.placeholderapi.com/
# Discord: https://helpch.at/discord # Discord: https://helpch.at/discord
# If you're seeing performance issues with plugins that use component replacement, or things don't look quite right,
# switch to the replacer provided by adventure itself by changing use_adventure_provided_replacer to true
# No placeholders are provided with this plugin by default. # No placeholders are provided with this plugin by default.
# Download placeholders: /papi ecloud # Download placeholders: /papi ecloud
check_updates: true check_updates: true
@@ -16,4 +18,5 @@ boolean:
'false': 'no' 'false': 'no'
date_format: MM/dd/yy HH:mm:ss date_format: MM/dd/yy HH:mm:ss
detect_malicious_expansions: true detect_malicious_expansions: true
use_adventure_provided_replacer: false
debug: false debug: false

View File

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

View File

@@ -20,15 +20,19 @@
package me.clip.placeholderapi; package me.clip.placeholderapi;
import me.clip.placeholderapi.replacer.ComponentReplacer;
import me.clip.placeholderapi.replacer.ExactReplacer; import me.clip.placeholderapi.replacer.ExactReplacer;
import me.clip.placeholderapi.replacer.RelationalExactReplacer; import me.clip.placeholderapi.replacer.RelationalExactReplacer;
import me.clip.placeholderapi.replacer.Replacer; import me.clip.placeholderapi.replacer.Replacer;
import net.kyori.adventure.text.Component; import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.ComponentLike;
import org.bukkit.OfflinePlayer; import org.bukkit.OfflinePlayer;
import org.bukkit.entity.Player; import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.List; import java.util.List;
import java.util.function.Function;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import static me.clip.placeholderapi.PlaceholderAPI.RELATIONAL_PLACEHOLDER_PATTERN; import static me.clip.placeholderapi.PlaceholderAPI.RELATIONAL_PLACEHOLDER_PATTERN;
@@ -42,119 +46,241 @@ public final class PAPIComponents {
* Translates all placeholders into their corresponding values. * Translates all placeholders into their corresponding values.
* <br>The pattern of a valid placeholder is {@literal %<identifier>_<params>%}. * <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 component Component to set the placeholder values in
* @return Component containing all translated placeholders * @return Component containing all translated placeholders
*/ */
@NotNull @NotNull
public static Component setPlaceholders(final OfflinePlayer player, @NotNull final Component component) { public static Component setPlaceholders(final OfflinePlayer player, @NotNull final Component component) {
// TODO: explore a custom TextReplacementRenderer which doesn't use regex for performance benefits i.e. merge CharsReplacer with kyori TextReplacementRenderer return setPlaceholders(player, component, null);
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))));
} }
/** /**
* Translates all placeholders into their corresponding values. * Translates all placeholders into their corresponding values.
* <br>The pattern of a valid placeholder is {@literal %<identifier>_<params>%}. * <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 * @param components List of Components to set the placeholder values in
* @return List of Components containing all translated placeholders * @return List of Components containing all translated placeholders
*/ */
@NotNull @NotNull
public static List<Component> setPlaceholders(final OfflinePlayer player, @NotNull final List<Component> components) { 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. * Translates all placeholders into their corresponding values.
* <br>The pattern of a valid placeholder is {@literal %<identifier>_<params>%}. * <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 * @param component Component to set the placeholder values in
* @return Component containing all translated placeholders * @return Component containing all translated placeholders
*/ */
@NotNull @NotNull
public static Component setPlaceholders(final Player player, @NotNull final Component component) { 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. * Translates all placeholders into their corresponding values.
* <br>The pattern of a valid placeholder is {@literal %<identifier>_<params>%}. * <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 * @param components List of Components to set the placeholder values in
* @return List of components containing all translated placeholders * @return List of components containing all translated placeholders
*/ */
@NotNull @NotNull
public static List<Component> setPlaceholders(final Player player, @NotNull final List<Component> components) { 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. * Translates all placeholders into their corresponding values.
* <br>The pattern of a valid placeholder is {@literal {<identifier>_<params>}}. * <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 component Component to set the placeholder values in
* @return Component containing all translated placeholders * @return Component containing all translated placeholders
*/ */
@NotNull @NotNull
public static Component setBracketPlaceholders(final OfflinePlayer player, @NotNull final Component component) { public static Component setBracketPlaceholders(final OfflinePlayer player, @NotNull final Component component) {
return component.replaceText(config -> config.match(PlaceholderAPI.BRACKET_PLACEHOLDER_PATTERN).replacement((result, builder) -> return setBracketPlaceholders(player, component, null);
builder.content(BRACKET_EXACT_REPLACER.apply(result.group(), player, PlaceholderAPIPlugin.getInstance().getLocalExpansionManager()::getExpansion))));
} }
/** /**
* Translates all placeholders into their corresponding values. * Translates all placeholders into their corresponding values.
* <br>The pattern of a valid placeholder is {@literal {<identifier>_<params>}}. * <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 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), 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 * @param components List of Components to set the placeholder values in
* @return List of Components containing all translated placeholders * @return List of Components containing all translated placeholders
*/ */
@NotNull @NotNull
public static List<Component> setBracketPlaceholders(final OfflinePlayer player, @NotNull final List<Component> components) { 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. * Translates all placeholders into their corresponding values.
* <br>The pattern of a valid placeholder is {@literal {<identifier>_<params>}}. * <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 * @param component Component to set the placeholder values in
* @return Component containing all translated placeholders * @return Component containing all translated placeholders
*/ */
@NotNull @NotNull
public static Component setBracketPlaceholders(final Player player, @NotNull final Component component) { 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. * Translates all placeholders into their corresponding values.
* <br>The pattern of a valid placeholder is {@literal {<identifier>_<params>}}. * <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 * @param components List of Components to set the placeholder values in
* @return List of Components containing all translated placeholders * @return List of Components containing all translated placeholders
*/ */
@NotNull @NotNull
public static List<Component> setBracketPlaceholders(final Player player, @NotNull final List<Component> components) { 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 * set relational placeholders in the text specified placeholders are matched with the pattern
* {@literal %<rel_(identifier)_(params)>%} when set with this method * {@literal %<rel_(identifier)_(params)>%} when set with this method
* *
* @param one First player to compare * @param one First player to compare
* @param two Second player to compare * @param two Second player to compare
* @param component Component to parse the placeholders in * @param component Component to parse the placeholders in
* @return The Component containing the parsed relational placeholders * @return The Component containing the parsed relational placeholders
*/ */
public static Component setRelationalPlaceholders(Player one, Player two, Component component) { public static Component setRelationalPlaceholders(Player one, Player two, Component component) {
//todo: custom replacer
return component.replaceText(config -> config.match(RELATIONAL_PLACEHOLDER_PATTERN).replacement((result, builder) -> return component.replaceText(config -> config.match(RELATIONAL_PLACEHOLDER_PATTERN).replacement((result, builder) ->
builder.content(RELATIONAL_EXACT_REPLACER.apply(result.group(2), one, two, PlaceholderAPIPlugin.getInstance().getLocalExpansionManager()::getExpansion)))); builder.content(RELATIONAL_EXACT_REPLACER.apply(result.group(2), one, two, PlaceholderAPIPlugin.getInstance().getLocalExpansionManager()::getExpansion))));
} }
@@ -163,8 +289,8 @@ public final class PAPIComponents {
* Translate placeholders in the provided List based on the relation of the two provided players. * 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>%}. * <br>The pattern of a valid placeholder is {@literal %rel_<identifier>_<param>%}.
* *
* @param one Player to compare * @param one Player to compare
* @param two Player to compare * @param two Player to compare
* @param components List of Components to parse the placeholder values to * @param components List of Components to parse the placeholder values to
* @return The List of Components containing the parsed relational placeholders * @return The List of Components containing the parsed relational placeholders
*/ */
@@ -172,9 +298,4 @@ public final class PAPIComponents {
return components.stream().map(line -> setRelationalPlaceholders(one, two, line)) return components.stream().map(line -> setRelationalPlaceholders(one, two, line))
.collect(Collectors.toList()); .collect(Collectors.toList());
} }
}
// kyori doesn't seem to have a method that can do a contains with regex, we don't want to do a more expensive replace
// public static boolean containsPlaceholders(@Nullable final Component text) {
// return text != null && text.replaceText()
// }
}

View File

@@ -0,0 +1,175 @@
package me.clip.placeholderapi.replacer;
import net.kyori.adventure.key.Key;
import net.kyori.adventure.nbt.api.BinaryTagHolder;
import net.kyori.adventure.text.*;
import net.kyori.adventure.text.event.ClickEvent;
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;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
public class ComponentReplacer {
@NotNull
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, @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 = 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, deserializer));
}
rebuilt = Component.translatable(translatable.key(), arguments);
} else if (component instanceof KeybindComponent) {
final KeybindComponent keybind = (KeybindComponent) component;
rebuilt = Component.keybind(keybind.keybind());
} else if (component instanceof ScoreComponent) {
final ScoreComponent score = (ScoreComponent) component;
rebuilt = Component.score(score.name(), score.objective());
} else if (component instanceof SelectorComponent) {
final SelectorComponent selector = (SelectorComponent) component;
rebuilt = Component.selector(selector.pattern());
} else {
rebuilt = Component.empty();
}
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, deserializer));
}
rebuilt = rebuilt.children(children);
}
return rebuilt;
}
@NotNull
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();
if (click != null) {
builder.clickEvent(rebuildClickEvent(click, replacer));
}
final HoverEvent<?> hover = style.hoverEvent();
if (hover != null) {
builder.hoverEvent(rebuildHoverEvent(hover, replacer, deserializer));
}
return builder.build();
}
@NotNull
private static ClickEvent rebuildClickEvent(@NotNull final ClickEvent click, @NotNull final Function<String, String> replacer) {
final ClickEvent.Payload payload = click.payload();
if (!(payload instanceof ClickEvent.Payload.Text)) {
return click;
}
final String original = ((ClickEvent.Payload.Text) payload).value();
final String replaced = replacer.apply(original);
final ClickEvent.Action action = click.action();
switch (action) {
case OPEN_URL:
return ClickEvent.openUrl(replaced);
case OPEN_FILE:
return ClickEvent.openFile(replaced);
case RUN_COMMAND:
return ClickEvent.runCommand(replaced);
case SUGGEST_COMMAND:
return ClickEvent.suggestCommand(replaced);
case COPY_TO_CLIPBOARD:
return ClickEvent.copyToClipboard(replaced);
default:
return click;
}
}
@NotNull
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, deserializer);
return HoverEvent.showText(rebuilt);
}
if (value instanceof HoverEvent.ShowItem) {
return rebuildShowItem((HoverEvent.ShowItem) value, replacer);
}
if (value instanceof HoverEvent.ShowEntity) {
final HoverEvent.ShowEntity entity = (HoverEvent.ShowEntity) value;
Component rebuiltName = null;
if (entity.name() != null) {
rebuiltName = rebuild(entity.name(), replacer, deserializer);
}
return HoverEvent.showEntity(entity.type(), entity.id(), rebuiltName);
}
return hover;
}
@NotNull
private static HoverEvent<?> rebuildShowItem(@NotNull final HoverEvent.ShowItem item, @NotNull final Function<String, String> replacer) {
final BinaryTagHolder nbt = item.nbt();
if (nbt != null && !nbt.string().isEmpty()) {
final String replaced = replacer.apply(nbt.string());
return HoverEvent.showItem(item.item(), item.count(), BinaryTagHolder.binaryTagHolder(replaced));
}
//I'm not 100% sure this is how we're meant to support data components but let's give it a go and see if it causes any issues :)
final Map<Key, DataComponentValue> components = item.dataComponents();
if (!components.isEmpty()) {
final Map<Key, DataComponentValue> rebuilt = new HashMap<>();
for (final Map.Entry<Key, DataComponentValue> entry : components.entrySet()) {
final DataComponentValue value = entry.getValue();
if (!(value instanceof BinaryTagHolder)) {
rebuilt.put(entry.getKey(), value);
continue;
}
rebuilt.put(entry.getKey(), BinaryTagHolder.binaryTagHolder(replacer.apply(((BinaryTagHolder) value).string())));
}
return HoverEvent.showItem(item.item(), item.count(), rebuilt);
}
return HoverEvent.showItem(item);
}
}

View File

@@ -30,6 +30,8 @@ import org.jetbrains.annotations.Nullable;
public interface Values { public interface Values {
String NO_ARGUMENTS_PLACEHOLDER = "%player%";
String EMPTY_ARGUMENT_PLACEHOLDER = "%player_%";
String SMALL_TEXT = "My name is %player_name%"; 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%"; 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 { 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_X = "10";
public static final String PLAYER_Y = "20"; public static final String PLAYER_Y = "20";
public static final String PLAYER_Z = "30"; public static final String PLAYER_Z = "30";
@@ -83,6 +86,8 @@ public interface Values {
return PLAYER_Y; return PLAYER_Y;
case "z": case "z":
return PLAYER_Z; return PLAYER_Z;
case "":
return EMPTY_ARGUMENT;
} }
return null; 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_X;
import static me.clip.placeholderapi.Values.MockPlayerPlaceholderExpansion.PLAYER_Y; 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.PLAYER_Z;
import static me.clip.placeholderapi.Values.MockPlayerPlaceholderExpansion.EMPTY_ARGUMENT;
import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertEquals;
import me.clip.placeholderapi.Values; import me.clip.placeholderapi.Values;
@@ -37,6 +38,18 @@ public final class ReplacerUnitTester {
Values.CHARS_REPLACER.apply("%player_name%", null, Values.PLACEHOLDERS::get)); 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 @Test
void testCharsReplacerProducesExpectedSentence() { void testCharsReplacerProducesExpectedSentence() {
assertEquals(String.format( assertEquals(String.format(