Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
*/
public enum SearchField {

ALL("all"),
NAME("name"),
MATERIAL("material"),
LORE("lore"),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package fr.maxlego08.zauctionhouse.api.filter;

import fr.maxlego08.zauctionhouse.api.configuration.records.SearchFilterConfiguration;

import java.util.Locale;

public final class SearchQueryFormatter {

private SearchQueryFormatter() {
}

public static String format(String raw, SearchFilterConfiguration configuration) {
SearchQuery query = SearchQuery.parse(raw, configuration);
if (query.value().isBlank()) {
return "None";
}

SearchField field = query.field() == null ? SearchField.ALL : query.field();
SearchFilterType type = query.type() == null ? SearchFilterType.CONTAINS_IGNORE_CASE : query.type();
return fieldLabel(field) + " " + modeLabel(type) + ": " + query.value();
}

private static String fieldLabel(SearchField field) {
if (field == SearchField.ALL) {
return "Everything";
}
String key = field.getKey().toLowerCase(Locale.ROOT);
return Character.toUpperCase(key.charAt(0)) + key.substring(1);
}

private static String modeLabel(SearchFilterType type) {
return switch (type) {
case EQUALS, EQUALS_IGNORE_CASE -> "exact";
case CONTAINS, CONTAINS_IGNORE_CASE -> "contains";
};
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package fr.maxlego08.zauctionhouse.api.filter;

/**
* Applies search filter operators to plain text values.
*/
public final class SearchTextMatcher {

private SearchTextMatcher() {
}

public static boolean matches(String fieldValue, SearchFilterType type, String queryValue) {
if (fieldValue == null || queryValue == null) {
return false;
}

return switch (type) {
case CONTAINS -> fieldValue.contains(queryValue);
case EQUALS -> fieldValue.equals(queryValue);
case CONTAINS_IGNORE_CASE -> SearchTextNormalizer.normalize(fieldValue).contains(SearchTextNormalizer.normalize(queryValue));
case EQUALS_IGNORE_CASE -> SearchTextNormalizer.normalize(fieldValue).equals(SearchTextNormalizer.normalize(queryValue));
};
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package fr.maxlego08.zauctionhouse.api.filter;

import java.text.Normalizer;

/**
* Normalizes searchable text so styled small-caps item names remain discoverable.
*/
public final class SearchTextNormalizer {

private SearchTextNormalizer() {
}

public static String normalize(String value) {
if (value == null || value.isEmpty()) {
return "";
}

String normalized = Normalizer.normalize(value, Normalizer.Form.NFKC);
StringBuilder builder = new StringBuilder(normalized.length());

normalized.codePoints().forEach(codePoint -> {
int mappedCodePoint = mapSmallCaps(codePoint);
builder.appendCodePoint(Character.toLowerCase(mappedCodePoint));
});

return builder.toString();
}

private static int mapSmallCaps(int codePoint) {
return switch (codePoint) {
case 0x1D00 -> 'a';
case 0x0299 -> 'b';
case 0x1D04 -> 'c';
case 0x1D05 -> 'd';
case 0x1D07 -> 'e';
case 0xA730 -> 'f';
case 0x0262 -> 'g';
case 0x029C -> 'h';
case 0x026A -> 'i';
case 0x1D0A -> 'j';
case 0x1D0B -> 'k';
case 0x029F -> 'l';
case 0x1D0D -> 'm';
case 0x0274 -> 'n';
case 0x1D0F -> 'o';
case 0x1D18 -> 'p';
case 0xA7AF -> 'q';
case 0x0280 -> 'r';
case 0xA731 -> 's';
case 0x1D1B -> 't';
case 0x1D1C -> 'u';
case 0x1D20 -> 'v';
case 0x1D21 -> 'w';
case 0x028F -> 'y';
case 0x1D22 -> 'z';
default -> codePoint;
};
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,7 @@ public enum Message {
MIGRATION_FAILED("<error>Migration failed: <white>%error%"),

// Search messages
SEARCH_START("<#8a8a8a>Please type your search in the chat."),
SEARCH_START("<#8a8a8a>Opening search dialog."),
SEARCH_CLEARED("<#8a8a8a>Search cleared."),
SEARCH_NO_RESULTS("<#8a8a8a>No items found for <#ffffff>%query%<#8a8a8a>."),
SEARCH_SEARCHING("<#8a8a8a>Searching for <#ffffff>%query%<#8a8a8a>..."),
Expand Down Expand Up @@ -265,4 +265,4 @@ public void setPlugin(AuctionPlugin plugin) {
public List<String> getMessageAsStringList() {
return this.messages.stream().filter(AuctionMessage -> AuctionMessage instanceof ClassicMessage).map(AuctionMessage -> (ClassicMessage) AuctionMessage).map(ClassicMessage::messages).flatMap(List::stream).toList();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package fr.maxlego08.zauctionhouse.api.filter;

import fr.maxlego08.zauctionhouse.api.configuration.records.SearchFilterConfiguration;
import org.junit.jupiter.api.Test;

import java.util.Map;

import static org.junit.jupiter.api.Assertions.assertEquals;

class SearchQueryFormatterTest {

private final SearchFilterConfiguration configuration = new SearchFilterConfiguration(Map.of(
SearchFilterType.CONTAINS, "~",
SearchFilterType.EQUALS, "=",
SearchFilterType.CONTAINS_IGNORE_CASE, "~=",
SearchFilterType.EQUALS_IGNORE_CASE, "=="
));

@Test
void formatsBlankQueryAsNone() {
assertEquals("None", SearchQueryFormatter.format("", configuration));
}

@Test
void formatsAllContainsQuery() {
assertEquals("Everything contains: sandaly", SearchQueryFormatter.format("all ~= sandaly", configuration));
}

@Test
void formatsFieldExactQuery() {
assertEquals("Name exact: Sandaly", SearchQueryFormatter.format("name == Sandaly", configuration));
}

@Test
void formatsDefaultQueryAsEverythingContains() {
assertEquals("Everything contains: diamond", SearchQueryFormatter.format("diamond", configuration));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package fr.maxlego08.zauctionhouse.api.filter;

import fr.maxlego08.zauctionhouse.api.configuration.records.SearchFilterConfiguration;
import org.junit.jupiter.api.Test;

import java.util.Map;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;

class SearchQueryTest {

private final SearchFilterConfiguration configuration = new SearchFilterConfiguration(Map.of(
SearchFilterType.CONTAINS, "~",
SearchFilterType.EQUALS, "=",
SearchFilterType.CONTAINS_IGNORE_CASE, "~=",
SearchFilterType.EQUALS_IGNORE_CASE, "=="
));

@Test
void parsesExplicitAllField() {
SearchQuery query = SearchQuery.parse("all ~= sandaly", configuration);

assertEquals(SearchField.ALL, query.field());
assertEquals(SearchFilterType.CONTAINS_IGNORE_CASE, query.type());
assertEquals("sandaly", query.value());
}

@Test
void parsesNameEqualsIgnoreCase() {
SearchQuery query = SearchQuery.parse("name == sandaly", configuration);

assertEquals(SearchField.NAME, query.field());
assertEquals(SearchFilterType.EQUALS_IGNORE_CASE, query.type());
assertEquals("sandaly", query.value());
}

@Test
void parsesSellerContainsIgnoreCase() {
SearchQuery query = SearchQuery.parse("seller ~= Notch", configuration);

assertEquals(SearchField.SELLER, query.field());
assertEquals(SearchFilterType.CONTAINS_IGNORE_CASE, query.type());
assertEquals("Notch", query.value());
}

@Test
void keepsBlankQueryAsDefaultSearch() {
SearchQuery query = SearchQuery.parse("", configuration);

assertNull(query.field());
assertNull(query.type());
assertEquals("", query.value());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package fr.maxlego08.zauctionhouse.api.filter;

import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;

class SearchTextMatcherTest {

@Test
void containsIgnoreCaseMatchesSmallCaps() {
assertTrue(SearchTextMatcher.matches("sᴀɴᴅᴀʟʏ ᴘᴏsʟᴀɴᴄᴀ", SearchFilterType.CONTAINS_IGNORE_CASE, "sandaly"));
}

@Test
void equalsIgnoreCaseMatchesSmallCaps() {
assertTrue(SearchTextMatcher.matches("sᴀɴᴅᴀʟʏ", SearchFilterType.EQUALS_IGNORE_CASE, "sandaly"));
}

@Test
void caseSensitiveContainsKeepsExistingBehavior() {
assertFalse(SearchTextMatcher.matches("Diamond Sword", SearchFilterType.CONTAINS, "diamond"));
}

@Test
void exactMatchKeepsExistingBehavior() {
assertFalse(SearchTextMatcher.matches("sᴀɴᴅᴀʟʏ", SearchFilterType.EQUALS, "sandaly"));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package fr.maxlego08.zauctionhouse.api.filter;

import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.assertEquals;

class SearchTextNormalizerTest {

@Test
void normalizesSmallCapsToPlainAscii() {
assertEquals("sandaly", SearchTextNormalizer.normalize("sᴀɴᴅᴀʟʏ"));
}

@Test
void normalizesMixedCaseAndSmallCapsTogether() {
assertEquals("sandaly poslanca", SearchTextNormalizer.normalize("Sᴀɴᴅᴀʟʏ Pᴏsʟᴀɴᴄᴀ"));
}

@Test
void lowercasesRegularText() {
assertEquals("diamond sword", SearchTextNormalizer.normalize("Diamond Sword"));
}
}
9 changes: 8 additions & 1 deletion build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import org.gradle.kotlin.dsl.invoke
import org.gradle.api.tasks.testing.Test

plugins {
`java-library`
Expand Down Expand Up @@ -63,13 +64,19 @@ allprojects {
}

dependencies {
compileOnly("io.papermc.paper:paper-api:1.21.10-R0.1-SNAPSHOT")
compileOnly("io.papermc.paper:paper-api:1.21.11-R0.1-SNAPSHOT")
compileOnly("me.clip:placeholderapi:2.11.6")
compileOnly("fr.maxlego08.menu:zmenu-api:1.1.1.0")

implementation("fr.maxlego08.sarah:sarah:1.23")
implementation("com.tcoded:FoliaLib:0.5.1")
implementation("fr.traqueur.currencies:currenciesapi:1.0.13")

testImplementation("org.junit.jupiter:junit-jupiter:5.10.3")
}

tasks.withType<Test>().configureEach {
useJUnitPlatform()
}
}

Expand Down
14 changes: 7 additions & 7 deletions src/main/java/fr/maxlego08/zauctionhouse/ZAuctionPlugin.java
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@
import fr.maxlego08.zauctionhouse.services.BroadcastService;
import fr.maxlego08.zauctionhouse.rule.ZItemRuleManager;
import fr.maxlego08.zauctionhouse.rule.ZRuleLoaderRegistry;
import fr.maxlego08.zauctionhouse.search.ChatSearchListener;
import fr.maxlego08.zauctionhouse.search.SearchDialogService;
import fr.maxlego08.zauctionhouse.storage.ZStorageManager;
import fr.maxlego08.zauctionhouse.utils.LocaleHelper;
import fr.maxlego08.zauctionhouse.utils.Metrics;
Expand Down Expand Up @@ -90,7 +90,7 @@ public class ZAuctionPlugin extends JavaPlugin implements AuctionPlugin {
private final MessageHelper messageHelper = new MessageHelper();
private LocaleHelper localeHelper;
private InventoriesLoader inventoriesLoader;
private ChatSearchListener chatSearchListener;
private SearchDialogService searchDialogService;
private BroadcastService broadcastService;
private DiscordWebhookService discordWebhookService;
private VersionChecker versionChecker;
Expand Down Expand Up @@ -137,9 +137,9 @@ public void onEnable() {
this.broadcastService = new BroadcastService(this);
this.discordWebhookService = new DiscordWebhookService(this);

this.chatSearchListener = new ChatSearchListener(this);
this.searchDialogService = new SearchDialogService(this);
this.addListener(new PlayerListener(this));
this.addListener(this.chatSearchListener);
this.addListener(this.searchDialogService);

java.util.List<String> aliases = new java.util.ArrayList<>(getConfig().getStringList("commands.main-command.aliases"));
String primaryCommand;
Expand Down Expand Up @@ -453,8 +453,8 @@ public Placeholder getPlaceholder() {
return this.placeholder;
}

public ChatSearchListener getChatSearchListener() {
return this.chatSearchListener;
public SearchDialogService getSearchDialogService() {
return this.searchDialogService;
}

public BroadcastService getBroadcastService() {
Expand Down Expand Up @@ -615,4 +615,4 @@ void send(AuctionPlugin plugin, org.bukkit.command.CommandSender sender, Message
this.message(plugin, sender, message, args);
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import fr.maxlego08.menu.api.utils.Placeholders;
import fr.maxlego08.zauctionhouse.api.AuctionPlugin;
import fr.maxlego08.zauctionhouse.api.cache.PlayerCacheKey;
import fr.maxlego08.zauctionhouse.api.filter.SearchQueryFormatter;
import org.bukkit.entity.Player;
import org.bukkit.event.inventory.InventoryClickEvent;
import org.bukkit.inventory.ItemStack;
Expand Down Expand Up @@ -46,7 +47,7 @@ public ItemStack getCustomItemStack(@NotNull Player player, boolean useCache, @N
return null;
}

placeholders.register("search_query", query);
placeholders.register("search_query", SearchQueryFormatter.format(query, this.plugin.getConfiguration().getSearchFilter()));
return this.getItemStack().build(player, false, placeholders);
}

Expand Down
Loading