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 4d880c1d0..fab392248 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 @@ -52,6 +52,7 @@ class ContextImpl implements Context { private String message; private final MiniMessage miniMessage; private final TagResolver tagResolver; + private final UnaryOperator preProcessor; private final UnaryOperator postProcessor; ContextImpl( @@ -60,6 +61,7 @@ class ContextImpl implements Context { final String message, final MiniMessage miniMessage, final @NotNull TagResolver extraTags, + final UnaryOperator preProcessor, final UnaryOperator postProcessor ) { this.strict = strict; @@ -67,6 +69,7 @@ class ContextImpl implements Context { this.message = message; this.miniMessage = miniMessage; this.tagResolver = extraTags; + this.preProcessor = preProcessor == null ? UnaryOperator.identity() : preProcessor; this.postProcessor = postProcessor == null ? UnaryOperator.identity() : postProcessor; } @@ -76,9 +79,10 @@ static ContextImpl of( final String input, final MiniMessageImpl miniMessage, final TagResolver extraTags, + final UnaryOperator preProcessor, final UnaryOperator postProcessor ) { - return new ContextImpl(strict, debugOutput, input, miniMessage, extraTags, postProcessor); + return new ContextImpl(strict, debugOutput, input, miniMessage, extraTags, preProcessor, postProcessor); } public boolean strict() { @@ -105,6 +109,10 @@ public UnaryOperator postProcessor() { return this.postProcessor; } + public UnaryOperator preProcessor() { + return this.preProcessor; + } + @Override public @NotNull Component deserialize(final @NotNull String message) { return this.miniMessage.deserialize(requireNonNull(message, "message"), this.tagResolver); diff --git a/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/MiniMessage.java b/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/MiniMessage.java index b8a452c40..ea3b98fac 100644 --- a/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/MiniMessage.java +++ b/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/MiniMessage.java @@ -272,6 +272,16 @@ interface Builder extends AbstractBuilder { */ @NotNull Builder postProcessor(final @NotNull UnaryOperator postProcessor); + /** + * Specify a function that takes the string at the start of the parser process. + *

By default, this does absolutely nothing.

+ * + * @param preProcessor method run at the start of parsing + * @return this builder + * @since 4.11.0 + */ + @NotNull Builder preProcessor(final @NotNull UnaryOperator preProcessor); + /** * Builds the serializer. * diff --git a/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/MiniMessageImpl.java b/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/MiniMessageImpl.java index e6548ffcf..353da378f 100644 --- a/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/MiniMessageImpl.java +++ b/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/MiniMessageImpl.java @@ -54,41 +54,44 @@ final class MiniMessageImpl implements MiniMessage { static final class Instances { static final MiniMessage INSTANCE = SERVICE .map(Provider::miniMessage) - .orElseGet(() -> new MiniMessageImpl(TagResolver.standard(), false, null, DEFAULT_COMPACTING_METHOD)); + .orElseGet(() -> new MiniMessageImpl(TagResolver.standard(), false, null, DEFAULT_NO_OP, DEFAULT_COMPACTING_METHOD)); } + static final UnaryOperator DEFAULT_NO_OP = UnaryOperator.identity(); static final UnaryOperator DEFAULT_COMPACTING_METHOD = Component::compact; private final boolean strict; private final @Nullable Consumer debugOutput; private final UnaryOperator postProcessor; + private final UnaryOperator preProcessor; final MiniMessageParser parser; - MiniMessageImpl(final @NotNull TagResolver resolver, final boolean strict, final @Nullable Consumer debugOutput, final @NotNull UnaryOperator postProcessor) { + MiniMessageImpl(final @NotNull TagResolver resolver, final boolean strict, final @Nullable Consumer debugOutput, final @NotNull UnaryOperator preProcessor, final @NotNull UnaryOperator postProcessor) { this.parser = new MiniMessageParser(resolver); this.strict = strict; this.debugOutput = debugOutput; + this.preProcessor = preProcessor; this.postProcessor = postProcessor; } @Override public @NotNull Component deserialize(final @NotNull String input) { - return this.parser.parseFormat(input, this.newContext(input, null)); + return this.parser.parseFormat(this.newContext(input, null)); } @Override public @NotNull Component deserialize(final @NotNull String input, final @NotNull TagResolver tagResolver) { - return this.parser.parseFormat(input, this.newContext(input, requireNonNull(tagResolver, "tagResolver"))); + return this.parser.parseFormat(this.newContext(input, requireNonNull(tagResolver, "tagResolver"))); } @Override public Node.@NotNull Root deserializeToTree(final @NotNull String input) { - return this.parser.parseToTree(input, this.newContext(input, null)); + return this.parser.parseToTree(this.newContext(input, null)); } @Override public Node.@NotNull Root deserializeToTree(final @NotNull String input, final @NotNull TagResolver tagResolver) { - return this.parser.parseToTree(input, this.newContext(input, requireNonNull(tagResolver, "tagResolver"))); + return this.parser.parseToTree(this.newContext(input, requireNonNull(tagResolver, "tagResolver"))); } @Override @@ -113,30 +116,30 @@ private SerializableResolver serialResolver(final @Nullable TagResolver extraRes @Override public @NotNull String escapeTags(final @NotNull String input) { - return this.parser.escapeTokens(input, this.newContext(input, null)); + return this.parser.escapeTokens(this.newContext(input, null)); } @Override public @NotNull String escapeTags(final @NotNull String input, final @NotNull TagResolver tagResolver) { - return this.parser.escapeTokens(input, this.newContext(input, tagResolver)); + return this.parser.escapeTokens(this.newContext(input, tagResolver)); } @Override public @NotNull String stripTags(final @NotNull String input) { - return this.parser.stripTokens(input, this.newContext(input, null)); + return this.parser.stripTokens(this.newContext(input, null)); } @Override public @NotNull String stripTags(final @NotNull String input, final @NotNull TagResolver tagResolver) { - return this.parser.stripTokens(input, this.newContext(input, tagResolver)); + return this.parser.stripTokens(this.newContext(input, tagResolver)); } private @NotNull ContextImpl newContext(final @NotNull String input, final @Nullable TagResolver resolver) { requireNonNull(input, "input"); if (resolver == null) { - return ContextImpl.of(this.strict, this.debugOutput, input, this, TagResolver.empty(), this.postProcessor); + return ContextImpl.of(this.strict, this.debugOutput, input, this, TagResolver.empty(), this.preProcessor, this.postProcessor); } else { - return ContextImpl.of(this.strict, this.debugOutput, input, this, resolver, this.postProcessor); + return ContextImpl.of(this.strict, this.debugOutput, input, this, resolver, this.preProcessor, this.postProcessor); } } @@ -145,6 +148,7 @@ static final class BuilderImpl implements Builder { private boolean strict = false; private Consumer debug = null; private UnaryOperator postProcessor = DEFAULT_COMPACTING_METHOD; + private UnaryOperator preProcessor = DEFAULT_NO_OP; BuilderImpl() { BUILDER.accept(this); @@ -156,6 +160,7 @@ static final class BuilderImpl implements Builder { this.strict = serializer.strict; this.debug = serializer.debugOutput; this.postProcessor = serializer.postProcessor; + this.preProcessor = serializer.preProcessor; } @Override @@ -191,9 +196,15 @@ static final class BuilderImpl implements Builder { return this; } + @Override + public @NotNull Builder preProcessor(final @NotNull UnaryOperator preProcessor) { + this.preProcessor = Objects.requireNonNull(preProcessor, "preProcessor"); + return this; + } + @Override public @NotNull MiniMessage build() { - return new MiniMessageImpl(this.tagResolver, this.strict, this.debug, this.postProcessor); + return new MiniMessageImpl(this.tagResolver, this.strict, this.debug, this.preProcessor, this.postProcessor); } } } 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 44897f2d4..fab96f608 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 @@ -59,13 +59,17 @@ final class MiniMessageParser { this.tagResolver = tagResolver; } - @NotNull String escapeTokens(final @NotNull String richMessage, final @NotNull ContextImpl context) { - final StringBuilder sb = new StringBuilder(richMessage.length()); - this.escapeTokens(sb, richMessage, context); + @NotNull String escapeTokens(final @NotNull ContextImpl context) { + final StringBuilder sb = new StringBuilder(context.message().length()); + this.escapeTokens(sb, context); return sb.toString(); } - void escapeTokens(final StringBuilder sb, final @NotNull String richMessage, final @NotNull ContextImpl context) { + void escapeTokens(final StringBuilder sb, final @NotNull ContextImpl context) { + this.escapeTokens(sb, context.message(), context); + } + + private void escapeTokens(final StringBuilder sb, final String richMessage, final ContextImpl context) { this.processTokens(sb, richMessage, context, (token, builder) -> { builder.append('\\').append(TokenParser.TAG_START); if (token.type() == TokenType.CLOSE_TAG) { @@ -82,12 +86,16 @@ void escapeTokens(final StringBuilder sb, final @NotNull String richMessage, fin }); } - @NotNull String stripTokens(final @NotNull String richMessage, final @NotNull ContextImpl context) { - final StringBuilder sb = new StringBuilder(richMessage.length()); - this.processTokens(sb, richMessage, context, (token, builder) -> {}); + @NotNull String stripTokens(final @NotNull ContextImpl context) { + final StringBuilder sb = new StringBuilder(context.message().length()); + this.processTokens(sb, context, (token, builder) -> {}); return sb.toString(); } + private void processTokens(final @NotNull StringBuilder sb, final @NotNull ContextImpl context, final BiConsumer tagHandler) { + this.processTokens(sb, context.message(), context, tagHandler); + } + private void processTokens(final @NotNull StringBuilder sb, final @NotNull String richMessage, final @NotNull ContextImpl context, final BiConsumer tagHandler) { final TagResolver combinedResolver = TagResolver.resolver(this.tagResolver, context.extraTags()); final List root = TokenParser.tokenize(richMessage); @@ -117,12 +125,13 @@ private void processTokens(final @NotNull StringBuilder sb, final @NotNull Strin } } - @NotNull RootNode parseToTree(final @NotNull String richMessage, final @NotNull ContextImpl context) { + @NotNull RootNode parseToTree(final @NotNull ContextImpl context) { final TagResolver combinedResolver = TagResolver.resolver(this.tagResolver, context.extraTags()); + final String processedMessage = context.preProcessor().apply(context.message()); final Consumer debug = context.debugOutput(); if (debug != null) { debug.accept("Beginning parsing message "); - debug.accept(richMessage); + debug.accept(processedMessage); debug.accept("\n"); } @@ -183,10 +192,10 @@ private void processTokens(final @NotNull StringBuilder sb, final @NotNull Strin return combinedResolver.has(sanitized); }; - final String preProcessed = TokenParser.resolvePreProcessTags(richMessage, 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, richMessage, context.strict()); + final RootNode root = TokenParser.parse(transformationFactory, tagNameChecker, preProcessed, processedMessage, context.strict()); if (debug != null) { debug.accept("Text parsed into element tree:\n"); @@ -196,8 +205,8 @@ private void processTokens(final @NotNull StringBuilder sb, final @NotNull Strin return root; } - @NotNull Component parseFormat(final @NotNull String richMessage, final @NotNull ContextImpl context) { - final ElementNode root = this.parseToTree(richMessage, context); + @NotNull Component parseFormat(final @NotNull ContextImpl context) { + final ElementNode root = this.parseToTree(context); return Objects.requireNonNull(context.postProcessor().apply(this.treeToComponent(root, context)), "Post-processor must not return null"); } diff --git a/text-minimessage/src/test/java/net/kyori/adventure/text/minimessage/AbstractTest.java b/text-minimessage/src/test/java/net/kyori/adventure/text/minimessage/AbstractTest.java index a7caba4a5..510a576a1 100644 --- a/text-minimessage/src/test/java/net/kyori/adventure/text/minimessage/AbstractTest.java +++ b/text-minimessage/src/test/java/net/kyori/adventure/text/minimessage/AbstractTest.java @@ -24,6 +24,7 @@ package net.kyori.adventure.text.minimessage; import java.util.Collections; +import java.util.function.UnaryOperator; import java.util.stream.Collectors; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.ComponentLike; @@ -68,7 +69,7 @@ protected final String prettyPrint(final Component component) { } public static Context dummyContext(final String originalMessage) { - return ContextImpl.of(false, null, originalMessage, (MiniMessageImpl) PARSER, TagResolver.empty(), Component::compact); + return ContextImpl.of(false, null, originalMessage, (MiniMessageImpl) PARSER, TagResolver.empty(), UnaryOperator.identity(), Component::compact); } public static ArgumentQueue emptyArgumentQueue(final Context context) { 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 e573355b1..2fcb60262 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 @@ -122,6 +122,15 @@ void testPlaceholderComponentMixed() { this.assertParsedEquals(miniMessage, expected, input, t1, t2); } + @Test + void testPreprocessing() { + final Component expected = MiniMessage.miniMessage().deserialize("Hello, world!"); + + final String input = "Hello"; + final MiniMessage miniMessage = MiniMessage.builder().preProcessor(str -> "" + str + ", world!").build(); + this.assertParsedEquals(miniMessage, expected, input); + } + // GH-103 @Test void testPlaceholderInHover() {