From cfe00fa47c19e71e191c2651deb61239d68ed18b Mon Sep 17 00:00:00 2001 From: Parker Hawke Date: Mon, 18 Sep 2023 17:14:18 -0400 Subject: [PATCH] #3490: Add ComponentBuilder#build() and ComponentSerializer#deserialize() Components traditionally use the extra data to represent components as a single BaseComponent object. While BaseComponent typically mirrors this behaviour, somewhere along the development of BungeeChat this practice was made unclear. Because ComponentBuilder#create() returns an array of BaseComponents, it has sort of been silently accepted that all components should be represented as arrays, which is incorrect. This heavily influenced the direction of Spigot's component API (with additions such as CommandSender#sendMessage(BaseComponent[])) which emphasizes this misconception of "all components are arrays". Adding new methods to ComponentBuilder and ComponentSerializer should steer use of the BungeeChat API to be more oriented towards single component instances, not arrays. --- .../bungee/api/chat/ComponentBuilder.java | 23 + .../bungee/api/chat/hover/content/Text.java | 6 + .../md_5/bungee/chat/ComponentSerializer.java | 39 +- .../md_5/bungee/api/chat/ComponentsTest.java | 401 ++++++++++++++---- 4 files changed, 383 insertions(+), 86 deletions(-) diff --git a/chat/src/main/java/net/md_5/bungee/api/chat/ComponentBuilder.java b/chat/src/main/java/net/md_5/bungee/api/chat/ComponentBuilder.java index bf990f1b..08325a01 100644 --- a/chat/src/main/java/net/md_5/bungee/api/chat/ComponentBuilder.java +++ b/chat/src/main/java/net/md_5/bungee/api/chat/ComponentBuilder.java @@ -454,9 +454,32 @@ public final class ComponentBuilder return this; } + /** + * Returns the component built by this builder. If this builder is + * empty, an empty text component will be returned. + * + * @return the component + */ + public BaseComponent build() + { + TextComponent base = new TextComponent(); + if ( !parts.isEmpty() ) + { + List cloned = new ArrayList<>( parts ); + cloned.replaceAll( BaseComponent::duplicate ); + base.setExtra( cloned ); + } + return base; + } + /** * Returns the components needed to display the message created by this * builder.git + *

+ * NOTE: {@link #build()} is preferred as it will + * consolidate all components into a single BaseComponent with extra + * contents as opposed to an array of components which is non-standard + * and may result in unexpected behavior. * * @return the created components */ diff --git a/chat/src/main/java/net/md_5/bungee/api/chat/hover/content/Text.java b/chat/src/main/java/net/md_5/bungee/api/chat/hover/content/Text.java index 58a343aa..214558f7 100644 --- a/chat/src/main/java/net/md_5/bungee/api/chat/hover/content/Text.java +++ b/chat/src/main/java/net/md_5/bungee/api/chat/hover/content/Text.java @@ -23,6 +23,12 @@ public class Text extends Content this.value = value; } + public Text(BaseComponent value) + { + // For legacy serialization reasons, this has to be an array of components + this( new BaseComponent[]{value} ); + } + public Text(String value) { this.value = value; diff --git a/chat/src/main/java/net/md_5/bungee/chat/ComponentSerializer.java b/chat/src/main/java/net/md_5/bungee/chat/ComponentSerializer.java index f1a38151..8b2dac9f 100644 --- a/chat/src/main/java/net/md_5/bungee/chat/ComponentSerializer.java +++ b/chat/src/main/java/net/md_5/bungee/chat/ComponentSerializer.java @@ -27,7 +27,6 @@ import net.md_5.bungee.api.chat.hover.content.TextSerializer; public class ComponentSerializer implements JsonDeserializer { - private static final JsonParser JSON_PARSER = new JsonParser(); private static final Gson gson = new GsonBuilder(). registerTypeAdapter( BaseComponent.class, new ComponentSerializer() ). registerTypeAdapter( TextComponent.class, new TextComponentSerializer() ). @@ -43,9 +42,25 @@ public class ComponentSerializer implements JsonDeserializer public static final ThreadLocal> serializedComponents = new ThreadLocal>(); + /** + * Parse a JSON-compliant String as an array of base components. The input + * can be one of either an array of components, or a single component object. + * If the input is an array, each component will be parsed individually and + * returned in the order that they were parsed. If the input is a single + * component object, a single-valued array with the component will be returned. + *

+ * NOTE: {@link #deserialize(String)} is preferred as it will + * parse only one component as opposed to an array of components which is non- + * standard behavior. This method is still appropriate for parsing multiple + * components at once, although such use case is rarely (if at all) exhibited + * in vanilla Minecraft. + * + * @param json the component json to parse + * @return an array of all parsed components + */ public static BaseComponent[] parse(String json) { - JsonElement jsonElement = JSON_PARSER.parse( json ); + JsonElement jsonElement = JsonParser.parseString( json ); if ( jsonElement.isJsonArray() ) { @@ -59,6 +74,26 @@ public class ComponentSerializer implements JsonDeserializer } } + /** + * Deserialize a JSON-compliant String as a single component. The input is + * expected to be a JSON object that represents only one component. + * + * @param json the component json to parse + * @return the deserialized component + * @throws IllegalArgumentException if anything other than a JSON object is + * passed as input + */ + public static BaseComponent deserialize(String json) + { + JsonElement jsonElement = JsonParser.parseString( json ); + if ( !jsonElement.isJsonObject() ) + { + throw new IllegalArgumentException( "Malformatted JSON. Expected object, got array for input \"" + json + "\"." ); + } + + return gson.fromJson( jsonElement, BaseComponent.class ); + } + public static String toString(Object object) { return gson.toJson( object ); diff --git a/chat/src/test/java/net/md_5/bungee/api/chat/ComponentsTest.java b/chat/src/test/java/net/md_5/bungee/api/chat/ComponentsTest.java index 9884f29b..95dd1620 100644 --- a/chat/src/test/java/net/md_5/bungee/api/chat/ComponentsTest.java +++ b/chat/src/test/java/net/md_5/bungee/api/chat/ComponentsTest.java @@ -1,6 +1,11 @@ package net.md_5.bungee.api.chat; import java.awt.Color; +import java.util.function.BiFunction; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.ObjIntConsumer; +import java.util.function.Supplier; import net.md_5.bungee.api.ChatColor; import net.md_5.bungee.api.chat.hover.content.Item; import net.md_5.bungee.api.chat.hover.content.Text; @@ -18,10 +23,24 @@ public class ComponentsTest Assert.assertEquals( TextComponent.toLegacyText( parsed ), TextComponent.toLegacyText( components ) ); } - public static void testDissembleReassemble(String json) + public static void testDissembleReassemble(BaseComponent component) { + String json = ComponentSerializer.toString( component ); BaseComponent[] parsed = ComponentSerializer.parse( json ); - Assert.assertEquals( json, ComponentSerializer.toString( parsed ) ); + Assert.assertEquals( TextComponent.toLegacyText( parsed ), TextComponent.toLegacyText( component ) ); + } + + public static void testAssembleDissemble(String json, boolean modern) + { + if ( modern ) + { + BaseComponent deserialized = ComponentSerializer.deserialize( json ); + Assert.assertEquals( json, ComponentSerializer.toString( deserialized ) ); + } else + { + BaseComponent[] parsed = ComponentSerializer.parse( json ); + Assert.assertEquals( json, ComponentSerializer.toString( parsed ) ); + } } @Test @@ -41,8 +60,10 @@ public class ComponentsTest { textComponent } ); + testDissembleReassemble( textComponent ); json = "{\"hoverEvent\":{\"action\":\"show_item\",\"value\":[{\"text\":\"{id:\\\"minecraft:netherrack\\\",Count:47b}\"}]},\"text\":\"Test\"}"; - testDissembleReassemble( json ); + testAssembleDissemble( json, false ); + testAssembleDissemble( json, true ); ////////// String hoverVal = "{\"text\":\"{id:\\\"minecraft:dirt\\\",Count:1b}\"}"; json = "{\"extra\":[{\"text\":\"[\"},{\"extra\":[{\"translate\":\"block.minecraft.dirt\"}],\"text\":\"\"},{\"text\":\"]\"}],\"hoverEvent\":{\"action\":\"show_item\",\"value\":[" + hoverVal + "]},\"text\":\"\"}"; @@ -66,18 +87,37 @@ public class ComponentsTest } @Test - public void testEmptyComponentBuilder() + public void testEmptyComponentBuilderCreate() + { + this.testEmptyComponentBuilder( + ComponentBuilder::create, + (components) -> Assert.assertEquals( components.length, 0 ), + (components, size) -> Assert.assertEquals( size, components.length ) + ); + } + + @Test + public void testEmptyComponentBuilderBuild() + { + this.testEmptyComponentBuilder( + ComponentBuilder::build, + (component) -> Assert.assertNull( component.getExtra() ), + (component, size) -> Assert.assertEquals( component.getExtra().size(), size ) + ); + } + + private void testEmptyComponentBuilder(Function componentBuilder, Consumer emptyAssertion, ObjIntConsumer sizedAssertion) { ComponentBuilder builder = new ComponentBuilder(); - BaseComponent[] parts = builder.create(); - Assert.assertEquals( parts.length, 0 ); + T component = componentBuilder.apply( builder ); + emptyAssertion.accept( component ); for ( int i = 0; i < 3; i++ ) { builder.append( "part:" + i ); - parts = builder.create(); - Assert.assertEquals( parts.length, i + 1 ); + component = componentBuilder.apply( builder ); + sizedAssertion.accept( component, i + 1 ); } } @@ -213,7 +253,7 @@ public class ComponentsTest } @Test - public void testHoverEventContents() + public void testHoverEventContentsCreate() { // First do the text using the newer contents system HoverEvent hoverEvent = new HoverEvent( @@ -222,21 +262,53 @@ public class ComponentsTest new Text( new ComponentBuilder( "Second" ).create() ) ); + this.testHoverEventContents( + hoverEvent, + ComponentSerializer::parse, + (components) -> components[0].getHoverEvent(), + ComponentsTest::testDissembleReassemble // BaseComponent + ); + + // check the test still works with the value method + hoverEvent = new HoverEvent( HoverEvent.Action.SHOW_TEXT, new ComponentBuilder( "Sample text" ).create() ); + TextComponent component = new TextComponent( "Sample text" ); + component.setHoverEvent( hoverEvent ); + + Assert.assertEquals( hoverEvent.getContents().size(), 1 ); + Assert.assertTrue( hoverEvent.isLegacy() ); + String serialized = ComponentSerializer.toString( component ); + BaseComponent[] deserialized = ComponentSerializer.parse( serialized ); + Assert.assertEquals( component.getHoverEvent(), deserialized[0].getHoverEvent() ); + } + + @Test + public void testHoverEventContentsBuild() + { + // First do the text using the newer contents system + HoverEvent hoverEvent = new HoverEvent( + HoverEvent.Action.SHOW_TEXT, + new Text( new ComponentBuilder( "First" ).build() ), + new Text( new ComponentBuilder( "Second" ).build() ) + ); + + this.testHoverEventContents( + hoverEvent, + ComponentSerializer::deserialize, + BaseComponent::getHoverEvent, + ComponentsTest::testDissembleReassemble // BaseComponent + ); + } + + private void testHoverEventContents(HoverEvent hoverEvent, Function deserializer, Function hoverEventGetter, Consumer dissembleReassembleTest) + { TextComponent component = new TextComponent( "Sample text" ); component.setHoverEvent( hoverEvent ); Assert.assertEquals( hoverEvent.getContents().size(), 2 ); Assert.assertFalse( hoverEvent.isLegacy() ); - String serialized = ComponentSerializer.toString( component ); - BaseComponent[] deserialized = ComponentSerializer.parse( serialized ); - Assert.assertEquals( component.getHoverEvent(), deserialized[0].getHoverEvent() ); - // check the test still works with the value method - hoverEvent = new HoverEvent( HoverEvent.Action.SHOW_TEXT, new ComponentBuilder( "Sample text" ).create() ); - Assert.assertEquals( hoverEvent.getContents().size(), 1 ); - Assert.assertTrue( hoverEvent.isLegacy() ); - serialized = ComponentSerializer.toString( component ); - deserialized = ComponentSerializer.parse( serialized ); - Assert.assertEquals( component.getHoverEvent(), deserialized[0].getHoverEvent() ); + String serialized = ComponentSerializer.toString( component ); + T deserialized = deserializer.apply( serialized ); + Assert.assertEquals( component.getHoverEvent(), hoverEventGetter.apply( deserialized ) ); // Test single content: String json = "{\"italic\":true,\"color\":\"gray\",\"translate\":\"chat.type.admin\",\"with\":[{\"text\":\"@\"}" @@ -248,17 +320,28 @@ public class ComponentsTest + "\"/tell Name \"},\"hoverEvent\":{\"action\":\"show_entity\",\"contents\":" + "{\"type\":\"minecraft:player\",\"id\":\"00000000-0000-0000-0000-00000000000000\",\"name\":" + "{\"text\":\"Name\"}}},\"text\":\"Name\"}]}]}"; - testDissembleReassemble( ComponentSerializer.parse( json ) ); + dissembleReassembleTest.accept( deserializer.apply( json ) ); } @Test - public void testFormatRetentionCopyFormatting() + public void testFormatRetentionCopyFormattingCreate() + { + this.testFormatRetentionCopyFormatting( () -> new HoverEvent( HoverEvent.Action.SHOW_TEXT, new ComponentBuilder( "Test" ).create() ) ); + } + + @Test + public void testFormatRetentionCopyFormattingBuild() + { + this.testFormatRetentionCopyFormatting( () -> new HoverEvent( HoverEvent.Action.SHOW_TEXT, new Text( new ComponentBuilder( "Test" ).build() ) ) ); + } + + private void testFormatRetentionCopyFormatting(Supplier hoverEventSupplier) { TextComponent first = new TextComponent( "Hello" ); first.setBold( true ); first.setColor( ChatColor.RED ); first.setClickEvent( new ClickEvent( ClickEvent.Action.RUN_COMMAND, "test" ) ); - first.setHoverEvent( new HoverEvent( HoverEvent.Action.SHOW_TEXT, new ComponentBuilder( "Test" ).create() ) ); + first.setHoverEvent( hoverEventSupplier.get() ); TextComponent second = new TextComponent( " world" ); second.copyFormatting( first, ComponentBuilder.FormatRetention.ALL, true ); @@ -269,16 +352,44 @@ public class ComponentsTest } @Test - public void testBuilderClone() + public void testBuilderCloneCreate() + { + this.testBuilderClone( (builder) -> TextComponent.toLegacyText( builder.create() ) ); + } + + @Test + public void testBuilderCloneBuild() + { + this.testBuilderClone( (builder) -> TextComponent.toLegacyText( builder.build() ) ); + } + + private void testBuilderClone(Function legacyTextFunction) { ComponentBuilder builder = new ComponentBuilder( "Hello " ).color( ChatColor.RED ).append( "world" ).color( ChatColor.DARK_RED ); ComponentBuilder cloned = new ComponentBuilder( builder ); - Assert.assertEquals( TextComponent.toLegacyText( builder.create() ), TextComponent.toLegacyText( cloned.create() ) ); + Assert.assertEquals( legacyTextFunction.apply( builder ), legacyTextFunction.apply( cloned ) ); } @Test - public void testBuilderAppendMixedComponents() + public void testBuilderAppendCreateMixedComponents() + { + this.testBuilderAppendMixedComponents( + ComponentBuilder::create, + (components, index) -> components[index] + ); + } + + @Test + public void testBuilderAppendBuildMixedComponents() + { + this.testBuilderAppendMixedComponents( + ComponentBuilder::build, + (component, index) -> component.getExtra().get( index ) + ); + } + + private void testBuilderAppendMixedComponents(Function componentBuilder, BiFunction extraGetter) { ComponentBuilder builder = new ComponentBuilder( "Hello " ); TextComponent textComponent = new TextComponent( "world " ); @@ -291,11 +402,11 @@ public class ComponentsTest } ); ScoreComponent scoreComponent = new ScoreComponent( "myscore", "myobjective" ); builder.append( scoreComponent ); // non array based BaseComponent append - BaseComponent[] components = builder.create(); - Assert.assertEquals( "Hello ", components[0].toPlainText() ); - Assert.assertEquals( textComponent.toPlainText(), components[1].toPlainText() ); - Assert.assertEquals( translatableComponent.toPlainText(), components[2].toPlainText() ); - Assert.assertEquals( scoreComponent.toPlainText(), components[3].toPlainText() ); + T component = componentBuilder.apply( builder ); + Assert.assertEquals( "Hello ", extraGetter.apply( component, 0 ).toPlainText() ); + Assert.assertEquals( textComponent.toPlainText(), extraGetter.apply( component, 1 ).toPlainText() ); + Assert.assertEquals( translatableComponent.toPlainText(), extraGetter.apply( component, 2 ).toPlainText() ); + Assert.assertEquals( scoreComponent.toPlainText(), extraGetter.apply( component, 3 ).toPlainText() ); } @Test @@ -309,32 +420,80 @@ public class ComponentsTest } @Test - public void testBuilderAppend() + public void testBuilderAppendCreate() { - ClickEvent clickEvent = new ClickEvent( ClickEvent.Action.RUN_COMMAND, "/help " ); - HoverEvent hoverEvent = new HoverEvent( HoverEvent.Action.SHOW_TEXT, new ComponentBuilder( "Hello world" ).create() ); - - ComponentBuilder builder = new ComponentBuilder( "Hello " ).color( ChatColor.YELLOW ); - builder.append( new ComponentBuilder( "world!" ).color( ChatColor.GREEN ).event( hoverEvent ).event( clickEvent ).create() ); - - BaseComponent[] components = builder.create(); - - Assert.assertEquals( components[1].getHoverEvent(), hoverEvent ); - Assert.assertEquals( components[1].getClickEvent(), clickEvent ); - Assert.assertEquals( "Hello world!", BaseComponent.toPlainText( components ) ); - Assert.assertEquals( ChatColor.YELLOW + "Hello " + ChatColor.GREEN + "world!", BaseComponent.toLegacyText( components ) ); + this.testBuilderAppend( + () -> new HoverEvent( HoverEvent.Action.SHOW_TEXT, new ComponentBuilder( "Hello world" ).create() ), + ComponentBuilder::create, + (components, index) -> components[index], + BaseComponent::toPlainText, + ChatColor.YELLOW + "Hello " + ChatColor.GREEN + "world!", + BaseComponent::toLegacyText + ); } @Test - public void testBuilderAppendLegacy() + public void testBuilderAppendBuild() + { + this.testBuilderAppend( + () -> new HoverEvent( HoverEvent.Action.SHOW_TEXT, new Text( new ComponentBuilder( "Hello world" ).build() ) ), + ComponentBuilder::build, + (component, index) -> component.getExtra().get( index ), + (component) -> BaseComponent.toPlainText( component ), + // An extra format code is appended to the beginning because there is an empty TextComponent at the start of every component + ChatColor.WHITE.toString() + ChatColor.YELLOW + "Hello " + ChatColor.GREEN + "world!", + (component) -> BaseComponent.toLegacyText( component ) + ); + } + + private void testBuilderAppend(Supplier hoverEventSupplier, Function componentBuilder, BiFunction extraGetter, Function toPlainTextFunction, String expectedLegacyText, Function toLegacyTextFunction) + { + ClickEvent clickEvent = new ClickEvent( ClickEvent.Action.RUN_COMMAND, "/help " ); + HoverEvent hoverEvent = hoverEventSupplier.get(); + + ComponentBuilder builder = new ComponentBuilder( "Hello " ).color( ChatColor.YELLOW ); + builder.append( new ComponentBuilder( "world!" ).color( ChatColor.GREEN ).event( hoverEvent ).event( clickEvent ).create() ); // Intentionally using create() to append multiple individual components + + T component = componentBuilder.apply( builder ); + + Assert.assertEquals( extraGetter.apply( component, 1 ).getHoverEvent(), hoverEvent ); + Assert.assertEquals( extraGetter.apply( component, 1 ).getClickEvent(), clickEvent ); + Assert.assertEquals( "Hello world!", toPlainTextFunction.apply( component ) ); + Assert.assertEquals( expectedLegacyText, toLegacyTextFunction.apply( component ) ); + } + + @Test + public void testBuilderAppendLegacyCreate() + { + this.testBuilderAppendLegacy( + ComponentBuilder::create, + BaseComponent::toPlainText, + ChatColor.YELLOW + "Hello " + ChatColor.GREEN + "world!", + BaseComponent::toLegacyText + ); + } + + @Test + public void testBuilderAppendLegacyBuild() + { + this.testBuilderAppendLegacy( + ComponentBuilder::build, + (component) -> BaseComponent.toPlainText( component ), + // An extra format code is appended to the beginning because there is an empty TextComponent at the start of every component + ChatColor.WHITE.toString() + ChatColor.YELLOW + "Hello " + ChatColor.GREEN + "world!", + (component) -> BaseComponent.toLegacyText( component ) + ); + } + + private void testBuilderAppendLegacy(Function componentBuilder, Function toPlainTextFunction, String expectedLegacyString, Function toLegacyTextFunction) { ComponentBuilder builder = new ComponentBuilder( "Hello " ).color( ChatColor.YELLOW ); builder.appendLegacy( "§aworld!" ); - BaseComponent[] components = builder.create(); + T component = componentBuilder.apply( builder ); - Assert.assertEquals( "Hello world!", BaseComponent.toPlainText( components ) ); - Assert.assertEquals( ChatColor.YELLOW + "Hello " + ChatColor.GREEN + "world!", BaseComponent.toLegacyText( components ) ); + Assert.assertEquals( "Hello world!", toPlainTextFunction.apply( component ) ); + Assert.assertEquals( expectedLegacyString, toLegacyTextFunction.apply( component ) ); } @Test @@ -397,57 +556,114 @@ public class ComponentsTest } @Test - public void testBuilder() + public void testBuilderCreate() { - BaseComponent[] components = new ComponentBuilder( "Hello " ).color( ChatColor.RED ). + this.testBuilder( + ComponentBuilder::create, + BaseComponent::toPlainText, + ChatColor.RED + "Hello " + ChatColor.BLUE + ChatColor.BOLD + + "World" + ChatColor.YELLOW + ChatColor.BOLD + "!", + BaseComponent::toLegacyText + ); + } + + @Test + public void testBuilderBuild() + { + this.testBuilder( + ComponentBuilder::build, + (component) -> BaseComponent.toPlainText( component ), + // An extra format code is appended to the beginning because there is an empty TextComponent at the start of every component + ChatColor.WHITE.toString() + ChatColor.RED + "Hello " + ChatColor.BLUE + ChatColor.BOLD + + "World" + ChatColor.YELLOW + ChatColor.BOLD + "!", + (component) -> BaseComponent.toLegacyText( component ) + ); + } + + private void testBuilder(Function componentBuilder, Function toPlainTextFunction, String expectedLegacyString, Function toLegacyTextFunction) + { + T component = componentBuilder.apply( new ComponentBuilder( "Hello " ).color( ChatColor.RED ). append( "World" ).bold( true ).color( ChatColor.BLUE ). - append( "!" ).color( ChatColor.YELLOW ).create(); + append( "!" ).color( ChatColor.YELLOW ) ); - Assert.assertEquals( "Hello World!", BaseComponent.toPlainText( components ) ); - Assert.assertEquals( ChatColor.RED + "Hello " + ChatColor.BLUE + ChatColor.BOLD - + "World" + ChatColor.YELLOW + ChatColor.BOLD + "!", BaseComponent.toLegacyText( components ) ); + Assert.assertEquals( "Hello World!", toPlainTextFunction.apply( component ) ); + Assert.assertEquals( expectedLegacyString, toLegacyTextFunction.apply( component ) ); } @Test - public void testBuilderReset() + public void testBuilderCreateReset() { - BaseComponent[] components = new ComponentBuilder( "Hello " ).color( ChatColor.RED ) - .append( "World" ).reset().create(); - - Assert.assertEquals( components[0].getColor(), ChatColor.RED ); - Assert.assertEquals( components[1].getColor(), ChatColor.WHITE ); + this.testBuilderReset( + ComponentBuilder::create, + (components, index) -> components[index] + ); } @Test - public void testBuilderFormatRetention() + public void testBuilderBuildReset() { - BaseComponent[] noneRetention = new ComponentBuilder( "Hello " ).color( ChatColor.RED ) - .append( "World", ComponentBuilder.FormatRetention.NONE ).create(); + this.testBuilderReset( + ComponentBuilder::build, + (component, index) -> component.getExtra().get( index ) + ); + } - Assert.assertEquals( noneRetention[0].getColor(), ChatColor.RED ); - Assert.assertEquals( noneRetention[1].getColor(), ChatColor.WHITE ); + private void testBuilderReset(Function componentBuilder, BiFunction extraGetter) + { + T component = componentBuilder.apply( new ComponentBuilder( "Hello " ).color( ChatColor.RED ) + .append( "World" ).reset() ); - HoverEvent testEvent = new HoverEvent( HoverEvent.Action.SHOW_TEXT, new ComponentBuilder( "test" ).create() ); + Assert.assertEquals( ChatColor.RED, extraGetter.apply( component, 0 ).getColor() ); + Assert.assertEquals( ChatColor.WHITE, extraGetter.apply( component, 1 ).getColor() ); + } - BaseComponent[] formattingRetention = new ComponentBuilder( "Hello " ).color( ChatColor.RED ) - .event( testEvent ).append( "World", ComponentBuilder.FormatRetention.FORMATTING ).create(); + @Test + public void testBuilderCreateFormatRetention() + { + this.testBuilderFormatRetention( + ComponentBuilder::create, + (components, index) -> components[index] + ); + } - Assert.assertEquals( formattingRetention[0].getColor(), ChatColor.RED ); - Assert.assertEquals( formattingRetention[0].getHoverEvent(), testEvent ); - Assert.assertEquals( formattingRetention[1].getColor(), ChatColor.RED ); - Assert.assertNull( formattingRetention[1].getHoverEvent() ); + @Test + public void testBuilderBuildFormatRetention() + { + this.testBuilderFormatRetention( + ComponentBuilder::build, + (component, index) -> component.getExtra().get( index ) + ); + } + + private void testBuilderFormatRetention(Function componentBuilder, BiFunction extraGetter) + { + T noneRetention = componentBuilder.apply( new ComponentBuilder( "Hello " ).color( ChatColor.RED ) + .append( "World", ComponentBuilder.FormatRetention.NONE ) ); + + Assert.assertEquals( ChatColor.RED, extraGetter.apply( noneRetention, 0 ).getColor() ); + Assert.assertEquals( ChatColor.WHITE, extraGetter.apply( noneRetention, 1 ).getColor() ); + + HoverEvent testEvent = new HoverEvent( HoverEvent.Action.SHOW_TEXT, new Text( new ComponentBuilder( "test" ).build() ) ); + + T formattingRetention = componentBuilder.apply( new ComponentBuilder( "Hello " ).color( ChatColor.RED ) + .event( testEvent ).append( "World", ComponentBuilder.FormatRetention.FORMATTING ) ); + + Assert.assertEquals( ChatColor.RED, extraGetter.apply( formattingRetention, 0 ).getColor() ); + Assert.assertEquals( testEvent, extraGetter.apply( formattingRetention, 0 ).getHoverEvent() ); + Assert.assertEquals( ChatColor.RED, extraGetter.apply( formattingRetention, 1 ).getColor() ); + Assert.assertNull( extraGetter.apply( formattingRetention, 1 ).getHoverEvent() ); ClickEvent testClickEvent = new ClickEvent( ClickEvent.Action.OPEN_URL, "http://www.example.com" ); - BaseComponent[] eventRetention = new ComponentBuilder( "Hello " ).color( ChatColor.RED ) - .event( testEvent ).event( testClickEvent ).append( "World", ComponentBuilder.FormatRetention.EVENTS ).create(); + T eventRetention = componentBuilder.apply( new ComponentBuilder( "Hello " ).color( ChatColor.RED ) + .event( testEvent ).event( testClickEvent ).append( "World", ComponentBuilder.FormatRetention.EVENTS ) ); - Assert.assertEquals( eventRetention[0].getColor(), ChatColor.RED ); - Assert.assertEquals( eventRetention[0].getHoverEvent(), testEvent ); - Assert.assertEquals( eventRetention[0].getClickEvent(), testClickEvent ); - Assert.assertEquals( eventRetention[1].getColor(), ChatColor.WHITE ); - Assert.assertEquals( eventRetention[1].getHoverEvent(), testEvent ); - Assert.assertEquals( eventRetention[1].getClickEvent(), testClickEvent ); + Assert.assertEquals( ChatColor.RED, extraGetter.apply( eventRetention, 0 ).getColor() ); + Assert.assertEquals( testEvent, extraGetter.apply( eventRetention, 0 ).getHoverEvent() ); + Assert.assertEquals( testClickEvent, extraGetter.apply( eventRetention, 0 ).getClickEvent() ); + Assert.assertEquals( ChatColor.WHITE, extraGetter.apply( eventRetention, 1 ).getColor() ); + Assert.assertEquals( testEvent, extraGetter.apply( eventRetention, 1 ).getHoverEvent() ); + Assert.assertEquals( testClickEvent, extraGetter.apply( eventRetention, 1 ).getClickEvent() ); } @Test(expected = IllegalArgumentException.class) @@ -572,12 +788,29 @@ public class ComponentsTest Assert.assertArrayEquals( hexColored, reColored ); } - /** + @Test + public void testLegacyResetInBuilderCreate() + { + this.testLegacyResetInBuilder( + ComponentBuilder::create, + ComponentSerializer::toString + ); + } + + @Test + public void testLegacyResetInBuilderBuild() + { + this.testLegacyResetInBuilder( + ComponentBuilder::build, + ComponentSerializer::toString + ); + } + + /* * In legacy chat, colors and reset both reset all formatting. * Make sure it works in combination with ComponentBuilder. */ - @Test - public void testLegacyResetInBuilder() + private void testLegacyResetInBuilder(Function componentBuilder, Function componentSerializer) { ComponentBuilder builder = new ComponentBuilder(); BaseComponent[] a = TextComponent.fromLegacyText( "§4§n44444§rdd§6§l6666" ); @@ -588,13 +821,13 @@ public class ComponentsTest builder.append( a ); - String test1 = ComponentSerializer.toString( builder.create() ); + String test1 = componentSerializer.apply( componentBuilder.apply( builder ) ); Assert.assertEquals( expected, test1 ); BaseComponent[] b = TextComponent.fromLegacyText( "§rrrrr" ); builder.append( b ); - String test2 = ComponentSerializer.toString( builder.create() ); + String test2 = componentSerializer.apply( componentBuilder.apply( builder ) ); Assert.assertEquals( "{\"extra\":[{\"underlined\":true,\"color\":\"dark_red\",\"text\":\"44444\"}," + "{\"color\":\"white\",\"text\":\"dd\"},{\"bold\":true,\"color\":\"gold\",\"text\":\"6666\"},"