From b8a71be62dc2baa7dba0c203887cc95d09341866 Mon Sep 17 00:00:00 2001 From: zml Date: Fri, 22 Sep 2023 17:05:21 -0700 Subject: [PATCH] feat(text-minimessage): Experimental MM template processor to try out new string templates --- text-minimessage/build.gradle.kts | 36 ++++++ .../MiniMessageTemplateProcessor.java | 108 ++++++++++++++++++ .../MiniMessageTemplateProcessorTest.java | 71 ++++++++++++ 3 files changed, 215 insertions(+) create mode 100644 text-minimessage/src/main/java21/net/kyori/adventure/text/minimessage/MiniMessageTemplateProcessor.java create mode 100644 text-minimessage/src/test/java21/net/kyori/adventure/text/minimessage/MiniMessageTemplateProcessorTest.java diff --git a/text-minimessage/build.gradle.kts b/text-minimessage/build.gradle.kts index 62b4e4d97..ee94b37cd 100644 --- a/text-minimessage/build.gradle.kts +++ b/text-minimessage/build.gradle.kts @@ -11,6 +11,42 @@ dependencies { annotationProcessor(projects.adventureAnnotationProcessors) } +indra.javaVersions { + testWith(21) +} + +sourceSets { + main { + multirelease.alternateVersions(21) + } + test { + multirelease.alternateVersions(21) + } +} + +afterEvaluate { + tasks.named("compileJava21Java", JavaCompile::class) { + options.compilerArgs = options.compilerArgs + listOf("--enable-preview") + } + tasks.named("compileTestJava21Java", JavaCompile::class) { + options.compilerArgs = options.compilerArgs + listOf("--enable-preview") + } + tasks.named("testJava21", Test::class) { + jvmArgs("--enable-preview") + } + if (JavaVersion.current() >= JavaVersion.VERSION_21) { + tasks.test { + jvmArgs("--enable-preview") + } + } + dependencies { + "testJava21Implementation"(sourceSets.named("java21").map { it.output }) + } + tasks.named("checkstyleTestJava21", Checkstyle::class) { + isEnabled = false // parser issue with J21 syntax + } +} + tasks.checkstyleJmh { exclude("**") } diff --git a/text-minimessage/src/main/java21/net/kyori/adventure/text/minimessage/MiniMessageTemplateProcessor.java b/text-minimessage/src/main/java21/net/kyori/adventure/text/minimessage/MiniMessageTemplateProcessor.java new file mode 100644 index 000000000..aa3d4f9b7 --- /dev/null +++ b/text-minimessage/src/main/java21/net/kyori/adventure/text/minimessage/MiniMessageTemplateProcessor.java @@ -0,0 +1,108 @@ +/* + * This file is part of adventure, licensed under the MIT License. + * + * Copyright (c) 2017-2023 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.List; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.ComponentLike; +import net.kyori.adventure.text.format.StyleBuilderApplicable; +import net.kyori.adventure.text.minimessage.tag.Tag; +import net.kyori.adventure.text.minimessage.tag.resolver.TagResolver; +import org.jetbrains.annotations.NotNull; + +import static java.util.Objects.requireNonNull; + +/** + * A template processor to produce MiniMessage strings from Java 21 string templates. + * + * @since 4.99.99 + */ +public final class MiniMessageTemplateProcessor implements StringTemplate.Processor { + private static final String ARG_NAME = "__template_arg"; + private final MiniMessage parser; + + /** + * Create a new template processor for a parser instance. + * + * @param parserInstance the parser instance + * @return the template processor instance + * @since 4.99.99 + */ + public static @NotNull MiniMessageTemplateProcessor templateProcessor(final @NotNull MiniMessage parserInstance) { + return new MiniMessageTemplateProcessor(requireNonNull(parserInstance)); + } + + /** + * Wrap a string so that it can be interpreted as a parsed tag, rather than the unparsed tag that regular arguments are. + * + * @param mm the MiniMessage content to interpret as a parsed tag + * @return the parsed tag + * @since 4.99.99 + */ + public static @NotNull Object parsed(final @NotNull String mm) { + return new ParsedWrapper(mm); + } + + MiniMessageTemplateProcessor(final @NotNull MiniMessage parser) { + this.parser = parser; + } + + @Override + public @NotNull Component process(final @NotNull StringTemplate stringTemplate) throws ParsingException { + final var extraArg = this.resolver(stringTemplate.values()); + final StringBuilder mmBuilder = new StringBuilder(); + final var stringFragments = stringTemplate.fragments(); + if (!stringFragments.isEmpty()) { + mmBuilder.append(stringFragments.get(0)); + } + + for (int i = 1; i < stringFragments.size(); i++) { + mmBuilder.append("<" + ARG_NAME + ":").append(i - 1).append(">"); + mmBuilder.append(stringFragments.get(i)); + } + + return this.parser.deserialize(mmBuilder.toString(), extraArg); + } + + private TagResolver resolver(final List values) { + return TagResolver.resolver(ARG_NAME, (args, ctx) -> { + final int tagIdx = args.popOr("index required").asInt() + .orElseThrow(() -> ctx.newException("tag index is not an int where it was expected to be")); + + final Object value = values.get(tagIdx); + return switch (value) { + case ComponentLike c -> Tag.selfClosingInserting(c); + case ParsedWrapper p -> Tag.preProcessParsed(p.input()); + case StyleBuilderApplicable s -> Tag.styling(s); + default -> Tag.selfClosingInserting(Component.text(String.valueOf(value))); + }; + }); + } + + private record ParsedWrapper(@NotNull String input) { + ParsedWrapper { + requireNonNull(input, "input"); + } + } +} diff --git a/text-minimessage/src/test/java21/net/kyori/adventure/text/minimessage/MiniMessageTemplateProcessorTest.java b/text-minimessage/src/test/java21/net/kyori/adventure/text/minimessage/MiniMessageTemplateProcessorTest.java new file mode 100644 index 000000000..698fff86e --- /dev/null +++ b/text-minimessage/src/test/java21/net/kyori/adventure/text/minimessage/MiniMessageTemplateProcessorTest.java @@ -0,0 +1,71 @@ +/* + * This file is part of adventure, licensed under the MIT License. + * + * Copyright (c) 2017-2023 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; +import net.kyori.adventure.text.format.NamedTextColor; +import net.kyori.adventure.text.format.TextDecoration; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +class MiniMessageTemplateProcessorTest { + private static final StringTemplate.Processor MM = MiniMessageTemplateProcessor.templateProcessor(MiniMessage.miniMessage()); + @Test + void testNoArgs() { + final var simple = MM."hello"; + + assertEquals(Component.text("hello"), simple); + } + + @Test + void testStyleApplication() { + final var withStyle = MM."hello \{NamedTextColor.RED}world"; + final Component expected = Component.text() + .content("hello ") + .append(Component.text("world", NamedTextColor.RED)) + .build(); + + assertEquals(expected, withStyle); + } + + @Test + void testComponentTag() { + final Component input = Component.text("meow :3").hoverEvent(Component.text("hii", NamedTextColor.AQUA)); + final var template = MM."Hello there \{input}"; + + final Component expected = Component.text() + .content("Hello there ") + .decorate(TextDecoration.UNDERLINED) + .append(input).build(); + + assertEquals(expected, template); + } + + @Test + void testOnlyComponent() { + final var template = MM."\{Component.text("hello world!", NamedTextColor.RED)}"; + assertEquals(Component.text("hello world!", NamedTextColor.RED), template); + } +}