diff --git a/api/src/main/java/net/kyori/adventure/text/AbstractComponentBuilder.java b/api/src/main/java/net/kyori/adventure/text/AbstractComponentBuilder.java index 24862f7b6..a6ed0b01f 100644 --- a/api/src/main/java/net/kyori/adventure/text/AbstractComponentBuilder.java +++ b/api/src/main/java/net/kyori/adventure/text/AbstractComponentBuilder.java @@ -209,6 +209,11 @@ private void prepareChildren() { return (B) this; } + @Override + public @NonNull List children() { + return Collections.unmodifiableList(this.children); + } + @Override @SuppressWarnings("unchecked") public @NonNull B style(final @NonNull Style style) { diff --git a/api/src/main/java/net/kyori/adventure/text/Component.java b/api/src/main/java/net/kyori/adventure/text/Component.java index ee52883ab..02cfc1cb6 100644 --- a/api/src/main/java/net/kyori/adventure/text/Component.java +++ b/api/src/main/java/net/kyori/adventure/text/Component.java @@ -33,6 +33,7 @@ import java.util.function.Function; import java.util.function.UnaryOperator; import java.util.regex.Pattern; +import java.util.stream.Collector; import net.kyori.adventure.key.Key; import net.kyori.adventure.text.event.ClickEvent; import net.kyori.adventure.text.event.HoverEvent; @@ -159,6 +160,44 @@ public interface Component extends ComponentBuilderApplicable, ComponentLike, Ex return builder.build(); } + /** + * Create a collector that will join components without a separator. + * + * @return a collector that can join components + * @since 4.6.0 + */ + static @NonNull Collector, Component> toComponent() { + return toComponent(Component.empty()); + } + + /** + * Create a collector that will join components using the provided separator. + * + * @param separator the separator to join with + * @return a collector that can join components + * @since 4.6.0 + */ + static @NonNull Collector, Component> toComponent(final @NonNull Component separator) { + return Collector.of( + Component::text, + (builder, add) -> { + if(separator != Component.empty() && !builder.children().isEmpty()) { + builder.append(separator); + } + builder.append(add); + }, (a, b) -> { + final List aChildren = a.children(); + final TextComponent.Builder ret = Component.text().append(aChildren); + if(!aChildren.isEmpty()) { + ret.append(separator); + } + ret.append(b.children()); + return ret; + }, + TextComponent.Builder::build + ); + } + /* * --------------------------- * ---- BlockNBTComponent ---- diff --git a/api/src/main/java/net/kyori/adventure/text/ComponentBuilder.java b/api/src/main/java/net/kyori/adventure/text/ComponentBuilder.java index 452f9fbf0..ce1d62a9c 100644 --- a/api/src/main/java/net/kyori/adventure/text/ComponentBuilder.java +++ b/api/src/main/java/net/kyori/adventure/text/ComponentBuilder.java @@ -23,6 +23,7 @@ */ package net.kyori.adventure.text; +import java.util.List; import java.util.Set; import java.util.function.Consumer; import java.util.function.Function; @@ -155,6 +156,14 @@ public interface ComponentBuilder, B extends @Contract("_ -> this") @NonNull B mapChildrenDeep(final @NonNull Function, ? extends BuildableComponent> function); + /** + * Get an unmodifiable list containing all children currently in this builder. + * + * @return the list of children + * @since 4.6.0 + */ + @NonNull List children(); + /** * Sets the style. * diff --git a/api/src/test/java/net/kyori/adventure/text/AbstractComponentTest.java b/api/src/test/java/net/kyori/adventure/text/AbstractComponentTest.java index 081ca5e0e..e8e65f113 100644 --- a/api/src/test/java/net/kyori/adventure/text/AbstractComponentTest.java +++ b/api/src/test/java/net/kyori/adventure/text/AbstractComponentTest.java @@ -29,6 +29,7 @@ import java.util.Collections; import java.util.List; import java.util.Map; +import java.util.stream.Stream; import net.kyori.adventure.text.event.ClickEvent; import net.kyori.adventure.text.event.HoverEvent; import net.kyori.adventure.text.format.NamedTextColor; @@ -376,4 +377,39 @@ void testBuilderApplyDeep() { assertThat(children).hasSize(2); forEachTransformAndAssert(children, Component::color, color -> assertEquals(NamedTextColor.GREEN, color)); } + + @Test + void testCollectorNoSeparator() { + final Component joined = Stream.of( + Component.text("Hello", NamedTextColor.RED), + Component.text("World") + ).collect(Component.toComponent()); + final Component expected = Component.text() + .append(Component.text("Hello", NamedTextColor.RED)) + .append(Component.text("World")) + .build(); + + assertEquals(expected, joined); + } + + @Test + void testCollectorWithSeparator() { + final Component joined = Stream.of( + Component.text("Hello", NamedTextColor.RED), + Component.text("World") + ).collect(Component.toComponent(Component.space())); + + final Component expected = Component.text() + .append(Component.text("Hello", NamedTextColor.RED)) + .append(Component.space()) + .append(Component.text("World")) + .build(); + + assertEquals(expected, joined); + } + + @Test + void testCollectOnEmptyStreamReturnsEmptyComponent() { + assertEquals(Component.empty(), Stream.of().collect(Component.toComponent())); + } }