From 2be39cc96046da901e091ffc7697adef8c243792 Mon Sep 17 00:00:00 2001 From: Strokkur24 Date: Wed, 17 Sep 2025 23:46:39 +0200 Subject: [PATCH 01/19] feat: start work on named arguments in tags --- .../adventure/text/minimessage/Context.java | 7 +++ .../text/minimessage/ContextImpl.java | 25 ++++++-- .../minimessage/NamedArgumentMapImpl.java | 58 +++++++++++++++++++ .../internal/parser/TokenParser.java | 8 +-- .../internal/parser/node/NamedTagPart.java | 25 ++++++++ .../internal/parser/node/TagNode.java | 8 +-- .../internal/parser/node/TagPart.java | 2 +- .../adventure/text/minimessage/tag/Tag.java | 4 ++ .../tag/resolver/NamedArgumentMap.java | 48 +++++++++++++++ .../tag/resolver/SequentialTagResolver.java | 38 ++++++++++++ .../minimessage/tag/resolver/TagResolver.java | 52 ++++++++++++++++- 11 files changed, 260 insertions(+), 15 deletions(-) create mode 100644 text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/NamedArgumentMapImpl.java create mode 100644 text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/internal/parser/node/NamedTagPart.java create mode 100644 text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/tag/resolver/NamedArgumentMap.java 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..b304640dd1 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 @@ -26,6 +26,7 @@ import net.kyori.adventure.pointer.Pointered; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.minimessage.tag.resolver.ArgumentQueue; +import net.kyori.adventure.text.minimessage.tag.resolver.NamedArgumentMap; import net.kyori.adventure.text.minimessage.tag.resolver.TagResolver; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; @@ -141,6 +142,12 @@ public interface Context { final @NotNull ArgumentQueue args ); + @NotNull ParsingException newException( + final @NotNull String message, + final @Nullable Throwable cause, + final @NotNull NamedArgumentMap args + ); + /** * Dictates if transformations may emit virtual components or not. * 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..14b54a2c7a 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 @@ -23,9 +23,6 @@ */ package net.kyori.adventure.text.minimessage; -import java.util.List; -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.minimessage.internal.parser.ParsingExceptionImpl; @@ -33,10 +30,16 @@ import net.kyori.adventure.text.minimessage.internal.parser.node.TagPart; 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.NamedArgumentMap; import net.kyori.adventure.text.minimessage.tag.resolver.TagResolver; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; +import java.util.List; +import java.util.Map; +import java.util.function.Consumer; +import java.util.function.UnaryOperator; + import static java.util.Objects.requireNonNull; /** @@ -149,7 +152,7 @@ public UnaryOperator preProcessor() { } @Override - public @NotNull Component deserialize(final @NotNull String message, final @NotNull TagResolver@NotNull... resolvers) { + public @NotNull Component deserialize(final @NotNull String message, final @NotNull TagResolver @NotNull ... resolvers) { requireNonNull(message, "message"); final TagResolver combinedResolver = TagResolver.builder().resolver(this.tagResolver).resolvers(resolvers).build(); return this.deserializeWithOptionalTarget(message, combinedResolver); @@ -170,6 +173,11 @@ public UnaryOperator preProcessor() { return new ParsingExceptionImpl(message, this.message, cause, false, tagsToTokens(((ArgumentQueueImpl) tags).args)); } + @Override + public @NotNull ParsingException newException(final @NotNull String message, final @Nullable Throwable cause, final @NotNull NamedArgumentMap args) { + return new ParsingExceptionImpl(message, this.message, cause, false, tagsToTokens(((NamedArgumentMapImpl) args).args)); + } + private @NotNull Component deserializeWithOptionalTarget(final @NotNull String message, final @NotNull TagResolver tagResolver) { if (this.target != null) { return this.miniMessage.deserialize(message, this.target, tagResolver); @@ -186,4 +194,13 @@ private static Token[] tagsToTokens(final List tags) { return tokens; } + private static Token[] tagsToTokens(final Map tags) { + final Token[] tokens = new Token[tags.size()]; + + int index = 0; + for (final Tag.Argument value : tags.values()) { + tokens[index++] = ((TagPart) value).token(); + } + return tokens; + } } diff --git a/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/NamedArgumentMapImpl.java b/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/NamedArgumentMapImpl.java new file mode 100644 index 0000000000..5fe13201bc --- /dev/null +++ b/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/NamedArgumentMapImpl.java @@ -0,0 +1,58 @@ +package net.kyori.adventure.text.minimessage; + +import net.kyori.adventure.text.minimessage.tag.Tag; +import net.kyori.adventure.text.minimessage.tag.resolver.NamedArgumentMap; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.Map; +import java.util.function.Supplier; + +import static java.util.Objects.requireNonNull; + +final class NamedArgumentMapImpl implements NamedArgumentMap { + private final Context context; + final Map args; + + public NamedArgumentMapImpl(final Context context, final Map args) { + this.context = context; + this.args = args; + } + + @Override + public boolean isPresent(final @NotNull String name) { + requireNonNull(name, "name"); + return this.args.containsKey(name); + } + + @Override + public int size() { + return this.args.size(); + } + + @Override + public Tag.@Nullable Argument get(final @NotNull String name) { + requireNonNull(name, "name"); + return this.args.get(name); + } + + @Override + public Tag.@NotNull Argument getOrThrow(@NotNull String name, final @NotNull String errorMessage) { + requireNonNull(errorMessage, "errorMessage"); + final Tag.Argument arg = get(name); + if (arg == null) { + throw this.context.newException(errorMessage); + } + return arg; + } + + @Override + public Tag.@NotNull Argument getOrThrow(@NotNull String name, final @NotNull Supplier errorMessage) { + requireNonNull(errorMessage, "errorMessage"); + final Tag.Argument arg = get(name); + if (arg == null) { + throw this.context.newException(errorMessage.get()); + } + return arg; + } +} diff --git a/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/internal/parser/TokenParser.java b/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/internal/parser/TokenParser.java index eabbf1b09f..eec960cedd 100644 --- a/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/internal/parser/TokenParser.java +++ b/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/internal/parser/TokenParser.java @@ -547,7 +547,7 @@ private static RootNode buildTree( * @param openParts The parts of the open tag * @return {@code true} if the given close parts closes the open tag parts. */ - private static boolean tagCloses(final List closeParts, final List openParts) { + private static boolean tagCloses(final List closeParts, final List openParts) { if (closeParts.size() > openParts.size()) { return false; } @@ -664,7 +664,7 @@ public static String unescape(final String text, final int startIndex, final int * @since 4.10.0 */ @ApiStatus.Internal - public interface TagProvider { + public interface TagProvider { /** * Look up a tag. * @@ -676,7 +676,7 @@ public interface TagProvider { * @return a tag * @since 4.10.0 */ - @Nullable Tag resolve(final @NotNull String name, final @NotNull List trimmedArgs, final @Nullable Token token); + @Nullable Tag resolve(final @NotNull String name, final @NotNull List trimmedArgs, final @Nullable Token token); /** * Resolve by sanitized name. @@ -699,7 +699,7 @@ public interface TagProvider { default @Nullable Tag resolve(final @NotNull TagNode node) { return this.resolve( sanitizePlaceholderName(node.name()), - node.parts().subList(1, node.parts().size()), + (List) node.parts().subList(1, node.parts().size()), node.token() ); } diff --git a/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/internal/parser/node/NamedTagPart.java b/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/internal/parser/node/NamedTagPart.java new file mode 100644 index 0000000000..ae22040eee --- /dev/null +++ b/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/internal/parser/node/NamedTagPart.java @@ -0,0 +1,25 @@ +package net.kyori.adventure.text.minimessage.internal.parser.node; + +import net.kyori.adventure.text.minimessage.internal.parser.Token; +import net.kyori.adventure.text.minimessage.internal.parser.TokenParser; +import net.kyori.adventure.text.minimessage.tag.Tag; +import org.jetbrains.annotations.NotNull; + +public final class NamedTagPart extends TagPart implements Tag.NamedArgument { + private final String name; + + public NamedTagPart( + final @NotNull String sourceMessage, + final @NotNull Token token, + final TokenParser.@NotNull TagProvider tagResolver, + final String name + ) { + super(sourceMessage, token, tagResolver); + this.name = name; + } + + @Override + public @NotNull String name() { + return this.name; + } +} diff --git a/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/internal/parser/node/TagNode.java b/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/internal/parser/node/TagNode.java index 7dd5e63069..58b6dd3c6a 100644 --- a/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/internal/parser/node/TagNode.java +++ b/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/internal/parser/node/TagNode.java @@ -38,8 +38,8 @@ * * @since 4.10.0 */ -public final class TagNode extends ElementNode { - private final List parts; +public final class TagNode extends ElementNode { + private final List parts; private @Nullable Tag tag = null; /** @@ -66,12 +66,12 @@ public TagNode( } } - private static @NotNull List genParts( + private static @NotNull List genParts( final @NotNull Token token, final @NotNull String sourceMessage, final TokenParser.@NotNull TagProvider tagProvider ) { - final ArrayList parts = new ArrayList<>(); + final ArrayList parts = new ArrayList<>(); if (token.childTokens() != null) { for (final Token childToken : token.childTokens()) { diff --git a/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/internal/parser/node/TagPart.java b/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/internal/parser/node/TagPart.java index 4800426e2c..95efbbd220 100644 --- a/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/internal/parser/node/TagPart.java +++ b/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/internal/parser/node/TagPart.java @@ -33,7 +33,7 @@ * * @since 4.10.0 */ -public final class TagPart implements Tag.Argument { +public /* sealed */ class TagPart implements Tag.Argument /* permits NamedTagPart */ { private final String value; private final Token token; diff --git a/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/tag/Tag.java b/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/tag/Tag.java index 26048d3fcd..91b6e0df6c 100644 --- a/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/tag/Tag.java +++ b/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/tag/Tag.java @@ -217,4 +217,8 @@ default boolean isFalse() { } } } + + interface NamedArgument extends Argument { + @NotNull String name(); + } } diff --git a/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/tag/resolver/NamedArgumentMap.java b/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/tag/resolver/NamedArgumentMap.java new file mode 100644 index 0000000000..da07a10a18 --- /dev/null +++ b/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/tag/resolver/NamedArgumentMap.java @@ -0,0 +1,48 @@ +package net.kyori.adventure.text.minimessage.tag.resolver; + +import net.kyori.adventure.text.minimessage.tag.Tag; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.function.Supplier; + +/** + * A map of named {@link Tag} arguments. + */ +@ApiStatus.NonExtendable +public interface NamedArgumentMap { + + /** + * {@return whether an argument by this name is present} + */ + boolean isPresent(@NotNull String name); + + /** + * {@return the number of arguments present} + */ + int size(); + + /** + * Get an argument by its name, returning {@code null} if none was found. + * + * @return the argument + */ + Tag.@Nullable Argument get(@NotNull String name); + + /** + * Get an argument by its name, throwing an exception if no argument with that name was present. + * + * @param errorMessage the error to throw if an argument with that name is not present + * @return the argument + */ + Tag.@NotNull Argument getOrThrow(@NotNull String name, @NotNull String errorMessage); + + /** + * Get an argument by its name, throwing an exception if no argument with that name was present. + * + * @param errorMessage the error to throw if an argument with that name is not present + * @return the argument + */ + Tag.@NotNull Argument getOrThrow(@NotNull String name, @NotNull Supplier errorMessage); +} 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..55edc1cfa7 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 @@ -40,10 +40,48 @@ final class SequentialTagResolver implements TagResolver, SerializableResolver { this.resolvers = resolvers; } + @Override + public @Nullable Tag resolveNamed(final @NotNull String name, final @NotNull NamedArgumentMap arguments, final @NotNull Context ctx) throws ParsingException { + @Nullable ParsingException thrown = null; + for (final TagResolver resolver : this.resolvers) { + if (!(resolver instanceof TagResolver.Named)) { + continue; + } + + try { + final @Nullable Tag placeholder = resolver.resolveNamed(name, arguments, ctx); + + if (placeholder != null) return placeholder; + } catch (final ParsingException ex) { + if (thrown == null) { + thrown = ex; + } else { + thrown.addSuppressed(ex); + } + } catch (final Exception ex) { + final ParsingException err = ctx.newException("Exception thrown while parsing <" + name + ">", ex, arguments); + if (thrown == null) { + thrown = err; + } else { + thrown.addSuppressed(err); + } + } + } + + if (thrown != null) { + throw thrown; + } + return null; + } + @Override public @Nullable Tag resolve(final @NotNull String name, final @NotNull ArgumentQueue arguments, final @NotNull Context ctx) throws ParsingException { @Nullable ParsingException thrown = null; for (final TagResolver resolver : this.resolvers) { + if (!(resolver instanceof TagResolver.Queued)) { + continue; + } + try { final @Nullable Tag placeholder = resolver.resolve(name, arguments, 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..f678adbbdb 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 @@ -127,7 +127,7 @@ public interface TagResolver { } requireNonNull(handler, "handler"); - return new TagResolver() { + return new TagResolver.Queued() { @Override public @Nullable Tag resolve(final @NotNull String name, final @NotNull ArgumentQueue arguments, final @NotNull Context ctx) throws ParsingException { if (!names.contains(name)) return null; @@ -142,6 +142,28 @@ public boolean has(final @NotNull String name) { }; } + static @NotNull TagResolver namedResolver(final @NotNull Set names, final @NotNull BiFunction handler) { + final Set ownNames = new HashSet<>(names); + for (final String name : ownNames) { + TagInternals.assertValidTagName(name); + } + requireNonNull(handler, "handler"); + + return new TagResolver.Named() { + @Override + public @Nullable Tag resolveNamed(final @NotNull String name, final @NotNull NamedArgumentMap arguments, final @NotNull Context ctx) throws ParsingException { + if (!names.contains(name)) return null; + + return handler.apply(arguments, ctx); + } + + @Override + public boolean has(final @NotNull String name) { + return names.contains(name); + } + }; + } + /** * Constructs a tag resolver capable of resolving from multiple sources. * @@ -223,6 +245,8 @@ public boolean has(final @NotNull String name) { */ @Nullable Tag resolve(@TagPattern final @NotNull String name, final @NotNull ArgumentQueue arguments, final @NotNull Context ctx) throws ParsingException; + @Nullable Tag resolveNamed(@TagPattern final @NotNull String name, final @NotNull NamedArgumentMap arguments, final @NotNull Context ctx) throws ParsingException; + /** * Get whether this resolver handles tags with a certain name. * @@ -280,7 +304,7 @@ default boolean has(final @NotNull String name) { * @since 4.10.0 */ @FunctionalInterface - interface WithoutArguments extends TagResolver { + interface WithoutArguments extends TagResolver.Queued { /** * Resolve a tag based only on the provided name. * @@ -312,6 +336,22 @@ default boolean has(final @NotNull String name) { } } + interface Queued extends TagResolver { + @Override + @Nullable + default Tag resolveNamed(final @NotNull String name, final @NotNull NamedArgumentMap arguments, final @NotNull Context ctx) throws ParsingException { + throw new UnsupportedOperationException(); + } + } + + interface Named extends TagResolver { + @Override + @Nullable + default Tag resolve(final @NotNull String name, final @NotNull ArgumentQueue arguments, final @NotNull Context ctx) throws ParsingException { + throw new UnsupportedOperationException(); + } + } + /** * A builder to gradually construct tag resolvers. * @@ -354,6 +394,14 @@ interface Builder { return this.resolver(TagResolver.resolver(names, handler)); } + default @NotNull Builder namedArgumentsTag(@TagPattern final @NotNull String name, final @NotNull BiFunction handler) { + return this.namedArgumentsTag(Collections.singleton(name), handler); + } + + default @NotNull Builder namedArgumentsTag(final @NotNull Set names, final @NotNull BiFunction handler) { + return this.resolver(TagResolver.namedResolver(names, handler)); + } + /** * Add a placeholder resolver to those queried by the result of this builder. * From 0778118874b37494af274f6bb566283148972dec Mon Sep 17 00:00:00 2001 From: Strokkur24 Date: Thu, 18 Sep 2025 21:19:22 +0200 Subject: [PATCH 02/19] feat: modify token parser to parse named arguments --- .../internal/parser/TokenParser.java | 36 +++++++++++++ .../internal/parser/TokenType.java | 1 + .../internal/parser/node/TagNode.java | 9 ++-- .../ComponentClaimingResolverImpl.java | 2 +- .../serializer/StyleClaimingResolverImpl.java | 2 +- .../tag/resolver/EmptyTagResolver.java | 6 +++ .../tag/standard/ColorTagResolver.java | 2 +- .../minimessage/translation/ArgumentTag.java | 2 +- .../minimessage/MiniMessageParserTest.java | 53 ++++++++++++++++--- 9 files changed, 99 insertions(+), 14 deletions(-) diff --git a/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/internal/parser/TokenParser.java b/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/internal/parser/TokenParser.java index eec960cedd..217b12c7ac 100644 --- a/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/internal/parser/TokenParser.java +++ b/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/internal/parser/TokenParser.java @@ -43,6 +43,7 @@ import net.kyori.adventure.text.minimessage.tag.Inserting; import net.kyori.adventure.text.minimessage.tag.ParserDirective; import net.kyori.adventure.text.minimessage.tag.Tag; +import net.kyori.adventure.util.TriState; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -60,6 +61,7 @@ public final class TokenParser { public static final char TAG_END = '>'; public static final char CLOSE_TAG = '/'; public static final char SEPARATOR = ':'; + public static final char NAME_VALUE_SEPARATOR = '='; // misc public static final char ESCAPE = '\\'; @@ -305,6 +307,8 @@ private static void parseSecondPass(final String message, final List toke boolean escaped = false; char currentStringChar = 0; + TriState namedArguments = TriState.NOT_SET; + // Marker is the starting index for the current token int marker = startIndex; @@ -344,6 +348,13 @@ private static void parseSecondPass(final String message, final List toke case NORMAL: // Values are split by : unless it's in a URL if (codePoint == SEPARATOR) { + if (namedArguments == TriState.NOT_SET) { + namedArguments = TriState.FALSE; + } else if (namedArguments == TriState.TRUE) { + // If the arguments are named, colons should be interpreted as plain text. + break; + } + if (boundsCheck(message, i, 2) && message.charAt(i + 1) == '/' && message.charAt(i + 2) == '/') { break; } @@ -358,6 +369,31 @@ private static void parseSecondPass(final String message, final List toke } else if (codePoint == '\'' || codePoint == '"') { state = SecondPassState.STRING; currentStringChar = (char) codePoint; + } else if (codePoint == ' ') { + if (namedArguments == TriState.NOT_SET) { + namedArguments = TriState.TRUE; + marker = i; + break; + } else if (namedArguments == TriState.FALSE) { + // If the arguments are unnamed, spaces are to be interpreted literally + break; + } + + if (marker == i) { + // If two whitespace follow up on each other, like , we just want to move the marker up by one without creating a token + marker++; + break; + } + + insert(token, new Token(marker, i, TokenType.TAG_VALUE)); + marker = i + 1; + } else if (codePoint == NAME_VALUE_SEPARATOR) { + if (namedArguments != TriState.TRUE) { + break; + } + + insert(token, new Token(marker, i, TokenType.TAG_VALUE_NAME)); + marker = i + 1; } break; case STRING: diff --git a/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/internal/parser/TokenType.java b/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/internal/parser/TokenType.java index 804f8cca7c..4165cfb1fe 100644 --- a/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/internal/parser/TokenType.java +++ b/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/internal/parser/TokenType.java @@ -33,5 +33,6 @@ public enum TokenType { OPEN_TAG, OPEN_CLOSE_TAG, // one token that both opens and closes a tag CLOSE_TAG, + TAG_VALUE_NAME, TAG_VALUE; } diff --git a/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/internal/parser/node/TagNode.java b/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/internal/parser/node/TagNode.java index 58b6dd3c6a..8515b32262 100644 --- a/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/internal/parser/node/TagNode.java +++ b/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/internal/parser/node/TagNode.java @@ -26,6 +26,7 @@ import java.util.ArrayList; import java.util.List; import java.util.Objects; + 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.TokenParser; @@ -38,8 +39,8 @@ * * @since 4.10.0 */ -public final class TagNode extends ElementNode { - private final List parts; +public final class TagNode extends ElementNode { + private final List parts; private @Nullable Tag tag = null; /** @@ -66,12 +67,12 @@ public TagNode( } } - private static @NotNull List genParts( + private static @NotNull List genParts( final @NotNull Token token, final @NotNull String sourceMessage, final TokenParser.@NotNull TagProvider tagProvider ) { - final ArrayList parts = new ArrayList<>(); + final ArrayList parts = new ArrayList<>(); if (token.childTokens() != null) { for (final Token childToken : token.childTokens()) { diff --git a/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/internal/serializer/ComponentClaimingResolverImpl.java b/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/internal/serializer/ComponentClaimingResolverImpl.java index 941dbe4262..6035017fc4 100644 --- a/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/internal/serializer/ComponentClaimingResolverImpl.java +++ b/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/internal/serializer/ComponentClaimingResolverImpl.java @@ -35,7 +35,7 @@ import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -class ComponentClaimingResolverImpl implements TagResolver, SerializableResolver.Single { +class ComponentClaimingResolverImpl implements TagResolver.Queued, SerializableResolver.Single { private final @NotNull Set names; private final @NotNull BiFunction handler; private final @NotNull Function componentClaim; diff --git a/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/internal/serializer/StyleClaimingResolverImpl.java b/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/internal/serializer/StyleClaimingResolverImpl.java index 99b82b0071..06daf967d3 100644 --- a/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/internal/serializer/StyleClaimingResolverImpl.java +++ b/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/internal/serializer/StyleClaimingResolverImpl.java @@ -33,7 +33,7 @@ import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -final class StyleClaimingResolverImpl implements TagResolver, SerializableResolver.Single { +final class StyleClaimingResolverImpl implements TagResolver.Queued, SerializableResolver.Single { private final @NotNull Set names; private final @NotNull BiFunction handler; private final @NotNull StyleClaim styleClaim; diff --git a/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/tag/resolver/EmptyTagResolver.java b/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/tag/resolver/EmptyTagResolver.java index 1c3c9607b3..cc95de6d83 100644 --- a/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/tag/resolver/EmptyTagResolver.java +++ b/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/tag/resolver/EmptyTagResolver.java @@ -26,6 +26,7 @@ import java.util.Map; 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.internal.serializer.ClaimConsumer; import net.kyori.adventure.text.minimessage.internal.serializer.SerializableResolver; import net.kyori.adventure.text.minimessage.tag.Tag; @@ -43,6 +44,11 @@ private EmptyTagResolver() { return null; } + @Override + public @Nullable Tag resolveNamed(final @NotNull String name, final @NotNull NamedArgumentMap arguments, final @NotNull Context ctx) throws ParsingException { + return null; + } + @Override public boolean has(final @NotNull String name) { return false; 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..1097051d6b 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 @@ -43,7 +43,7 @@ * * @since 4.10.0 */ -final class ColorTagResolver implements TagResolver, SerializableResolver.Single { +final class ColorTagResolver implements TagResolver.Queued, SerializableResolver.Single { private static final String COLOR_3 = "c"; private static final String COLOR_2 = "colour"; private static final String COLOR = "color"; diff --git a/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/translation/ArgumentTag.java b/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/translation/ArgumentTag.java index 8b466d7a9a..dcad13fe3c 100644 --- a/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/translation/ArgumentTag.java +++ b/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/translation/ArgumentTag.java @@ -32,7 +32,7 @@ import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -final class ArgumentTag implements TagResolver { +final class ArgumentTag implements TagResolver.Queued { private static final String NAME = "argument"; private static final String NAME_1 = "arg"; diff --git a/text-minimessage/src/test/java/net/kyori/adventure/text/minimessage/MiniMessageParserTest.java b/text-minimessage/src/test/java/net/kyori/adventure/text/minimessage/MiniMessageParserTest.java index 43f24fed29..0e6b9a430e 100644 --- a/text-minimessage/src/test/java/net/kyori/adventure/text/minimessage/MiniMessageParserTest.java +++ b/text-minimessage/src/test/java/net/kyori/adventure/text/minimessage/MiniMessageParserTest.java @@ -23,8 +23,6 @@ */ package net.kyori.adventure.text.minimessage; -import java.util.Collections; -import java.util.List; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.TextComponent; import net.kyori.adventure.text.format.NamedTextColor; @@ -40,6 +38,10 @@ import org.jetbrains.annotations.NotNull; import org.junit.jupiter.api.Test; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + import static net.kyori.adventure.text.Component.empty; import static net.kyori.adventure.text.Component.text; import static net.kyori.adventure.text.Component.translatable; @@ -330,6 +332,45 @@ void testEscapeIncompleteTags() { this.assertParsedEquals(expected, escaped); } + @Test + void testNamedArgumentsTokens() { + final String basicInput = ""; + final List expectedTokensBasicInput = Collections.singletonList(new Token(0, basicInput.length(), TokenType.OPEN_TAG)); + assertIterableEquals(expectedTokensBasicInput, TokenParser.tokenize(basicInput, false)); + + final int toggleLength = "toggle".length(); + + final String booleanToggleInput = ""; + final List expectedTokensBooleanToggleInput = new ArrayList<>(); + final Token parentToken = new Token(0, booleanToggleInput.length(), TokenType.OPEN_TAG); + parentToken.childTokens(new ArrayList<>()); + parentToken.childTokens().add(new Token(0, toggleLength, TokenType.TEXT)); + parentToken.childTokens().add(new Token(toggleLength + 2, "enabled".length(), TokenType.TAG_VALUE)); + expectedTokensBooleanToggleInput.add(parentToken); + assertIterableEquals(expectedTokensBooleanToggleInput, TokenParser.tokenize(booleanToggleInput, false)); + + final String namedArgumentInput = ""; + final List expectedTokensNamedArgumentInput = new ArrayList<>(); + final Token parentTokenNamed = new Token(0, namedArgumentInput.length(), TokenType.OPEN_TAG); + parentTokenNamed.childTokens(new ArrayList<>()); + parentTokenNamed.childTokens().add(new Token(0, 1, TokenType.TEXT)); + parentTokenNamed.childTokens().add(new Token(2, 1, TokenType.TAG_VALUE_NAME)); + parentTokenNamed.childTokens().add(new Token(4, 1, TokenType.TAG_VALUE)); + expectedTokensNamedArgumentInput.add(parentTokenNamed); + assertIterableEquals(expectedTokensNamedArgumentInput, TokenParser.tokenize(namedArgumentInput, false)); + + final String mixedArgumentInput = ""; + final List expectedTokensMixedArgumentInput = new ArrayList<>(); + final Token parentTokenMixed = new Token(0, mixedArgumentInput.length(), TokenType.OPEN_TAG); + parentTokenMixed.childTokens(new ArrayList<>()); + parentTokenMixed.childTokens().add(new Token(0, 1, TokenType.TEXT)); + parentTokenMixed.childTokens().add(new Token(2, 1, TokenType.TAG_VALUE_NAME)); + parentTokenMixed.childTokens().add(new Token(4, 1, TokenType.TAG_VALUE)); + parentTokenMixed.childTokens().add(new Token(6, toggleLength, TokenType.TAG_VALUE)); + expectedTokensMixedArgumentInput.add(parentTokenMixed); + assertIterableEquals(expectedTokensMixedArgumentInput, TokenParser.tokenize(mixedArgumentInput, false)); + } + // GH-68, GH-93 @Test void testAngleBracketsShit() { @@ -379,9 +420,9 @@ void testEscapesEscapablePlainText() { void testEscapeInsideOfContext() { final String input = "Test"; final Component expected = text() - .content("Test") - .hoverEvent(text("Look at\\ this '")) - .build(); + .content("Test") + .hoverEvent(text("Look at\\ this '")) + .build(); this.assertParsedEquals(expected, input); } @@ -530,7 +571,7 @@ void testValidTagNames() { void invalidPreprocessTagNames() { final String input = "Some<##>of<>theseare<3 >tags"; final Component expected = Component.text("Some<##>of<>these(meow)are<3 >tags"); - final TagResolver alwaysMatchingResolver = new TagResolver() { + final TagResolver alwaysMatchingResolver = new TagResolver.Queued() { @Override public Tag resolve(final @NotNull String name, final @NotNull ArgumentQueue arguments, final @NotNull Context ctx) throws ParsingException { return Tag.preProcessParsed("(meow)"); From 7811e0e5b4a88763682b2e1f194d8ab0e5990973 Mon Sep 17 00:00:00 2001 From: Strokkur24 Date: Thu, 18 Sep 2025 21:29:11 +0200 Subject: [PATCH 03/19] chore: introduce new TokenType to uniquely distinguish value-less toggle arguments --- .../text/minimessage/internal/parser/TokenParser.java | 5 ++++- .../text/minimessage/internal/parser/TokenType.java | 1 + .../adventure/text/minimessage/MiniMessageParserTest.java | 4 ++-- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/internal/parser/TokenParser.java b/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/internal/parser/TokenParser.java index 217b12c7ac..d6a1003b7c 100644 --- a/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/internal/parser/TokenParser.java +++ b/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/internal/parser/TokenParser.java @@ -308,6 +308,7 @@ private static void parseSecondPass(final String message, final List toke char currentStringChar = 0; TriState namedArguments = TriState.NOT_SET; + boolean nextNormalIsArgumentValue = false; // Marker is the starting index for the current token int marker = startIndex; @@ -385,13 +386,15 @@ private static void parseSecondPass(final String message, final List toke break; } - insert(token, new Token(marker, i, TokenType.TAG_VALUE)); + insert(token, new Token(marker, i, nextNormalIsArgumentValue ? TokenType.TAG_VALUE : TokenType.TAG_VALUE_TOGGLE)); marker = i + 1; + nextNormalIsArgumentValue = false; } else if (codePoint == NAME_VALUE_SEPARATOR) { if (namedArguments != TriState.TRUE) { break; } + nextNormalIsArgumentValue = true; insert(token, new Token(marker, i, TokenType.TAG_VALUE_NAME)); marker = i + 1; } diff --git a/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/internal/parser/TokenType.java b/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/internal/parser/TokenType.java index 4165cfb1fe..56c658ae40 100644 --- a/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/internal/parser/TokenType.java +++ b/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/internal/parser/TokenType.java @@ -33,6 +33,7 @@ public enum TokenType { OPEN_TAG, OPEN_CLOSE_TAG, // one token that both opens and closes a tag CLOSE_TAG, + TAG_VALUE_TOGGLE, TAG_VALUE_NAME, TAG_VALUE; } diff --git a/text-minimessage/src/test/java/net/kyori/adventure/text/minimessage/MiniMessageParserTest.java b/text-minimessage/src/test/java/net/kyori/adventure/text/minimessage/MiniMessageParserTest.java index 0e6b9a430e..7d4b3c2a96 100644 --- a/text-minimessage/src/test/java/net/kyori/adventure/text/minimessage/MiniMessageParserTest.java +++ b/text-minimessage/src/test/java/net/kyori/adventure/text/minimessage/MiniMessageParserTest.java @@ -345,7 +345,7 @@ void testNamedArgumentsTokens() { final Token parentToken = new Token(0, booleanToggleInput.length(), TokenType.OPEN_TAG); parentToken.childTokens(new ArrayList<>()); parentToken.childTokens().add(new Token(0, toggleLength, TokenType.TEXT)); - parentToken.childTokens().add(new Token(toggleLength + 2, "enabled".length(), TokenType.TAG_VALUE)); + parentToken.childTokens().add(new Token(toggleLength + 2, "enabled".length(), TokenType.TAG_VALUE_TOGGLE)); expectedTokensBooleanToggleInput.add(parentToken); assertIterableEquals(expectedTokensBooleanToggleInput, TokenParser.tokenize(booleanToggleInput, false)); @@ -366,7 +366,7 @@ void testNamedArgumentsTokens() { parentTokenMixed.childTokens().add(new Token(0, 1, TokenType.TEXT)); parentTokenMixed.childTokens().add(new Token(2, 1, TokenType.TAG_VALUE_NAME)); parentTokenMixed.childTokens().add(new Token(4, 1, TokenType.TAG_VALUE)); - parentTokenMixed.childTokens().add(new Token(6, toggleLength, TokenType.TAG_VALUE)); + parentTokenMixed.childTokens().add(new Token(6, toggleLength, TokenType.TAG_VALUE_TOGGLE)); expectedTokensMixedArgumentInput.add(parentTokenMixed); assertIterableEquals(expectedTokensMixedArgumentInput, TokenParser.tokenize(mixedArgumentInput, false)); } From e3dfad96c6eb4484a24db7be8bd3c14bb873fe93 Mon Sep 17 00:00:00 2001 From: Strokkur24 Date: Thu, 18 Sep 2025 22:20:03 +0200 Subject: [PATCH 04/19] feat: (WIP) abstracting away TagProvider into QueuedTagProvider and NamedTagProvider and start adjusting MiniMessageParser to distinguish tags with named arguments --- .../text/minimessage/MiniMessageParser.java | 17 +++-- .../internal/parser/TokenParser.java | 71 +++++++++++++++++-- .../StringResolvingMatchedTokenConsumer.java | 13 ++-- 3 files changed, 84 insertions(+), 17 deletions(-) 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..d6fb4a40bc 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 @@ -135,9 +135,16 @@ private void processTokens(final @NotNull StringBuilder sb, final @NotNull Strin debug.accept("\n"); } - final TokenParser.TagProvider transformationFactory; + final TokenParser.QueuedTagProvider queueTransformationFactory; + final TokenParser.NamedTagProvider namedTransformationFactory = (name, args, token) -> { + try { + return combinedResolver.resolveNamed(name, new NamedArgumentMapImpl<>(context, args), context); + } catch (final ParsingException ignored) { + return null; + } + }; if (debug != null) { - transformationFactory = (name, args, token) -> { + queueTransformationFactory = (name, args, token) -> { try { debug.accept("Attempting to match node '"); debug.accept(name); @@ -179,7 +186,7 @@ private void processTokens(final @NotNull StringBuilder sb, final @NotNull Strin } }; } else { - transformationFactory = (name, args, token) -> { + queueTransformationFactory = (name, args, token) -> { try { return combinedResolver.resolve(name, new ArgumentQueueImpl<>(context, args), context); } catch (final ParsingException ignored) { @@ -192,10 +199,10 @@ private void processTokens(final @NotNull StringBuilder sb, final @NotNull Strin return combinedResolver.has(sanitized); }; - final String preProcessed = TokenParser.resolvePreProcessTags(processedMessage, transformationFactory); + final String preProcessed = TokenParser.resolvePreProcessTags(processedMessage, queueTransformationFactory); context.message(preProcessed); // Then, once MiniMessage placeholders have been inserted, we can do the real parse - final RootNode root = TokenParser.parse(transformationFactory, tagNameChecker, preProcessed, processedMessage, context.strict()); + final RootNode root = TokenParser.parse(queueTransformationFactory, namedTransformationFactory, tagNameChecker, preProcessed, processedMessage, context.strict()); if (debug != null) { debug.accept("Text parsed into element tree:\n"); diff --git a/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/internal/parser/TokenParser.java b/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/internal/parser/TokenParser.java index d6a1003b7c..ce4ed6c37b 100644 --- a/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/internal/parser/TokenParser.java +++ b/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/internal/parser/TokenParser.java @@ -28,6 +28,8 @@ import java.util.List; import java.util.ListIterator; import java.util.Locale; +import java.util.Map; +import java.util.TreeMap; import java.util.function.IntPredicate; import java.util.function.Predicate; import net.kyori.adventure.text.minimessage.ParsingException; @@ -81,7 +83,8 @@ private TokenParser() { * @since 4.10.0 */ public static RootNode parse( - final @NotNull TagProvider tagProvider, + final @NotNull TokenParser.TagProvider tagProvider, + final @NotNull TokenParser.NamedTagProvider namedTagProvider, final @NotNull Predicate tagNameChecker, final @NotNull String message, final @NotNull String originalMessage, @@ -91,7 +94,7 @@ public static RootNode parse( final List tokens = tokenize(message, false); // then build the tree! - return buildTree(tagProvider, tagNameChecker, tokens, message, originalMessage, strict); + return buildTree(tagProvider, namedTagProvider, tagNameChecker, tokens, message, originalMessage, strict); } /** @@ -423,7 +426,8 @@ private static void parseSecondPass(final String message, final List toke * Build a tree from the OPEN_TAG and CLOSE_TAG tokens */ private static RootNode buildTree( - final @NotNull TagProvider tagProvider, + final @NotNull TokenParser.TagProvider tagProvider, + final @NotNull TokenParser.NamedTagProvider namedTagProvider, final @NotNull Predicate tagNameChecker, final @NotNull List tokens, final @NotNull String message, @@ -703,7 +707,7 @@ public static String unescape(final String text, final int startIndex, final int * @since 4.10.0 */ @ApiStatus.Internal - public interface TagProvider { + public interface TagProvider { /** * Look up a tag. * @@ -715,7 +719,11 @@ public interface TagProvider { * @return a tag * @since 4.10.0 */ - @Nullable Tag resolve(final @NotNull String name, final @NotNull List trimmedArgs, final @Nullable Token token); + @Nullable Tag resolve(final @NotNull String name, final @NotNull S trimmedArgs, final @Nullable Token token); + + S createEmptyArgs(); + + S createFromNode(TagNode node); /** * Resolve by sanitized name. @@ -725,7 +733,7 @@ public interface TagProvider { * @since 4.10.0 */ default @Nullable Tag resolve(final @NotNull String name) { - return this.resolve(name, Collections.emptyList(), null); + return this.resolve(name, createEmptyArgs(), null); } /** @@ -738,7 +746,7 @@ public interface TagProvider { default @Nullable Tag resolve(final @NotNull TagNode node) { return this.resolve( sanitizePlaceholderName(node.name()), - (List) node.parts().subList(1, node.parts().size()), + createFromNode(node), node.token() ); } @@ -756,4 +764,53 @@ public interface TagProvider { return name.toLowerCase(Locale.ROOT); } } + + @ApiStatus.Internal + public interface QueuedTagProvider extends TagProvider> { + + @Override + default List createFromNode(TagNode node) { + return (List) node.parts().subList(1, node.parts().size()); + } + + @Override + default List createEmptyArgs() { + return Collections.emptyList(); + } + } + + @ApiStatus.Internal + public interface NamedTagProvider extends TagProvider> { + + @Override + default Map createEmptyArgs() { + return Collections.emptyMap(); + } + + @Override + default Map createFromNode(TagNode node) { + final Map map = new TreeMap<>(); + + @NotNull List parts = node.parts(); + for (int i = 1, partsSize = parts.size(); i < partsSize; i++) { + final TagPart part = parts.get(i); + + if (part.token().type() == TokenType.TAG_VALUE_NAME) { + if (i + 1 == partsSize || parts.get(i + 1).token().type() != TokenType.TAG_VALUE) { + throw new IllegalStateException("Somehow a tag name has no value afterwards."); + } + + map.put(part.value(), (T) parts.get(i + 1)); + i++; + continue; + } + + if (part.token().type() == TokenType.TAG_VALUE_TOGGLE) { + map.put(part.value(), (T) part); + } + } + + return map; + } + } } diff --git a/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/internal/parser/match/StringResolvingMatchedTokenConsumer.java b/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/internal/parser/match/StringResolvingMatchedTokenConsumer.java index b2d6583c0d..868f303e07 100644 --- a/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/internal/parser/match/StringResolvingMatchedTokenConsumer.java +++ b/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/internal/parser/match/StringResolvingMatchedTokenConsumer.java @@ -58,7 +58,7 @@ public final class StringResolvingMatchedTokenConsumer extends MatchedTokenConsu */ public StringResolvingMatchedTokenConsumer( final @NotNull String input, - final @NotNull TagProvider tagProvider + final @NotNull TokenParser.TagProvider tagProvider ) { super(input); this.builder = new StringBuilder(input.length()); @@ -90,12 +90,15 @@ public void accept(final int start, final int end, final @NotNull TokenType toke parts.add(new TagPart(match, childs.get(i), this.tagProvider)); } } + // we might care if it's a pre-process! - final @Nullable Tag replacement = this.tagProvider.resolve(TokenParser.TagProvider.sanitizePlaceholderName(tag), parts, tokens.get(0)); + if (this.tagProvider instanceof TokenParser.QueuedTagProvider) { + final @Nullable Tag replacement = this.tagProvider.resolve(TagProvider.sanitizePlaceholderName(tag), parts, tokens.get(0)); - if (replacement instanceof PreProcess) { - this.builder.append(Objects.requireNonNull(((PreProcess) replacement).value(), "PreProcess replacements cannot return null")); - return; + if (replacement instanceof PreProcess) { + this.builder.append(Objects.requireNonNull(((PreProcess) replacement).value(), "PreProcess replacements cannot return null")); + return; + } } } From f44b15c5950cfc6b06010ce03484394cdc4cf7cb Mon Sep 17 00:00:00 2001 From: Strokkur24 Date: Fri, 19 Sep 2025 22:30:33 +0200 Subject: [PATCH 05/19] fix: (WIP) parser should now theoretically be able to distinguish named arguments --- .../text/minimessage/MiniMessageParser.java | 73 ++++++-- .../internal/parser/TokenParser.java | 177 +++++++++++------- .../StringResolvingMatchedTokenConsumer.java | 2 +- .../internal/parser/node/TagNode.java | 1 + .../text/minimessage/MiniMessageTest.java | 14 +- 5 files changed, 182 insertions(+), 85 deletions(-) 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 d6fb4a40bc..f19da09851 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 @@ -135,18 +135,13 @@ private void processTokens(final @NotNull StringBuilder sb, final @NotNull Strin debug.accept("\n"); } - final TokenParser.QueuedTagProvider queueTransformationFactory; - final TokenParser.NamedTagProvider namedTransformationFactory = (name, args, token) -> { - try { - return combinedResolver.resolveNamed(name, new NamedArgumentMapImpl<>(context, args), context); - } catch (final ParsingException ignored) { - return null; - } - }; + final TokenParser.QueuedTagProvider queuedTagProvider; + final TokenParser.NamedTagProvider namedTagProvider; + if (debug != null) { - queueTransformationFactory = (name, args, token) -> { + queuedTagProvider = (name, args, token) -> { try { - debug.accept("Attempting to match node '"); + debug.accept("Attempting to match node as queued '"); debug.accept(name); debug.accept("'"); if (token != null) { @@ -174,7 +169,48 @@ private void processTokens(final @NotNull StringBuilder sb, final @NotNull Strin if (token != null && e instanceof ParsingExceptionImpl) { final ParsingExceptionImpl impl = (ParsingExceptionImpl) e; if (impl.tokens().length == 0) { - impl.tokens(new Token[] {token}); + impl.tokens(new Token[]{token}); + } + } + debug.accept("Could not match node '"); + debug.accept(name); + debug.accept("' - "); + debug.accept(e.getMessage()); + debug.accept("\n"); + return null; + } + }; + namedTagProvider = (name, args, token) -> { + try { + debug.accept("Attempting to match node as named '"); + debug.accept(name); + debug.accept("'"); + if (token != null) { + debug.accept(" at column "); + debug.accept(String.valueOf(token.startIndex())); + } + debug.accept("\n"); + + final @Nullable Tag transformation = combinedResolver.resolveNamed(name, new NamedArgumentMapImpl<>(context, args), context); + + if (transformation == null) { + debug.accept("Could not match node '"); + debug.accept(name); + debug.accept("'\n"); + } else { + debug.accept("Successfully matched node '"); + debug.accept(name); + debug.accept("' to tag "); + debug.accept(transformation instanceof Examinable ? ((Examinable) transformation).examinableName() : transformation.getClass().getName()); + debug.accept("\n"); + } + + return transformation; + } catch (final ParsingException e) { + if (token != null && e instanceof ParsingExceptionImpl) { + final ParsingExceptionImpl impl = (ParsingExceptionImpl) e; + if (impl.tokens().length == 0) { + impl.tokens(new Token[]{token}); } } debug.accept("Could not match node '"); @@ -186,23 +222,32 @@ private void processTokens(final @NotNull StringBuilder sb, final @NotNull Strin } }; } else { - queueTransformationFactory = (name, args, token) -> { + queuedTagProvider = (name, args, token) -> { try { return combinedResolver.resolve(name, new ArgumentQueueImpl<>(context, args), context); } catch (final ParsingException ignored) { return null; } }; + namedTagProvider = (name, args, token) -> { + try { + return combinedResolver.resolveNamed(name, new NamedArgumentMapImpl<>(context, args), context); + } catch (final ParsingException ignored) { + return null; + } + }; } + + final TokenParser.TagProvider transformationFactory = new TokenParser.TagProviderImpl(queuedTagProvider, namedTagProvider); final Predicate tagNameChecker = name -> { final String sanitized = TokenParser.TagProvider.sanitizePlaceholderName(name); return combinedResolver.has(sanitized); }; - final String preProcessed = TokenParser.resolvePreProcessTags(processedMessage, queueTransformationFactory); + final String preProcessed = TokenParser.resolvePreProcessTags(processedMessage, transformationFactory); context.message(preProcessed); // Then, once MiniMessage placeholders have been inserted, we can do the real parse - final RootNode root = TokenParser.parse(queueTransformationFactory, namedTransformationFactory, tagNameChecker, preProcessed, processedMessage, context.strict()); + final RootNode root = TokenParser.parse(transformationFactory, tagNameChecker, preProcessed, processedMessage, context.strict()); if (debug != null) { debug.accept("Text parsed into element tree:\n"); diff --git a/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/internal/parser/TokenParser.java b/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/internal/parser/TokenParser.java index ce4ed6c37b..dde32809d6 100644 --- a/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/internal/parser/TokenParser.java +++ b/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/internal/parser/TokenParser.java @@ -23,15 +23,6 @@ */ package net.kyori.adventure.text.minimessage.internal.parser; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.ListIterator; -import java.util.Locale; -import java.util.Map; -import java.util.TreeMap; -import java.util.function.IntPredicate; -import java.util.function.Predicate; import net.kyori.adventure.text.minimessage.ParsingException; import net.kyori.adventure.text.minimessage.internal.TagInternals; import net.kyori.adventure.text.minimessage.internal.parser.match.MatchedTokenConsumer; @@ -50,6 +41,16 @@ import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.ListIterator; +import java.util.Locale; +import java.util.Map; +import java.util.TreeMap; +import java.util.function.IntPredicate; +import java.util.function.Predicate; + /** * Handles parsing a string into a list of tokens and then into a tree of nodes. * @@ -84,7 +85,6 @@ private TokenParser() { */ public static RootNode parse( final @NotNull TokenParser.TagProvider tagProvider, - final @NotNull TokenParser.NamedTagProvider namedTagProvider, final @NotNull Predicate tagNameChecker, final @NotNull String message, final @NotNull String originalMessage, @@ -94,7 +94,7 @@ public static RootNode parse( final List tokens = tokenize(message, false); // then build the tree! - return buildTree(tagProvider, namedTagProvider, tagNameChecker, tokens, message, originalMessage, strict); + return buildTree(tagProvider, tagNameChecker, tokens, message, originalMessage, strict); } /** @@ -427,7 +427,6 @@ private static void parseSecondPass(final String message, final List toke */ private static RootNode buildTree( final @NotNull TokenParser.TagProvider tagProvider, - final @NotNull TokenParser.NamedTagProvider namedTagProvider, final @NotNull Predicate tagNameChecker, final @NotNull List tokens, final @NotNull String message, @@ -498,7 +497,7 @@ private static RootNode buildTree( final String closeTagName = closeValues.get(0); if (tagNameChecker.test(closeTagName)) { - final Tag tag = tagProvider.resolve(closeTagName); + final Tag tag = tagProvider.resolveQueued(closeTagName); if (tag == ParserDirective.RESET) { // This is a synthetic node, closing it means nothing in the context of building a tree @@ -701,13 +700,8 @@ public static String unescape(final String text, final int startIndex, final int return sb.toString(); } - /** - * Normalizing provider for tag information. - * - * @since 4.10.0 - */ @ApiStatus.Internal - public interface TagProvider { + public interface QueuedTagProvider { /** * Look up a tag. * @@ -719,11 +713,56 @@ public interface TagProvider { * @return a tag * @since 4.10.0 */ - @Nullable Tag resolve(final @NotNull String name, final @NotNull S trimmedArgs, final @Nullable Token token); + @Nullable Tag resolveQueued(final @NotNull String name, final @NotNull List trimmedArgs, final @Nullable Token token); + } - S createEmptyArgs(); + @ApiStatus.Internal + public interface NamedTagProvider { + /** + * Look up a tag. + * + *

Parsing exceptions must be caught and handled within this method.

+ * + * @param name the tag name, pre-sanitized + * @param trimmedArgs arguments, with the tag name trimmed off + * @param token the token, if this tag is from a parse stream + * @return a tag + * @since 4.10.0 + */ + @Nullable Tag resolveNamed(final @NotNull String name, final @NotNull Map trimmedArgs, final @Nullable Token token); + } + + /** + * Normalizing provider for tag information. + * + * @since 4.10.0 + */ + @ApiStatus.Internal + public interface TagProvider extends QueuedTagProvider, NamedTagProvider { - S createFromNode(TagNode node); + default boolean isNamed(final @NotNull List trimmedTokens) { + for (final Token trimmedToken : trimmedTokens) { + if (trimmedToken.type() == TokenType.TAG_VALUE_NAME || trimmedToken.type() == TokenType.TAG_VALUE_TOGGLE) { + return true; + } + } + return false; + } + + default boolean isNamed(final @NotNull TagNode node) { + return isNamed(node.token().childTokens()); + } + + /** + * Resolve by sanitized name. + * + * @param name sanitized name + * @return a tag, if any is available + * @since 4.10.0 + */ + default @Nullable Tag resolveQueued(final @NotNull String name) { + return this.resolveQueued(name, Collections.emptyList(), null); + } /** * Resolve by sanitized name. @@ -732,8 +771,12 @@ public interface TagProvider { * @return a tag, if any is available * @since 4.10.0 */ - default @Nullable Tag resolve(final @NotNull String name) { - return this.resolve(name, createEmptyArgs(), null); + default @Nullable Tag resolveNamed(final @NotNull String name) { + return this.resolveNamed(name, Collections.emptyMap(), null); + } + + default @Nullable Tag resolve(final @NotNull TagNode node) { + return isNamed(node) ? resolveNamed(node) : resolveQueued(node); } /** @@ -743,52 +786,22 @@ public interface TagProvider { * @return a tag, if any is available * @since 4.10.0 */ - default @Nullable Tag resolve(final @NotNull TagNode node) { - return this.resolve( - sanitizePlaceholderName(node.name()), - createFromNode(node), + default @Nullable Tag resolveQueued(final @NotNull TagNode node) { + return this.resolveQueued( + TagProvider.sanitizePlaceholderName(node.name()), + (List) node.parts().subList(1, node.parts().size()), node.token() ); } /** - * Sanitize placeholder names. - * - *

This makes all placeholder names lower-case.

+ * Resolve by node. * - * @param name the raw name - * @return a sanitized name + * @param node tag node + * @return a tag, if any is available * @since 4.10.0 */ - static @NotNull String sanitizePlaceholderName(final @NotNull String name) { - return name.toLowerCase(Locale.ROOT); - } - } - - @ApiStatus.Internal - public interface QueuedTagProvider extends TagProvider> { - - @Override - default List createFromNode(TagNode node) { - return (List) node.parts().subList(1, node.parts().size()); - } - - @Override - default List createEmptyArgs() { - return Collections.emptyList(); - } - } - - @ApiStatus.Internal - public interface NamedTagProvider extends TagProvider> { - - @Override - default Map createEmptyArgs() { - return Collections.emptyMap(); - } - - @Override - default Map createFromNode(TagNode node) { + default @Nullable Tag resolveNamed(final @NotNull TagNode node) { final Map map = new TreeMap<>(); @NotNull List parts = node.parts(); @@ -810,7 +823,45 @@ default Map createFromNode(TagNode node) { } } - return map; + return this.resolveNamed( + TagProvider.sanitizePlaceholderName(node.name()), + map, + node.token() + ); + } + + /** + * Sanitize placeholder names. + * + *

This makes all placeholder names lower-case.

+ * + * @param name the raw name + * @return a sanitized name + * @since 4.10.0 + */ + static @NotNull String sanitizePlaceholderName(final @NotNull String name) { + return name.toLowerCase(Locale.ROOT); + } + } + + @ApiStatus.Internal + public static final class TagProviderImpl implements TagProvider { + private final QueuedTagProvider queued; + private final NamedTagProvider named; + + public TagProviderImpl(final QueuedTagProvider queued, final NamedTagProvider named) { + this.queued = queued; + this.named = named; + } + + @Override + public @Nullable Tag resolveNamed(final @NotNull String name, final @NotNull Map trimmedArgs, final @Nullable Token token) { + return named.resolveNamed(name, trimmedArgs, token); + } + + @Override + public @Nullable Tag resolveQueued(final @NotNull String name, final @NotNull List trimmedArgs, final @Nullable Token token) { + return queued.resolveQueued(name, trimmedArgs, token); } } } diff --git a/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/internal/parser/match/StringResolvingMatchedTokenConsumer.java b/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/internal/parser/match/StringResolvingMatchedTokenConsumer.java index 868f303e07..52e2b921e3 100644 --- a/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/internal/parser/match/StringResolvingMatchedTokenConsumer.java +++ b/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/internal/parser/match/StringResolvingMatchedTokenConsumer.java @@ -93,7 +93,7 @@ public void accept(final int start, final int end, final @NotNull TokenType toke // we might care if it's a pre-process! if (this.tagProvider instanceof TokenParser.QueuedTagProvider) { - final @Nullable Tag replacement = this.tagProvider.resolve(TagProvider.sanitizePlaceholderName(tag), parts, tokens.get(0)); + final @Nullable Tag replacement = this.tagProvider.resolveQueued(TagProvider.sanitizePlaceholderName(tag), parts, tokens.get(0)); if (replacement instanceof PreProcess) { this.builder.append(Objects.requireNonNull(((PreProcess) replacement).value(), "PreProcess replacements cannot return null")); diff --git a/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/internal/parser/node/TagNode.java b/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/internal/parser/node/TagNode.java index 8515b32262..c28820cdfc 100644 --- a/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/internal/parser/node/TagNode.java +++ b/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/internal/parser/node/TagNode.java @@ -30,6 +30,7 @@ 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.TokenParser; +import net.kyori.adventure.text.minimessage.internal.parser.TokenType; import net.kyori.adventure.text.minimessage.tag.Tag; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; diff --git a/text-minimessage/src/test/java/net/kyori/adventure/text/minimessage/MiniMessageTest.java b/text-minimessage/src/test/java/net/kyori/adventure/text/minimessage/MiniMessageTest.java index 6ec851ae02..8ecb1034ca 100644 --- a/text-minimessage/src/test/java/net/kyori/adventure/text/minimessage/MiniMessageTest.java +++ b/text-minimessage/src/test/java/net/kyori/adventure/text/minimessage/MiniMessageTest.java @@ -372,7 +372,7 @@ void debugModeSimple() { final List messages = Arrays.asList(sb.toString().split("\n")); assertTrue(messages.contains("Beginning parsing message RED ")); - assertTrue(messages.contains("Attempting to match node 'red' at column 0")); + assertTrue(messages.contains("Attempting to match node as queued 'red' at column 0")); assertTrue(anyMatch(messages, it -> it.startsWith("Successfully matched node 'red' to tag "))); assertTrue(messages.contains("Text parsed into element tree:")); assertTrue(messages.contains("Node {")); @@ -391,11 +391,11 @@ void debugModeMoreComplex() { final List messages = Arrays.asList(sb.toString().split("\n")); assertTrue(messages.contains("Beginning parsing message RED BLUE bad click ")); - assertTrue(messages.contains("Attempting to match node 'red' at column 0")); + assertTrue(messages.contains("Attempting to match node as queued 'red' at column 0")); assertTrue(anyMatch(messages, it -> it.startsWith("Successfully matched node 'red' to tag "))); - assertTrue(messages.contains("Attempting to match node 'blue' at column 10")); + assertTrue(messages.contains("Attempting to match node as queued 'blue' at column 10")); assertTrue(anyMatch(messages, it -> it.startsWith("Successfully matched node 'blue' to tag "))); - assertTrue(messages.contains("Attempting to match node 'click' at column 22")); + assertTrue(messages.contains("Attempting to match node as queued 'click' at column 22")); assertTrue(messages.contains("Could not match node 'click' - A click tag requires an action of one of [run_command, open_file, custom, open_url, copy_to_clipboard, change_page, show_dialog, suggest_command]")); assertTrue(messages.contains("\t RED BLUE bad click ")); assertTrue(messages.contains("\t ^~~~~~^")); @@ -419,11 +419,11 @@ void debugModeMoreComplexNoError() { final List messages = Arrays.asList(sb.toString().split("\n")); assertTrue(messages.contains("Beginning parsing message RED BLUE good click ")); - assertTrue(messages.contains("Attempting to match node 'red' at column 0")); + assertTrue(messages.contains("Attempting to match node as queued 'red' at column 0")); assertTrue(anyMatch(messages, it -> it.startsWith("Successfully matched node 'red' to tag "))); - assertTrue(messages.contains("Attempting to match node 'blue' at column 10")); + assertTrue(messages.contains("Attempting to match node as queued 'blue' at column 10")); assertTrue(anyMatch(messages, it -> it.startsWith("Successfully matched node 'blue' to tag "))); - assertTrue(messages.contains("Attempting to match node 'click' at column 22")); + assertTrue(messages.contains("Attempting to match node as queued 'click' at column 22")); assertTrue(anyMatch(messages, it -> it.startsWith("Successfully matched node 'click' to tag "))); assertTrue(messages.contains("Text parsed into element tree:")); assertTrue(messages.contains("Node {")); From 57437e67ec80f1e6ed126114416975d1a3825242 Mon Sep 17 00:00:00 2001 From: Strokkur24 Date: Fri, 19 Sep 2025 23:53:13 +0200 Subject: [PATCH 06/19] feat: flesh out parsing logic further and fix a bunch of issues --- .../internal/parser/TokenParser.java | 36 ++++++++++++++++++- .../StringResolvingMatchedTokenConsumer.java | 10 +++--- .../minimessage/tag/resolver/TagResolver.java | 8 +++-- .../minimessage/MiniMessageParserTest.java | 33 ++++++++++------- .../text/minimessage/MiniMessageTest.java | 34 ++++++++++++++++++ 5 files changed, 99 insertions(+), 22 deletions(-) diff --git a/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/internal/parser/TokenParser.java b/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/internal/parser/TokenParser.java index dde32809d6..48d3828df6 100644 --- a/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/internal/parser/TokenParser.java +++ b/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/internal/parser/TokenParser.java @@ -375,8 +375,18 @@ private static void parseSecondPass(final String message, final List toke currentStringChar = (char) codePoint; } else if (codePoint == ' ') { if (namedArguments == TriState.NOT_SET) { + // Having a whitespace here is nice and all, but there is a slight issue. In the event of a tag just looking like this , + // it should not actually be interpreted as a named argument tag, since it has no arguments which would actually use that. + // We can simply check whether the remainer of this message is blank. + final String substring = message.substring(marker + 1, endIndex); + if (isBlank(substring)) { + i += substring.length(); + break; + } + + insert(token, new Token(marker, i, TokenType.TAG_VALUE)); namedArguments = TriState.TRUE; - marker = i; + marker = i + 1; break; } else if (namedArguments == TriState.FALSE) { // If the arguments are unnamed, spaces are to be interpreted literally @@ -413,6 +423,17 @@ private static void parseSecondPass(final String message, final List toke // anything not matched is the final part if (token.childTokens() == null || token.childTokens().isEmpty()) { insert(token, new Token(startIndex, endIndex, TokenType.TAG_VALUE)); + } else if (namedArguments == TriState.TRUE) { + if (marker < endIndex) { + if (nextNormalIsArgumentValue) { + insert(token, new Token(marker, endIndex, TokenType.TAG_VALUE_NAME)); + } else { + // If there are only whitespace characters remaining, we do not want to create a new token here, as it would be empty + if (!isBlank(message.substring(marker, endIndex))) { + insert(token, new Token(marker, endIndex, TokenType.TAG_VALUE_TOGGLE)); + } + } + } } else { final int end = token.childTokens().get(token.childTokens().size() - 1).endIndex(); if (end != endIndex) { @@ -422,6 +443,19 @@ private static void parseSecondPass(final String message, final List toke } } + private static boolean isBlank(final CharSequence cs) { + int index = 0; + boolean isBlank = true; + while (index < cs.length()) { + if (!Character.isWhitespace(cs.charAt(index++))) { + isBlank = false; + break; + } + } + + return isBlank; + } + /* * Build a tree from the OPEN_TAG and CLOSE_TAG tokens */ diff --git a/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/internal/parser/match/StringResolvingMatchedTokenConsumer.java b/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/internal/parser/match/StringResolvingMatchedTokenConsumer.java index 52e2b921e3..ffa201f665 100644 --- a/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/internal/parser/match/StringResolvingMatchedTokenConsumer.java +++ b/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/internal/parser/match/StringResolvingMatchedTokenConsumer.java @@ -92,13 +92,11 @@ public void accept(final int start, final int end, final @NotNull TokenType toke } // we might care if it's a pre-process! - if (this.tagProvider instanceof TokenParser.QueuedTagProvider) { - final @Nullable Tag replacement = this.tagProvider.resolveQueued(TagProvider.sanitizePlaceholderName(tag), parts, tokens.get(0)); + final @Nullable Tag replacement = this.tagProvider.resolveQueued(TagProvider.sanitizePlaceholderName(tag), parts, tokens.get(0)); - if (replacement instanceof PreProcess) { - this.builder.append(Objects.requireNonNull(((PreProcess) replacement).value(), "PreProcess replacements cannot return null")); - return; - } + if (replacement instanceof PreProcess) { + this.builder.append(Objects.requireNonNull(((PreProcess) replacement).value(), "PreProcess replacements cannot return null")); + return; } } 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 f678adbbdb..9924c30f67 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 @@ -142,6 +142,10 @@ public boolean has(final @NotNull String name) { }; } + static @NotNull TagResolver namedResolver(final @NotNull String name, final @NotNull BiFunction handler) { + return namedResolver(Collections.singleton(name), handler); + } + static @NotNull TagResolver namedResolver(final @NotNull Set names, final @NotNull BiFunction handler) { final Set ownNames = new HashSet<>(names); for (final String name : ownNames) { @@ -340,7 +344,7 @@ interface Queued extends TagResolver { @Override @Nullable default Tag resolveNamed(final @NotNull String name, final @NotNull NamedArgumentMap arguments, final @NotNull Context ctx) throws ParsingException { - throw new UnsupportedOperationException(); + return null; } } @@ -348,7 +352,7 @@ interface Named extends TagResolver { @Override @Nullable default Tag resolve(final @NotNull String name, final @NotNull ArgumentQueue arguments, final @NotNull Context ctx) throws ParsingException { - throw new UnsupportedOperationException(); + return null; } } diff --git a/text-minimessage/src/test/java/net/kyori/adventure/text/minimessage/MiniMessageParserTest.java b/text-minimessage/src/test/java/net/kyori/adventure/text/minimessage/MiniMessageParserTest.java index 7d4b3c2a96..bfa866e57b 100644 --- a/text-minimessage/src/test/java/net/kyori/adventure/text/minimessage/MiniMessageParserTest.java +++ b/text-minimessage/src/test/java/net/kyori/adventure/text/minimessage/MiniMessageParserTest.java @@ -31,11 +31,13 @@ import net.kyori.adventure.text.minimessage.internal.parser.TokenType; 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.NamedArgumentMap; import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder; import net.kyori.adventure.text.minimessage.tag.resolver.TagResolver; import net.kyori.adventure.text.minimessage.tree.Node; import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; import org.junit.jupiter.api.Test; import java.util.ArrayList; @@ -571,19 +573,7 @@ void testValidTagNames() { void invalidPreprocessTagNames() { final String input = "Some<##>of<>theseare<3 >tags"; final Component expected = Component.text("Some<##>of<>these(meow)are<3 >tags"); - final TagResolver alwaysMatchingResolver = new TagResolver.Queued() { - @Override - public Tag resolve(final @NotNull String name, final @NotNull ArgumentQueue arguments, final @NotNull Context ctx) throws ParsingException { - return Tag.preProcessParsed("(meow)"); - } - - @Override - public boolean has(final @NotNull String name) { - return true; - } - }; - - this.assertParsedEquals(expected, input, alwaysMatchingResolver); + this.assertParsedEquals(expected, input, new AlwaysMatchingResolver()); } // https://github.com/KyoriPowered/adventure/issues/1011 @@ -594,4 +584,21 @@ void testNonTerminatingQuoteArgument() { this.assertParsedEquals(expected, input); } + + private static final class AlwaysMatchingResolver implements TagResolver.Queued, TagResolver.Named { + @Override + public @NotNull Tag resolve(final @NotNull String name, final @NotNull ArgumentQueue arguments, final @NotNull Context ctx) throws ParsingException { + return Tag.preProcessParsed("(meow)"); + } + + @Override + public @NotNull Tag resolveNamed(final @NotNull String name, final @NotNull NamedArgumentMap arguments, final @NotNull Context ctx) throws ParsingException { + return Tag.preProcessParsed("(meow)"); + } + + @Override + public boolean has(final @NotNull String name) { + return true; + } + } } diff --git a/text-minimessage/src/test/java/net/kyori/adventure/text/minimessage/MiniMessageTest.java b/text-minimessage/src/test/java/net/kyori/adventure/text/minimessage/MiniMessageTest.java index 8ecb1034ca..598dfc8384 100644 --- a/text-minimessage/src/test/java/net/kyori/adventure/text/minimessage/MiniMessageTest.java +++ b/text-minimessage/src/test/java/net/kyori/adventure/text/minimessage/MiniMessageTest.java @@ -27,6 +27,8 @@ import java.util.Collection; import java.util.List; import java.util.function.Predicate; + +import com.google.common.collect.testing.google.TestStringBiMapGenerator; import net.kyori.adventure.pointer.Pointered; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.minimessage.tag.Tag; @@ -439,6 +441,38 @@ void debugModeMoreComplexNoError() { assertTrue(messages.contains("}")); } + @Test + void debugNamedArguments() { + final String input = " I have a red text!"; + + final StringBuilder sb = new StringBuilder(); + MiniMessage.builder() + .tags(TagResolver.resolver( + // At the time of writing, the tag did not yet exist. + TagResolver.namedResolver("head", (args, ctx) -> Tag.selfClosingInserting(Component.text("dummy"))), + TagResolver.standard() + )).debug(sb::append).build().deserialize(input); + final List messages = Arrays.asList(sb.toString().split("\n")); + + assertTrue(messages.contains("Beginning parsing message I have a red text!")); + assertTrue(messages.contains("Attempting to match node as queued 'red' at column 0")); + assertTrue(anyMatch(messages, it -> it.startsWith("Successfully matched node 'red' to tag "))); + assertTrue(messages.contains("Attempting to match node as named 'head' at column 0")); + assertTrue(anyMatch(messages, it -> it.startsWith("Successfully matched node 'head' to tag "))); + assertTrue(messages.contains("Attempting to match node as queued 'red' at column 52")); + assertTrue(anyMatch(messages, it -> it.startsWith("Successfully matched node 'red' to tag "))); + assertTrue(messages.contains("Text parsed into element tree:")); + assertTrue(messages.contains("Node {")); + assertTrue(messages.contains(" TagNode('head', 'name', 'Strokkur24', 'disable_outer_layer') {")); + assertTrue(messages.contains(" }")); + assertTrue(messages.contains(" TextNode(' I have a ')")); + assertTrue(messages.contains(" TagNode('red') {")); + assertTrue(messages.contains(" TextNode('red')")); + assertTrue(messages.contains(" }")); + assertTrue(messages.contains(" TextNode(' text!')")); + assertTrue(messages.contains("}")); + } + static class TestTarget1 implements Pointered { public String data; } From 08cbeafddbe71fa001075ce37a73f27d4e5d87c2 Mon Sep 17 00:00:00 2001 From: Strokkur24 Date: Sat, 20 Sep 2025 00:04:39 +0200 Subject: [PATCH 07/19] feat: add test for basic named argument parsing --- .../minimessage/internal/parser/TokenParser.java | 2 +- .../text/minimessage/MiniMessageParserTest.java | 15 ++++++++++++++- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/internal/parser/TokenParser.java b/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/internal/parser/TokenParser.java index 48d3828df6..5f680ab2db 100644 --- a/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/internal/parser/TokenParser.java +++ b/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/internal/parser/TokenParser.java @@ -426,7 +426,7 @@ private static void parseSecondPass(final String message, final List toke } else if (namedArguments == TriState.TRUE) { if (marker < endIndex) { if (nextNormalIsArgumentValue) { - insert(token, new Token(marker, endIndex, TokenType.TAG_VALUE_NAME)); + insert(token, new Token(marker, endIndex, TokenType.TAG_VALUE)); } else { // If there are only whitespace characters remaining, we do not want to create a new token here, as it would be empty if (!isBlank(message.substring(marker, endIndex))) { diff --git a/text-minimessage/src/test/java/net/kyori/adventure/text/minimessage/MiniMessageParserTest.java b/text-minimessage/src/test/java/net/kyori/adventure/text/minimessage/MiniMessageParserTest.java index bfa866e57b..d6f41d486f 100644 --- a/text-minimessage/src/test/java/net/kyori/adventure/text/minimessage/MiniMessageParserTest.java +++ b/text-minimessage/src/test/java/net/kyori/adventure/text/minimessage/MiniMessageParserTest.java @@ -37,7 +37,6 @@ import net.kyori.adventure.text.minimessage.tree.Node; import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer; import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; import org.junit.jupiter.api.Test; import java.util.ArrayList; @@ -500,6 +499,20 @@ void testTreeOutput() { assertEquals(expected, tree.toString()); } + @Test + void testBasicInsertingNamedTagArguments() { + final String input = ""; + final Component parsed = MiniMessage.builder() + .tags(TagResolver.namedResolver("insert", (args, ctx) -> Tag.selfClosingInserting( + Component.text(args.getOrThrow("value", "value is missing").value()) + ))) + .build() + .deserialize(input); + + final String parsedString = PlainTextComponentSerializer.plainText().serialize(parsed); + assertEquals("twentyfive", parsedString); + } + @Test void testTagsSelfClosable() { final String input = "hello there"; From 265ad37d1686e4b7f1ca0dde6bbec0fb6a335933 Mon Sep 17 00:00:00 2001 From: Strokkur24 Date: Sat, 20 Sep 2025 00:28:58 +0200 Subject: [PATCH 08/19] feat: start adding more tests --- .../tag/resolver/NamedArgumentMap.java | 1 + .../MiniMessageNamedArgumentsTest.java | 94 +++++++++++++++++++ .../minimessage/MiniMessageParserTest.java | 14 --- 3 files changed, 95 insertions(+), 14 deletions(-) create mode 100644 text-minimessage/src/test/java/net/kyori/adventure/text/minimessage/MiniMessageNamedArgumentsTest.java diff --git a/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/tag/resolver/NamedArgumentMap.java b/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/tag/resolver/NamedArgumentMap.java index da07a10a18..566bde4f3b 100644 --- a/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/tag/resolver/NamedArgumentMap.java +++ b/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/tag/resolver/NamedArgumentMap.java @@ -2,6 +2,7 @@ import net.kyori.adventure.text.minimessage.tag.Tag; import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.Contract; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; diff --git a/text-minimessage/src/test/java/net/kyori/adventure/text/minimessage/MiniMessageNamedArgumentsTest.java b/text-minimessage/src/test/java/net/kyori/adventure/text/minimessage/MiniMessageNamedArgumentsTest.java new file mode 100644 index 0000000000..cc49c41e66 --- /dev/null +++ b/text-minimessage/src/test/java/net/kyori/adventure/text/minimessage/MiniMessageNamedArgumentsTest.java @@ -0,0 +1,94 @@ +package net.kyori.adventure.text.minimessage; + +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.TextColor; +import net.kyori.adventure.text.minimessage.tag.Tag; +import net.kyori.adventure.text.minimessage.tag.resolver.TagResolver; +import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer; +import org.junit.jupiter.api.Test; + +import static net.kyori.adventure.text.Component.text; +import static net.kyori.adventure.text.format.NamedTextColor.BLUE; +import static net.kyori.adventure.text.format.NamedTextColor.RED; +import static net.kyori.adventure.text.format.TextDecoration.BOLD; +import static net.kyori.adventure.text.format.TextDecoration.ITALIC; +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class MiniMessageNamedArgumentsTest extends AbstractTest { + + private static final TagResolver INSERT_VALUE_RESOLVER = TagResolver.namedResolver("insert", (args, ctx) -> Tag.selfClosingInserting( + text(args.getOrThrow("value", "value is missing").value()) + )); + + @Test + void testBasicInsertingNamedTagArguments() { + final String input = ""; + final Component parsed = MiniMessage.builder() + .editTags(b -> b.resolver(INSERT_VALUE_RESOLVER)) + .build() + .deserialize(input); + + final String parsedString = PlainTextComponentSerializer.plainText().serialize(parsed); + assertEquals("twentyfive", parsedString); + } + + @Test + void testWithColorNesting() { + final String input = "This is text!"; + final Component expected = text() + .color(RED) + .append(text("This is ")) + .append(text("some", BLUE)) + .append(text(" text!")) + .build(); + + assertParsedEquals(MiniMessage.miniMessage(), expected, input, INSERT_VALUE_RESOLVER); + } + + @Test + void testMultipleArguments() { + final String input = ""; + final Component expected = text("Hello, World Hello, World Hello, World Hello, World Hello, World"); + + assertParsedEquals(MiniMessage.miniMessage(), expected, input, TagResolver.namedResolver("repeat", + (args, ctx) -> { + final int amount = args.isPresent("amount") ? args.get("amount").asInt().getAsInt() : 1; + final String text = args.getOrThrow("text", "text is missing").value(); + final StringBuilder builder = new StringBuilder(); + for (int i = 0; i < amount; i++) { + builder.append(text); + if (i + 1 < amount) { + builder.append(" "); + } + } + return Tag.selfClosingInserting(text(builder.toString())); + }) + ); + } + + @Test + void testComplexArguments() { + final String input = "This is orange, bold, and italic styled text!"; + final Component expected = text() + .append(text("This is ")) + .append(text("orange, bold, and italic styled", TextColor.color(0xffaa00), BOLD, ITALIC)) + .append(text(" text!")) + .build(); + + assertParsedEquals(MiniMessage.miniMessage(), expected, input, TagResolver.namedResolver("styled", + (args, ctx) -> Tag.styling(builder -> { + if (args.isPresent("color")) { + builder.color(TextColor.fromCSSHexString(args.getOrThrow("color", "color is missing").value())); + } + + if (args.isPresent("bold")) { + builder.decorate(BOLD); + } + + if (args.isPresent("italic")) { + builder.decorate(ITALIC); + } + })) + ); + } +} diff --git a/text-minimessage/src/test/java/net/kyori/adventure/text/minimessage/MiniMessageParserTest.java b/text-minimessage/src/test/java/net/kyori/adventure/text/minimessage/MiniMessageParserTest.java index d6f41d486f..7aae987992 100644 --- a/text-minimessage/src/test/java/net/kyori/adventure/text/minimessage/MiniMessageParserTest.java +++ b/text-minimessage/src/test/java/net/kyori/adventure/text/minimessage/MiniMessageParserTest.java @@ -499,20 +499,6 @@ void testTreeOutput() { assertEquals(expected, tree.toString()); } - @Test - void testBasicInsertingNamedTagArguments() { - final String input = ""; - final Component parsed = MiniMessage.builder() - .tags(TagResolver.namedResolver("insert", (args, ctx) -> Tag.selfClosingInserting( - Component.text(args.getOrThrow("value", "value is missing").value()) - ))) - .build() - .deserialize(input); - - final String parsedString = PlainTextComponentSerializer.plainText().serialize(parsed); - assertEquals("twentyfive", parsedString); - } - @Test void testTagsSelfClosable() { final String input = "hello there"; From 2a67407443e427ca51b65d50bcd6c0f8dc669aaa Mon Sep 17 00:00:00 2001 From: Strokkur24 Date: Sat, 20 Sep 2025 00:51:20 +0200 Subject: [PATCH 09/19] fix: tag with space being recognized as valid tag --- .../internal/parser/TokenParser.java | 2 +- .../MiniMessageNamedArgumentsTest.java | 20 +++++++++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/internal/parser/TokenParser.java b/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/internal/parser/TokenParser.java index 5f680ab2db..9e7d6b4212 100644 --- a/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/internal/parser/TokenParser.java +++ b/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/internal/parser/TokenParser.java @@ -378,7 +378,7 @@ private static void parseSecondPass(final String message, final List toke // Having a whitespace here is nice and all, but there is a slight issue. In the event of a tag just looking like this , // it should not actually be interpreted as a named argument tag, since it has no arguments which would actually use that. // We can simply check whether the remainer of this message is blank. - final String substring = message.substring(marker + 1, endIndex); + final String substring = message.substring(i, endIndex); if (isBlank(substring)) { i += substring.length(); break; diff --git a/text-minimessage/src/test/java/net/kyori/adventure/text/minimessage/MiniMessageNamedArgumentsTest.java b/text-minimessage/src/test/java/net/kyori/adventure/text/minimessage/MiniMessageNamedArgumentsTest.java index cc49c41e66..a8cf2194e5 100644 --- a/text-minimessage/src/test/java/net/kyori/adventure/text/minimessage/MiniMessageNamedArgumentsTest.java +++ b/text-minimessage/src/test/java/net/kyori/adventure/text/minimessage/MiniMessageNamedArgumentsTest.java @@ -91,4 +91,24 @@ void testComplexArguments() { })) ); } + + @Test + void testWithExtraWhitespace() { + final String input = ""; + final Component expected = text("too much?"); + assertParsedEquals(MiniMessage.miniMessage(), expected, input, INSERT_VALUE_RESOLVER); + } + + @Test + void testWithQueuedAndExtraWhitespace() { + final String input = "This tag does not count, this does not either, but the red one does!"; + final Component expected = text() + .append(text("This tag does not count, this does not either, but the ")) + .append(text("red one does!", RED)) + .build(); + assertParsedEquals(MiniMessage + .builder() + .debug(System.out::print) + .build(), expected, input, INSERT_VALUE_RESOLVER); + } } From 84bd76c30e5c659273c57222791f8e91b585aa17 Mon Sep 17 00:00:00 2001 From: Strokkur24 Date: Sat, 20 Sep 2025 00:59:19 +0200 Subject: [PATCH 10/19] feat: add a bunch more tests --- .../MiniMessageNamedArgumentsTest.java | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/text-minimessage/src/test/java/net/kyori/adventure/text/minimessage/MiniMessageNamedArgumentsTest.java b/text-minimessage/src/test/java/net/kyori/adventure/text/minimessage/MiniMessageNamedArgumentsTest.java index a8cf2194e5..5f5560a367 100644 --- a/text-minimessage/src/test/java/net/kyori/adventure/text/minimessage/MiniMessageNamedArgumentsTest.java +++ b/text-minimessage/src/test/java/net/kyori/adventure/text/minimessage/MiniMessageNamedArgumentsTest.java @@ -111,4 +111,38 @@ void testWithQueuedAndExtraWhitespace() { .debug(System.out::print) .build(), expected, input, INSERT_VALUE_RESOLVER); } + + @Test + void testQueuedTreatedAsNamed() { + final String input = ""; + final Component expected = text(""); + assertParsedEquals(expected, input); + } + + @Test + void testNamedTreatedAsQueued() { + final String input = ""; + final Component expected = text(""); + assertParsedEquals(MiniMessage.miniMessage(), expected, input, TagResolver.namedResolver("named", (args, ctx) -> Tag.inserting(text("wrong!")))); + } + + @Test + void testNoArgsAlwaysTreatedAsQueued() { + final String input = ""; + final Component expected = text("pass"); + assertParsedEquals(MiniMessage.miniMessage(), expected, input, + TagResolver.namedResolver("test", (args, ctx) -> Tag.inserting(text("fail"))), + TagResolver.resolver("test", (args, ctx) -> Tag.inserting(text("pass"))) + ); + } + + @Test + void testNamedQueuedCanCoexist() { + final String input = " "; + final Component expected = text("Hello World!"); + assertParsedEquals(MiniMessage.miniMessage(), expected, input, + TagResolver.resolver("test", (args, ctx) -> Tag.inserting(text("Hello"))), + TagResolver.namedResolver("test", (args, ctx) -> Tag.inserting(text("World!"))) + ); + } } From aac5f9ab669f712bd139e7ead73279681a4154a3 Mon Sep 17 00:00:00 2001 From: Strokkur24 Date: Sat, 20 Sep 2025 01:38:34 +0200 Subject: [PATCH 11/19] chore: fix all compile time issues --- .../adventure/text/minimessage/Context.java | 8 ++ .../text/minimessage/ContextImpl.java | 9 +- .../minimessage/NamedArgumentMapImpl.java | 38 +++++-- .../internal/parser/TokenParser.java | 103 ++++++++++++++---- .../StringResolvingMatchedTokenConsumer.java | 2 +- .../internal/parser/node/NamedTagPart.java | 25 ----- .../internal/parser/node/TagNode.java | 2 - .../internal/parser/node/TagPart.java | 2 +- .../adventure/text/minimessage/tag/Tag.java | 4 - .../tag/resolver/NamedArgumentMap.java | 50 +++++++-- .../minimessage/tag/resolver/TagResolver.java | 70 +++++++++++- .../MiniMessageNamedArgumentsTest.java | 29 ++++- .../minimessage/MiniMessageParserTest.java | 7 +- .../text/minimessage/MiniMessageTest.java | 2 - 14 files changed, 264 insertions(+), 87 deletions(-) delete mode 100644 text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/internal/parser/node/NamedTagPart.java 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 b304640dd1..690feed3b1 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 @@ -142,6 +142,14 @@ public interface Context { final @NotNull ArgumentQueue args ); + /** + * Create a new parsing exception. + * + * @param message a detail message describing the error + * @param cause the cause + * @param args arguments that caused the errors + * @return the new parsing exception + */ @NotNull ParsingException newException( final @NotNull String message, final @Nullable Throwable cause, 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 14b54a2c7a..684e35e1d1 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 @@ -23,6 +23,10 @@ */ 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.minimessage.internal.parser.ParsingExceptionImpl; @@ -35,11 +39,6 @@ import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -import java.util.List; -import java.util.Map; -import java.util.function.Consumer; -import java.util.function.UnaryOperator; - import static java.util.Objects.requireNonNull; /** diff --git a/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/NamedArgumentMapImpl.java b/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/NamedArgumentMapImpl.java index 5fe13201bc..da065292fe 100644 --- a/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/NamedArgumentMapImpl.java +++ b/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/NamedArgumentMapImpl.java @@ -1,20 +1,42 @@ +/* + * 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; +import java.util.function.Supplier; import net.kyori.adventure.text.minimessage.tag.Tag; import net.kyori.adventure.text.minimessage.tag.resolver.NamedArgumentMap; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -import java.util.Map; -import java.util.function.Supplier; - import static java.util.Objects.requireNonNull; final class NamedArgumentMapImpl implements NamedArgumentMap { private final Context context; final Map args; - public NamedArgumentMapImpl(final Context context, final Map args) { + NamedArgumentMapImpl(final Context context, final Map args) { this.context = context; this.args = args; } @@ -37,9 +59,9 @@ public int size() { } @Override - public Tag.@NotNull Argument getOrThrow(@NotNull String name, final @NotNull String errorMessage) { + public Tag.@NotNull Argument elseThrow(final @NotNull String name, final @NotNull String errorMessage) { requireNonNull(errorMessage, "errorMessage"); - final Tag.Argument arg = get(name); + final Tag.Argument arg = this.get(name); if (arg == null) { throw this.context.newException(errorMessage); } @@ -47,9 +69,9 @@ public int size() { } @Override - public Tag.@NotNull Argument getOrThrow(@NotNull String name, final @NotNull Supplier errorMessage) { + public Tag.@NotNull Argument elseThrow(final @NotNull String name, final @NotNull Supplier errorMessage) { requireNonNull(errorMessage, "errorMessage"); - final Tag.Argument arg = get(name); + final Tag.Argument arg = this.get(name); if (arg == null) { throw this.context.newException(errorMessage.get()); } diff --git a/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/internal/parser/TokenParser.java b/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/internal/parser/TokenParser.java index 9e7d6b4212..b14d7ac2f5 100644 --- a/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/internal/parser/TokenParser.java +++ b/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/internal/parser/TokenParser.java @@ -23,6 +23,15 @@ */ package net.kyori.adventure.text.minimessage.internal.parser; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.ListIterator; +import java.util.Locale; +import java.util.Map; +import java.util.TreeMap; +import java.util.function.IntPredicate; +import java.util.function.Predicate; import net.kyori.adventure.text.minimessage.ParsingException; import net.kyori.adventure.text.minimessage.internal.TagInternals; import net.kyori.adventure.text.minimessage.internal.parser.match.MatchedTokenConsumer; @@ -41,16 +50,6 @@ import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.ListIterator; -import java.util.Locale; -import java.util.Map; -import java.util.TreeMap; -import java.util.function.IntPredicate; -import java.util.function.Predicate; - /** * Handles parsing a string into a list of tokens and then into a tree of nodes. * @@ -84,7 +83,7 @@ private TokenParser() { * @since 4.10.0 */ public static RootNode parse( - final @NotNull TokenParser.TagProvider tagProvider, + final TokenParser.@NotNull TagProvider tagProvider, final @NotNull Predicate tagNameChecker, final @NotNull String message, final @NotNull String originalMessage, @@ -460,7 +459,7 @@ private static boolean isBlank(final CharSequence cs) { * Build a tree from the OPEN_TAG and CLOSE_TAG tokens */ private static RootNode buildTree( - final @NotNull TokenParser.TagProvider tagProvider, + final TokenParser.@NotNull TagProvider tagProvider, final @NotNull Predicate tagNameChecker, final @NotNull List tokens, final @NotNull String message, @@ -621,6 +620,7 @@ private static RootNode buildTree( * * @param closeParts The parts of the close tag * @param openParts The parts of the open tag + * @param tag part * @return {@code true} if the given close parts closes the open tag parts. */ private static boolean tagCloses(final List closeParts, final List openParts) { @@ -734,6 +734,11 @@ public static String unescape(final String text, final int startIndex, final int return sb.toString(); } + /** + * A special tag provider for tags with queued arguments. + * + * @param argument + */ @ApiStatus.Internal public interface QueuedTagProvider { /** @@ -750,6 +755,11 @@ public interface QueuedTagProvider { @Nullable Tag resolveQueued(final @NotNull String name, final @NotNull List trimmedArgs, final @Nullable Token token); } + /** + * A special tag provider for tags with named arguments. + * + * @param argument + */ @ApiStatus.Internal public interface NamedTagProvider { /** @@ -769,11 +779,19 @@ public interface NamedTagProvider { /** * Normalizing provider for tag information. * + * @param tag argument * @since 4.10.0 */ @ApiStatus.Internal public interface TagProvider extends QueuedTagProvider, NamedTagProvider { + /** + * Get whether a list of tokens contains a {@link TokenType#TAG_VALUE_NAME} or {@link TokenType#TAG_VALUE_TOGGLE}. + * + * @param trimmedTokens list of tokens + * @return whether a list of tokens contains a {@link TokenType#TAG_VALUE_NAME} or {@link TokenType#TAG_VALUE_TOGGLE} + * @since 4.25.0 + */ default boolean isNamed(final @NotNull List trimmedTokens) { for (final Token trimmedToken : trimmedTokens) { if (trimmedToken.type() == TokenType.TAG_VALUE_NAME || trimmedToken.type() == TokenType.TAG_VALUE_TOGGLE) { @@ -783,8 +801,15 @@ default boolean isNamed(final @NotNull List trimmedTokens) { return false; } + /** + * Get whether this tag node has named arguments. + * + * @param node the node + * @return whether this tag node has named arguments + * @since 4.25.0 + */ default boolean isNamed(final @NotNull TagNode node) { - return isNamed(node.token().childTokens()); + return this.isNamed(node.token().childTokens()); } /** @@ -792,7 +817,7 @@ default boolean isNamed(final @NotNull TagNode node) { * * @param name sanitized name * @return a tag, if any is available - * @since 4.10.0 + * @since 4.25.0 */ default @Nullable Tag resolveQueued(final @NotNull String name) { return this.resolveQueued(name, Collections.emptyList(), null); @@ -803,14 +828,27 @@ default boolean isNamed(final @NotNull TagNode node) { * * @param name sanitized name * @return a tag, if any is available - * @since 4.10.0 + * @since 4.25.0 */ default @Nullable Tag resolveNamed(final @NotNull String name) { return this.resolveNamed(name, Collections.emptyMap(), null); } + /** + * Resolve the provided node smartly. + * + *

+ * This method first checks if the node is named and then routes + * the call to either {@link #resolveNamed(TagNode)} or {@link #resolveQueued(TagNode)} + * depending on the result. + *

+ * + * @param node the node + * @return the resolved tag, or null + * @since 4.25.0 + */ default @Nullable Tag resolve(final @NotNull TagNode node) { - return isNamed(node) ? resolveNamed(node) : resolveQueued(node); + return this.isNamed(node) ? this.resolveNamed(node) : this.resolveQueued(node); } /** @@ -818,7 +856,7 @@ default boolean isNamed(final @NotNull TagNode node) { * * @param node tag node * @return a tag, if any is available - * @since 4.10.0 + * @since 4.25.0 */ default @Nullable Tag resolveQueued(final @NotNull TagNode node) { return this.resolveQueued( @@ -833,12 +871,12 @@ default boolean isNamed(final @NotNull TagNode node) { * * @param node tag node * @return a tag, if any is available - * @since 4.10.0 + * @since 4.25.0 */ default @Nullable Tag resolveNamed(final @NotNull TagNode node) { final Map map = new TreeMap<>(); - @NotNull List parts = node.parts(); + final List parts = node.parts(); for (int i = 1, partsSize = parts.size(); i < partsSize; i++) { final TagPart part = parts.get(i); @@ -878,24 +916,47 @@ default boolean isNamed(final @NotNull TagNode node) { } } + /** + * A basic implementation of a {@link TagProvider} for convenience. + * + * @param tag argument + * @since 4.25.0 + */ @ApiStatus.Internal public static final class TagProviderImpl implements TagProvider { private final QueuedTagProvider queued; private final NamedTagProvider named; + /** + * Construct a new {@link TagProviderImpl} object. + * + * @param queued the queued provider + * @param named the named provider + * @since 4.25.0 + */ public TagProviderImpl(final QueuedTagProvider queued, final NamedTagProvider named) { this.queued = queued; this.named = named; } + /** + * {@inheritDoc} + * + * @since 4.25.0 + */ @Override public @Nullable Tag resolveNamed(final @NotNull String name, final @NotNull Map trimmedArgs, final @Nullable Token token) { - return named.resolveNamed(name, trimmedArgs, token); + return this.named.resolveNamed(name, trimmedArgs, token); } + /** + * {@inheritDoc} + * + * @since 4.25.0 + */ @Override public @Nullable Tag resolveQueued(final @NotNull String name, final @NotNull List trimmedArgs, final @Nullable Token token) { - return queued.resolveQueued(name, trimmedArgs, token); + return this.queued.resolveQueued(name, trimmedArgs, token); } } } diff --git a/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/internal/parser/match/StringResolvingMatchedTokenConsumer.java b/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/internal/parser/match/StringResolvingMatchedTokenConsumer.java index ffa201f665..237e57ebe0 100644 --- a/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/internal/parser/match/StringResolvingMatchedTokenConsumer.java +++ b/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/internal/parser/match/StringResolvingMatchedTokenConsumer.java @@ -58,7 +58,7 @@ public final class StringResolvingMatchedTokenConsumer extends MatchedTokenConsu */ public StringResolvingMatchedTokenConsumer( final @NotNull String input, - final @NotNull TokenParser.TagProvider tagProvider + final TokenParser.@NotNull TagProvider tagProvider ) { super(input); this.builder = new StringBuilder(input.length()); diff --git a/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/internal/parser/node/NamedTagPart.java b/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/internal/parser/node/NamedTagPart.java deleted file mode 100644 index ae22040eee..0000000000 --- a/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/internal/parser/node/NamedTagPart.java +++ /dev/null @@ -1,25 +0,0 @@ -package net.kyori.adventure.text.minimessage.internal.parser.node; - -import net.kyori.adventure.text.minimessage.internal.parser.Token; -import net.kyori.adventure.text.minimessage.internal.parser.TokenParser; -import net.kyori.adventure.text.minimessage.tag.Tag; -import org.jetbrains.annotations.NotNull; - -public final class NamedTagPart extends TagPart implements Tag.NamedArgument { - private final String name; - - public NamedTagPart( - final @NotNull String sourceMessage, - final @NotNull Token token, - final TokenParser.@NotNull TagProvider tagResolver, - final String name - ) { - super(sourceMessage, token, tagResolver); - this.name = name; - } - - @Override - public @NotNull String name() { - return this.name; - } -} diff --git a/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/internal/parser/node/TagNode.java b/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/internal/parser/node/TagNode.java index c28820cdfc..7dd5e63069 100644 --- a/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/internal/parser/node/TagNode.java +++ b/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/internal/parser/node/TagNode.java @@ -26,11 +26,9 @@ import java.util.ArrayList; import java.util.List; import java.util.Objects; - 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.TokenParser; -import net.kyori.adventure.text.minimessage.internal.parser.TokenType; import net.kyori.adventure.text.minimessage.tag.Tag; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; diff --git a/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/internal/parser/node/TagPart.java b/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/internal/parser/node/TagPart.java index 95efbbd220..eeb1b9b663 100644 --- a/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/internal/parser/node/TagPart.java +++ b/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/internal/parser/node/TagPart.java @@ -33,7 +33,7 @@ * * @since 4.10.0 */ -public /* sealed */ class TagPart implements Tag.Argument /* permits NamedTagPart */ { +public class TagPart implements Tag.Argument { private final String value; private final Token token; diff --git a/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/tag/Tag.java b/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/tag/Tag.java index 91b6e0df6c..26048d3fcd 100644 --- a/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/tag/Tag.java +++ b/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/tag/Tag.java @@ -217,8 +217,4 @@ default boolean isFalse() { } } } - - interface NamedArgument extends Argument { - @NotNull String name(); - } } diff --git a/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/tag/resolver/NamedArgumentMap.java b/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/tag/resolver/NamedArgumentMap.java index 566bde4f3b..7373b0f285 100644 --- a/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/tag/resolver/NamedArgumentMap.java +++ b/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/tag/resolver/NamedArgumentMap.java @@ -1,49 +1,85 @@ +/* + * 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.tag.resolver; +import java.util.function.Supplier; import net.kyori.adventure.text.minimessage.tag.Tag; import org.jetbrains.annotations.ApiStatus; -import org.jetbrains.annotations.Contract; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -import java.util.function.Supplier; - /** * A map of named {@link Tag} arguments. + * + * @since 4.25.0 */ @ApiStatus.NonExtendable public interface NamedArgumentMap { /** - * {@return whether an argument by this name is present} + * Get whether an argument of that name exists. + * + * @param name name of the argument or flag + * @return whether an argument by this name is present + * @since 4.25.0 */ boolean isPresent(@NotNull String name); /** - * {@return the number of arguments present} + * Get the number of arguments present. + * + * @return the number of arguments present + * @since 4.25.0 */ int size(); /** * Get an argument by its name, returning {@code null} if none was found. * + * @param name name of the argument * @return the argument + * @since 4.25.0 */ Tag.@Nullable Argument get(@NotNull String name); /** * Get an argument by its name, throwing an exception if no argument with that name was present. * + * @param name name of the argument * @param errorMessage the error to throw if an argument with that name is not present * @return the argument + * @since 4.25.0 */ - Tag.@NotNull Argument getOrThrow(@NotNull String name, @NotNull String errorMessage); + Tag.@NotNull Argument elseThrow(@NotNull String name, @NotNull String errorMessage); /** * Get an argument by its name, throwing an exception if no argument with that name was present. * + * @param name name of the argument * @param errorMessage the error to throw if an argument with that name is not present * @return the argument + * @since 4.25.0 */ - Tag.@NotNull Argument getOrThrow(@NotNull String name, @NotNull Supplier errorMessage); + Tag.@NotNull Argument elseThrow(@NotNull String name, @NotNull Supplier errorMessage); } 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 9924c30f67..a49b55118c 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 @@ -142,10 +142,34 @@ public boolean has(final @NotNull String name) { }; } + /** + * Create a tag resolver that only responds to certain tag names, and whose value does not depend on that name. + * + *

+ * This method creates a special resolver which listens to tags with named arguments instead of sequences ones. + *

+ * + * @param name the name to respond to + * @param handler the tag handler, may throw {@link ParsingException} if provided arguments are in an invalid format + * @return a resolver that creates tags using the provided handler + * @since 4.25.0 + */ static @NotNull TagResolver namedResolver(final @NotNull String name, final @NotNull BiFunction handler) { return namedResolver(Collections.singleton(name), handler); } + /** + * Create a tag resolver that only responds to certain tag names, and whose value does not depend on that name. + * + *

+ * This method creates a special resolver which listens to tags with named arguments instead of sequences ones. + *

+ * + * @param names the names to respond to + * @param handler the tag handler, may throw {@link ParsingException} if provided arguments are in an invalid format + * @return a resolver that creates tags using the provided handler + * @since 4.25.0 + */ static @NotNull TagResolver namedResolver(final @NotNull Set names, final @NotNull BiFunction handler) { final Set ownNames = new HashSet<>(names); for (final String name : ownNames) { @@ -249,6 +273,16 @@ public boolean has(final @NotNull String name) { */ @Nullable Tag resolve(@TagPattern final @NotNull String name, final @NotNull ArgumentQueue arguments, final @NotNull Context ctx) throws ParsingException; + /** + * Gets a tag with named arguments from this resolver based on the current state. + * + * @param name the tag name + * @param arguments the arguments passed to the tag + * @param ctx the parse context + * @return a possible tag + * @throws ParsingException if the provided arguments are invalid + * @since 4.25.0 + */ @Nullable Tag resolveNamed(@TagPattern final @NotNull String name, final @NotNull NamedArgumentMap arguments, final @NotNull Context ctx) throws ParsingException; /** @@ -340,18 +374,26 @@ default boolean has(final @NotNull String name) { } } + /** + * A {@link TagResolver} which only listens to sequential arguments. + * + * @since 4.25.0 + */ interface Queued extends TagResolver { @Override - @Nullable - default Tag resolveNamed(final @NotNull String name, final @NotNull NamedArgumentMap arguments, final @NotNull Context ctx) throws ParsingException { + default @Nullable Tag resolveNamed(final @NotNull String name, final @NotNull NamedArgumentMap arguments, final @NotNull Context ctx) throws ParsingException { return null; } } + /** + * A {@link TagResolver} which only listens to named arguments. + * + * @since 4.25.0 + */ interface Named extends TagResolver { @Override - @Nullable - default Tag resolve(final @NotNull String name, final @NotNull ArgumentQueue arguments, final @NotNull Context ctx) throws ParsingException { + default @Nullable Tag resolve(final @NotNull String name, final @NotNull ArgumentQueue arguments, final @NotNull Context ctx) throws ParsingException { return null; } } @@ -398,10 +440,30 @@ interface Builder { return this.resolver(TagResolver.resolver(names, handler)); } + /** + * Add a single dynamically created tag to this resolver. + * + *

This method adds a special resolver which looks for named arguments instead of sequential arguments.

+ * + * @param name the name to respond to + * @param handler the tag handler, may throw {@link ParsingException} if provided arguments are in an invalid format + * @return this builder + * @since 4.25.0 + */ default @NotNull Builder namedArgumentsTag(@TagPattern final @NotNull String name, final @NotNull BiFunction handler) { return this.namedArgumentsTag(Collections.singleton(name), handler); } + /** + * Add a single dynamically created tag to this resolver. + * + *

This method adds a special resolver which looks for named arguments instead of sequential arguments.

+ * + * @param names the names to respond to + * @param handler the tag handler, may throw {@link ParsingException} if provided arguments are in an invalid format + * @return this builder + * @since 4.25.0 + */ default @NotNull Builder namedArgumentsTag(final @NotNull Set names, final @NotNull BiFunction handler) { return this.resolver(TagResolver.namedResolver(names, handler)); } diff --git a/text-minimessage/src/test/java/net/kyori/adventure/text/minimessage/MiniMessageNamedArgumentsTest.java b/text-minimessage/src/test/java/net/kyori/adventure/text/minimessage/MiniMessageNamedArgumentsTest.java index 5f5560a367..8019936262 100644 --- a/text-minimessage/src/test/java/net/kyori/adventure/text/minimessage/MiniMessageNamedArgumentsTest.java +++ b/text-minimessage/src/test/java/net/kyori/adventure/text/minimessage/MiniMessageNamedArgumentsTest.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 net.kyori.adventure.text.Component; @@ -17,7 +40,7 @@ public class MiniMessageNamedArgumentsTest extends AbstractTest { private static final TagResolver INSERT_VALUE_RESOLVER = TagResolver.namedResolver("insert", (args, ctx) -> Tag.selfClosingInserting( - text(args.getOrThrow("value", "value is missing").value()) + text(args.elseThrow("value", "value is missing").value()) )); @Test @@ -53,7 +76,7 @@ void testMultipleArguments() { assertParsedEquals(MiniMessage.miniMessage(), expected, input, TagResolver.namedResolver("repeat", (args, ctx) -> { final int amount = args.isPresent("amount") ? args.get("amount").asInt().getAsInt() : 1; - final String text = args.getOrThrow("text", "text is missing").value(); + final String text = args.elseThrow("text", "text is missing").value(); final StringBuilder builder = new StringBuilder(); for (int i = 0; i < amount; i++) { builder.append(text); @@ -78,7 +101,7 @@ void testComplexArguments() { assertParsedEquals(MiniMessage.miniMessage(), expected, input, TagResolver.namedResolver("styled", (args, ctx) -> Tag.styling(builder -> { if (args.isPresent("color")) { - builder.color(TextColor.fromCSSHexString(args.getOrThrow("color", "color is missing").value())); + builder.color(TextColor.fromCSSHexString(args.elseThrow("color", "color is missing").value())); } if (args.isPresent("bold")) { diff --git a/text-minimessage/src/test/java/net/kyori/adventure/text/minimessage/MiniMessageParserTest.java b/text-minimessage/src/test/java/net/kyori/adventure/text/minimessage/MiniMessageParserTest.java index 7aae987992..38618c591c 100644 --- a/text-minimessage/src/test/java/net/kyori/adventure/text/minimessage/MiniMessageParserTest.java +++ b/text-minimessage/src/test/java/net/kyori/adventure/text/minimessage/MiniMessageParserTest.java @@ -23,6 +23,9 @@ */ package net.kyori.adventure.text.minimessage; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.TextComponent; import net.kyori.adventure.text.format.NamedTextColor; @@ -39,10 +42,6 @@ import org.jetbrains.annotations.NotNull; import org.junit.jupiter.api.Test; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; - import static net.kyori.adventure.text.Component.empty; import static net.kyori.adventure.text.Component.text; import static net.kyori.adventure.text.Component.translatable; diff --git a/text-minimessage/src/test/java/net/kyori/adventure/text/minimessage/MiniMessageTest.java b/text-minimessage/src/test/java/net/kyori/adventure/text/minimessage/MiniMessageTest.java index 598dfc8384..de819b596e 100644 --- a/text-minimessage/src/test/java/net/kyori/adventure/text/minimessage/MiniMessageTest.java +++ b/text-minimessage/src/test/java/net/kyori/adventure/text/minimessage/MiniMessageTest.java @@ -27,8 +27,6 @@ import java.util.Collection; import java.util.List; import java.util.function.Predicate; - -import com.google.common.collect.testing.google.TestStringBiMapGenerator; import net.kyori.adventure.pointer.Pointered; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.minimessage.tag.Tag; From 557be892099f502b53fff271b259a4398377af00 Mon Sep 17 00:00:00 2001 From: Strokkur24 Date: Sat, 20 Sep 2025 01:40:22 +0200 Subject: [PATCH 12/19] feat: add test --- .../text/minimessage/MiniMessageNamedArgumentsTest.java | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/text-minimessage/src/test/java/net/kyori/adventure/text/minimessage/MiniMessageNamedArgumentsTest.java b/text-minimessage/src/test/java/net/kyori/adventure/text/minimessage/MiniMessageNamedArgumentsTest.java index 8019936262..5b5431aa09 100644 --- a/text-minimessage/src/test/java/net/kyori/adventure/text/minimessage/MiniMessageNamedArgumentsTest.java +++ b/text-minimessage/src/test/java/net/kyori/adventure/text/minimessage/MiniMessageNamedArgumentsTest.java @@ -135,6 +135,13 @@ void testWithQueuedAndExtraWhitespace() { .build(), expected, input, INSERT_VALUE_RESOLVER); } + @Test + void testWhitespaceBeforeQueued() { + final String input = ""; + final Component expected = text(""); + assertParsedEquals(expected, input); + } + @Test void testQueuedTreatedAsNamed() { final String input = ""; From bc563a1f8b75b24c89e92a42a4e8a4155bd25d6e Mon Sep 17 00:00:00 2001 From: Strokkur24 Date: Sat, 20 Sep 2025 01:53:37 +0200 Subject: [PATCH 13/19] chore: cleanup diff and rename to sequential --- .../text/minimessage/MiniMessageParser.java | 10 ++--- .../internal/parser/TokenParser.java | 39 +++++++++---------- .../StringResolvingMatchedTokenConsumer.java | 6 +-- .../internal/parser/node/TagPart.java | 2 +- .../ComponentClaimingResolverImpl.java | 2 +- .../serializer/StyleClaimingResolverImpl.java | 2 +- .../tag/resolver/SequentialTagResolver.java | 4 +- .../minimessage/tag/resolver/TagResolver.java | 6 +-- .../tag/standard/ColorTagResolver.java | 2 +- .../minimessage/translation/ArgumentTag.java | 2 +- .../minimessage/MiniMessageParserTest.java | 2 +- .../text/minimessage/MiniMessageTest.java | 18 ++++----- 12 files changed, 46 insertions(+), 49 deletions(-) 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 f19da09851..4fa366162f 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 @@ -135,13 +135,13 @@ private void processTokens(final @NotNull StringBuilder sb, final @NotNull Strin debug.accept("\n"); } - final TokenParser.QueuedTagProvider queuedTagProvider; + final TokenParser.SequentialTagProvider sequentialTagProvider; final TokenParser.NamedTagProvider namedTagProvider; if (debug != null) { - queuedTagProvider = (name, args, token) -> { + sequentialTagProvider = (name, args, token) -> { try { - debug.accept("Attempting to match node as queued '"); + debug.accept("Attempting to match node as sequential '"); debug.accept(name); debug.accept("'"); if (token != null) { @@ -222,7 +222,7 @@ private void processTokens(final @NotNull StringBuilder sb, final @NotNull Strin } }; } else { - queuedTagProvider = (name, args, token) -> { + sequentialTagProvider = (name, args, token) -> { try { return combinedResolver.resolve(name, new ArgumentQueueImpl<>(context, args), context); } catch (final ParsingException ignored) { @@ -238,7 +238,7 @@ private void processTokens(final @NotNull StringBuilder sb, final @NotNull Strin }; } - final TokenParser.TagProvider transformationFactory = new TokenParser.TagProviderImpl(queuedTagProvider, namedTagProvider); + final TokenParser.TagProvider transformationFactory = new TokenParser.TagProviderImpl(sequentialTagProvider, namedTagProvider); final Predicate tagNameChecker = name -> { final String sanitized = TokenParser.TagProvider.sanitizePlaceholderName(name); return combinedResolver.has(sanitized); diff --git a/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/internal/parser/TokenParser.java b/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/internal/parser/TokenParser.java index b14d7ac2f5..96e50a4658 100644 --- a/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/internal/parser/TokenParser.java +++ b/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/internal/parser/TokenParser.java @@ -83,7 +83,7 @@ private TokenParser() { * @since 4.10.0 */ public static RootNode parse( - final TokenParser.@NotNull TagProvider tagProvider, + final @NotNull TagProvider tagProvider, final @NotNull Predicate tagNameChecker, final @NotNull String message, final @NotNull String originalMessage, @@ -459,7 +459,7 @@ private static boolean isBlank(final CharSequence cs) { * Build a tree from the OPEN_TAG and CLOSE_TAG tokens */ private static RootNode buildTree( - final TokenParser.@NotNull TagProvider tagProvider, + final @NotNull TagProvider tagProvider, final @NotNull Predicate tagNameChecker, final @NotNull List tokens, final @NotNull String message, @@ -530,7 +530,7 @@ private static RootNode buildTree( final String closeTagName = closeValues.get(0); if (tagNameChecker.test(closeTagName)) { - final Tag tag = tagProvider.resolveQueued(closeTagName); + final Tag tag = tagProvider.resolveSequential(closeTagName); if (tag == ParserDirective.RESET) { // This is a synthetic node, closing it means nothing in the context of building a tree @@ -620,10 +620,9 @@ private static RootNode buildTree( * * @param closeParts The parts of the close tag * @param openParts The parts of the open tag - * @param tag part * @return {@code true} if the given close parts closes the open tag parts. */ - private static boolean tagCloses(final List closeParts, final List openParts) { + private static boolean tagCloses(final List closeParts, final List openParts) { if (closeParts.size() > openParts.size()) { return false; } @@ -740,7 +739,7 @@ public static String unescape(final String text, final int startIndex, final int * @param argument */ @ApiStatus.Internal - public interface QueuedTagProvider { + public interface SequentialTagProvider { /** * Look up a tag. * @@ -752,7 +751,7 @@ public interface QueuedTagProvider { * @return a tag * @since 4.10.0 */ - @Nullable Tag resolveQueued(final @NotNull String name, final @NotNull List trimmedArgs, final @Nullable Token token); + @Nullable Tag resolveSequential(final @NotNull String name, final @NotNull List trimmedArgs, final @Nullable Token token); } /** @@ -783,7 +782,7 @@ public interface NamedTagProvider { * @since 4.10.0 */ @ApiStatus.Internal - public interface TagProvider extends QueuedTagProvider, NamedTagProvider { + public interface TagProvider extends SequentialTagProvider, NamedTagProvider { /** * Get whether a list of tokens contains a {@link TokenType#TAG_VALUE_NAME} or {@link TokenType#TAG_VALUE_TOGGLE}. @@ -819,8 +818,8 @@ default boolean isNamed(final @NotNull TagNode node) { * @return a tag, if any is available * @since 4.25.0 */ - default @Nullable Tag resolveQueued(final @NotNull String name) { - return this.resolveQueued(name, Collections.emptyList(), null); + default @Nullable Tag resolveSequential(final @NotNull String name) { + return this.resolveSequential(name, Collections.emptyList(), null); } /** @@ -839,7 +838,7 @@ default boolean isNamed(final @NotNull TagNode node) { * *

* This method first checks if the node is named and then routes - * the call to either {@link #resolveNamed(TagNode)} or {@link #resolveQueued(TagNode)} + * the call to either {@link #resolveNamed(TagNode)} or {@link #resolveSequential(TagNode)} * depending on the result. *

* @@ -848,7 +847,7 @@ default boolean isNamed(final @NotNull TagNode node) { * @since 4.25.0 */ default @Nullable Tag resolve(final @NotNull TagNode node) { - return this.isNamed(node) ? this.resolveNamed(node) : this.resolveQueued(node); + return this.isNamed(node) ? this.resolveNamed(node) : this.resolveSequential(node); } /** @@ -858,8 +857,8 @@ default boolean isNamed(final @NotNull TagNode node) { * @return a tag, if any is available * @since 4.25.0 */ - default @Nullable Tag resolveQueued(final @NotNull TagNode node) { - return this.resolveQueued( + default @Nullable Tag resolveSequential(final @NotNull TagNode node) { + return this.resolveSequential( TagProvider.sanitizePlaceholderName(node.name()), (List) node.parts().subList(1, node.parts().size()), node.token() @@ -924,18 +923,18 @@ default boolean isNamed(final @NotNull TagNode node) { */ @ApiStatus.Internal public static final class TagProviderImpl implements TagProvider { - private final QueuedTagProvider queued; + private final SequentialTagProvider sequential; private final NamedTagProvider named; /** * Construct a new {@link TagProviderImpl} object. * - * @param queued the queued provider + * @param sequential the sequential provider * @param named the named provider * @since 4.25.0 */ - public TagProviderImpl(final QueuedTagProvider queued, final NamedTagProvider named) { - this.queued = queued; + public TagProviderImpl(final SequentialTagProvider sequential, final NamedTagProvider named) { + this.sequential = sequential; this.named = named; } @@ -955,8 +954,8 @@ public TagProviderImpl(final QueuedTagProvider queued, final NamedTagProvider * @since 4.25.0 */ @Override - public @Nullable Tag resolveQueued(final @NotNull String name, final @NotNull List trimmedArgs, final @Nullable Token token) { - return this.queued.resolveQueued(name, trimmedArgs, token); + public @Nullable Tag resolveSequential(final @NotNull String name, final @NotNull List trimmedArgs, final @Nullable Token token) { + return this.sequential.resolveSequential(name, trimmedArgs, token); } } } diff --git a/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/internal/parser/match/StringResolvingMatchedTokenConsumer.java b/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/internal/parser/match/StringResolvingMatchedTokenConsumer.java index 237e57ebe0..30c585394d 100644 --- a/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/internal/parser/match/StringResolvingMatchedTokenConsumer.java +++ b/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/internal/parser/match/StringResolvingMatchedTokenConsumer.java @@ -28,7 +28,6 @@ import java.util.Objects; import net.kyori.adventure.text.minimessage.internal.TagInternals; import net.kyori.adventure.text.minimessage.internal.parser.Token; -import net.kyori.adventure.text.minimessage.internal.parser.TokenParser; import net.kyori.adventure.text.minimessage.internal.parser.TokenParser.TagProvider; import net.kyori.adventure.text.minimessage.internal.parser.TokenType; import net.kyori.adventure.text.minimessage.internal.parser.node.TagPart; @@ -58,7 +57,7 @@ public final class StringResolvingMatchedTokenConsumer extends MatchedTokenConsu */ public StringResolvingMatchedTokenConsumer( final @NotNull String input, - final TokenParser.@NotNull TagProvider tagProvider + final @NotNull TagProvider tagProvider ) { super(input); this.builder = new StringBuilder(input.length()); @@ -90,9 +89,8 @@ public void accept(final int start, final int end, final @NotNull TokenType toke parts.add(new TagPart(match, childs.get(i), this.tagProvider)); } } - // we might care if it's a pre-process! - final @Nullable Tag replacement = this.tagProvider.resolveQueued(TagProvider.sanitizePlaceholderName(tag), parts, tokens.get(0)); + final @Nullable Tag replacement = this.tagProvider.resolveSequential(TagProvider.sanitizePlaceholderName(tag), parts, tokens.get(0)); if (replacement instanceof PreProcess) { this.builder.append(Objects.requireNonNull(((PreProcess) replacement).value(), "PreProcess replacements cannot return null")); diff --git a/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/internal/parser/node/TagPart.java b/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/internal/parser/node/TagPart.java index eeb1b9b663..4800426e2c 100644 --- a/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/internal/parser/node/TagPart.java +++ b/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/internal/parser/node/TagPart.java @@ -33,7 +33,7 @@ * * @since 4.10.0 */ -public class TagPart implements Tag.Argument { +public final class TagPart implements Tag.Argument { private final String value; private final Token token; diff --git a/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/internal/serializer/ComponentClaimingResolverImpl.java b/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/internal/serializer/ComponentClaimingResolverImpl.java index 6035017fc4..f5041cee6d 100644 --- a/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/internal/serializer/ComponentClaimingResolverImpl.java +++ b/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/internal/serializer/ComponentClaimingResolverImpl.java @@ -35,7 +35,7 @@ import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -class ComponentClaimingResolverImpl implements TagResolver.Queued, SerializableResolver.Single { +class ComponentClaimingResolverImpl implements TagResolver.Sequential, SerializableResolver.Single { private final @NotNull Set names; private final @NotNull BiFunction handler; private final @NotNull Function componentClaim; diff --git a/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/internal/serializer/StyleClaimingResolverImpl.java b/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/internal/serializer/StyleClaimingResolverImpl.java index 06daf967d3..a7618f2b9e 100644 --- a/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/internal/serializer/StyleClaimingResolverImpl.java +++ b/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/internal/serializer/StyleClaimingResolverImpl.java @@ -33,7 +33,7 @@ import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -final class StyleClaimingResolverImpl implements TagResolver.Queued, SerializableResolver.Single { +final class StyleClaimingResolverImpl implements TagResolver.Sequential, SerializableResolver.Single { private final @NotNull Set names; private final @NotNull BiFunction handler; private final @NotNull StyleClaim styleClaim; 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 55edc1cfa7..ad9e03d187 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 @@ -44,7 +44,7 @@ final class SequentialTagResolver implements TagResolver, SerializableResolver { public @Nullable Tag resolveNamed(final @NotNull String name, final @NotNull NamedArgumentMap arguments, final @NotNull Context ctx) throws ParsingException { @Nullable ParsingException thrown = null; for (final TagResolver resolver : this.resolvers) { - if (!(resolver instanceof TagResolver.Named)) { + if (!(resolver instanceof Named)) { continue; } @@ -78,7 +78,7 @@ final class SequentialTagResolver implements TagResolver, SerializableResolver { public @Nullable Tag resolve(final @NotNull String name, final @NotNull ArgumentQueue arguments, final @NotNull Context ctx) throws ParsingException { @Nullable ParsingException thrown = null; for (final TagResolver resolver : this.resolvers) { - if (!(resolver instanceof TagResolver.Queued)) { + if (!(resolver instanceof Sequential)) { continue; } 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 a49b55118c..883d99c573 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 @@ -127,7 +127,7 @@ public interface TagResolver { } requireNonNull(handler, "handler"); - return new TagResolver.Queued() { + return new Sequential() { @Override public @Nullable Tag resolve(final @NotNull String name, final @NotNull ArgumentQueue arguments, final @NotNull Context ctx) throws ParsingException { if (!names.contains(name)) return null; @@ -342,7 +342,7 @@ default boolean has(final @NotNull String name) { * @since 4.10.0 */ @FunctionalInterface - interface WithoutArguments extends TagResolver.Queued { + interface WithoutArguments extends Sequential { /** * Resolve a tag based only on the provided name. * @@ -379,7 +379,7 @@ default boolean has(final @NotNull String name) { * * @since 4.25.0 */ - interface Queued extends TagResolver { + interface Sequential extends TagResolver { @Override default @Nullable Tag resolveNamed(final @NotNull String name, final @NotNull NamedArgumentMap arguments, final @NotNull Context ctx) throws ParsingException { return null; 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 1097051d6b..9f13803d19 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 @@ -43,7 +43,7 @@ * * @since 4.10.0 */ -final class ColorTagResolver implements TagResolver.Queued, SerializableResolver.Single { +final class ColorTagResolver implements TagResolver.Sequential, SerializableResolver.Single { private static final String COLOR_3 = "c"; private static final String COLOR_2 = "colour"; private static final String COLOR = "color"; diff --git a/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/translation/ArgumentTag.java b/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/translation/ArgumentTag.java index dcad13fe3c..cd1d4887dd 100644 --- a/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/translation/ArgumentTag.java +++ b/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/translation/ArgumentTag.java @@ -32,7 +32,7 @@ import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -final class ArgumentTag implements TagResolver.Queued { +final class ArgumentTag implements TagResolver.Sequential { private static final String NAME = "argument"; private static final String NAME_1 = "arg"; diff --git a/text-minimessage/src/test/java/net/kyori/adventure/text/minimessage/MiniMessageParserTest.java b/text-minimessage/src/test/java/net/kyori/adventure/text/minimessage/MiniMessageParserTest.java index 38618c591c..faa46d9af6 100644 --- a/text-minimessage/src/test/java/net/kyori/adventure/text/minimessage/MiniMessageParserTest.java +++ b/text-minimessage/src/test/java/net/kyori/adventure/text/minimessage/MiniMessageParserTest.java @@ -583,7 +583,7 @@ void testNonTerminatingQuoteArgument() { this.assertParsedEquals(expected, input); } - private static final class AlwaysMatchingResolver implements TagResolver.Queued, TagResolver.Named { + private static final class AlwaysMatchingResolver implements TagResolver.Sequential, TagResolver.Named { @Override public @NotNull Tag resolve(final @NotNull String name, final @NotNull ArgumentQueue arguments, final @NotNull Context ctx) throws ParsingException { return Tag.preProcessParsed("(meow)"); diff --git a/text-minimessage/src/test/java/net/kyori/adventure/text/minimessage/MiniMessageTest.java b/text-minimessage/src/test/java/net/kyori/adventure/text/minimessage/MiniMessageTest.java index de819b596e..1cbfc5ce65 100644 --- a/text-minimessage/src/test/java/net/kyori/adventure/text/minimessage/MiniMessageTest.java +++ b/text-minimessage/src/test/java/net/kyori/adventure/text/minimessage/MiniMessageTest.java @@ -372,7 +372,7 @@ void debugModeSimple() { final List messages = Arrays.asList(sb.toString().split("\n")); assertTrue(messages.contains("Beginning parsing message RED ")); - assertTrue(messages.contains("Attempting to match node as queued 'red' at column 0")); + assertTrue(messages.contains("Attempting to match node as sequential 'red' at column 0")); assertTrue(anyMatch(messages, it -> it.startsWith("Successfully matched node 'red' to tag "))); assertTrue(messages.contains("Text parsed into element tree:")); assertTrue(messages.contains("Node {")); @@ -391,11 +391,11 @@ void debugModeMoreComplex() { final List messages = Arrays.asList(sb.toString().split("\n")); assertTrue(messages.contains("Beginning parsing message RED BLUE bad click ")); - assertTrue(messages.contains("Attempting to match node as queued 'red' at column 0")); + assertTrue(messages.contains("Attempting to match node as sequential 'red' at column 0")); assertTrue(anyMatch(messages, it -> it.startsWith("Successfully matched node 'red' to tag "))); - assertTrue(messages.contains("Attempting to match node as queued 'blue' at column 10")); + assertTrue(messages.contains("Attempting to match node as sequential 'blue' at column 10")); assertTrue(anyMatch(messages, it -> it.startsWith("Successfully matched node 'blue' to tag "))); - assertTrue(messages.contains("Attempting to match node as queued 'click' at column 22")); + assertTrue(messages.contains("Attempting to match node as sequential 'click' at column 22")); assertTrue(messages.contains("Could not match node 'click' - A click tag requires an action of one of [run_command, open_file, custom, open_url, copy_to_clipboard, change_page, show_dialog, suggest_command]")); assertTrue(messages.contains("\t RED BLUE bad click ")); assertTrue(messages.contains("\t ^~~~~~^")); @@ -419,11 +419,11 @@ void debugModeMoreComplexNoError() { final List messages = Arrays.asList(sb.toString().split("\n")); assertTrue(messages.contains("Beginning parsing message RED BLUE good click ")); - assertTrue(messages.contains("Attempting to match node as queued 'red' at column 0")); + assertTrue(messages.contains("Attempting to match node as sequential 'red' at column 0")); assertTrue(anyMatch(messages, it -> it.startsWith("Successfully matched node 'red' to tag "))); - assertTrue(messages.contains("Attempting to match node as queued 'blue' at column 10")); + assertTrue(messages.contains("Attempting to match node as sequential 'blue' at column 10")); assertTrue(anyMatch(messages, it -> it.startsWith("Successfully matched node 'blue' to tag "))); - assertTrue(messages.contains("Attempting to match node as queued 'click' at column 22")); + assertTrue(messages.contains("Attempting to match node as sequential 'click' at column 22")); assertTrue(anyMatch(messages, it -> it.startsWith("Successfully matched node 'click' to tag "))); assertTrue(messages.contains("Text parsed into element tree:")); assertTrue(messages.contains("Node {")); @@ -453,11 +453,11 @@ void debugNamedArguments() { final List messages = Arrays.asList(sb.toString().split("\n")); assertTrue(messages.contains("Beginning parsing message I have a red text!")); - assertTrue(messages.contains("Attempting to match node as queued 'red' at column 0")); + assertTrue(messages.contains("Attempting to match node as sequential 'red' at column 0")); assertTrue(anyMatch(messages, it -> it.startsWith("Successfully matched node 'red' to tag "))); assertTrue(messages.contains("Attempting to match node as named 'head' at column 0")); assertTrue(anyMatch(messages, it -> it.startsWith("Successfully matched node 'head' to tag "))); - assertTrue(messages.contains("Attempting to match node as queued 'red' at column 52")); + assertTrue(messages.contains("Attempting to match node as sequential 'red' at column 52")); assertTrue(anyMatch(messages, it -> it.startsWith("Successfully matched node 'red' to tag "))); assertTrue(messages.contains("Text parsed into element tree:")); assertTrue(messages.contains("Node {")); From dc151dcbb64f57ef78d4523807e71f252286b8d4 Mon Sep 17 00:00:00 2001 From: Strokkur24 Date: Sat, 20 Sep 2025 09:38:03 +0200 Subject: [PATCH 14/19] chore: add a bunch more tests --- .../MiniMessageNamedArgumentsTest.java | 30 ++++++++++++++----- 1 file changed, 23 insertions(+), 7 deletions(-) diff --git a/text-minimessage/src/test/java/net/kyori/adventure/text/minimessage/MiniMessageNamedArgumentsTest.java b/text-minimessage/src/test/java/net/kyori/adventure/text/minimessage/MiniMessageNamedArgumentsTest.java index 5b5431aa09..5f774a9489 100644 --- a/text-minimessage/src/test/java/net/kyori/adventure/text/minimessage/MiniMessageNamedArgumentsTest.java +++ b/text-minimessage/src/test/java/net/kyori/adventure/text/minimessage/MiniMessageNamedArgumentsTest.java @@ -46,13 +46,8 @@ public class MiniMessageNamedArgumentsTest extends AbstractTest { @Test void testBasicInsertingNamedTagArguments() { final String input = ""; - final Component parsed = MiniMessage.builder() - .editTags(b -> b.resolver(INSERT_VALUE_RESOLVER)) - .build() - .deserialize(input); - - final String parsedString = PlainTextComponentSerializer.plainText().serialize(parsed); - assertEquals("twentyfive", parsedString); + final Component expected = text("twentyfive"); + assertParsedEquals(MiniMessage.miniMessage(), expected, input, INSERT_VALUE_RESOLVER); } @Test @@ -175,4 +170,25 @@ void testNamedQueuedCanCoexist() { TagResolver.namedResolver("test", (args, ctx) -> Tag.inserting(text("World!"))) ); } + + @Test + void testUrlInNamedArgs() { + final String input = ""; + final Component expected = text("https://github.com/KyoriPowered/adventure"); + assertParsedEquals(MiniMessage.miniMessage(), expected, input, INSERT_VALUE_RESOLVER); + } + + @Test + void testABunchOfMoreSymbolsAreArguments() { + final String input = ""; // The last / is interpreted as an explicit self-closing tag. + final Component expected = text("H%%^Is@@cool;;:/"); + assertParsedEquals(MiniMessage.miniMessage(), expected, input, INSERT_VALUE_RESOLVER); + } + + @Test + void testStringValue() { + final String input = ""; + final Component expected = text("This is great =)"); + assertParsedEquals(MiniMessage.miniMessage(), expected, input, INSERT_VALUE_RESOLVER); + } } From 2fec65545bb9f8ae23082dbd01bd68f3f04ba89a Mon Sep 17 00:00:00 2001 From: Strokkur24 Date: Sat, 20 Sep 2025 10:12:25 +0200 Subject: [PATCH 15/19] feat: add inverted flag arguments --- .../minimessage/NamedArgumentMapImpl.java | 21 ++++++++- .../tag/resolver/NamedArgumentMap.java | 28 +++++++++++- .../MiniMessageNamedArgumentsTest.java | 45 ++++++++++++++++--- 3 files changed, 85 insertions(+), 9 deletions(-) diff --git a/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/NamedArgumentMapImpl.java b/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/NamedArgumentMapImpl.java index da065292fe..25af2d4c6e 100644 --- a/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/NamedArgumentMapImpl.java +++ b/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/NamedArgumentMapImpl.java @@ -27,6 +27,7 @@ import java.util.function.Supplier; import net.kyori.adventure.text.minimessage.tag.Tag; import net.kyori.adventure.text.minimessage.tag.resolver.NamedArgumentMap; +import net.kyori.adventure.util.TriState; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -59,7 +60,23 @@ public int size() { } @Override - public Tag.@NotNull Argument elseThrow(final @NotNull String name, final @NotNull String errorMessage) { + public @NotNull TriState flag(final @NotNull String name) { + final Tag.Argument argument = this.get(name); + if (argument == null) { + // The normal flag is not preset, so try the inverted flag + final Tag.Argument invertedArgument = this.get('!' + name); + if (invertedArgument == null) { + return TriState.NOT_SET; + } + + return TriState.FALSE; + } + + return TriState.TRUE; + } + + @Override + public Tag.@NotNull Argument orThrow(final @NotNull String name, final @NotNull String errorMessage) { requireNonNull(errorMessage, "errorMessage"); final Tag.Argument arg = this.get(name); if (arg == null) { @@ -69,7 +86,7 @@ public int size() { } @Override - public Tag.@NotNull Argument elseThrow(final @NotNull String name, final @NotNull Supplier errorMessage) { + public Tag.@NotNull Argument orThrow(final @NotNull String name, final @NotNull Supplier errorMessage) { requireNonNull(errorMessage, "errorMessage"); final Tag.Argument arg = this.get(name); if (arg == null) { diff --git a/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/tag/resolver/NamedArgumentMap.java b/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/tag/resolver/NamedArgumentMap.java index 7373b0f285..1e767aa2ff 100644 --- a/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/tag/resolver/NamedArgumentMap.java +++ b/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/tag/resolver/NamedArgumentMap.java @@ -25,6 +25,7 @@ import java.util.function.Supplier; import net.kyori.adventure.text.minimessage.tag.Tag; +import net.kyori.adventure.util.TriState; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -63,6 +64,29 @@ public interface NamedArgumentMap { */ Tag.@Nullable Argument get(@NotNull String name); + /** + * Get the value of a flag. If a flag is present {@code flag}, + * this method return {@link TriState#TRUE}. If a flag + * is inverted {@code !flag}, {@link TriState#FALSE} is returned. + * Otherwise, {@link TriState#NOT_SET} is returned. + * + * @param name the name of the flag + * @return its presence status in the tag + * @since 4.25.0 + */ + @NotNull TriState flag(@NotNull String name); + + /** + * Get an argument by its name, throwing an exception if no argument with that name was present. + * + * @param name name of the argument + * @return the argument + * @since 4.25.0 + */ + default Tag.@NotNull Argument orThrow(final @NotNull String name) { + return this.orThrow(name, name + " is not present"); + } + /** * Get an argument by its name, throwing an exception if no argument with that name was present. * @@ -71,7 +95,7 @@ public interface NamedArgumentMap { * @return the argument * @since 4.25.0 */ - Tag.@NotNull Argument elseThrow(@NotNull String name, @NotNull String errorMessage); + Tag.@NotNull Argument orThrow(@NotNull String name, @NotNull String errorMessage); /** * Get an argument by its name, throwing an exception if no argument with that name was present. @@ -81,5 +105,5 @@ public interface NamedArgumentMap { * @return the argument * @since 4.25.0 */ - Tag.@NotNull Argument elseThrow(@NotNull String name, @NotNull Supplier errorMessage); + Tag.@NotNull Argument orThrow(@NotNull String name, @NotNull Supplier errorMessage); } diff --git a/text-minimessage/src/test/java/net/kyori/adventure/text/minimessage/MiniMessageNamedArgumentsTest.java b/text-minimessage/src/test/java/net/kyori/adventure/text/minimessage/MiniMessageNamedArgumentsTest.java index 5f774a9489..c60a0bff9e 100644 --- a/text-minimessage/src/test/java/net/kyori/adventure/text/minimessage/MiniMessageNamedArgumentsTest.java +++ b/text-minimessage/src/test/java/net/kyori/adventure/text/minimessage/MiniMessageNamedArgumentsTest.java @@ -23,11 +23,13 @@ */ package net.kyori.adventure.text.minimessage; +import java.util.ArrayList; +import java.util.List; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.format.TextColor; import net.kyori.adventure.text.minimessage.tag.Tag; import net.kyori.adventure.text.minimessage.tag.resolver.TagResolver; -import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer; +import net.kyori.adventure.util.TriState; import org.junit.jupiter.api.Test; import static net.kyori.adventure.text.Component.text; @@ -35,12 +37,11 @@ import static net.kyori.adventure.text.format.NamedTextColor.RED; import static net.kyori.adventure.text.format.TextDecoration.BOLD; import static net.kyori.adventure.text.format.TextDecoration.ITALIC; -import static org.junit.jupiter.api.Assertions.assertEquals; public class MiniMessageNamedArgumentsTest extends AbstractTest { private static final TagResolver INSERT_VALUE_RESOLVER = TagResolver.namedResolver("insert", (args, ctx) -> Tag.selfClosingInserting( - text(args.elseThrow("value", "value is missing").value()) + text(args.orThrow("value", "value is missing").value()) )); @Test @@ -71,7 +72,7 @@ void testMultipleArguments() { assertParsedEquals(MiniMessage.miniMessage(), expected, input, TagResolver.namedResolver("repeat", (args, ctx) -> { final int amount = args.isPresent("amount") ? args.get("amount").asInt().getAsInt() : 1; - final String text = args.elseThrow("text", "text is missing").value(); + final String text = args.orThrow("text", "text is missing").value(); final StringBuilder builder = new StringBuilder(); for (int i = 0; i < amount; i++) { builder.append(text); @@ -96,7 +97,7 @@ void testComplexArguments() { assertParsedEquals(MiniMessage.miniMessage(), expected, input, TagResolver.namedResolver("styled", (args, ctx) -> Tag.styling(builder -> { if (args.isPresent("color")) { - builder.color(TextColor.fromCSSHexString(args.elseThrow("color", "color is missing").value())); + builder.color(TextColor.fromCSSHexString(args.orThrow("color", "color is missing").value())); } if (args.isPresent("bold")) { @@ -191,4 +192,38 @@ void testStringValue() { final Component expected = text("This is great =)"); assertParsedEquals(MiniMessage.miniMessage(), expected, input, INSERT_VALUE_RESOLVER); } + + @Test + void testInvertedFlags() { + final String input = " !"; + final Component expected = text("Adventure is very cool!"); + assertParsedEquals(MiniMessage.miniMessage(), expected, input, TagResolver.namedResolver( + "test", (args, ctx) -> { + final List strings = new ArrayList<>(); + final TriState flag = args.flag("flag"); + final TriState otherFlag = args.flag("other_flag"); + + if (flag == TriState.TRUE) { + strings.add("Adventure"); + } else if (flag == TriState.FALSE) { + strings.add("very"); + } + + if (otherFlag == TriState.TRUE) { + strings.add("is"); + } else if (otherFlag == TriState.FALSE) { + strings.add("cool"); + } + + return Tag.selfClosingInserting(text(String.join(" ", strings))); + } + )); + } + + @Test + void testWhitespaceAroundEquals() { + final String input = ""; + final Component expected = text(""); + assertParsedEquals(MiniMessage.miniMessage(), expected, input, INSERT_VALUE_RESOLVER); + } } From 8dfb4a34d6cd2d155e95bf284d42ac8bfcd49e5f Mon Sep 17 00:00:00 2001 From: Strokkur24 Date: Sat, 20 Sep 2025 10:24:52 +0200 Subject: [PATCH 16/19] feat: split the claiming resolvers into named and sequenced resolvers --- .../NamedComponentClaimingResolverImpl.java | 65 ++++++++++++++++ .../NamedStyleClaimingResolverImpl.java | 63 ++++++++++++++++ ...uentialComponentClaimingResolverImpl.java} | 4 +- ... SequentialStyleClaimingResolverImpl.java} | 4 +- .../serializer/SerializableResolver.java | 75 ++++++++++++++++++- 5 files changed, 205 insertions(+), 6 deletions(-) create mode 100644 text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/internal/serializer/NamedComponentClaimingResolverImpl.java create mode 100644 text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/internal/serializer/NamedStyleClaimingResolverImpl.java rename text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/internal/serializer/{ComponentClaimingResolverImpl.java => SequentialComponentClaimingResolverImpl.java} (89%) rename text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/internal/serializer/{StyleClaimingResolverImpl.java => SequentialStyleClaimingResolverImpl.java} (89%) diff --git a/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/internal/serializer/NamedComponentClaimingResolverImpl.java b/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/internal/serializer/NamedComponentClaimingResolverImpl.java new file mode 100644 index 0000000000..fa675cddce --- /dev/null +++ b/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/internal/serializer/NamedComponentClaimingResolverImpl.java @@ -0,0 +1,65 @@ +/* + * 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.internal.serializer; + +import java.util.Set; +import java.util.function.BiFunction; +import java.util.function.Function; +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.tag.Tag; +import net.kyori.adventure.text.minimessage.tag.resolver.NamedArgumentMap; +import net.kyori.adventure.text.minimessage.tag.resolver.TagResolver; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +class NamedComponentClaimingResolverImpl implements TagResolver.Named, SerializableResolver.Single { + private final @NotNull Set names; + private final @NotNull BiFunction handler; + private final @NotNull Function componentClaim; + + NamedComponentClaimingResolverImpl(final Set names, final BiFunction handler, final Function componentClaim) { + this.names = names; + this.handler = handler; + this.componentClaim = componentClaim; + } + + @Override + public @Nullable Tag resolveNamed(final @NotNull String name, final @NotNull NamedArgumentMap arguments, final @NotNull Context ctx) throws ParsingException { + if (!this.names.contains(name)) return null; + + return this.handler.apply(arguments, ctx); + } + + @Override + public boolean has(final @NotNull String name) { + return this.names.contains(name); + } + + @Override + public @Nullable Emitable claimComponent(final @NotNull Component component) { + return this.componentClaim.apply(component); + } +} diff --git a/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/internal/serializer/NamedStyleClaimingResolverImpl.java b/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/internal/serializer/NamedStyleClaimingResolverImpl.java new file mode 100644 index 0000000000..8b76e7f899 --- /dev/null +++ b/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/internal/serializer/NamedStyleClaimingResolverImpl.java @@ -0,0 +1,63 @@ +/* + * 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.internal.serializer; + +import java.util.Set; +import java.util.function.BiFunction; +import net.kyori.adventure.text.minimessage.Context; +import net.kyori.adventure.text.minimessage.ParsingException; +import net.kyori.adventure.text.minimessage.tag.Tag; +import net.kyori.adventure.text.minimessage.tag.resolver.NamedArgumentMap; +import net.kyori.adventure.text.minimessage.tag.resolver.TagResolver; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +final class NamedStyleClaimingResolverImpl implements TagResolver.Named, SerializableResolver.Single { + private final @NotNull Set names; + private final @NotNull BiFunction handler; + private final @NotNull StyleClaim styleClaim; + + NamedStyleClaimingResolverImpl(final @NotNull Set names, final @NotNull BiFunction handler, final @NotNull StyleClaim styleClaim) { + this.names = names; + this.handler = handler; + this.styleClaim = styleClaim; + } + + @Override + public @Nullable Tag resolveNamed(final @NotNull String name, final @NotNull NamedArgumentMap arguments, final @NotNull Context ctx) throws ParsingException { + if (!this.names.contains(name)) return null; + + return this.handler.apply(arguments, ctx); + } + + @Override + public boolean has(final @NotNull String name) { + return this.names.contains(name); + } + + @Override + public StyleClaim claimStyle() { + return this.styleClaim; + } +} diff --git a/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/internal/serializer/ComponentClaimingResolverImpl.java b/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/internal/serializer/SequentialComponentClaimingResolverImpl.java similarity index 89% rename from text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/internal/serializer/ComponentClaimingResolverImpl.java rename to text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/internal/serializer/SequentialComponentClaimingResolverImpl.java index f5041cee6d..7dd74d086d 100644 --- a/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/internal/serializer/ComponentClaimingResolverImpl.java +++ b/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/internal/serializer/SequentialComponentClaimingResolverImpl.java @@ -35,12 +35,12 @@ import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -class ComponentClaimingResolverImpl implements TagResolver.Sequential, SerializableResolver.Single { +class SequentialComponentClaimingResolverImpl implements TagResolver.Sequential, SerializableResolver.Single { private final @NotNull Set names; private final @NotNull BiFunction handler; private final @NotNull Function componentClaim; - ComponentClaimingResolverImpl(final Set names, final BiFunction handler, final Function componentClaim) { + SequentialComponentClaimingResolverImpl(final Set names, final BiFunction handler, final Function componentClaim) { this.names = names; this.handler = handler; this.componentClaim = componentClaim; diff --git a/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/internal/serializer/StyleClaimingResolverImpl.java b/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/internal/serializer/SequentialStyleClaimingResolverImpl.java similarity index 89% rename from text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/internal/serializer/StyleClaimingResolverImpl.java rename to text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/internal/serializer/SequentialStyleClaimingResolverImpl.java index a7618f2b9e..2d76db8a33 100644 --- a/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/internal/serializer/StyleClaimingResolverImpl.java +++ b/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/internal/serializer/SequentialStyleClaimingResolverImpl.java @@ -33,12 +33,12 @@ import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -final class StyleClaimingResolverImpl implements TagResolver.Sequential, SerializableResolver.Single { +final class SequentialStyleClaimingResolverImpl implements TagResolver.Sequential, SerializableResolver.Single { private final @NotNull Set names; private final @NotNull BiFunction handler; private final @NotNull StyleClaim styleClaim; - StyleClaimingResolverImpl(final @NotNull Set names, final @NotNull BiFunction handler, final @NotNull StyleClaim styleClaim) { + SequentialStyleClaimingResolverImpl(final @NotNull Set names, final @NotNull BiFunction handler, final @NotNull StyleClaim styleClaim) { this.names = names; this.handler = handler; this.styleClaim = styleClaim; 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..f12b125328 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 @@ -34,6 +34,7 @@ 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.NamedArgumentMap; import net.kyori.adventure.text.minimessage.tag.resolver.TagResolver; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -59,6 +60,21 @@ public interface SerializableResolver { return claimingComponent(Collections.singleton(name), handler, componentClaim); } + /** + * Create a tag resolver that only responds to a single tag name, and whose value does not depend on that name. + * + *

The resolver created is a special resolver which listens to named arguments instead of sequenced ones.

+ * + * @param name the name to respond to + * @param handler the tag handler, may throw {@link ParsingException} if provided arguments are in an invalid format + * @param componentClaim the claim to test components against + * @return a resolver that creates tags using the provided handler + * @since 4.25.0 + */ + static @NotNull TagResolver claimingComponentNamed(final @NotNull String name, final @NotNull BiFunction handler, final @NotNull Function componentClaim) { + return claimingComponentNamed(Collections.singleton(name), handler, componentClaim); + } + /** * Create a tag resolver that only responds to certain tag names, and whose value does not depend on that name. * @@ -74,7 +90,27 @@ public interface SerializableResolver { TagInternals.assertValidTagName(name); } requireNonNull(handler, "handler"); - return new ComponentClaimingResolverImpl(ownNames, handler, componentClaim); + return new SequentialComponentClaimingResolverImpl(ownNames, handler, componentClaim); + } + + /** + * Create a tag resolver that only responds to certain tag names, and whose value does not depend on that name. + * + *

The resolver created is a special resolver which listens to named arguments instead of sequenced ones.

+ * + * @param names the names to respond to + * @param handler the tag handler, may throw {@link ParsingException} if provided arguments are in an invalid format + * @param componentClaim the claim to test components against + * @return a resolver that creates tags using the provided handler + * @since 4.25.0 + */ + static @NotNull TagResolver claimingComponentNamed(final @NotNull Set names, final @NotNull BiFunction handler, final @NotNull Function componentClaim) { + final Set ownNames = new HashSet<>(names); + for (final String name : ownNames) { + TagInternals.assertValidTagName(name); + } + requireNonNull(handler, "handler"); + return new NamedComponentClaimingResolverImpl(ownNames, handler, componentClaim); } /** @@ -90,6 +126,21 @@ public interface SerializableResolver { return claimingStyle(Collections.singleton(name), handler, styleClaim); } + /** + * Create a tag resolver that only responds to a single tag name, and whose value does not depend on that name. + * + *

The resolver created is a special resolver which listens to named arguments instead of sequenced ones.

+ * + * @param name the name to respond to + * @param handler the tag handler, may throw {@link ParsingException} if provided arguments are in an invalid format + * @param styleClaim the extractor for style claims on components + * @return a resolver that creates tags using the provided handler + * @since 4.25.0 + */ + static @NotNull TagResolver claimingStyleNamed(final @NotNull String name, final @NotNull BiFunction handler, final @NotNull StyleClaim styleClaim) { + return claimingStyleNamed(Collections.singleton(name), handler, styleClaim); + } + /** * Create a tag resolver that only responds to certain tag names, and whose value does not depend on that name. * @@ -105,7 +156,27 @@ public interface SerializableResolver { TagInternals.assertValidTagName(name); } requireNonNull(handler, "handler"); - return new StyleClaimingResolverImpl(ownNames, handler, styleClaim); + return new SequentialStyleClaimingResolverImpl(ownNames, handler, styleClaim); + } + + /** + * Create a tag resolver that only responds to certain tag names, and whose value does not depend on that name. + * + *

The resolver created is a special resolver which listens to named arguments instead of sequenced ones.

+ * + * @param names the names to respond to + * @param handler the tag handler, may throw {@link ParsingException} if provided arguments are in an invalid format + * @param styleClaim the extractor for style claims on components + * @return a resolver that creates tags using the provided handler + * @since 4.25.0 + */ + static @NotNull TagResolver claimingStyleNamed(final @NotNull Set names, final @NotNull BiFunction handler, final @NotNull StyleClaim styleClaim) { + final Set ownNames = new HashSet<>(names); + for (final String name : ownNames) { + TagInternals.assertValidTagName(name); + } + requireNonNull(handler, "handler"); + return new NamedStyleClaimingResolverImpl(ownNames, handler, styleClaim); } /** From a23bb5f69789fb76db98fee99368bb7d0704bc31 Mon Sep 17 00:00:00 2001 From: Strokkur24 Date: Sat, 20 Sep 2025 11:14:24 +0200 Subject: [PATCH 17/19] feat: add missing context newException method and try to parse tag with no arguments as named if no sequential resolver exists --- .../kyori/adventure/text/minimessage/Context.java | 13 +++++++++++++ .../adventure/text/minimessage/ContextImpl.java | 5 +++++ .../minimessage/internal/parser/TokenParser.java | 12 +++++++++++- .../minimessage/MiniMessageNamedArgumentsTest.java | 9 +++++++++ 4 files changed, 38 insertions(+), 1 deletion(-) 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 690feed3b1..b286fdf75f 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 @@ -118,6 +118,19 @@ public interface Context { final @NotNull ArgumentQueue tags ); + /** + * Create a new parsing exception. + * + * @param message a detail message describing the error + * @param tags the tag parts which caused the error + * @return the new parsing exception + * @since 4.25.0 + */ + @NotNull ParsingException newException( + final @NotNull String message, + final @NotNull NamedArgumentMap tags + ); + /** * Create a new parsing exception without reference to a specific location. * 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 684e35e1d1..dca372496e 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 @@ -167,6 +167,11 @@ public UnaryOperator preProcessor() { return new ParsingExceptionImpl(message, this.message, null, false, tagsToTokens(((ArgumentQueueImpl) tags).args)); } + @Override + public @NotNull ParsingException newException(final @NotNull String message, final @NotNull NamedArgumentMap tags) { + return new ParsingExceptionImpl(message, this.message, null, false, tagsToTokens(((NamedArgumentMapImpl) tags).args)); + } + @Override public @NotNull ParsingException newException(final @NotNull String message, final @Nullable Throwable cause, final @NotNull ArgumentQueue tags) { return new ParsingExceptionImpl(message, this.message, cause, false, tagsToTokens(((ArgumentQueueImpl) tags).args)); diff --git a/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/internal/parser/TokenParser.java b/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/internal/parser/TokenParser.java index 96e50a4658..d3ebec838d 100644 --- a/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/internal/parser/TokenParser.java +++ b/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/internal/parser/TokenParser.java @@ -847,7 +847,17 @@ default boolean isNamed(final @NotNull TagNode node) { * @since 4.25.0 */ default @Nullable Tag resolve(final @NotNull TagNode node) { - return this.isNamed(node) ? this.resolveNamed(node) : this.resolveSequential(node); + if (this.isNamed(node)) { + return this.resolveNamed(node); + } + + final Tag out = this.resolveSequential(node); + if (node.parts().size() == 1 && out == null) { + // This might be a named tag which has no arguments provided. + return this.resolveNamed(node); + } + + return out; } /** diff --git a/text-minimessage/src/test/java/net/kyori/adventure/text/minimessage/MiniMessageNamedArgumentsTest.java b/text-minimessage/src/test/java/net/kyori/adventure/text/minimessage/MiniMessageNamedArgumentsTest.java index c60a0bff9e..a9182ce495 100644 --- a/text-minimessage/src/test/java/net/kyori/adventure/text/minimessage/MiniMessageNamedArgumentsTest.java +++ b/text-minimessage/src/test/java/net/kyori/adventure/text/minimessage/MiniMessageNamedArgumentsTest.java @@ -172,6 +172,15 @@ void testNamedQueuedCanCoexist() { ); } + @Test + void testArgumentlessNamedTag() { + final String input = ""; + final Component expected = text("Works!"); + assertParsedEquals(MiniMessage.miniMessage(), expected, input, TagResolver.namedResolver( + "no_args", (args, ctx) -> Tag.inserting(text("Works!")) + )); + } + @Test void testUrlInNamedArgs() { final String input = ""; From 70fdce28977d2590b95b32fa831db53ca18f0ace Mon Sep 17 00:00:00 2001 From: Strokkur24 Date: Sat, 20 Sep 2025 11:22:31 +0200 Subject: [PATCH 18/19] feat: add isFlagPresent --- .../adventure/text/minimessage/NamedArgumentMapImpl.java | 8 ++++++++ .../text/minimessage/tag/resolver/NamedArgumentMap.java | 9 +++++++++ 2 files changed, 17 insertions(+) diff --git a/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/NamedArgumentMapImpl.java b/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/NamedArgumentMapImpl.java index 25af2d4c6e..8975d89316 100644 --- a/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/NamedArgumentMapImpl.java +++ b/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/NamedArgumentMapImpl.java @@ -75,6 +75,14 @@ public int size() { return TriState.TRUE; } + @Override + public boolean isFlagPresent(final @NotNull String name) { + if (this.isPresent(name)) { + return true; + } + return this.isPresent('!' + name); + } + @Override public Tag.@NotNull Argument orThrow(final @NotNull String name, final @NotNull String errorMessage) { requireNonNull(errorMessage, "errorMessage"); diff --git a/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/tag/resolver/NamedArgumentMap.java b/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/tag/resolver/NamedArgumentMap.java index 1e767aa2ff..ac0e197909 100644 --- a/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/tag/resolver/NamedArgumentMap.java +++ b/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/tag/resolver/NamedArgumentMap.java @@ -76,6 +76,15 @@ public interface NamedArgumentMap { */ @NotNull TriState flag(@NotNull String name); + /** + * Get whether this flag is set, inverted or not. + * + * @param name the name of the flag + * @return whether it is present + * @since 4.25.0 + */ + boolean isFlagPresent(@NotNull String name); + /** * Get an argument by its name, throwing an exception if no argument with that name was present. * From 233e9fea02ed0e2ac9bf9311970fe2a23aa51b82 Mon Sep 17 00:00:00 2001 From: Strokkur24 Date: Sat, 20 Sep 2025 11:23:38 +0200 Subject: [PATCH 19/19] feat: add style tag --- .../tag/standard/StandardTags.java | 13 ++- .../minimessage/tag/standard/StyleTag.java | 108 ++++++++++++++++++ .../tag/standard/StyleTagTest.java | 78 +++++++++++++ 3 files changed, 198 insertions(+), 1 deletion(-) create mode 100644 text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/tag/standard/StyleTag.java create mode 100644 text-minimessage/src/test/java/net/kyori/adventure/text/minimessage/tag/standard/StyleTagTest.java diff --git a/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/tag/standard/StandardTags.java b/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/tag/standard/StandardTags.java index d5e44841bc..17c785a2ce 100644 --- a/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/tag/standard/StandardTags.java +++ b/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/tag/standard/StandardTags.java @@ -65,7 +65,8 @@ private StandardTags() { ScoreTag.RESOLVER, NbtTag.RESOLVER, PrideTag.RESOLVER, - ShadowColorTag.RESOLVER + ShadowColorTag.RESOLVER, + StyleTag.RESOLVER ) .build(); @@ -288,6 +289,16 @@ public static TagResolver transition() { return ShadowColorTag.RESOLVER; } + /** + * Get a resolver for the {@value StyleTag#STYLE} tags. + * + * @return a resolver for the {@value StyleTag#STYLE} tags + * @since 4.25.0 + */ + public static @NotNull TagResolver style() { + return StyleTag.RESOLVER; + } + /** * Get a resolver that handles all default standard tags. * diff --git a/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/tag/standard/StyleTag.java b/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/tag/standard/StyleTag.java new file mode 100644 index 0000000000..5ee3943455 --- /dev/null +++ b/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/tag/standard/StyleTag.java @@ -0,0 +1,108 @@ +/* + * 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.tag.standard; + +import net.kyori.adventure.text.format.ShadowColor; +import net.kyori.adventure.text.format.TextColor; +import net.kyori.adventure.text.format.TextDecoration; +import net.kyori.adventure.text.minimessage.Context; +import net.kyori.adventure.text.minimessage.ParsingException; +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; +import net.kyori.adventure.text.minimessage.tag.resolver.NamedArgumentMap; +import net.kyori.adventure.text.minimessage.tag.resolver.TagResolver; +import net.kyori.adventure.util.TriState; +import org.intellij.lang.annotations.Subst; +import org.jetbrains.annotations.Nullable; + +/** + * A style tag for setting multiple styles. + * + * @since 4.25.0 + */ +final class StyleTag { + private static final String STYLE = "style"; + + static final TagResolver RESOLVER = SerializableResolver.claimingStyleNamed( + STYLE, + StyleTag::create, + StyleClaim.claim( + "style_tag", + style -> style, + style -> false, // this tag should never be emitted + (style, emitter) -> {} + ) + ); + + private StyleTag() { + } + + static Tag create(final NamedArgumentMap args, final Context ctx) throws ParsingException { + final TriState bold = args.isFlagPresent("bold") ? args.flag("bold") : args.flag("b"); + final TriState italic = args.isFlagPresent("italic") ? args.flag("italic") : args.flag("i"); + final TriState underlined = args.isFlagPresent("underlined") ? args.flag("underlined") : args.flag("u"); + final TriState obfuscated = args.isFlagPresent("obfuscated") ? args.flag("obfuscated") : args.flag("obf"); + final TriState strikethrough = args.isFlagPresent("strikethrough") ? args.flag("strikethrough") : args.flag("st"); + + final Tag.@Nullable Argument colorArgument = args.isPresent("color") ? args.get("color") : args.get("c"); + final TextColor color; + if (colorArgument != null) { + color = TextColor.fromCSSHexString(colorArgument.value()); + if (color == null) { + throw ctx.newException("Color '" + colorArgument.value() + "' is in an invalid format. Please use #RRGGBB or #RGB."); + } + } else { + color = null; + } + + final Tag.@Nullable Argument shadowArgument = args.isPresent("shadow") ? args.get("shadow") : args.get("s"); + final ShadowColor shadow; + if (shadowArgument != null) { + final @Subst("#00000000") String value = shadowArgument.value(); + shadow = ShadowColor.fromHexString(value); + if (shadow == null) { + throw ctx.newException("Shadow color '" + value + "' is in an invalid format. Please use #RRGGBBAA."); + } + } else { + shadow = null; + } + + return Tag.styling(builder -> { + builder.decoration(TextDecoration.BOLD, TextDecoration.State.byTriState(bold)); + builder.decoration(TextDecoration.ITALIC, TextDecoration.State.byTriState(italic)); + builder.decoration(TextDecoration.UNDERLINED, TextDecoration.State.byTriState(underlined)); + builder.decoration(TextDecoration.OBFUSCATED, TextDecoration.State.byTriState(obfuscated)); + builder.decoration(TextDecoration.STRIKETHROUGH, TextDecoration.State.byTriState(strikethrough)); + + if (color != null) { + builder.color(color); + } + + if (shadow != null) { + builder.shadowColor(shadow); + } + }); + } +} diff --git a/text-minimessage/src/test/java/net/kyori/adventure/text/minimessage/tag/standard/StyleTagTest.java b/text-minimessage/src/test/java/net/kyori/adventure/text/minimessage/tag/standard/StyleTagTest.java new file mode 100644 index 0000000000..0a3043b91a --- /dev/null +++ b/text-minimessage/src/test/java/net/kyori/adventure/text/minimessage/tag/standard/StyleTagTest.java @@ -0,0 +1,78 @@ +/* + * 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.tag.standard; + +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.ShadowColor; +import net.kyori.adventure.text.format.Style; +import net.kyori.adventure.text.format.TextColor; +import net.kyori.adventure.text.format.TextDecoration; +import net.kyori.adventure.text.minimessage.AbstractTest; +import org.junit.jupiter.api.Test; + +import static net.kyori.adventure.text.Component.text; + +public class StyleTagTest extends AbstractTest { + + @Test + void testEmptyTag() { + final String input = ""; + final Component expected = text("Hey there!"); + assertParsedEquals(expected, input); + } + + @Test + void testBasic() { + final String input = ""; + final Component expected = text("Fancy", TextColor.fromCSSHexString("#f00"), TextDecoration.BOLD, TextDecoration.ITALIC, TextDecoration.UNDERLINED); + assertParsedEquals(expected, input); + } + + @Test + void testInversion() { + final String input = "Bold, , bold!"; + final Component expected = text() + .decorate(TextDecoration.BOLD) + .append(text("Bold, ")) + .append(text("not bold", Style.style(b -> b.decoration(TextDecoration.BOLD, TextDecoration.State.FALSE)))) + .append(text(", bold!")) + .build(); + assertParsedEquals(expected, input); + } + + @Test + void testPrecedence() { + final String input = "