From 0a6053ea7ea75ff508e7a8d0e0bfa56d404b9c94 Mon Sep 17 00:00:00 2001 From: Strokkur24 Date: Sat, 27 Sep 2025 18:39:44 +0200 Subject: [PATCH 1/4] feat: add named colors as extra context in the MiniMessage instance --- .../net/kyori/adventure/util/RGBLike.java | 8 ++ .../net/kyori/adventure/util/RGBLikeImpl.java | 36 +++++++++ .../adventure/text/minimessage/Context.java | 2 +- .../text/minimessage/ContextImpl.java | 13 +++ .../text/minimessage/MiniMessage.java | 17 +++- .../text/minimessage/MiniMessageImpl.java | 80 ++++++++++++++++++- .../text/minimessage/MiniMessageParser.java | 4 +- .../minimessage/MiniMessageSerializer.java | 18 +++-- .../minimessage/SerializationContext.java | 33 ++++++++ .../serializer/SerializableResolver.java | 12 ++- .../internal/serializer/StyleClaim.java | 16 +++- .../internal/serializer/StyleClaimImpl.java | 12 +-- .../tag/resolver/CachingTagResolver.java | 8 +- .../tag/resolver/SequentialTagResolver.java | 15 +++- .../minimessage/tag/resolver/TagResolver.java | 14 ++++ .../tag/standard/ColorTagResolver.java | 43 +++++----- .../minimessage/tag/standard/GradientTag.java | 3 +- .../tag/standard/TransitionTag.java | 3 +- .../minimessage/SerializerCollectorTest.java | 6 +- .../tag/standard/ColorTagTest.java | 2 +- .../tag/standard/DecorationTagTest.java | 3 +- 21 files changed, 294 insertions(+), 54 deletions(-) create mode 100644 api/src/main/java/net/kyori/adventure/util/RGBLikeImpl.java create mode 100644 text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/SerializationContext.java diff --git a/api/src/main/java/net/kyori/adventure/util/RGBLike.java b/api/src/main/java/net/kyori/adventure/util/RGBLike.java index 1ce00147ea..71606e9f05 100644 --- a/api/src/main/java/net/kyori/adventure/util/RGBLike.java +++ b/api/src/main/java/net/kyori/adventure/util/RGBLike.java @@ -32,6 +32,14 @@ * @since 4.0.0 */ public interface RGBLike { + static RGBLike of(final @Range(from = 0x0, to = 0xff) int red, final @Range(from = 0x0, to = 0xff) int green, final @Range(from = 0x0, to = 0xff) int blue) { + return new RGBLikeImpl(red, green, blue); + } + + static RGBLike of(final @Range(from = 0x0, to = 0xffffff) int value) { + return new RGBLikeImpl(value); + } + /** * Gets the red component. * diff --git a/api/src/main/java/net/kyori/adventure/util/RGBLikeImpl.java b/api/src/main/java/net/kyori/adventure/util/RGBLikeImpl.java new file mode 100644 index 0000000000..a02350a9bf --- /dev/null +++ b/api/src/main/java/net/kyori/adventure/util/RGBLikeImpl.java @@ -0,0 +1,36 @@ +package net.kyori.adventure.util; + +import org.jetbrains.annotations.Range; + +class RGBLikeImpl implements RGBLike { + private final int red; + private final int green; + private final int blue; + + public RGBLikeImpl(final @Range(from = 0x0, to = 0xff) int red, final @Range(from = 0x0, to = 0xff) int green, final @Range(from = 0x0, to = 0xff) int blue) { + this.red = red; + this.green = green; + this.blue = blue; + } + + public RGBLikeImpl(final @Range(from = 0x0, to = 0xffffff) int value) { + this.red = value >> 16 & 0xFF; + this.green = value >> 8 & 0xFF; + this.blue = value & 0xFF; + } + + @Override + public @Range(from = 0x0, to = 0xff) int red() { + return red; + } + + @Override + public @Range(from = 0x0, to = 0xff) int green() { + return green; + } + + @Override + public @Range(from = 0x0, to = 0xff) int blue() { + return blue; + } +} diff --git a/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/Context.java b/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/Context.java index 078979669c..20c42efc1c 100644 --- a/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/Context.java +++ b/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/Context.java @@ -39,7 +39,7 @@ * @since 4.10.0 */ @ApiStatus.NonExtendable -public interface Context { +public interface Context extends SerializationContext { /** * The target of the parse context, if provided. diff --git a/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/ContextImpl.java b/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/ContextImpl.java index 4b59800c40..f118da6b83 100644 --- a/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/ContextImpl.java +++ b/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/ContextImpl.java @@ -24,10 +24,12 @@ package net.kyori.adventure.text.minimessage; import java.util.List; +import java.util.Map; import java.util.function.Consumer; import java.util.function.UnaryOperator; import net.kyori.adventure.pointer.Pointered; import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.TextColor; import net.kyori.adventure.text.minimessage.internal.parser.ParsingExceptionImpl; import net.kyori.adventure.text.minimessage.internal.parser.Token; import net.kyori.adventure.text.minimessage.internal.parser.node.TagPart; @@ -36,6 +38,7 @@ import net.kyori.adventure.text.minimessage.tag.resolver.TagResolver; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; +import org.jetbrains.annotations.Unmodifiable; import static java.util.Objects.requireNonNull; @@ -170,6 +173,16 @@ public UnaryOperator preProcessor() { return new ParsingExceptionImpl(message, this.message, cause, false, tagsToTokens(((ArgumentQueueImpl) tags).args)); } + @Override + public @NotNull @Unmodifiable Map namedColors() { + return this.miniMessage.namedColors(); + } + + @Override + public @NotNull @Unmodifiable Map namedColorAliases() { + return this.miniMessage.namedColorAliases(); + } + private @NotNull Component deserializeWithOptionalTarget(final @NotNull String message, final @NotNull TagResolver tagResolver) { if (this.target != null) { return this.miniMessage.deserialize(message, this.target, tagResolver); diff --git a/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/MiniMessage.java b/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/MiniMessage.java index e6f4ab80e2..3a36ac4002 100644 --- a/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/MiniMessage.java +++ b/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/MiniMessage.java @@ -23,11 +23,14 @@ */ package net.kyori.adventure.text.minimessage; +import java.util.Map; import java.util.function.Consumer; import java.util.function.UnaryOperator; import net.kyori.adventure.builder.AbstractBuilder; import net.kyori.adventure.pointer.Pointered; import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.TextColor; +import net.kyori.adventure.text.minimessage.tag.TagPattern; import net.kyori.adventure.text.minimessage.tag.resolver.TagResolver; import net.kyori.adventure.text.minimessage.tree.Node; import net.kyori.adventure.text.serializer.ComponentSerializer; @@ -44,7 +47,7 @@ * * @since 4.10.0 */ -public interface MiniMessage extends ComponentSerializer { +public interface MiniMessage extends ComponentSerializer, SerializationContext { /** * Gets a simple instance with default settings. * @@ -331,6 +334,18 @@ interface Builder extends AbstractBuilder { */ @NotNull Builder editTags(final @NotNull Consumer adder); + @NotNull Builder namedColors(@NotNull Map colors); + + @NotNull Builder namedColor(@NotNull @TagPattern String name, @NotNull TextColor color); + + @NotNull Builder removeNamedColor(@NotNull @TagPattern String name); + + @NotNull Builder namedColorAliases(@NotNull Map aliases); + + @NotNull Builder namedColorAlias(@NotNull @TagPattern String name, @NotNull @TagPattern String color); + + @NotNull Builder removeNamedColorAlias(@NotNull @TagPattern String name); + /** * Enables strict mode (disabled by default). * diff --git a/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/MiniMessageImpl.java b/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/MiniMessageImpl.java index 5ab9d97cbe..67995d51cb 100644 --- a/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/MiniMessageImpl.java +++ b/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/MiniMessageImpl.java @@ -23,18 +23,25 @@ */ package net.kyori.adventure.text.minimessage; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; import java.util.Objects; import java.util.Optional; +import java.util.TreeMap; import java.util.function.Consumer; import java.util.function.UnaryOperator; import net.kyori.adventure.pointer.Pointered; import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; +import net.kyori.adventure.text.format.TextColor; import net.kyori.adventure.text.minimessage.internal.serializer.SerializableResolver; import net.kyori.adventure.text.minimessage.tag.resolver.TagResolver; import net.kyori.adventure.text.minimessage.tree.Node; import net.kyori.adventure.util.Services; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; +import org.jetbrains.annotations.Unmodifiable; import static java.util.Objects.requireNonNull; @@ -55,7 +62,7 @@ final class MiniMessageImpl implements MiniMessage { static final class Instances { static final MiniMessage INSTANCE = SERVICE .map(Provider::miniMessage) - .orElseGet(() -> new MiniMessageImpl(TagResolver.standard(), false, true, null, DEFAULT_NO_OP, DEFAULT_COMPACTING_METHOD)); + .orElseGet(() -> new MiniMessageImpl(TagResolver.standard(), false, true, null, DEFAULT_NO_OP, DEFAULT_COMPACTING_METHOD, defaultNamedColors(), defaultNamedColorAliases())); } static final UnaryOperator DEFAULT_NO_OP = UnaryOperator.identity(); @@ -66,15 +73,30 @@ static final class Instances { private final @Nullable Consumer debugOutput; private final UnaryOperator postProcessor; private final UnaryOperator preProcessor; + private final Map namedColors; + private final Map namedColorAliases; final MiniMessageParser parser; - MiniMessageImpl(final @NotNull TagResolver resolver, final boolean strict, final boolean emitVirtuals, final @Nullable Consumer debugOutput, final @NotNull UnaryOperator preProcessor, final @NotNull UnaryOperator postProcessor) { + MiniMessageImpl(final @NotNull TagResolver resolver, final boolean strict, final boolean emitVirtuals, final @Nullable Consumer debugOutput, final @NotNull UnaryOperator preProcessor, final @NotNull UnaryOperator postProcessor, final Map namedColors, final Map namedColorAliases) { this.parser = new MiniMessageParser(resolver); this.strict = strict; this.emitVirtuals = emitVirtuals; this.debugOutput = debugOutput; this.preProcessor = preProcessor; this.postProcessor = postProcessor; + this.namedColors = namedColors; + this.namedColorAliases = namedColorAliases; + } + + private static @NotNull Map defaultNamedColors() { + return new TreeMap<>(NamedTextColor.NAMES.keyToValue()); + } + + private static @NotNull Map defaultNamedColorAliases() { + final Map out = new TreeMap<>(); + out.put("grey", "gray"); + out.put("dark_grey", "dark_gray"); + return out; } @Override @@ -119,7 +141,7 @@ static final class Instances { @Override public @NotNull String serialize(final @NotNull Component component) { - return MiniMessageSerializer.serialize(component, this.serialResolver(null), this.strict); + return MiniMessageSerializer.serialize(component, this.serialResolver(null), this.strict, this); } private SerializableResolver serialResolver(final @Nullable TagResolver extraResolver) { @@ -157,6 +179,16 @@ private SerializableResolver serialResolver(final @Nullable TagResolver extraRes return this.parser.stripTokens(this.newContext(input, null, tagResolver)); } + @Override + public @NotNull @Unmodifiable Map namedColors() { + return Collections.unmodifiableMap(this.namedColors); + } + + @Override + public @NotNull @Unmodifiable Map namedColorAliases() { + return Collections.unmodifiableMap(this.namedColorAliases); + } + @Override public boolean strict() { return this.strict; @@ -179,6 +211,8 @@ static final class BuilderImpl implements Builder { private Consumer debug = null; private UnaryOperator postProcessor = DEFAULT_COMPACTING_METHOD; private UnaryOperator preProcessor = DEFAULT_NO_OP; + private final Map namedColors = defaultNamedColors(); + private final Map namedColorAliases = defaultNamedColorAliases(); BuilderImpl() { BUILDER.accept(this); @@ -208,6 +242,44 @@ static final class BuilderImpl implements Builder { return this; } + @Override + public @NotNull Builder namedColors(final @NotNull Map colors) { + this.namedColors.clear(); + this.namedColors.putAll(colors); + return this; + } + + @Override + public @NotNull Builder namedColor(final @NotNull String name, final @NotNull TextColor color) { + this.namedColors.put(name, color); + return this; + } + + @Override + public @NotNull Builder removeNamedColor(final @NotNull String name) { + this.namedColors.remove(name); + return this; + } + + @Override + public @NotNull Builder namedColorAliases(@NotNull final Map aliases) { + this.namedColorAliases.clear(); + this.namedColorAliases.putAll(aliases); + return this; + } + + @Override + public @NotNull Builder namedColorAlias(@NotNull final String name, @NotNull final String color) { + this.namedColorAliases.put(name, color); + return this; + } + + @Override + public @NotNull Builder removeNamedColorAlias(@NotNull final String name) { + this.namedColorAliases.remove(name); + return this; + } + @Override public @NotNull Builder strict(final boolean strict) { this.strict = strict; @@ -240,7 +312,7 @@ static final class BuilderImpl implements Builder { @Override public @NotNull MiniMessage build() { - return new MiniMessageImpl(this.tagResolver, this.strict, this.emitVirtuals, this.debug, this.preProcessor, this.postProcessor); + return new MiniMessageImpl(this.tagResolver, this.strict, this.emitVirtuals, this.debug, this.preProcessor, this.postProcessor, new HashMap<>(this.namedColors), new HashMap<>(this.namedColorAliases)); } } } diff --git a/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/MiniMessageParser.java b/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/MiniMessageParser.java index 45742289b8..09d7cac5ee 100644 --- a/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/MiniMessageParser.java +++ b/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/MiniMessageParser.java @@ -113,7 +113,7 @@ private void processTokens(final @NotNull StringBuilder sb, final @NotNull Strin continue; } final String sanitized = TokenParser.TagProvider.sanitizePlaceholderName(token.childTokens().get(0).get(richMessage).toString()); - if (combinedResolver.has(sanitized)) { + if (combinedResolver.has(sanitized, context)) { tagHandler.accept(token, sb); } else { sb.append(richMessage, token.startIndex(), token.endIndex()); @@ -189,7 +189,7 @@ private void processTokens(final @NotNull StringBuilder sb, final @NotNull Strin } final Predicate tagNameChecker = name -> { final String sanitized = TokenParser.TagProvider.sanitizePlaceholderName(name); - return combinedResolver.has(sanitized); + return combinedResolver.has(sanitized, context); }; final String preProcessed = TokenParser.resolvePreProcessTags(processedMessage, transformationFactory); diff --git a/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/MiniMessageSerializer.java b/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/MiniMessageSerializer.java index db385cd5e2..8030fc4795 100644 --- a/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/MiniMessageSerializer.java +++ b/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/MiniMessageSerializer.java @@ -49,12 +49,12 @@ private MiniMessageSerializer() { // - abbreviated vs long tag names (tag-specific option) // - static @NotNull String serialize(final @NotNull Component component, final @NotNull SerializableResolver resolver, final boolean strict) { + static @NotNull String serialize(final @NotNull Component component, final @NotNull SerializableResolver resolver, final boolean strict, final @NotNull SerializationContext ctx) { final StringBuilder sb = new StringBuilder(); - final Collector emitter = new Collector(resolver, strict, sb); + final Collector emitter = new Collector(resolver, strict, sb, ctx); emitter.mark(); - visit(component, emitter, resolver, true); + visit(component, emitter, resolver, true, ctx); if (strict) { // If we are in strict mode, we need to close all tags at the end of our serialization journey emitter.popAll(); @@ -65,9 +65,9 @@ private MiniMessageSerializer() { return sb.toString(); } - private static void visit(final @NotNull Component component, final Collector emitter, final SerializableResolver resolver, final boolean lastChild) { + private static void visit(final @NotNull Component component, final Collector emitter, final SerializableResolver resolver, final boolean lastChild, final @NotNull SerializationContext ctx) { // visit self - resolver.handle(component, emitter); + resolver.handle(component, emitter, ctx); Component childSource = emitter.flushClaims(component); if (childSource == null) { childSource = component; @@ -76,7 +76,7 @@ private static void visit(final @NotNull Component component, final Collector em // then children for (final Iterator it = childSource.children().iterator(); it.hasNext();) { emitter.mark(); - visit(it.next(), emitter, resolver, lastChild && !it.hasNext()); + visit(it.next(), emitter, resolver, lastChild && !it.hasNext(), ctx); } if (!lastChild) { @@ -106,6 +106,7 @@ enum TagState { private static final char[] SINGLE_QUOTED_ESCAPES = {TokenParser.ESCAPE, '\''}; private static final char[] DOUBLE_QUOTED_ESCAPES = {TokenParser.ESCAPE, '"'}; + private final SerializationContext ctx; private final SerializableResolver resolver; private final boolean strict; private final StringBuilder consumer; @@ -113,7 +114,8 @@ enum TagState { private int tagLevel = 0; private TagState tagState = TagState.TEXT; - Collector(final SerializableResolver resolver, final boolean strict, final StringBuilder consumer) { + Collector(final SerializableResolver resolver, final boolean strict, final StringBuilder consumer, final SerializationContext ctx) { + this.ctx = ctx; this.resolver = resolver; this.strict = strict; this.consumer = consumer; @@ -211,7 +213,7 @@ void completeTag() { @Override public @NotNull TokenEmitter argument(final @NotNull Component arg) { - final String serialized = MiniMessageSerializer.serialize(arg, this.resolver, this.strict); + final String serialized = MiniMessageSerializer.serialize(arg, this.resolver, this.strict, this.ctx); return this.argument(serialized, QuotingOverride.QUOTED); // always quote tokens } diff --git a/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/SerializationContext.java b/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/SerializationContext.java new file mode 100644 index 0000000000..a4d81baade --- /dev/null +++ b/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/SerializationContext.java @@ -0,0 +1,33 @@ +package net.kyori.adventure.text.minimessage; + +import java.util.Map; +import net.kyori.adventure.text.format.TextColor; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.jetbrains.annotations.Unmodifiable; + +@ApiStatus.NonExtendable +public interface SerializationContext { + @NotNull + @Unmodifiable + Map namedColors(); + + @NotNull + @Unmodifiable + Map namedColorAliases(); + + default @Nullable TextColor namedColor(final @NotNull String name) { + final TextColor color = namedColors().get(name); + if (color != null) { + return color; + } + + final String alias = namedColorAliases().get(name); + if (alias != null) { + return namedColor(alias); + } + + return null; + } +} diff --git a/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/internal/serializer/SerializableResolver.java b/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/internal/serializer/SerializableResolver.java index ab63a18279..5ec0565ddb 100644 --- a/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/internal/serializer/SerializableResolver.java +++ b/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/internal/serializer/SerializableResolver.java @@ -30,6 +30,7 @@ import java.util.function.Function; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.minimessage.Context; +import net.kyori.adventure.text.minimessage.SerializationContext; import net.kyori.adventure.text.minimessage.ParsingException; import net.kyori.adventure.text.minimessage.internal.TagInternals; import net.kyori.adventure.text.minimessage.tag.Tag; @@ -117,6 +118,10 @@ public interface SerializableResolver { */ void handle(final @NotNull Component serializable, final @NotNull ClaimConsumer consumer); + default void handle(final @NotNull Component serializable, final @NotNull ClaimConsumer consumer, final @NotNull SerializationContext serializationContext) { + this.handle(serializable, consumer); + } + /** * A subinterface for resolvers that only handle one single tag. * @@ -125,9 +130,14 @@ public interface SerializableResolver { interface Single extends SerializableResolver { @Override default void handle(final @NotNull Component serializable, final @NotNull ClaimConsumer consumer) { + throw new IllegalStateException("TagResolver#has(String) should not be called if TagResolver#has(String,SerializationContext) is present!"); + } + + @Override + default void handle(final @NotNull Component serializable, final @NotNull ClaimConsumer consumer, final @NotNull SerializationContext ctx) { final @Nullable StyleClaim style = this.claimStyle(); if (style != null && !consumer.styleClaimed(style.claimKey())) { - final @Nullable Emitable applied = style.apply(serializable.style()); + final @Nullable Emitable applied = style.apply(serializable.style(), ctx); if (applied != null) { consumer.style(style.claimKey(), applied); } diff --git a/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/internal/serializer/StyleClaim.java b/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/internal/serializer/StyleClaim.java index 633e9f6663..b3dc3b20e8 100644 --- a/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/internal/serializer/StyleClaim.java +++ b/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/internal/serializer/StyleClaim.java @@ -27,6 +27,7 @@ import java.util.function.Function; import java.util.function.Predicate; import net.kyori.adventure.text.format.Style; +import net.kyori.adventure.text.minimessage.SerializationContext; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -53,6 +54,10 @@ public interface StyleClaim { return claim(claimKey, lens, $ -> true, emitable); } + static @NotNull StyleClaim claim(final @NotNull String claimKey, final @NotNull Function lens, final @NotNull ContextAwareEmittable emitable) { + return claim(claimKey, lens, $ -> true, emitable); + } + /** * Create a new style claim that will emit content for any non-null value that passes the filter. * @@ -65,6 +70,10 @@ public interface StyleClaim { * @since 4.10.0 */ static @NotNull StyleClaim claim(final @NotNull String claimKey, final @NotNull Function lens, final @NotNull Predicate filter, final @NotNull BiConsumer emitable) { + return claim(claimKey, lens, filter, (type, emitter, ctx) -> emitable.accept(type, emitter)); + } + + static @NotNull StyleClaim claim(final @NotNull String claimKey, final @NotNull Function lens, final @NotNull Predicate filter, final @NotNull ContextAwareEmittable emitable) { return new StyleClaimImpl<>( requireNonNull(claimKey, "claimKey"), requireNonNull(lens, "lens"), @@ -88,5 +97,10 @@ public interface StyleClaim { * @return an emitable for this style claim, if it is applicable to the provided style * @since 4.10.0 */ - @Nullable Emitable apply(final @NotNull Style style); + @Nullable Emitable apply(final @NotNull Style style, final @NotNull SerializationContext ctx); + + @FunctionalInterface + interface ContextAwareEmittable { + void emit(T type, TokenEmitter emitter, SerializationContext ctx); + } } diff --git a/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/internal/serializer/StyleClaimImpl.java b/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/internal/serializer/StyleClaimImpl.java index 6cb901802f..9a546183c3 100644 --- a/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/internal/serializer/StyleClaimImpl.java +++ b/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/internal/serializer/StyleClaimImpl.java @@ -24,10 +24,10 @@ package net.kyori.adventure.text.minimessage.internal.serializer; import java.util.Objects; -import java.util.function.BiConsumer; import java.util.function.Function; import java.util.function.Predicate; import net.kyori.adventure.text.format.Style; +import net.kyori.adventure.text.minimessage.SerializationContext; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -35,13 +35,13 @@ class StyleClaimImpl implements StyleClaim { private final String claimKey; private final Function lens; private final Predicate filter; - private final BiConsumer emitable; + private final ContextAwareEmittable emittable; - StyleClaimImpl(final String claimKey, final Function lens, final Predicate filter, final BiConsumer emitable) { + StyleClaimImpl(final String claimKey, final Function lens, final Predicate filter, final ContextAwareEmittable emittable) { this.claimKey = claimKey; this.lens = lens; this.filter = filter; - this.emitable = emitable; + this.emittable = emittable; } @Override @@ -50,11 +50,11 @@ class StyleClaimImpl implements StyleClaim { } @Override - public @Nullable Emitable apply(final @NotNull Style style) { + public @Nullable Emitable apply(final @NotNull Style style, final @NotNull SerializationContext ctx) { final V element = this.lens.apply(style); if (element == null || !this.filter.test(element)) return null; - return emitter -> this.emitable.accept(element, emitter); + return emitter -> this.emittable.emit(element, emitter, ctx); } @Override diff --git a/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/tag/resolver/CachingTagResolver.java b/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/tag/resolver/CachingTagResolver.java index 22fceeef14..dce663c4c1 100644 --- a/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/tag/resolver/CachingTagResolver.java +++ b/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/tag/resolver/CachingTagResolver.java @@ -27,6 +27,7 @@ import java.util.Map; import java.util.Objects; import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.minimessage.SerializationContext; import net.kyori.adventure.text.minimessage.internal.serializer.ClaimConsumer; import net.kyori.adventure.text.minimessage.internal.serializer.SerializableResolver; import net.kyori.adventure.text.minimessage.tag.Inserting; @@ -75,8 +76,13 @@ public boolean contributeToMap(final @NotNull Map map) { @Override public void handle(final @NotNull Component serializable, final @NotNull ClaimConsumer consumer) { + throw new IllegalStateException("TagResolver#has(String) should not be called if TagResolver#has(String,SerializationContext) is present!"); + } + + @Override + public void handle(final @NotNull Component serializable, final @NotNull ClaimConsumer consumer, final @NotNull SerializationContext ctx) { if (this.resolver instanceof SerializableResolver) { - ((SerializableResolver) this.resolver).handle(serializable, consumer); + ((SerializableResolver) this.resolver).handle(serializable, consumer, ctx); } } diff --git a/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/tag/resolver/SequentialTagResolver.java b/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/tag/resolver/SequentialTagResolver.java index ae7e5a0456..871ea00981 100644 --- a/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/tag/resolver/SequentialTagResolver.java +++ b/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/tag/resolver/SequentialTagResolver.java @@ -27,6 +27,7 @@ import net.kyori.adventure.text.Component; import net.kyori.adventure.text.minimessage.Context; import net.kyori.adventure.text.minimessage.ParsingException; +import net.kyori.adventure.text.minimessage.SerializationContext; import net.kyori.adventure.text.minimessage.internal.serializer.ClaimConsumer; import net.kyori.adventure.text.minimessage.internal.serializer.SerializableResolver; import net.kyori.adventure.text.minimessage.tag.Tag; @@ -74,8 +75,13 @@ final class SequentialTagResolver implements TagResolver, SerializableResolver { @Override public boolean has(final @NotNull String name) { + throw new IllegalStateException("TagResolver#has(String) should not be called if TagResolver#has(String,SerializationContext) is present!"); + } + + @Override + public boolean has(final @NotNull String name, final SerializationContext ctx) { for (final TagResolver resolver : this.resolvers) { - if (resolver.has(name)) { + if (resolver.has(name, ctx)) { return true; } } @@ -84,9 +90,14 @@ public boolean has(final @NotNull String name) { @Override public void handle(final @NotNull Component serializable, final @NotNull ClaimConsumer consumer) { + throw new IllegalStateException("TagResolver#has(String) should not be called if TagResolver#has(String,SerializationContext) is present!"); + } + + @Override + public void handle(final @NotNull Component serializable, final @NotNull ClaimConsumer consumer, final @NotNull SerializationContext ctx) { for (final TagResolver resolver : this.resolvers) { if (resolver instanceof SerializableResolver) { - ((SerializableResolver) resolver).handle(serializable, consumer); + ((SerializableResolver) resolver).handle(serializable, consumer, ctx); } } } diff --git a/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/tag/resolver/TagResolver.java b/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/tag/resolver/TagResolver.java index c3a3327aa1..fdd697ad8a 100644 --- a/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/tag/resolver/TagResolver.java +++ b/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/tag/resolver/TagResolver.java @@ -31,6 +31,7 @@ import java.util.function.BiFunction; import java.util.stream.Collector; import net.kyori.adventure.text.minimessage.Context; +import net.kyori.adventure.text.minimessage.SerializationContext; import net.kyori.adventure.text.minimessage.ParsingException; import net.kyori.adventure.text.minimessage.internal.TagInternals; import net.kyori.adventure.text.minimessage.tag.Tag; @@ -234,6 +235,19 @@ public boolean has(final @NotNull String name) { */ boolean has(final @NotNull String name); + /** + * Get whether this resolver handles tags with a certain name. + * + *

This does not allow validating arguments.

+ * + * @param name the tag name + * @return whether this resolver has a tag with this name + * @since 4.10.0 + */ + default boolean has(final @NotNull String name, final SerializationContext context) { + return has(name); + } + /** * A resolver that only handles a single tag key. * diff --git a/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/tag/standard/ColorTagResolver.java b/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/tag/standard/ColorTagResolver.java index 7cb534b639..03583a67dd 100644 --- a/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/tag/standard/ColorTagResolver.java +++ b/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/tag/standard/ColorTagResolver.java @@ -23,13 +23,13 @@ */ package net.kyori.adventure.text.minimessage.tag.standard; -import java.util.HashMap; import java.util.Map; import net.kyori.adventure.text.format.NamedTextColor; import net.kyori.adventure.text.format.Style; import net.kyori.adventure.text.format.TextColor; import net.kyori.adventure.text.minimessage.Context; import net.kyori.adventure.text.minimessage.ParsingException; +import net.kyori.adventure.text.minimessage.SerializationContext; import net.kyori.adventure.text.minimessage.internal.serializer.SerializableResolver; import net.kyori.adventure.text.minimessage.internal.serializer.StyleClaim; import net.kyori.adventure.text.minimessage.tag.Tag; @@ -49,22 +49,17 @@ final class ColorTagResolver implements TagResolver, SerializableResolver.Single private static final String COLOR = "color"; static final TagResolver INSTANCE = new ColorTagResolver(); - private static final StyleClaim STYLE = StyleClaim.claim(COLOR, Style::color, (color, emitter) -> { - // TODO: custom aliases + private static final StyleClaim STYLE = StyleClaim.claim(COLOR, Style::color, (color, emitter, ctx) -> { // TODO: compact vs expanded format? COLOR vs color:COLOR vs c:COLOR - if (color instanceof NamedTextColor) { - emitter.tag(NamedTextColor.NAMES.key((NamedTextColor) color)); - } else { - emitter.tag(color.asHexString()); + for (final Map.Entry entry : ctx.namedColors().entrySet()) { + if (entry.getValue().equals(color)) { + emitter.tag(entry.getKey()); + return; + } } - }); - private static final Map COLOR_ALIASES = new HashMap<>(); - - static { - COLOR_ALIASES.put("dark_grey", NamedTextColor.DARK_GRAY); - COLOR_ALIASES.put("grey", NamedTextColor.GRAY); - } + emitter.tag(color.asHexString()); + }); private static boolean isColorOrAbbreviation(final String name) { return name.equals(COLOR) || name.equals(COLOR_2) || name.equals(COLOR_3); @@ -75,7 +70,7 @@ private static boolean isColorOrAbbreviation(final String name) { @Override public @Nullable Tag resolve(final @NotNull String name, final @NotNull ArgumentQueue args, final @NotNull Context ctx) throws ParsingException { - if (!this.has(name)) { + if (!this.has(name, ctx)) { return null; } @@ -90,21 +85,19 @@ private static boolean isColorOrAbbreviation(final String name) { return Tag.styling(color); } - static @Nullable TextColor resolveColorOrNull(final String colorName) { + static @Nullable TextColor resolveColorOrNull(final String colorName, final SerializationContext ctx) { final TextColor color; - if (COLOR_ALIASES.containsKey(colorName)) { - color = COLOR_ALIASES.get(colorName); - } else if (colorName.charAt(0) == TextColor.HEX_CHARACTER) { + if (colorName.charAt(0) == TextColor.HEX_CHARACTER) { color = TextColor.fromHexString(colorName); } else { - color = NamedTextColor.NAMES.value(colorName); + color = ctx.namedColor(colorName); } return color; } static @NotNull TextColor resolveColor(final @NotNull String colorName, final @NotNull Context ctx) throws ParsingException { - final TextColor color = resolveColorOrNull(colorName); + final TextColor color = resolveColorOrNull(colorName, ctx); if (color == null) { throw ctx.newException(String.format("Unable to parse a color from '%s'. Please use named colours or hex (#RRGGBB) colors.", colorName)); } @@ -113,9 +106,15 @@ private static boolean isColorOrAbbreviation(final String name) { @Override public boolean has(final @NotNull String name) { + // This method should never be called. If it does, that's a bug. + throw new IllegalStateException("TagResolver#has(String) should not be called if TagResolver#has(String,SerializationContext) is present!"); + } + + @Override + public boolean has(final @NotNull String name, final SerializationContext ctx) { return isColorOrAbbreviation(name) || NamedTextColor.NAMES.value(name) != null - || COLOR_ALIASES.containsKey(name) + || ctx.namedColor(name) != null || TextColor.fromHexString(name) != null; } diff --git a/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/tag/standard/GradientTag.java b/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/tag/standard/GradientTag.java index 1e85d1f176..c43cdd78af 100644 --- a/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/tag/standard/GradientTag.java +++ b/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/tag/standard/GradientTag.java @@ -27,6 +27,7 @@ import java.util.Arrays; import java.util.Collections; import java.util.List; +import java.util.Map; import java.util.Objects; import java.util.OptionalDouble; import java.util.function.Consumer; @@ -75,7 +76,7 @@ static Tag create(final ArgumentQueue args, final Context ctx) { // Determine if this is a color first. Double#parseDouble is "slow" in cases where we hit a string. final String argValue = arg.value(); - final TextColor color = ColorTagResolver.resolveColorOrNull(argValue); + final TextColor color = ColorTagResolver.resolveColorOrNull(argValue, ctx); if (color != null) { textColors.add(color); diff --git a/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/tag/standard/TransitionTag.java b/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/tag/standard/TransitionTag.java index b949dc1e58..5458284f0b 100644 --- a/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/tag/standard/TransitionTag.java +++ b/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/tag/standard/TransitionTag.java @@ -27,6 +27,7 @@ import java.util.Arrays; import java.util.Collections; import java.util.List; +import java.util.Map; import java.util.Objects; import java.util.OptionalDouble; import java.util.stream.Stream; @@ -65,7 +66,7 @@ static Tag create(final ArgumentQueue args, final Context ctx) { // Determine if this is a color first. Double#parseDouble is "slow" in cases where we hit a string. final String argValue = arg.value(); - final TextColor color = ColorTagResolver.resolveColorOrNull(argValue); + final TextColor color = ColorTagResolver.resolveColorOrNull(argValue, ctx); if (color != null) { textColors.add(color); diff --git a/text-minimessage/src/test/java/net/kyori/adventure/text/minimessage/SerializerCollectorTest.java b/text-minimessage/src/test/java/net/kyori/adventure/text/minimessage/SerializerCollectorTest.java index da894655d9..3db579a4a5 100644 --- a/text-minimessage/src/test/java/net/kyori/adventure/text/minimessage/SerializerCollectorTest.java +++ b/text-minimessage/src/test/java/net/kyori/adventure/text/minimessage/SerializerCollectorTest.java @@ -89,8 +89,12 @@ void testPopMultiLevelMark() { } String serializeToString(final Consumer handler) { + return serializeToString(handler, MiniMessage.miniMessage()); + } + + String serializeToString(final Consumer handler, SerializationContext ctx) { final StringBuilder output = new StringBuilder(); - final MiniMessageSerializer.Collector collector = new MiniMessageSerializer.Collector((SerializableResolver) StandardTags.defaults(), false, output); + final MiniMessageSerializer.Collector collector = new MiniMessageSerializer.Collector((SerializableResolver) StandardTags.defaults(), false, output, ctx); handler.accept(collector); return output.toString(); diff --git a/text-minimessage/src/test/java/net/kyori/adventure/text/minimessage/tag/standard/ColorTagTest.java b/text-minimessage/src/test/java/net/kyori/adventure/text/minimessage/tag/standard/ColorTagTest.java index bfe9abed2f..a42023e285 100644 --- a/text-minimessage/src/test/java/net/kyori/adventure/text/minimessage/tag/standard/ColorTagTest.java +++ b/text-minimessage/src/test/java/net/kyori/adventure/text/minimessage/tag/standard/ColorTagTest.java @@ -119,7 +119,7 @@ void test() { void testBritish() { final String input1 = "This is english"; // no it's british final String input2 = "This is english"; - final String input3 = "This is still english"; // British is superior english + final String input3 = "This is still english"; // British is fake english final String input4 = "This is still english"; final Component out1 = PARSER.deserialize(input1); final Component out2 = PARSER.deserialize(input2); diff --git a/text-minimessage/src/test/java/net/kyori/adventure/text/minimessage/tag/standard/DecorationTagTest.java b/text-minimessage/src/test/java/net/kyori/adventure/text/minimessage/tag/standard/DecorationTagTest.java index eb6716d00b..779ff3f8e8 100644 --- a/text-minimessage/src/test/java/net/kyori/adventure/text/minimessage/tag/standard/DecorationTagTest.java +++ b/text-minimessage/src/test/java/net/kyori/adventure/text/minimessage/tag/standard/DecorationTagTest.java @@ -27,6 +27,7 @@ import net.kyori.adventure.text.TextComponent; import net.kyori.adventure.text.format.TextDecoration; import net.kyori.adventure.text.minimessage.AbstractTest; +import net.kyori.adventure.text.minimessage.MiniMessage; import net.kyori.adventure.text.minimessage.tag.resolver.TagResolver; import org.junit.jupiter.api.Test; @@ -42,7 +43,7 @@ class DecorationTagTest extends AbstractTest { void testCompleteness() { final TagResolver decorations = StandardTags.decorations(); for (final String key : TextDecoration.NAMES.keys()) { - assertTrue(decorations.has(key), () -> "missing " + key); + assertTrue(decorations.has(key, MiniMessage.miniMessage()), () -> "missing " + key); } } From 11af1930c8e9291599d93ba7216eefa9e9af2727 Mon Sep 17 00:00:00 2001 From: Strokkur24 Date: Sat, 27 Sep 2025 18:59:37 +0200 Subject: [PATCH 2/4] chore: add tests --- .../MiniMessageSerializerTest.java | 13 +++++++++++++ .../tag/standard/GradientTagTest.java | 19 +++++++++++++++++++ .../tag/standard/TransitionTagTest.java | 13 +++++++++++++ 3 files changed, 45 insertions(+) diff --git a/text-minimessage/src/test/java/net/kyori/adventure/text/minimessage/MiniMessageSerializerTest.java b/text-minimessage/src/test/java/net/kyori/adventure/text/minimessage/MiniMessageSerializerTest.java index 9aaee7dbf5..526f4ea42a 100644 --- a/text-minimessage/src/test/java/net/kyori/adventure/text/minimessage/MiniMessageSerializerTest.java +++ b/text-minimessage/src/test/java/net/kyori/adventure/text/minimessage/MiniMessageSerializerTest.java @@ -26,6 +26,7 @@ import net.kyori.adventure.key.Key; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.format.NamedTextColor; +import net.kyori.adventure.text.format.TextColor; import net.kyori.adventure.text.format.TextDecoration; import org.junit.jupiter.api.Test; @@ -78,4 +79,16 @@ void testDoubleOpenRoundTrippedEscaped() { this.assertParsedEquals(component, expected); } + @Test + void testCustomColors() { + final MiniMessage mm = MiniMessage.builder() + .namedColor("orange", TextColor.color(0xFFBB00)) + .build(); + + final String input = "Some text!"; + final Component component = Component.text("Some text!", TextColor.color(0xFFBB00)); + + assertEquals(input, mm.serialize(component)); + assertParsedEquals(mm, component, input); + } } diff --git a/text-minimessage/src/test/java/net/kyori/adventure/text/minimessage/tag/standard/GradientTagTest.java b/text-minimessage/src/test/java/net/kyori/adventure/text/minimessage/tag/standard/GradientTagTest.java index f418c5d474..42d1815fab 100644 --- a/text-minimessage/src/test/java/net/kyori/adventure/text/minimessage/tag/standard/GradientTagTest.java +++ b/text-minimessage/src/test/java/net/kyori/adventure/text/minimessage/tag/standard/GradientTagTest.java @@ -23,6 +23,7 @@ */ package net.kyori.adventure.text.minimessage.tag.standard; +import java.util.Objects; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.format.NamedTextColor; import net.kyori.adventure.text.format.TextColor; @@ -48,6 +49,7 @@ import static net.kyori.adventure.text.format.TextColor.color; import static net.kyori.adventure.text.format.TextDecoration.BOLD; import static net.kyori.adventure.text.minimessage.tag.resolver.Placeholder.component; +import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; class GradientTagTest extends AbstractTest { @@ -425,6 +427,23 @@ void testGradientPhase() { this.assertParsedEquals(expected, input); } + @Test + void testGradientWithCustomColors() { + final String ice = "#6BF8FF"; + final String fire = "#C97118"; + + final MiniMessage mm = MiniMessage.builder() + .namedColor("ice", Objects.requireNonNull(TextColor.fromHexString(ice))) + .namedColor("fire", Objects.requireNonNull(TextColor.fromHexString(fire))) + .build(); + + final String withCustomColors = "||||||||||||||||||||||||||||||||"; + final String withHexColors = String.format("||||||||||||||||||||||||||||||||", fire, ice); + + final Component expected = MiniMessage.miniMessage().deserialize(withHexColors); + this.assertParsedEquals(mm, expected, withCustomColors); + } + // see #91 @Test void testGradientWithInnerTokens() { diff --git a/text-minimessage/src/test/java/net/kyori/adventure/text/minimessage/tag/standard/TransitionTagTest.java b/text-minimessage/src/test/java/net/kyori/adventure/text/minimessage/tag/standard/TransitionTagTest.java index b9f83e908e..8ecc1d9de2 100644 --- a/text-minimessage/src/test/java/net/kyori/adventure/text/minimessage/tag/standard/TransitionTagTest.java +++ b/text-minimessage/src/test/java/net/kyori/adventure/text/minimessage/tag/standard/TransitionTagTest.java @@ -26,6 +26,7 @@ import net.kyori.adventure.text.Component; import net.kyori.adventure.text.format.TextColor; import net.kyori.adventure.text.minimessage.AbstractTest; +import net.kyori.adventure.text.minimessage.MiniMessage; import org.junit.jupiter.api.Test; import static net.kyori.adventure.text.Component.text; @@ -45,4 +46,16 @@ void testTransition() { this.assertParsedEquals(expected, input); } } + + @Test + void testTransitionWithCustomColors() { + final MiniMessage mm = MiniMessage.builder() + .namedColor("leadership", TextColor.color(0xFE305A)) + .namedColor("community", TextColor.color(0xD56377)) + .build(); + + final String input = "Is this development team?"; + final Component expected = mm.deserialize("Is this development team?"); + this.assertParsedEquals(mm, expected, input); + } } From a2f8c1b744348026c4e93e4f8b53e5ae0e4ebfaf Mon Sep 17 00:00:00 2001 From: Strokkur24 Date: Sat, 27 Sep 2025 19:39:23 +0200 Subject: [PATCH 3/4] chore: cleanup and spotless apply --- .../net/kyori/adventure/util/RGBLike.java | 8 --- .../net/kyori/adventure/util/RGBLikeImpl.java | 36 ----------- .../text/minimessage/ContextImpl.java | 5 +- .../text/minimessage/MiniMessage.java | 61 +++++++++++++++++-- .../text/minimessage/MiniMessageImpl.java | 30 ++++++--- .../minimessage/SerializationContext.java | 59 +++++++++++++++--- .../serializer/SerializableResolver.java | 14 ++++- .../internal/serializer/StyleClaim.java | 38 +++++++++++- .../minimessage/tag/resolver/TagResolver.java | 8 ++- .../minimessage/tag/standard/GradientTag.java | 1 - .../tag/standard/TransitionTag.java | 1 - .../minimessage/SerializerCollectorTest.java | 4 +- .../tag/standard/GradientTagTest.java | 1 - 13 files changed, 189 insertions(+), 77 deletions(-) delete mode 100644 api/src/main/java/net/kyori/adventure/util/RGBLikeImpl.java diff --git a/api/src/main/java/net/kyori/adventure/util/RGBLike.java b/api/src/main/java/net/kyori/adventure/util/RGBLike.java index 71606e9f05..1ce00147ea 100644 --- a/api/src/main/java/net/kyori/adventure/util/RGBLike.java +++ b/api/src/main/java/net/kyori/adventure/util/RGBLike.java @@ -32,14 +32,6 @@ * @since 4.0.0 */ public interface RGBLike { - static RGBLike of(final @Range(from = 0x0, to = 0xff) int red, final @Range(from = 0x0, to = 0xff) int green, final @Range(from = 0x0, to = 0xff) int blue) { - return new RGBLikeImpl(red, green, blue); - } - - static RGBLike of(final @Range(from = 0x0, to = 0xffffff) int value) { - return new RGBLikeImpl(value); - } - /** * Gets the red component. * diff --git a/api/src/main/java/net/kyori/adventure/util/RGBLikeImpl.java b/api/src/main/java/net/kyori/adventure/util/RGBLikeImpl.java deleted file mode 100644 index a02350a9bf..0000000000 --- a/api/src/main/java/net/kyori/adventure/util/RGBLikeImpl.java +++ /dev/null @@ -1,36 +0,0 @@ -package net.kyori.adventure.util; - -import org.jetbrains.annotations.Range; - -class RGBLikeImpl implements RGBLike { - private final int red; - private final int green; - private final int blue; - - public RGBLikeImpl(final @Range(from = 0x0, to = 0xff) int red, final @Range(from = 0x0, to = 0xff) int green, final @Range(from = 0x0, to = 0xff) int blue) { - this.red = red; - this.green = green; - this.blue = blue; - } - - public RGBLikeImpl(final @Range(from = 0x0, to = 0xffffff) int value) { - this.red = value >> 16 & 0xFF; - this.green = value >> 8 & 0xFF; - this.blue = value & 0xFF; - } - - @Override - public @Range(from = 0x0, to = 0xff) int red() { - return red; - } - - @Override - public @Range(from = 0x0, to = 0xff) int green() { - return green; - } - - @Override - public @Range(from = 0x0, to = 0xff) int blue() { - return blue; - } -} diff --git a/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/ContextImpl.java b/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/ContextImpl.java index f118da6b83..6e39810b76 100644 --- a/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/ContextImpl.java +++ b/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/ContextImpl.java @@ -174,12 +174,12 @@ public UnaryOperator preProcessor() { } @Override - public @NotNull @Unmodifiable Map namedColors() { + public @Unmodifiable @NotNull Map namedColors() { return this.miniMessage.namedColors(); } @Override - public @NotNull @Unmodifiable Map namedColorAliases() { + public @Unmodifiable @NotNull Map namedColorAliases() { return this.miniMessage.namedColorAliases(); } @@ -198,5 +198,4 @@ private static Token[] tagsToTokens(final List tags) { } return tokens; } - } diff --git a/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/MiniMessage.java b/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/MiniMessage.java index 3a36ac4002..f79a72f9d0 100644 --- a/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/MiniMessage.java +++ b/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/MiniMessage.java @@ -334,17 +334,70 @@ interface Builder extends AbstractBuilder { */ @NotNull Builder editTags(final @NotNull Consumer adder); + /** + * Set the known named colors of this MiniMessage instance. + * + * @param colors the colors to use + * @return this builder + * @since 4.26.0 + */ @NotNull Builder namedColors(@NotNull Map colors); - @NotNull Builder namedColor(@NotNull @TagPattern String name, @NotNull TextColor color); + /** + * Add to the set of known named colors of this MiniMessage instance. + * + * @param name the name of the color + * @param color the color + * @return this builder + * @since 4.26.0 + */ + @NotNull Builder namedColor(@TagPattern @NotNull String name, @NotNull TextColor color); - @NotNull Builder removeNamedColor(@NotNull @TagPattern String name); + /** + * Remove from the set of known named colors of this MiniMessage instance. + * + * @param name the name of the color to remove + * @return this builder + * @since 4.26.0 + */ + @NotNull Builder removeNamedColor(@TagPattern @NotNull String name); + /** + * Set the known named color aliases of this MiniMessage instance. + * + *

Named color aliases point to another color name. If the name does not exist, the aliases is silently ignored. + * Aliases not tied to a specific color value and are never serialized.

+ * + * @param aliases the aliases to use + * @return this builder + * @since 4.26.0 + */ @NotNull Builder namedColorAliases(@NotNull Map aliases); - @NotNull Builder namedColorAlias(@NotNull @TagPattern String name, @NotNull @TagPattern String color); + /** + * Add to the set of known named color aliases of this MiniMessage instance. + * + *

Named color aliases point to another color name. If the name does not exist, the aliases is silently ignored. + * Aliases not tied to a specific color value and are never serialized.

+ * + * @param name the name of the color alias + * @param color the name of the named color + * @return this builder + * @since 4.26.0 + */ + @NotNull Builder namedColorAlias(@TagPattern @NotNull String name, @TagPattern @NotNull String color); - @NotNull Builder removeNamedColorAlias(@NotNull @TagPattern String name); + /** + * Remove from the set of known named color aliases of this MiniMessage instance. + * + *

Named color aliases point to another color name. If the name does not exist, the aliases is silently ignored. + * Aliases not tied to a specific color value and are never serialized.

+ * + * @param name the name of the color alias + * @return this builder + * @since 4.26.0 + */ + @NotNull Builder removeNamedColorAlias(@TagPattern @NotNull String name); /** * Enables strict mode (disabled by default). diff --git a/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/MiniMessageImpl.java b/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/MiniMessageImpl.java index 67995d51cb..8e519e72ca 100644 --- a/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/MiniMessageImpl.java +++ b/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/MiniMessageImpl.java @@ -35,10 +35,12 @@ import net.kyori.adventure.text.Component; import net.kyori.adventure.text.format.NamedTextColor; import net.kyori.adventure.text.format.TextColor; +import net.kyori.adventure.text.minimessage.internal.TagInternals; import net.kyori.adventure.text.minimessage.internal.serializer.SerializableResolver; import net.kyori.adventure.text.minimessage.tag.resolver.TagResolver; import net.kyori.adventure.text.minimessage.tree.Node; import net.kyori.adventure.util.Services; +import org.intellij.lang.annotations.Subst; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Unmodifiable; @@ -84,8 +86,8 @@ static final class Instances { this.debugOutput = debugOutput; this.preProcessor = preProcessor; this.postProcessor = postProcessor; - this.namedColors = namedColors; - this.namedColorAliases = namedColorAliases; + this.namedColors = Collections.unmodifiableMap(namedColors); + this.namedColorAliases = Collections.unmodifiableMap(namedColorAliases); } private static @NotNull Map defaultNamedColors() { @@ -180,13 +182,13 @@ private SerializableResolver serialResolver(final @Nullable TagResolver extraRes } @Override - public @NotNull @Unmodifiable Map namedColors() { - return Collections.unmodifiableMap(this.namedColors); + public @Unmodifiable @NotNull Map namedColors() { + return this.namedColors; } @Override - public @NotNull @Unmodifiable Map namedColorAliases() { - return Collections.unmodifiableMap(this.namedColorAliases); + public @Unmodifiable @NotNull Map namedColorAliases() { + return this.namedColorAliases; } @Override @@ -244,6 +246,9 @@ static final class BuilderImpl implements Builder { @Override public @NotNull Builder namedColors(final @NotNull Map colors) { + for (final @Subst("color") String name : colors.keySet()) { + TagInternals.assertValidTagName(name); + } this.namedColors.clear(); this.namedColors.putAll(colors); return this; @@ -251,31 +256,38 @@ static final class BuilderImpl implements Builder { @Override public @NotNull Builder namedColor(final @NotNull String name, final @NotNull TextColor color) { + TagInternals.assertValidTagName(name); this.namedColors.put(name, color); return this; } @Override public @NotNull Builder removeNamedColor(final @NotNull String name) { + TagInternals.assertValidTagName(name); this.namedColors.remove(name); return this; } @Override - public @NotNull Builder namedColorAliases(@NotNull final Map aliases) { + public @NotNull Builder namedColorAliases(final @NotNull Map aliases) { + for (final @Subst("color") String name : aliases.keySet()) { + TagInternals.assertValidTagName(name); + } this.namedColorAliases.clear(); this.namedColorAliases.putAll(aliases); return this; } @Override - public @NotNull Builder namedColorAlias(@NotNull final String name, @NotNull final String color) { + public @NotNull Builder namedColorAlias(final @NotNull String name, final @NotNull String color) { + TagInternals.assertValidTagName(name); this.namedColorAliases.put(name, color); return this; } @Override - public @NotNull Builder removeNamedColorAlias(@NotNull final String name) { + public @NotNull Builder removeNamedColorAlias(final @NotNull String name) { + TagInternals.assertValidTagName(name); this.namedColorAliases.remove(name); return this; } diff --git a/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/SerializationContext.java b/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/SerializationContext.java index a4d81baade..2da60a9c2d 100644 --- a/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/SerializationContext.java +++ b/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/SerializationContext.java @@ -1,3 +1,26 @@ +/* + * This file is part of adventure, licensed under the MIT License. + * + * Copyright (c) 2017-2025 KyoriPowered + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ package net.kyori.adventure.text.minimessage; import java.util.Map; @@ -7,25 +30,47 @@ import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Unmodifiable; +/** + * A context which is used by serialization and deserialization of MiniMessage strings. + * + * @since 4.26.0 + */ @ApiStatus.NonExtendable public interface SerializationContext { - @NotNull + /** + * Get a map of all named colors and their {@link TextColor} instances. + * + * @return map of all named colors and their {@link TextColor} instances. + * @since 4.26.0 + */ @Unmodifiable - Map namedColors(); + @NotNull Map namedColors(); - @NotNull + /** + * Get a map of all named color aliases and the named colors they point to. + * + * @return map of all named color aliases and the named colors they point to. + * @since 4.26.0 + */ @Unmodifiable - Map namedColorAliases(); + @NotNull Map namedColorAliases(); + /** + * Get the {@link TextColor} of a registered color name. This method also resolves aliases. + * + * @param name the name of the color to retrieve + * @return the retrieved color, or null if none found + * @since 4.26.0 + */ default @Nullable TextColor namedColor(final @NotNull String name) { - final TextColor color = namedColors().get(name); + final TextColor color = this.namedColors().get(name); if (color != null) { return color; } - final String alias = namedColorAliases().get(name); + final String alias = this.namedColorAliases().get(name); if (alias != null) { - return namedColor(alias); + return this.namedColor(alias); } return null; diff --git a/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/internal/serializer/SerializableResolver.java b/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/internal/serializer/SerializableResolver.java index 5ec0565ddb..6fde1b567c 100644 --- a/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/internal/serializer/SerializableResolver.java +++ b/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/internal/serializer/SerializableResolver.java @@ -30,12 +30,13 @@ import java.util.function.Function; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.minimessage.Context; -import net.kyori.adventure.text.minimessage.SerializationContext; import net.kyori.adventure.text.minimessage.ParsingException; +import net.kyori.adventure.text.minimessage.SerializationContext; import net.kyori.adventure.text.minimessage.internal.TagInternals; import net.kyori.adventure.text.minimessage.tag.Tag; import net.kyori.adventure.text.minimessage.tag.resolver.ArgumentQueue; import net.kyori.adventure.text.minimessage.tag.resolver.TagResolver; +import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -112,12 +113,23 @@ public interface SerializableResolver { /** * Attempt to process a component for serialization. * + *

This method should never be called directly but is safe to override.

+ * * @param serializable the component to serialize * @param consumer a consumer for component claims, must not be stored * @since 4.10.0 */ + @ApiStatus.Obsolete void handle(final @NotNull Component serializable, final @NotNull ClaimConsumer consumer); + /** + * Attempt to process a component for serialization. + * + * @param serializable the component to serialize + * @param consumer a consumer for component claims, must not be stored + * @param serializationContext the serialisation context + * @since 4.26.0 + */ default void handle(final @NotNull Component serializable, final @NotNull ClaimConsumer consumer, final @NotNull SerializationContext serializationContext) { this.handle(serializable, consumer); } diff --git a/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/internal/serializer/StyleClaim.java b/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/internal/serializer/StyleClaim.java index b3dc3b20e8..3cb578b240 100644 --- a/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/internal/serializer/StyleClaim.java +++ b/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/internal/serializer/StyleClaim.java @@ -54,6 +54,16 @@ public interface StyleClaim { return claim(claimKey, lens, $ -> true, emitable); } + /** + * Create a new style claim that will emit content for any non-null value. + * + * @param the value type + * @param claimKey claim key for de-duplication + * @param lens value extractor from a {@link Style} instance + * @param emitable the function that handles emitting + * @return a new claim + * @since 4.26.0 + */ static @NotNull StyleClaim claim(final @NotNull String claimKey, final @NotNull Function lens, final @NotNull ContextAwareEmittable emitable) { return claim(claimKey, lens, $ -> true, emitable); } @@ -73,6 +83,17 @@ public interface StyleClaim { return claim(claimKey, lens, filter, (type, emitter, ctx) -> emitable.accept(type, emitter)); } + /** + * Create a new style claim that will emit content for any non-null value that passes the filter. + * + * @param the value type + * @param claimKey claim key for de-duplication + * @param lens value extractor from a {@link Style} instance + * @param filter a filter for values, will only receive non-null values + * @param emitable the function that handles emitting + * @return a new claim + * @since 4.26.0 + */ static @NotNull StyleClaim claim(final @NotNull String claimKey, final @NotNull Function lens, final @NotNull Predicate filter, final @NotNull ContextAwareEmittable emitable) { return new StyleClaimImpl<>( requireNonNull(claimKey, "claimKey"), @@ -94,13 +115,28 @@ public interface StyleClaim { * Prepare an emitable to apply this claim based on the style. * * @param style the style to test + * @param ctx the serialization context * @return an emitable for this style claim, if it is applicable to the provided style - * @since 4.10.0 + * @since 4.26.0 */ @Nullable Emitable apply(final @NotNull Style style, final @NotNull SerializationContext ctx); + /** + * A context aware emittable function. + * + * @param the value type + * @since 4.26.0 + */ @FunctionalInterface interface ContextAwareEmittable { + /** + * A method which handles emitting. + * + * @param type the value + * @param emitter the emitter + * @param ctx the serialization context + * @since 4.26.0 + */ void emit(T type, TokenEmitter emitter, SerializationContext ctx); } } diff --git a/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/tag/resolver/TagResolver.java b/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/tag/resolver/TagResolver.java index fdd697ad8a..c50ea5d7b4 100644 --- a/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/tag/resolver/TagResolver.java +++ b/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/tag/resolver/TagResolver.java @@ -31,8 +31,8 @@ import java.util.function.BiFunction; import java.util.stream.Collector; import net.kyori.adventure.text.minimessage.Context; -import net.kyori.adventure.text.minimessage.SerializationContext; import net.kyori.adventure.text.minimessage.ParsingException; +import net.kyori.adventure.text.minimessage.SerializationContext; import net.kyori.adventure.text.minimessage.internal.TagInternals; import net.kyori.adventure.text.minimessage.tag.Tag; import net.kyori.adventure.text.minimessage.tag.TagPattern; @@ -233,6 +233,7 @@ public boolean has(final @NotNull String name) { * @return whether this resolver has a tag with this name * @since 4.10.0 */ + @ApiStatus.Obsolete boolean has(final @NotNull String name); /** @@ -241,11 +242,12 @@ public boolean has(final @NotNull String name) { *

This does not allow validating arguments.

* * @param name the tag name + * @param context the serialization context * @return whether this resolver has a tag with this name - * @since 4.10.0 + * @since 4.26.0 */ default boolean has(final @NotNull String name, final SerializationContext context) { - return has(name); + return this.has(name); } /** diff --git a/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/tag/standard/GradientTag.java b/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/tag/standard/GradientTag.java index c43cdd78af..6f7128237b 100644 --- a/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/tag/standard/GradientTag.java +++ b/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/tag/standard/GradientTag.java @@ -27,7 +27,6 @@ import java.util.Arrays; import java.util.Collections; import java.util.List; -import java.util.Map; import java.util.Objects; import java.util.OptionalDouble; import java.util.function.Consumer; diff --git a/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/tag/standard/TransitionTag.java b/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/tag/standard/TransitionTag.java index 5458284f0b..d7499183c4 100644 --- a/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/tag/standard/TransitionTag.java +++ b/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/tag/standard/TransitionTag.java @@ -27,7 +27,6 @@ import java.util.Arrays; import java.util.Collections; import java.util.List; -import java.util.Map; import java.util.Objects; import java.util.OptionalDouble; import java.util.stream.Stream; diff --git a/text-minimessage/src/test/java/net/kyori/adventure/text/minimessage/SerializerCollectorTest.java b/text-minimessage/src/test/java/net/kyori/adventure/text/minimessage/SerializerCollectorTest.java index 3db579a4a5..08cc3d11d7 100644 --- a/text-minimessage/src/test/java/net/kyori/adventure/text/minimessage/SerializerCollectorTest.java +++ b/text-minimessage/src/test/java/net/kyori/adventure/text/minimessage/SerializerCollectorTest.java @@ -89,10 +89,10 @@ void testPopMultiLevelMark() { } String serializeToString(final Consumer handler) { - return serializeToString(handler, MiniMessage.miniMessage()); + return this.serializeToString(handler, MiniMessage.miniMessage()); } - String serializeToString(final Consumer handler, SerializationContext ctx) { + String serializeToString(final Consumer handler, final SerializationContext ctx) { final StringBuilder output = new StringBuilder(); final MiniMessageSerializer.Collector collector = new MiniMessageSerializer.Collector((SerializableResolver) StandardTags.defaults(), false, output, ctx); handler.accept(collector); diff --git a/text-minimessage/src/test/java/net/kyori/adventure/text/minimessage/tag/standard/GradientTagTest.java b/text-minimessage/src/test/java/net/kyori/adventure/text/minimessage/tag/standard/GradientTagTest.java index 42d1815fab..c187837f3b 100644 --- a/text-minimessage/src/test/java/net/kyori/adventure/text/minimessage/tag/standard/GradientTagTest.java +++ b/text-minimessage/src/test/java/net/kyori/adventure/text/minimessage/tag/standard/GradientTagTest.java @@ -49,7 +49,6 @@ import static net.kyori.adventure.text.format.TextColor.color; import static net.kyori.adventure.text.format.TextDecoration.BOLD; import static net.kyori.adventure.text.minimessage.tag.resolver.Placeholder.component; -import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; class GradientTagTest extends AbstractTest { From 1e1dd13971c6628d1552e362a9734c106fdbc0e5 Mon Sep 17 00:00:00 2001 From: Strokkur24 Date: Sat, 27 Sep 2025 19:43:59 +0200 Subject: [PATCH 4/4] chore: add note about using TagResolver#has without serialization context parameter --- .../adventure/text/minimessage/tag/resolver/TagResolver.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/tag/resolver/TagResolver.java b/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/tag/resolver/TagResolver.java index c50ea5d7b4..63a978d594 100644 --- a/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/tag/resolver/TagResolver.java +++ b/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/tag/resolver/TagResolver.java @@ -229,6 +229,8 @@ public boolean has(final @NotNull String name) { * *

This does not allow validating arguments.

* + *

This method should never be called directly but is safe to override.

+ * * @param name the tag name * @return whether this resolver has a tag with this name * @since 4.10.0