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
diff --git a/src/main/java/me/clip/placeholderapi/replacer/CharsReplacer.java b/src/main/java/me/clip/placeholderapi/replacer/CharsReplacer.java
index a0fe4dc..e07ef00 100644
--- a/src/main/java/me/clip/placeholderapi/replacer/CharsReplacer.java
+++ b/src/main/java/me/clip/placeholderapi/replacer/CharsReplacer.java
@@ -36,102 +36,113 @@ public final class CharsReplacer implements Replacer {
this.closure = closure;
}
-
+ /**
+ * Translates placeholders within the provided text using a high-performance
+ * character-scanning approach.
+ * * 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}.
+ *
+ * @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 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);
- continue;
+ final char tail = closure.tail;
+
+ loop: do {
+ // Append plain text preceding the placeholder
+ if (startPlaceholder > cursor) {
+ builder.append(text, cursor, startPlaceholder);
}
- boolean identified = false;
- boolean invalid = true;
- boolean hadSpace = false;
+ final int endPlaceholder = text.indexOf(tail, startPlaceholder + 1);
- while (++i < chars.length) {
- final char p = chars[i];
+ if (endPlaceholder == -1) {
+ builder.append(text, startPlaceholder, length);
+ return builder.toString();
+ }
- if (p == ' ' && !identified) {
- hadSpace = true;
- break;
- }
- if (p == closure.tail && identified) {
- invalid = false;
- break;
- }
- if (p == closure.tail) {
- identifier.append(p);
- break;
+ int underscoreIndex = -1;
+
+ for (int i = startPlaceholder + 1; i < endPlaceholder; i++) {
+ final char current = text.charAt(i);
+
+ if (current == ' ') {
+ // Invalid placeholder (contains space).
+ // 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 (p == '_' && !identified) {
- identified = true;
- continue;
- }
-
- if (identified) {
- parameters.append(p);
- } else {
- identifier.append(p);
+ if (current == '_' && underscoreIndex == -1) {
+ underscoreIndex = i;
}
}
- final String identifierString = identifier.toString();
- final String lowercaseIdentifierString = identifierString.toLowerCase(Locale.ROOT);
- final String parametersString = parameters.toString();
+ String identifier;
+ String parameters = "";
- identifier.setLength(0);
- parameters.setLength(0);
-
- if (invalid) {
- builder.append(closure.head).append(identifierString);
-
- if (identified) {
- builder.append('_').append(parametersString);
+ if (underscoreIndex != -1) {
+ identifier = text.substring(startPlaceholder + 1, underscoreIndex);
+ if (underscoreIndex + 1 < endPlaceholder) {
+ parameters = text.substring(underscoreIndex + 1, endPlaceholder);
}
-
- if (hadSpace) {
- builder.append(' ');
- }
- continue;
+ } else {
+ identifier = text.substring(startPlaceholder + 1, endPlaceholder);
}
- final PlaceholderExpansion placeholder = lookup.apply(lowercaseIdentifierString);
- if (placeholder == null) {
- builder.append(closure.head).append(identifierString);
+ final PlaceholderExpansion expansion = lookup.apply(identifier.toLowerCase(Locale.ROOT));
+ String replacement = null;
- if (identified) {
- builder.append('_');
- }
-
- builder.append(parametersString).append(closure.tail);
- continue;
+ if (expansion != null) {
+ replacement = expansion.onRequest(player, parameters);
}
- final String replacement = placeholder.onRequest(player, parametersString);
- if (replacement == null) {
- builder.append(closure.head).append(identifierString);
-
- if (identified) {
- builder.append('_');
+ if (replacement != null) {
+ builder.append(replacement);
+ } else {
+ // Fallback: Restore original placeholder format
+ builder.append(head).append(identifier);
+ if (underscoreIndex != -1) {
+ builder.append('_').append(parameters);
}
-
- builder.append(parametersString).append(closure.tail);
- continue;
+ builder.append(tail);
}
- builder.append(replacement);
+ cursor = endPlaceholder + 1;
+ startPlaceholder = text.indexOf(head, cursor);
+
+ } while (startPlaceholder != -1);
+
+ if (cursor < length) {
+ builder.append(text, cursor, length);
}
return builder.toString();
diff --git a/src/test/java/me/clip/placeholderapi/replacer/ReplacerBenchmarks.java b/src/test/java/me/clip/placeholderapi/replacer/ReplacerBenchmarks.java
deleted file mode 100644
index 1283299..0000000
--- a/src/test/java/me/clip/placeholderapi/replacer/ReplacerBenchmarks.java
+++ /dev/null
@@ -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 .
- */
-
-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);
- }
-
-}
\ No newline at end of file