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 207b870f19..25da62a0f6 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 @@ -98,6 +98,7 @@ private void processTokens(final @NotNull StringBuilder sb, final @NotNull Strin break; case OPEN_TAG: case CLOSE_TAG: + case OPEN_CLOSE_TAG: // extract tag name if (token.childTokens().isEmpty()) { sb.append(richMessage, token.startIndex(), token.endIndex()); diff --git a/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/MiniMessageSerializer.java b/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/MiniMessageSerializer.java index 611538da4f..ae2a2a2879 100644 --- a/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/MiniMessageSerializer.java +++ b/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/MiniMessageSerializer.java @@ -128,7 +128,6 @@ void mark() { } void popToMark() { - this.completeTag(); if (this.tagLevel == 0) { return; } @@ -139,7 +138,6 @@ void popToMark() { } void popAll() { - this.completeTag(); while (this.tagLevel > 0) { final String tag = this.activeTags[--this.tagLevel]; if (tag != MARK) { @@ -193,15 +191,6 @@ public TokenEmitter argument(final String arg) { return this.argument(serialized, QuotingOverride.QUOTED); // always quote tokens } - @Override - public Collector selfClosing(final String token) { - this.completeTag(); - this.consumer.append(TokenParser.TAG_START); - this.escapeTagContent(token, QuotingOverride.UNQUOTED); - this.midTag = true; // TODO: `` syntax - return this; - } - @Override public Collector text(final String text) { this.completeTag(); @@ -276,17 +265,21 @@ static void appendEscaping(final StringBuilder builder, final String text, final @Override public Collector pop() { - this.completeTag(); this.emitClose(this.popTag(false)); return this; } private void emitClose(final @NotNull String tag) { // currently: we don't keep any arguments, does it ever make sense to? - this.consumer.append(TokenParser.TAG_START) - .append(TokenParser.CLOSE_TAG); - this.escapeTagContent(tag, QuotingOverride.UNQUOTED); - this.consumer.append(TokenParser.TAG_END); + if (this.midTag) { + this.consumer.append(TokenParser.CLOSE_TAG).append(TokenParser.TAG_END); + this.midTag = false; + } else { + this.consumer.append(TokenParser.TAG_START) + .append(TokenParser.CLOSE_TAG); + this.escapeTagContent(tag, QuotingOverride.UNQUOTED); + this.consumer.append(TokenParser.TAG_END); + } } // ClaimCollector diff --git a/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/parser/TokenParser.java b/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/parser/TokenParser.java index 30db9879ce..d0880ec7d2 100644 --- a/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/parser/TokenParser.java +++ b/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/parser/TokenParser.java @@ -213,8 +213,10 @@ public static void parseString(final String message, final MatchedTokenConsumer< // closing tags start with thisType = TokenType.CLOSE_TAG; + } else if (boundsCheck(message, marker, 2) && message.charAt(i - 1) == CLOSE_TAG) { // + thisType = TokenType.OPEN_CLOSE_TAG; } consumer.accept(marker, currentTokenEnd, thisType); state = FirstPassState.NORMAL; @@ -254,13 +256,13 @@ public static void parseString(final String message, final MatchedTokenConsumer< private static void parseSecondPass(final String message, final List tokens) { for (final Token token : tokens) { final TokenType type = token.type(); - if (type != TokenType.OPEN_TAG && type != TokenType.CLOSE_TAG) { + if (type != TokenType.OPEN_TAG && type != TokenType.OPEN_CLOSE_TAG && type != TokenType.CLOSE_TAG) { continue; } // Only look inside the tag <[/] and > - final int startIndex = type == TokenType.OPEN_TAG ? token.startIndex() + 1 : token.startIndex() + 2; - final int endIndex = token.endIndex() - 1; + final int startIndex = type == TokenType.CLOSE_TAG ? token.startIndex() + 2 : token.startIndex() + 1; + final int endIndex = type == TokenType.OPEN_CLOSE_TAG ? token.endIndex() - 2 : token.endIndex() - 1; SecondPassState state = SecondPassState.NORMAL; boolean escaped = false; @@ -362,6 +364,7 @@ private static ElementNode buildTree( break; case OPEN_TAG: + case OPEN_CLOSE_TAG: final TagNode tagNode = new TagNode(node, token, message, tagProvider); if (tagNameChecker.test(tagNode.name())) { final Tag tag = tagProvider.resolve(tagNode); @@ -380,8 +383,8 @@ private static ElementNode buildTree( // This is a recognized tag, goes in the tree tagNode.tag(tag); node.addChild(tagNode); - if (!(tag instanceof Inserting) || ((Inserting) tag).allowsChildren()) { - node = tagNode; // TODO: self-terminating tags (i.e. ) don't set this, so they don't have children + if (type != TokenType.OPEN_CLOSE_TAG && (!(tag instanceof Inserting) || ((Inserting) tag).allowsChildren())) { + node = tagNode; } } } else { diff --git a/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/parser/TokenType.java b/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/parser/TokenType.java index d2f6c0a682..2ca8538ed1 100644 --- a/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/parser/TokenType.java +++ b/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/parser/TokenType.java @@ -31,6 +31,7 @@ public enum TokenType { TEXT, OPEN_TAG, + OPEN_CLOSE_TAG, // one token that both opens and closes a tag CLOSE_TAG, TAG_VALUE; } diff --git a/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/serializer/TokenEmitter.java b/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/serializer/TokenEmitter.java index 2acdd3222e..c0163188fa 100644 --- a/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/serializer/TokenEmitter.java +++ b/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/serializer/TokenEmitter.java @@ -41,19 +41,10 @@ public interface TokenEmitter { */ @NotNull TokenEmitter tag(final @NotNull String token); // TODO: some sort of TagFlags, with things like SELF_CLOSING, CLOSE_WITH_ARGUMENTS, etc? - /** - * Create a self-contained tag without arguments. - * - * @param token the token to emit - * @return this emitter - * @since 4.10.0 - */ - @NotNull TokenEmitter selfClosing(final @NotNull String token); - /** * Add arguments to the current tag. * - *

Must be called after {@link #tag(String)} or {@link #selfClosing(String)}, but before any call to {@link #text(String)}.

+ *

Must be called after {@link #tag(String)}, but before any call to {@link #text(String)}.

* * @param args args to add * @return this emitter @@ -69,7 +60,7 @@ public interface TokenEmitter { /** * Add a single argument to the current tag. * - *

Must be called after {@link #tag(String)} or {@link #selfClosing(String)}, but before any call to {@link #text(String)}.

+ *

Must be called after {@link #tag(String)}, but before any call to {@link #text(String)}.

* * @param arg argument to add * @return this emitter @@ -80,7 +71,7 @@ public interface TokenEmitter { /** * Add a single argument to the current tag. * - *

Must be called after {@link #tag(String)} or {@link #selfClosing(String)}, but before any call to {@link #text(String)}.

+ *

Must be called after {@link #tag(String)}, but before any call to {@link #text(String)}.

* * @param arg argument to add * @param quotingPreference an argument-specific quoting instruction @@ -92,7 +83,7 @@ public interface TokenEmitter { /** * Add a single argument to the current tag. * - *

Must be called after {@link #tag(String)} or {@link #selfClosing(String)}, but before any call to {@link #text(String)}.

+ *

Must be called after {@link #tag(String)}, but before any call to {@link #text(String)}.

* * @param arg argument to add, serialized as a nested MiniMessage string * @return this emitter 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 61a66c2f66..f4a219f3e2 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 @@ -440,4 +440,33 @@ void testTreeOutput() { assertEquals(expected, tree.toString()); } + + @Test + void testTagsSelfClosable() { + final String input = "hello there"; + + final Component parsed = Component.text() + .content("hello ") + .color(NamedTextColor.RED) + .append( + Component.translatable("gameMode.creative"), + Component.text(" there") + ) + .build(); + + this.assertParsedEquals(parsed, input); + } + + @Test + void testIgnorableSelfClosable() { + final String input = "things"; + + final Component parsed = Component.text().append( + Component.text("", NamedTextColor.RED), + Component.text("things") + ) + .build(); + + this.assertParsedEquals(parsed, input); + } } diff --git a/text-minimessage/src/test/java/net/kyori/adventure/text/minimessage/tag/standard/KeybindTagTest.java b/text-minimessage/src/test/java/net/kyori/adventure/text/minimessage/tag/standard/KeybindTagTest.java index 4df675e7f7..8b250168d4 100644 --- a/text-minimessage/src/test/java/net/kyori/adventure/text/minimessage/tag/standard/KeybindTagTest.java +++ b/text-minimessage/src/test/java/net/kyori/adventure/text/minimessage/tag/standard/KeybindTagTest.java @@ -36,7 +36,7 @@ class KeybindTagTest extends AbstractTest { @Test void testSerializeKeyBind() { - final String expected = "Press to jump!"; + final String expected = "Press to jump!"; final TextComponent.Builder builder = Component.text() .content("Press ") diff --git a/text-minimessage/src/test/java/net/kyori/adventure/text/minimessage/tag/standard/TranslatableTagTest.java b/text-minimessage/src/test/java/net/kyori/adventure/text/minimessage/tag/standard/TranslatableTagTest.java index 2aecd6f346..20f463a644 100644 --- a/text-minimessage/src/test/java/net/kyori/adventure/text/minimessage/tag/standard/TranslatableTagTest.java +++ b/text-minimessage/src/test/java/net/kyori/adventure/text/minimessage/tag/standard/TranslatableTagTest.java @@ -38,7 +38,7 @@ class TranslatableTagTest extends AbstractTest { @Test void testSerializeTranslatable() { - final String expected = "You should get a !"; + final String expected = "You should get a !"; final TextComponent.Builder builder = Component.text() .content("You should get a ")