diff --git a/api/build.gradle.kts b/api/build.gradle.kts
index 7f0708b4..c9a5538b 100644
--- a/api/build.gradle.kts
+++ b/api/build.gradle.kts
@@ -11,6 +11,7 @@ java {
dependencies {
compileOnly(libs.paperApi)
compileOnlyApi(libs.checkerQual)
+ compileOnlyApi(libs.adventureApi)
}
indra {
diff --git a/api/src/main/java/xyz/jpenilla/squaremap/api/HtmlComponentSerializer.java b/api/src/main/java/xyz/jpenilla/squaremap/api/HtmlComponentSerializer.java
new file mode 100644
index 00000000..85d4114b
--- /dev/null
+++ b/api/src/main/java/xyz/jpenilla/squaremap/api/HtmlComponentSerializer.java
@@ -0,0 +1,35 @@
+package xyz.jpenilla.squaremap.api;
+
+import net.kyori.adventure.text.Component;
+import net.kyori.adventure.text.flattener.ComponentFlattener;
+import net.kyori.adventure.text.serializer.ComponentEncoder;
+import org.checkerframework.checker.nullness.qual.NonNull;
+import org.checkerframework.framework.qual.DefaultQualifier;
+import org.jetbrains.annotations.ApiStatus;
+
+/**
+ * Safely encodes {@link Component Components} as HTML text.
+ *
+ *
Mostly useful for marker tooltips.
+ *
+ * @see Squaremap#htmlComponentSerializer()
+ */
+@DefaultQualifier(NonNull.class)
+public interface HtmlComponentSerializer extends ComponentEncoder {
+
+ /**
+ * Create a new {@link HtmlComponentSerializer} using the provided {@link ComponentFlattener}.
+ *
+ * @param flattener component flattener
+ * @return serializer
+ */
+ static HtmlComponentSerializer withFlattener(final ComponentFlattener flattener) {
+ return ProviderHolder.HTML_SERIALIZER.create(flattener);
+ }
+
+ @ApiStatus.Internal
+ interface Provider {
+ HtmlComponentSerializer create(ComponentFlattener flattener);
+ }
+
+}
diff --git a/api/src/main/java/xyz/jpenilla/squaremap/api/HtmlStripper.java b/api/src/main/java/xyz/jpenilla/squaremap/api/HtmlStripper.java
new file mode 100644
index 00000000..2b4e40cd
--- /dev/null
+++ b/api/src/main/java/xyz/jpenilla/squaremap/api/HtmlStripper.java
@@ -0,0 +1,36 @@
+package xyz.jpenilla.squaremap.api;
+
+import org.checkerframework.checker.nullness.qual.NonNull;
+import org.checkerframework.framework.qual.DefaultQualifier;
+import org.jetbrains.annotations.ApiStatus;
+
+/**
+ * Strips HTML tags from text. Allows sanitizing untrusted strings before use in
+ * marker tooltips.
+ */
+@DefaultQualifier(NonNull.class)
+public interface HtmlStripper {
+
+ /**
+ * Get an {@link HtmlStripper}.
+ *
+ * @return HTML stripper
+ */
+ static HtmlStripper htmlStripper() {
+ return ProviderHolder.HTML_STRIPPER.instance();
+ }
+
+ /**
+ * Strips HTML tags from the provided string.
+ *
+ * @param string untrusted string
+ * @return sanitized string
+ */
+ String stripHtml(String string);
+
+ @ApiStatus.Internal
+ interface Provider {
+ HtmlStripper instance();
+ }
+
+}
diff --git a/api/src/main/java/xyz/jpenilla/squaremap/api/ProviderHolder.java b/api/src/main/java/xyz/jpenilla/squaremap/api/ProviderHolder.java
new file mode 100644
index 00000000..b823a03b
--- /dev/null
+++ b/api/src/main/java/xyz/jpenilla/squaremap/api/ProviderHolder.java
@@ -0,0 +1,13 @@
+package xyz.jpenilla.squaremap.api;
+
+import net.kyori.adventure.util.Services;
+
+final class ProviderHolder {
+ static final HtmlComponentSerializer.Provider HTML_SERIALIZER = service(HtmlComponentSerializer.Provider.class);
+ static final HtmlStripper.Provider HTML_STRIPPER = service(HtmlStripper.Provider.class);
+
+ private static T service(final Class clazz) {
+ return Services.service(clazz)
+ .orElseThrow(() -> new IllegalStateException("Could not find " + clazz.getName() + " implementation"));
+ }
+}
diff --git a/api/src/main/java/xyz/jpenilla/squaremap/api/Squaremap.java b/api/src/main/java/xyz/jpenilla/squaremap/api/Squaremap.java
index a13d98b5..76e36b7e 100644
--- a/api/src/main/java/xyz/jpenilla/squaremap/api/Squaremap.java
+++ b/api/src/main/java/xyz/jpenilla/squaremap/api/Squaremap.java
@@ -4,6 +4,7 @@
import java.nio.file.Path;
import java.util.Collection;
import java.util.Optional;
+import net.kyori.adventure.text.flattener.ComponentFlattener;
import org.checkerframework.checker.nullness.qual.NonNull;
/**
@@ -63,4 +64,11 @@ public interface Squaremap {
*/
@NonNull Path webDir();
+ /**
+ * Get an {@link HtmlComponentSerializer} using the platform {@link ComponentFlattener}.
+ *
+ * @return serializer
+ */
+ @NonNull HtmlComponentSerializer htmlComponentSerializer();
+
}
diff --git a/common/src/main/java/xyz/jpenilla/squaremap/common/SquaremapApiProvider.java b/common/src/main/java/xyz/jpenilla/squaremap/common/SquaremapApiProvider.java
index 18a7f8db..a936640a 100644
--- a/common/src/main/java/xyz/jpenilla/squaremap/common/SquaremapApiProvider.java
+++ b/common/src/main/java/xyz/jpenilla/squaremap/common/SquaremapApiProvider.java
@@ -1,6 +1,7 @@
package xyz.jpenilla.squaremap.common;
import com.google.inject.Inject;
+import com.google.inject.Provider;
import com.google.inject.Singleton;
import java.awt.image.BufferedImage;
import java.nio.file.Path;
@@ -8,8 +9,10 @@
import java.util.Collections;
import java.util.Optional;
import java.util.function.Function;
+import net.kyori.adventure.text.flattener.ComponentFlattener;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.checkerframework.framework.qual.DefaultQualifier;
+import xyz.jpenilla.squaremap.api.HtmlComponentSerializer;
import xyz.jpenilla.squaremap.api.MapWorld;
import xyz.jpenilla.squaremap.api.PlayerManager;
import xyz.jpenilla.squaremap.api.Registry;
@@ -24,17 +27,20 @@ public final class SquaremapApiProvider implements Squaremap {
private final PlayerManager playerManager;
private final WorldManager worldManager;
private final IconRegistry iconRegistry;
+ private final Provider flattener;
@Inject
private SquaremapApiProvider(
final DirectoryProvider directoryProvider,
final AbstractPlayerManager playerManager,
- final WorldManager worldManager
+ final WorldManager worldManager,
+ final Provider flattener
) {
this.directoryProvider = directoryProvider;
this.playerManager = playerManager;
this.worldManager = worldManager;
this.iconRegistry = new IconRegistry(directoryProvider);
+ this.flattener = flattener;
}
@Override
@@ -61,4 +67,9 @@ public PlayerManager playerManager() {
public Path webDir() {
return this.directoryProvider.webDirectory();
}
+
+ @Override
+ public HtmlComponentSerializer htmlComponentSerializer() {
+ return HtmlComponentSerializer.withFlattener(this.flattener.get());
+ }
}
diff --git a/common/src/main/java/xyz/jpenilla/squaremap/common/task/UpdatePlayers.java b/common/src/main/java/xyz/jpenilla/squaremap/common/task/UpdatePlayers.java
index 1d635656..acc7b1a6 100644
--- a/common/src/main/java/xyz/jpenilla/squaremap/common/task/UpdatePlayers.java
+++ b/common/src/main/java/xyz/jpenilla/squaremap/common/task/UpdatePlayers.java
@@ -16,13 +16,13 @@
import org.checkerframework.checker.nullness.qual.NonNull;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.checkerframework.framework.qual.DefaultQualifier;
+import xyz.jpenilla.squaremap.api.HtmlComponentSerializer;
import xyz.jpenilla.squaremap.common.AbstractPlayerManager;
import xyz.jpenilla.squaremap.common.ServerAccess;
import xyz.jpenilla.squaremap.common.config.ConfigManager;
import xyz.jpenilla.squaremap.common.config.WorldConfig;
import xyz.jpenilla.squaremap.common.data.DirectoryProvider;
import xyz.jpenilla.squaremap.common.util.FileUtil;
-import xyz.jpenilla.squaremap.common.util.HtmlComponentSerializer;
import xyz.jpenilla.squaremap.common.util.Util;
@DefaultQualifier(NonNull.class)
diff --git a/common/src/main/java/xyz/jpenilla/squaremap/common/util/HtmlComponentSerializer.java b/common/src/main/java/xyz/jpenilla/squaremap/common/util/HtmlComponentSerializer.java
deleted file mode 100644
index 7cd2292c..00000000
--- a/common/src/main/java/xyz/jpenilla/squaremap/common/util/HtmlComponentSerializer.java
+++ /dev/null
@@ -1,15 +0,0 @@
-package xyz.jpenilla.squaremap.common.util;
-
-import net.kyori.adventure.text.ComponentLike;
-import net.kyori.adventure.text.flattener.ComponentFlattener;
-import org.checkerframework.checker.nullness.qual.NonNull;
-import org.checkerframework.framework.qual.DefaultQualifier;
-
-@DefaultQualifier(NonNull.class)
-public interface HtmlComponentSerializer {
- static HtmlComponentSerializer withFlattener(ComponentFlattener flattener) {
- return new HtmlComponentSerializerImpl(flattener);
- }
-
- String serialize(ComponentLike componentLike);
-}
diff --git a/common/src/main/java/xyz/jpenilla/squaremap/common/util/HtmlComponentSerializerImpl.java b/common/src/main/java/xyz/jpenilla/squaremap/common/util/HtmlComponentSerializerImpl.java
index 8fef9da8..4e74c941 100644
--- a/common/src/main/java/xyz/jpenilla/squaremap/common/util/HtmlComponentSerializerImpl.java
+++ b/common/src/main/java/xyz/jpenilla/squaremap/common/util/HtmlComponentSerializerImpl.java
@@ -3,7 +3,7 @@
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.concurrent.ThreadLocalRandom;
-import net.kyori.adventure.text.ComponentLike;
+import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.flattener.ComponentFlattener;
import net.kyori.adventure.text.flattener.FlattenerListener;
import net.kyori.adventure.text.format.Style;
@@ -15,6 +15,7 @@
import org.checkerframework.framework.qual.DefaultQualifier;
import org.owasp.html.PolicyFactory;
import org.owasp.html.Sanitizers;
+import xyz.jpenilla.squaremap.api.HtmlComponentSerializer;
@DefaultQualifier(NonNull.class)
final class HtmlComponentSerializerImpl implements HtmlComponentSerializer {
@@ -27,7 +28,7 @@ final class HtmlComponentSerializerImpl implements HtmlComponentSerializer {
}
@Override
- public String serialize(final ComponentLike componentLike) {
+ public String serialize(final Component componentLike) {
final HtmlFlattener state = new HtmlFlattener();
this.flattener.flatten(componentLike.asComponent(), state);
return SANITIZER.sanitize(state.toString());
@@ -118,4 +119,11 @@ private static String asHtml(final TextFormat format) {
throw new IllegalArgumentException("Cannot handle format: " + format + " (" + format.getClass().getTypeName() + ")");
}
}
+
+ public static final class Provider implements HtmlComponentSerializer.Provider {
+ @Override
+ public HtmlComponentSerializer create(final ComponentFlattener flattener) {
+ return new HtmlComponentSerializerImpl(flattener);
+ }
+ }
}
diff --git a/common/src/main/java/xyz/jpenilla/squaremap/common/util/HtmlStripperImpl.java b/common/src/main/java/xyz/jpenilla/squaremap/common/util/HtmlStripperImpl.java
new file mode 100644
index 00000000..9a76ab5d
--- /dev/null
+++ b/common/src/main/java/xyz/jpenilla/squaremap/common/util/HtmlStripperImpl.java
@@ -0,0 +1,31 @@
+package xyz.jpenilla.squaremap.common.util;
+
+import java.util.Objects;
+import org.checkerframework.checker.nullness.qual.NonNull;
+import org.checkerframework.framework.qual.DefaultQualifier;
+import org.owasp.html.HtmlPolicyBuilder;
+import org.owasp.html.PolicyFactory;
+import xyz.jpenilla.squaremap.api.HtmlStripper;
+
+@DefaultQualifier(NonNull.class)
+public final class HtmlStripperImpl implements HtmlStripper {
+ private static final PolicyFactory SANITIZER = new HtmlPolicyBuilder().toFactory();
+
+ private HtmlStripperImpl() {
+ }
+
+ @Override
+ public String stripHtml(final String string) {
+ Objects.requireNonNull(string, "Parameter 'string' must not be null");
+ return SANITIZER.sanitize(string);
+ }
+
+ public static final class Provider implements HtmlStripper.Provider {
+ private static final HtmlStripper INSTANCE = new HtmlStripperImpl();
+
+ @Override
+ public HtmlStripper instance() {
+ return INSTANCE;
+ }
+ }
+}
diff --git a/common/src/main/resources/META-INF/services/xyz.jpenilla.squaremap.api.HtmlComponentSerializer$Provider b/common/src/main/resources/META-INF/services/xyz.jpenilla.squaremap.api.HtmlComponentSerializer$Provider
new file mode 100644
index 00000000..3c5a71e7
--- /dev/null
+++ b/common/src/main/resources/META-INF/services/xyz.jpenilla.squaremap.api.HtmlComponentSerializer$Provider
@@ -0,0 +1 @@
+xyz.jpenilla.squaremap.common.util.HtmlComponentSerializerImpl$Provider
diff --git a/common/src/main/resources/META-INF/services/xyz.jpenilla.squaremap.api.HtmlStripper$Provider b/common/src/main/resources/META-INF/services/xyz.jpenilla.squaremap.api.HtmlStripper$Provider
new file mode 100644
index 00000000..0b536061
--- /dev/null
+++ b/common/src/main/resources/META-INF/services/xyz.jpenilla.squaremap.api.HtmlStripper$Provider
@@ -0,0 +1 @@
+xyz.jpenilla.squaremap.common.util.HtmlStripperImpl$Provider