#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.
This commit is contained in:
Parker Hawke 2023-09-18 17:14:18 -04:00 committed by GitHub
parent d68ebd1eaf
commit cfe00fa47c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 383 additions and 86 deletions

View File

@ -454,9 +454,32 @@ public final class ComponentBuilder
return this; 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<BaseComponent> cloned = new ArrayList<>( parts );
cloned.replaceAll( BaseComponent::duplicate );
base.setExtra( cloned );
}
return base;
}
/** /**
* Returns the components needed to display the message created by this * Returns the components needed to display the message created by this
* builder.git * builder.git
* <p>
* <strong>NOTE:</strong> {@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 * @return the created components
*/ */

View File

@ -23,6 +23,12 @@ public class Text extends Content
this.value = value; 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) public Text(String value)
{ {
this.value = value; this.value = value;

View File

@ -27,7 +27,6 @@ import net.md_5.bungee.api.chat.hover.content.TextSerializer;
public class ComponentSerializer implements JsonDeserializer<BaseComponent> public class ComponentSerializer implements JsonDeserializer<BaseComponent>
{ {
private static final JsonParser JSON_PARSER = new JsonParser();
private static final Gson gson = new GsonBuilder(). private static final Gson gson = new GsonBuilder().
registerTypeAdapter( BaseComponent.class, new ComponentSerializer() ). registerTypeAdapter( BaseComponent.class, new ComponentSerializer() ).
registerTypeAdapter( TextComponent.class, new TextComponentSerializer() ). registerTypeAdapter( TextComponent.class, new TextComponentSerializer() ).
@ -43,9 +42,25 @@ public class ComponentSerializer implements JsonDeserializer<BaseComponent>
public static final ThreadLocal<Set<BaseComponent>> serializedComponents = new ThreadLocal<Set<BaseComponent>>(); public static final ThreadLocal<Set<BaseComponent>> serializedComponents = new ThreadLocal<Set<BaseComponent>>();
/**
* 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.
* <p>
* <strong>NOTE:</strong> {@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) public static BaseComponent[] parse(String json)
{ {
JsonElement jsonElement = JSON_PARSER.parse( json ); JsonElement jsonElement = JsonParser.parseString( json );
if ( jsonElement.isJsonArray() ) if ( jsonElement.isJsonArray() )
{ {
@ -59,6 +74,26 @@ public class ComponentSerializer implements JsonDeserializer<BaseComponent>
} }
} }
/**
* 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) public static String toString(Object object)
{ {
return gson.toJson( object ); return gson.toJson( object );

View File

@ -1,6 +1,11 @@
package net.md_5.bungee.api.chat; package net.md_5.bungee.api.chat;
import java.awt.Color; 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.ChatColor;
import net.md_5.bungee.api.chat.hover.content.Item; import net.md_5.bungee.api.chat.hover.content.Item;
import net.md_5.bungee.api.chat.hover.content.Text; 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 ) ); 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 ); 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 @Test
@ -41,8 +60,10 @@ public class ComponentsTest
{ {
textComponent textComponent
} ); } );
testDissembleReassemble( textComponent );
json = "{\"hoverEvent\":{\"action\":\"show_item\",\"value\":[{\"text\":\"{id:\\\"minecraft:netherrack\\\",Count:47b}\"}]},\"text\":\"Test\"}"; 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}\"}"; 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\":\"\"}"; 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 @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 <T> void testEmptyComponentBuilder(Function<ComponentBuilder, T> componentBuilder, Consumer<T> emptyAssertion, ObjIntConsumer<T> sizedAssertion)
{ {
ComponentBuilder builder = new ComponentBuilder(); ComponentBuilder builder = new ComponentBuilder();
BaseComponent[] parts = builder.create(); T component = componentBuilder.apply( builder );
Assert.assertEquals( parts.length, 0 ); emptyAssertion.accept( component );
for ( int i = 0; i < 3; i++ ) for ( int i = 0; i < 3; i++ )
{ {
builder.append( "part:" + i ); builder.append( "part:" + i );
parts = builder.create(); component = componentBuilder.apply( builder );
Assert.assertEquals( parts.length, i + 1 ); sizedAssertion.accept( component, i + 1 );
} }
} }
@ -213,7 +253,7 @@ public class ComponentsTest
} }
@Test @Test
public void testHoverEventContents() public void testHoverEventContentsCreate()
{ {
// First do the text using the newer contents system // First do the text using the newer contents system
HoverEvent hoverEvent = new HoverEvent( HoverEvent hoverEvent = new HoverEvent(
@ -222,21 +262,53 @@ public class ComponentsTest
new Text( new ComponentBuilder( "Second" ).create() ) 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 <T> void testHoverEventContents(HoverEvent hoverEvent, Function<String, T> deserializer, Function<T, HoverEvent> hoverEventGetter, Consumer<T> dissembleReassembleTest)
{
TextComponent component = new TextComponent( "Sample text" ); TextComponent component = new TextComponent( "Sample text" );
component.setHoverEvent( hoverEvent ); component.setHoverEvent( hoverEvent );
Assert.assertEquals( hoverEvent.getContents().size(), 2 ); Assert.assertEquals( hoverEvent.getContents().size(), 2 );
Assert.assertFalse( hoverEvent.isLegacy() ); 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 String serialized = ComponentSerializer.toString( component );
hoverEvent = new HoverEvent( HoverEvent.Action.SHOW_TEXT, new ComponentBuilder( "Sample text" ).create() ); T deserialized = deserializer.apply( serialized );
Assert.assertEquals( hoverEvent.getContents().size(), 1 ); Assert.assertEquals( component.getHoverEvent(), hoverEventGetter.apply( deserialized ) );
Assert.assertTrue( hoverEvent.isLegacy() );
serialized = ComponentSerializer.toString( component );
deserialized = ComponentSerializer.parse( serialized );
Assert.assertEquals( component.getHoverEvent(), deserialized[0].getHoverEvent() );
// Test single content: // Test single content:
String json = "{\"italic\":true,\"color\":\"gray\",\"translate\":\"chat.type.admin\",\"with\":[{\"text\":\"@\"}" 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\":" + "\"/tell Name \"},\"hoverEvent\":{\"action\":\"show_entity\",\"contents\":"
+ "{\"type\":\"minecraft:player\",\"id\":\"00000000-0000-0000-0000-00000000000000\",\"name\":" + "{\"type\":\"minecraft:player\",\"id\":\"00000000-0000-0000-0000-00000000000000\",\"name\":"
+ "{\"text\":\"Name\"}}},\"text\":\"Name\"}]}]}"; + "{\"text\":\"Name\"}}},\"text\":\"Name\"}]}]}";
testDissembleReassemble( ComponentSerializer.parse( json ) ); dissembleReassembleTest.accept( deserializer.apply( json ) );
} }
@Test @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<HoverEvent> hoverEventSupplier)
{ {
TextComponent first = new TextComponent( "Hello" ); TextComponent first = new TextComponent( "Hello" );
first.setBold( true ); first.setBold( true );
first.setColor( ChatColor.RED ); first.setColor( ChatColor.RED );
first.setClickEvent( new ClickEvent( ClickEvent.Action.RUN_COMMAND, "test" ) ); 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" ); TextComponent second = new TextComponent( " world" );
second.copyFormatting( first, ComponentBuilder.FormatRetention.ALL, true ); second.copyFormatting( first, ComponentBuilder.FormatRetention.ALL, true );
@ -269,16 +352,44 @@ public class ComponentsTest
} }
@Test @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<ComponentBuilder, String> legacyTextFunction)
{ {
ComponentBuilder builder = new ComponentBuilder( "Hello " ).color( ChatColor.RED ).append( "world" ).color( ChatColor.DARK_RED ); ComponentBuilder builder = new ComponentBuilder( "Hello " ).color( ChatColor.RED ).append( "world" ).color( ChatColor.DARK_RED );
ComponentBuilder cloned = new ComponentBuilder( builder ); ComponentBuilder cloned = new ComponentBuilder( builder );
Assert.assertEquals( TextComponent.toLegacyText( builder.create() ), TextComponent.toLegacyText( cloned.create() ) ); Assert.assertEquals( legacyTextFunction.apply( builder ), legacyTextFunction.apply( cloned ) );
} }
@Test @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 <T> void testBuilderAppendMixedComponents(Function<ComponentBuilder, T> componentBuilder, BiFunction<T, Integer, BaseComponent> extraGetter)
{ {
ComponentBuilder builder = new ComponentBuilder( "Hello " ); ComponentBuilder builder = new ComponentBuilder( "Hello " );
TextComponent textComponent = new TextComponent( "world " ); TextComponent textComponent = new TextComponent( "world " );
@ -291,11 +402,11 @@ public class ComponentsTest
} ); } );
ScoreComponent scoreComponent = new ScoreComponent( "myscore", "myobjective" ); ScoreComponent scoreComponent = new ScoreComponent( "myscore", "myobjective" );
builder.append( scoreComponent ); // non array based BaseComponent append builder.append( scoreComponent ); // non array based BaseComponent append
BaseComponent[] components = builder.create(); T component = componentBuilder.apply( builder );
Assert.assertEquals( "Hello ", components[0].toPlainText() ); Assert.assertEquals( "Hello ", extraGetter.apply( component, 0 ).toPlainText() );
Assert.assertEquals( textComponent.toPlainText(), components[1].toPlainText() ); Assert.assertEquals( textComponent.toPlainText(), extraGetter.apply( component, 1 ).toPlainText() );
Assert.assertEquals( translatableComponent.toPlainText(), components[2].toPlainText() ); Assert.assertEquals( translatableComponent.toPlainText(), extraGetter.apply( component, 2 ).toPlainText() );
Assert.assertEquals( scoreComponent.toPlainText(), components[3].toPlainText() ); Assert.assertEquals( scoreComponent.toPlainText(), extraGetter.apply( component, 3 ).toPlainText() );
} }
@Test @Test
@ -309,32 +420,80 @@ public class ComponentsTest
} }
@Test @Test
public void testBuilderAppend() public void testBuilderAppendCreate()
{ {
ClickEvent clickEvent = new ClickEvent( ClickEvent.Action.RUN_COMMAND, "/help " ); this.testBuilderAppend(
HoverEvent hoverEvent = new HoverEvent( HoverEvent.Action.SHOW_TEXT, new ComponentBuilder( "Hello world" ).create() ); () -> new HoverEvent( HoverEvent.Action.SHOW_TEXT, new ComponentBuilder( "Hello world" ).create() ),
ComponentBuilder::create,
ComponentBuilder builder = new ComponentBuilder( "Hello " ).color( ChatColor.YELLOW ); (components, index) -> components[index],
builder.append( new ComponentBuilder( "world!" ).color( ChatColor.GREEN ).event( hoverEvent ).event( clickEvent ).create() ); BaseComponent::toPlainText,
ChatColor.YELLOW + "Hello " + ChatColor.GREEN + "world!",
BaseComponent[] components = builder.create(); BaseComponent::toLegacyText
);
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 ) );
} }
@Test @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 <T> void testBuilderAppend(Supplier<HoverEvent> hoverEventSupplier, Function<ComponentBuilder, T> componentBuilder, BiFunction<T, Integer, BaseComponent> extraGetter, Function<T, String> toPlainTextFunction, String expectedLegacyText, Function<T, String> 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 <T> void testBuilderAppendLegacy(Function<ComponentBuilder, T> componentBuilder, Function<T, String> toPlainTextFunction, String expectedLegacyString, Function<T, String> toLegacyTextFunction)
{ {
ComponentBuilder builder = new ComponentBuilder( "Hello " ).color( ChatColor.YELLOW ); ComponentBuilder builder = new ComponentBuilder( "Hello " ).color( ChatColor.YELLOW );
builder.appendLegacy( "§aworld!" ); builder.appendLegacy( "§aworld!" );
BaseComponent[] components = builder.create(); T component = componentBuilder.apply( builder );
Assert.assertEquals( "Hello world!", BaseComponent.toPlainText( components ) ); Assert.assertEquals( "Hello world!", toPlainTextFunction.apply( component ) );
Assert.assertEquals( ChatColor.YELLOW + "Hello " + ChatColor.GREEN + "world!", BaseComponent.toLegacyText( components ) ); Assert.assertEquals( expectedLegacyString, toLegacyTextFunction.apply( component ) );
} }
@Test @Test
@ -397,57 +556,114 @@ public class ComponentsTest
} }
@Test @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 <T> void testBuilder(Function<ComponentBuilder, T> componentBuilder, Function<T, String> toPlainTextFunction, String expectedLegacyString, Function<T, String> toLegacyTextFunction)
{
T component = componentBuilder.apply( new ComponentBuilder( "Hello " ).color( ChatColor.RED ).
append( "World" ).bold( true ).color( ChatColor.BLUE ). 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( "Hello World!", toPlainTextFunction.apply( component ) );
Assert.assertEquals( ChatColor.RED + "Hello " + ChatColor.BLUE + ChatColor.BOLD Assert.assertEquals( expectedLegacyString, toLegacyTextFunction.apply( component ) );
+ "World" + ChatColor.YELLOW + ChatColor.BOLD + "!", BaseComponent.toLegacyText( components ) );
} }
@Test @Test
public void testBuilderReset() public void testBuilderCreateReset()
{ {
BaseComponent[] components = new ComponentBuilder( "Hello " ).color( ChatColor.RED ) this.testBuilderReset(
.append( "World" ).reset().create(); ComponentBuilder::create,
(components, index) -> components[index]
Assert.assertEquals( components[0].getColor(), ChatColor.RED ); );
Assert.assertEquals( components[1].getColor(), ChatColor.WHITE );
} }
@Test @Test
public void testBuilderFormatRetention() public void testBuilderBuildReset()
{ {
BaseComponent[] noneRetention = new ComponentBuilder( "Hello " ).color( ChatColor.RED ) this.testBuilderReset(
.append( "World", ComponentBuilder.FormatRetention.NONE ).create(); ComponentBuilder::build,
(component, index) -> component.getExtra().get( index )
);
}
Assert.assertEquals( noneRetention[0].getColor(), ChatColor.RED ); private <T> void testBuilderReset(Function<ComponentBuilder, T> componentBuilder, BiFunction<T, Integer, BaseComponent> extraGetter)
Assert.assertEquals( noneRetention[1].getColor(), ChatColor.WHITE ); {
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 ) @Test
.event( testEvent ).append( "World", ComponentBuilder.FormatRetention.FORMATTING ).create(); public void testBuilderCreateFormatRetention()
{
this.testBuilderFormatRetention(
ComponentBuilder::create,
(components, index) -> components[index]
);
}
Assert.assertEquals( formattingRetention[0].getColor(), ChatColor.RED ); @Test
Assert.assertEquals( formattingRetention[0].getHoverEvent(), testEvent ); public void testBuilderBuildFormatRetention()
Assert.assertEquals( formattingRetention[1].getColor(), ChatColor.RED ); {
Assert.assertNull( formattingRetention[1].getHoverEvent() ); this.testBuilderFormatRetention(
ComponentBuilder::build,
(component, index) -> component.getExtra().get( index )
);
}
private <T> void testBuilderFormatRetention(Function<ComponentBuilder, T> componentBuilder, BiFunction<T, Integer, BaseComponent> 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" ); ClickEvent testClickEvent = new ClickEvent( ClickEvent.Action.OPEN_URL, "http://www.example.com" );
BaseComponent[] eventRetention = new ComponentBuilder( "Hello " ).color( ChatColor.RED ) T eventRetention = componentBuilder.apply( new ComponentBuilder( "Hello " ).color( ChatColor.RED )
.event( testEvent ).event( testClickEvent ).append( "World", ComponentBuilder.FormatRetention.EVENTS ).create(); .event( testEvent ).event( testClickEvent ).append( "World", ComponentBuilder.FormatRetention.EVENTS ) );
Assert.assertEquals( eventRetention[0].getColor(), ChatColor.RED ); Assert.assertEquals( ChatColor.RED, extraGetter.apply( eventRetention, 0 ).getColor() );
Assert.assertEquals( eventRetention[0].getHoverEvent(), testEvent ); Assert.assertEquals( testEvent, extraGetter.apply( eventRetention, 0 ).getHoverEvent() );
Assert.assertEquals( eventRetention[0].getClickEvent(), testClickEvent ); Assert.assertEquals( testClickEvent, extraGetter.apply( eventRetention, 0 ).getClickEvent() );
Assert.assertEquals( eventRetention[1].getColor(), ChatColor.WHITE ); Assert.assertEquals( ChatColor.WHITE, extraGetter.apply( eventRetention, 1 ).getColor() );
Assert.assertEquals( eventRetention[1].getHoverEvent(), testEvent ); Assert.assertEquals( testEvent, extraGetter.apply( eventRetention, 1 ).getHoverEvent() );
Assert.assertEquals( eventRetention[1].getClickEvent(), testClickEvent ); Assert.assertEquals( testClickEvent, extraGetter.apply( eventRetention, 1 ).getClickEvent() );
} }
@Test(expected = IllegalArgumentException.class) @Test(expected = IllegalArgumentException.class)
@ -572,12 +788,29 @@ public class ComponentsTest
Assert.assertArrayEquals( hexColored, reColored ); 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. * In legacy chat, colors and reset both reset all formatting.
* Make sure it works in combination with ComponentBuilder. * Make sure it works in combination with ComponentBuilder.
*/ */
@Test private <T> void testLegacyResetInBuilder(Function<ComponentBuilder, T> componentBuilder, Function<T, String> componentSerializer)
public void testLegacyResetInBuilder()
{ {
ComponentBuilder builder = new ComponentBuilder(); ComponentBuilder builder = new ComponentBuilder();
BaseComponent[] a = TextComponent.fromLegacyText( "§4§n44444§rdd§6§l6666" ); BaseComponent[] a = TextComponent.fromLegacyText( "§4§n44444§rdd§6§l6666" );
@ -588,13 +821,13 @@ public class ComponentsTest
builder.append( a ); builder.append( a );
String test1 = ComponentSerializer.toString( builder.create() ); String test1 = componentSerializer.apply( componentBuilder.apply( builder ) );
Assert.assertEquals( expected, test1 ); Assert.assertEquals( expected, test1 );
BaseComponent[] b = TextComponent.fromLegacyText( "§rrrrr" ); BaseComponent[] b = TextComponent.fromLegacyText( "§rrrrr" );
builder.append( b ); builder.append( b );
String test2 = ComponentSerializer.toString( builder.create() ); String test2 = componentSerializer.apply( componentBuilder.apply( builder ) );
Assert.assertEquals( Assert.assertEquals(
"{\"extra\":[{\"underlined\":true,\"color\":\"dark_red\",\"text\":\"44444\"}," "{\"extra\":[{\"underlined\":true,\"color\":\"dark_red\",\"text\":\"44444\"},"
+ "{\"color\":\"white\",\"text\":\"dd\"},{\"bold\":true,\"color\":\"gold\",\"text\":\"6666\"}," + "{\"color\":\"white\",\"text\":\"dd\"},{\"bold\":true,\"color\":\"gold\",\"text\":\"6666\"},"