Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
a2fe2c0
feat: start work on named arguments in tags
Strokkur424 Sep 17, 2025
5314452
feat: modify token parser to parse named arguments
Strokkur424 Sep 18, 2025
0d783d7
chore: introduce new TokenType to uniquely distinguish value-less tog…
Strokkur424 Sep 18, 2025
22cb404
feat: (WIP) abstracting away TagProvider into QueuedTagProvider and N…
Strokkur424 Sep 18, 2025
a53e84b
fix: (WIP) parser should now theoretically be able to distinguish nam…
Strokkur424 Sep 19, 2025
3d7d3e9
feat: flesh out parsing logic further and fix a bunch of issues
Strokkur424 Sep 19, 2025
300e7fd
feat: add test for basic named argument parsing
Strokkur424 Sep 19, 2025
828f0b0
feat: start adding more tests
Strokkur424 Sep 19, 2025
b5d3c0b
fix: <red > tag with space being recognized as valid tag
Strokkur424 Sep 19, 2025
79647a0
feat: add a bunch more tests
Strokkur424 Sep 19, 2025
09265f7
chore: fix all compile time issues
Strokkur424 Sep 19, 2025
82e4e62
feat: add test
Strokkur424 Sep 19, 2025
a788770
chore: cleanup diff and rename to sequential
Strokkur424 Sep 19, 2025
a624524
chore: add a bunch more tests
Strokkur424 Sep 20, 2025
b412b8e
feat: add inverted flag arguments
Strokkur424 Sep 20, 2025
03e6342
feat: split the claiming resolvers into named and sequenced resolvers
Strokkur424 Sep 20, 2025
5901dee
feat: add missing context newException method and try to parse tag wi…
Strokkur424 Sep 20, 2025
bf049e4
feat: add isFlagPresent
Strokkur424 Sep 20, 2025
5ccbb63
fix: invalid tag in test
Strokkur424 Sep 20, 2025
951e763
feat: add named argument support to token emitter
Strokkur424 Sep 23, 2025
703ca92
feat: add flag support to token emitter
Strokkur424 Sep 23, 2025
2a1d129
chore: remove unused import
Strokkur424 Sep 23, 2025
9c7bc98
chore: default implement TagResolver methods with null
Strokkur424 Sep 23, 2025
49afd1f
chore: remove instanceof check in SequentialTagResolver in order to n…
Strokkur424 Sep 23, 2025
d2acca5
chore: add missing (at)since annotations
Strokkur424 Sep 23, 2025
4008b17
chore: spelling mistakes
Strokkur424 Sep 24, 2025
3b6a721
Merge branch 'main/5' into feat/named-arguments
Strokkur424 Nov 5, 2025
b1881d7
chore: fix some main/5 rebase issues
Strokkur424 Nov 5, 2025
2a70bdf
Merge branch 'main/5' into feat/named-arguments
Strokkur424 Nov 5, 2025
67af697
refactor: combine named and sequential tag arguments
Strokkur424 Nov 5, 2025
c7b608d
fix: remaining tests
Strokkur424 Nov 6, 2025
c341b7e
refactor: minor second pass token parser changes and added banchmark …
Strokkur424 Nov 6, 2025
ac6bffe
chore: spotless apply
Strokkur424 Nov 6, 2025
9c4bd6f
chore: fix checkstyle violations and do a minor diff cleanup
Strokkur424 Nov 6, 2025
2a40dcf
feat: add tests for combined arguments and for sequenced args with a …
Strokkur424 Nov 6, 2025
9b35d55
chore: final cleanup
Strokkur424 Nov 6, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -23,21 +23,24 @@
*/
package net.kyori.adventure.text.minimessage.benchmark;

import java.util.List;
import java.util.concurrent.TimeUnit;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor;
import net.kyori.adventure.text.format.TextColor;
import net.kyori.adventure.text.minimessage.MiniMessage;
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.resolver.Placeholder;
import org.openjdk.jmh.annotations.Benchmark;
import org.openjdk.jmh.annotations.BenchmarkMode;
import org.openjdk.jmh.annotations.Fork;
import org.openjdk.jmh.annotations.Mode;
import org.openjdk.jmh.annotations.OutputTimeUnit;

@Fork(value = 1, warmups = 1)
@Fork(value = 2, warmups = 5)
@BenchmarkMode(Mode.SampleTime)
@OutputTimeUnit(TimeUnit.MICROSECONDS)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
public class MiniMessageBenchmark {
private static final Component MANY_COLORS = Component.textOfChildren(
Component.text("red", NamedTextColor.RED),
Expand All @@ -47,6 +50,12 @@ public class MiniMessageBenchmark {
Component.text("another", TextColor.color(0xf6a6a6))
);

@Benchmark
public List<Token> testTokenization() {
final String input = "<red>A very <gradient:red:blue>cool<b> but also</b> <i>some <!i>what <i>complex<!i> and <click:run_command:/lol>crazy (<click:open_url:https://youtube.com/>click here for cool video</click>) <b><st><gold>MiniMessage String.";
return TokenParser.tokenize(input, true);
}

@Benchmark
public Component testNiceMix() {
final String input = "<yellow><test> random <gradient:red:blue:green><bold>stranger</gradient></bold><click:run_command:test command><underlined><red>click here</click><blue> to <rainbow><b>FEEL</rainbow></underlined> it";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,62 +23,69 @@
*/
package net.kyori.adventure.text.minimessage;

import java.util.List;
import java.util.function.Supplier;
import net.kyori.adventure.text.minimessage.internal.util.ListMapHolder;
import net.kyori.adventure.text.minimessage.tag.Tag;
import net.kyori.adventure.text.minimessage.tag.resolver.ArgumentQueue;
import net.kyori.adventure.util.TriState;
import org.jspecify.annotations.NonNull;
import org.jspecify.annotations.Nullable;

import static java.util.Objects.requireNonNull;

/*
* Note to anyone looking at this class and wondering about the {@link NonNull}s. For
* some reason, IntelliJ completely ignores any {@link NullMarked} annotations on the package
* or on the class, so these pop methods had to be annotated explicitly ¯\_(ツ)_/¯.
*/
final class ArgumentQueueImpl<T extends Tag.Argument> implements ArgumentQueue {
private final Context context;
private final List<T> args;
private final ListMapHolder<T, String, T> args;
private int ptr = 0;

ArgumentQueueImpl(final Context context, final List<T> args) {
ArgumentQueueImpl(final Context context, final ListMapHolder<T, String, T> args) {
this.context = context;
this.args = args;
}

public List<T> args() {
public ListMapHolder<T, String, T> args() {
return this.args;
}

@Override
public T pop() {
public @NonNull T pop() {
if (!this.hasNext()) {
throw this.context.newException("Missing argument for this tag!", this);
}
return this.args.get(this.ptr++);
return this.args.list().get(this.ptr++);
}

@Override
public T popOr(final String errorMessage) {
public @NonNull T popOr(final String errorMessage) {
requireNonNull(errorMessage, "errorMessage");
if (!this.hasNext()) {
throw this.context.newException(errorMessage, this);
}
return this.args.get(this.ptr++);
return this.args.list().get(this.ptr++);
}

@Override
public T popOr(final Supplier<String> errorMessage) {
public @NonNull T popOr(final Supplier<String> errorMessage) {
requireNonNull(errorMessage, "errorMessage");
if (!this.hasNext()) {
throw this.context.newException(requireNonNull(errorMessage.get(), "errorMessage.get()"), this);
}
return this.args.get(this.ptr++);
return this.args.list().get(this.ptr++);
}

@Override
public @Nullable T peek() {
return this.hasNext() ? this.args.get(this.ptr) : null;
return this.hasNext() ? this.args.list().get(this.ptr) : null;
}

@Override
public boolean hasNext() {
return this.ptr < this.args.size();
return this.ptr < this.args.list().size();
}

@Override
Expand All @@ -90,4 +97,60 @@ public void reset() {
public String toString() {
return this.args.toString();
}

@Override
public boolean isPresent(final String name) {
requireNonNull(name, "name");
return this.args.map().containsKey(name);
}

@Override
public Tag.@Nullable Argument get(final String name) {
requireNonNull(name, "name");
return this.args.map().get(name);
}

@Override
public TriState flag(final 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 boolean isFlagPresent(final String name) {
if (this.isPresent(name)) {
return true;
}
return this.isPresent('!' + name);
}

@Override
public Tag.Argument orThrow(final String name, final String errorMessage) {
requireNonNull(errorMessage, "errorMessage");
final Tag.Argument arg = this.get(name);
if (arg == null) {
throw this.context.newException(errorMessage);
}
return arg;
}

@Override
public Tag.Argument orThrow(final String name, final Supplier<String> errorMessage) {
requireNonNull(errorMessage, "errorMessage");
final Tag.Argument arg = this.get(name);
if (arg == null) {
throw this.context.newException(errorMessage.get());
}
return arg;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
*/
package net.kyori.adventure.text.minimessage;

import java.util.ArrayList;
import java.util.List;
import java.util.function.Consumer;
import java.util.function.UnaryOperator;
Expand All @@ -31,6 +32,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.node.TagPart;
import net.kyori.adventure.text.minimessage.internal.util.ListMapHolder;
import net.kyori.adventure.text.minimessage.tag.Tag;
import net.kyori.adventure.text.minimessage.tag.resolver.ArgumentQueue;
import net.kyori.adventure.text.minimessage.tag.resolver.TagResolver;
Expand Down Expand Up @@ -177,11 +179,17 @@ private Component deserializeWithOptionalTarget(final String message, final TagR
}
}

private static Token[] tagsToTokens(final List<? extends Tag.Argument> tags) {
final Token[] tokens = new Token[tags.size()];
for (int i = 0, length = tokens.length; i < length; i++) {
tokens[i] = ((TagPart) tags.get(i)).token();
private static <T extends Tag.Argument> Token[] tagsToTokens(final ListMapHolder<T, String, T> tags) {
final List<Token> tokens = new ArrayList<>(tags.map().size() + tags.list().size());

for (final Tag.Argument value : tags.map().values()) {
tokens.add(((TagPart) value).token());
}

for (final T tag : tags.list()) {
tokens.add(((TagPart) tag).token());
}
return tokens;

return tokens.toArray(Token[]::new);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
import net.kyori.adventure.text.minimessage.tag.Modifying;
import net.kyori.adventure.text.minimessage.tag.Tag;
import net.kyori.adventure.text.minimessage.tag.resolver.TagResolver;
import org.jspecify.annotations.Nullable;

record MiniMessageParser(TagResolver tagResolver) {

Expand All @@ -61,7 +62,7 @@ private void escapeTokens(final StringBuilder sb, final String richMessage, fina
if (token.type() == TokenType.CLOSE_TAG) {
builder.append(TokenParser.CLOSE_TAG);
}
final List<Token> childTokens = token.childTokens();
final List<Token> childTokens = Objects.requireNonNull(token.childTokens());
for (int i = 0; i < childTokens.size(); i++) {
if (i != 0) {
builder.append(TokenParser.SEPARATOR);
Expand Down Expand Up @@ -91,7 +92,7 @@ private void processTokens(final StringBuilder sb, final String richMessage, fin
case TEXT -> sb.append(richMessage, token.startIndex(), token.endIndex());
case OPEN_TAG, CLOSE_TAG, OPEN_CLOSE_TAG -> {
// extract tag name
if (token.childTokens().isEmpty()) {
if (Objects.requireNonNull(token.childTokens()).isEmpty()) {
sb.append(richMessage, token.startIndex(), token.endIndex());
continue;
}
Expand All @@ -107,7 +108,7 @@ private void processTokens(final StringBuilder sb, final String richMessage, fin
}
}

RootNode parseToTree(final ContextImpl context) {
<T extends Tag.Argument> RootNode parseToTree(final ContextImpl context) {
final TagResolver combinedResolver = TagResolver.resolver(this.tagResolver, context.extraTags());
final String processedMessage = context.preProcessor().apply(context.message());
final Consumer<String> debug = context.debugOutput();
Expand All @@ -117,7 +118,29 @@ RootNode parseToTree(final ContextImpl context) {
debug.accept("\n");
}

final TokenParser.TagProvider transformationFactory;
final TokenParser.TagProvider<T> transformationFactory = this.transformationFactory(context, debug, combinedResolver);

final Predicate<String> tagNameChecker = name -> {
final String sanitized = TokenParser.TagProvider.sanitizePlaceholderName(name);
return combinedResolver.has(sanitized);
};

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(transformationFactory, tagNameChecker, preProcessed, processedMessage, context.strict());

if (debug != null) {
debug.accept("Text parsed into element tree:\n");
debug.accept(root.toString());
}

return root;
}

private <T extends Tag.Argument> TokenParser.TagProvider<T> transformationFactory(final ContextImpl context, final @Nullable Consumer<String> debug, final TagResolver combinedResolver) {
final TokenParser.TagProvider<T> transformationFactory;

if (debug != null) {
transformationFactory = (name, args, token) -> {
try {
Expand All @@ -132,30 +155,9 @@ RootNode parseToTree(final ContextImpl context) {

final Tag transformation = combinedResolver.resolve(name, new ArgumentQueueImpl<>(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.getClass().getName());
debug.accept("\n");
}

return transformation;
return this.debugPrintTransformation(debug, name, transformation);
} catch (final ParsingException e) {
if (token != null && e instanceof final ParsingExceptionImpl impl) {
if (impl.tokens().length == 0) {
impl.tokens(new Token[]{token});
}
}
debug.accept("Could not match node '");
debug.accept(name);
debug.accept("' - ");
debug.accept(e.getMessage());
debug.accept("\n");
this.handleParsingException(e, token, debug, name);
return null;
}
};
Expand All @@ -168,22 +170,36 @@ RootNode parseToTree(final ContextImpl context) {
}
};
}
final Predicate<String> tagNameChecker = name -> {
final String sanitized = TokenParser.TagProvider.sanitizePlaceholderName(name);
return combinedResolver.has(sanitized);
};
return transformationFactory;
}

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(transformationFactory, tagNameChecker, preProcessed, processedMessage, context.strict());
private void handleParsingException(final ParsingException exception, final @Nullable Token token, final Consumer<String> debug, final String name) {
if (token != null && exception instanceof final ParsingExceptionImpl impl) {
if (impl.tokens().length == 0) {
impl.tokens(new Token[]{token});
}
}
debug.accept("Could not match node '");
debug.accept(name);
debug.accept("' - ");
debug.accept(exception.getMessage());
debug.accept("\n");
}

if (debug != null) {
debug.accept("Text parsed into element tree:\n");
debug.accept(root.toString());
private @Nullable Tag debugPrintTransformation(final Consumer<String> debug, final String name, final @Nullable Tag transformation) {
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.getClass().getName());
debug.accept("\n");
}

return root;
return transformation;
}

Component parseFormat(final ContextImpl context) {
Expand Down
Loading