From 259561c3f8b7ed196acdaa3dbe9372fbf5c328c5 Mon Sep 17 00:00:00 2001
From: Riley Park <riley.park@meino.net>
Date: Tue, 29 Nov 2022 23:53:42 -0800
Subject: [PATCH] feat(api): virtual components

---
 .../net/kyori/adventure/text/Component.java   | 19 +++++++
 .../adventure/text/TextComponentImpl.java     | 12 +++--
 .../adventure/text/VirtualComponent.java      | 46 ++++++++++++++++
 .../text/VirtualComponentHolder.java          | 45 ++++++++++++++++
 .../adventure/text/VirtualComponentImpl.java  | 52 +++++++++++++++++++
 .../renderer/AbstractComponentRenderer.java   | 17 +++++-
 6 files changed, 186 insertions(+), 5 deletions(-)
 create mode 100644 api/src/main/java/net/kyori/adventure/text/VirtualComponent.java
 create mode 100644 api/src/main/java/net/kyori/adventure/text/VirtualComponentHolder.java
 create mode 100644 api/src/main/java/net/kyori/adventure/text/VirtualComponentImpl.java

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 434f1bd979..c579a7e184 100644
--- a/api/src/main/java/net/kyori/adventure/text/Component.java
+++ b/api/src/main/java/net/kyori/adventure/text/Component.java
@@ -1225,6 +1225,25 @@ public interface Component extends ComponentBuilderApplicable, ComponentLike, Ex
     return text(String.valueOf(value), color, decorations);
   }
 
+  /*
+   * --------------------------
+   * ---- VirtualComponent ----
+   * --------------------------
+   */
+
+  /**
+   * Creates a virtual component with a value.
+   *
+   * @param virtual the value
+   * @return a virtual component
+   * @since 4.13.0
+   */
+  @Contract(value = "_ -> new", pure = true)
+  static @NotNull VirtualComponent virtual(final @NotNull VirtualComponentHolder<?> virtual) {
+    requireNonNull(virtual, "virtual");
+    return VirtualComponentImpl.createVirtual(virtual);
+  }
+
   /*
    * -------------------------------
    * ---- TranslatableComponent ----
diff --git a/api/src/main/java/net/kyori/adventure/text/TextComponentImpl.java b/api/src/main/java/net/kyori/adventure/text/TextComponentImpl.java
index b876f57dcf..f214ac3914 100644
--- a/api/src/main/java/net/kyori/adventure/text/TextComponentImpl.java
+++ b/api/src/main/java/net/kyori/adventure/text/TextComponentImpl.java
@@ -36,7 +36,7 @@
 
 import static java.util.Objects.requireNonNull;
 
-final class TextComponentImpl extends AbstractComponent implements TextComponent {
+class TextComponentImpl extends AbstractComponent implements TextComponent {
   private static final boolean WARN_WHEN_LEGACY_FORMATTING_DETECTED = Boolean.TRUE.equals(AdventureProperties.TEXT_WARN_WHEN_LEGACY_FORMATTING_DETECTED.value());
   @VisibleForTesting
   static final char SECTION_CHAR = 'ยง';
@@ -56,6 +56,10 @@ static TextComponent create(final @NotNull List<? extends ComponentLike> childre
     );
   }
 
+  TextComponent create0(final @NotNull List<? extends ComponentLike> children, final @NotNull Style style, final @NotNull String content) {
+    return create(children, style, content);
+  }
+
   private static @NotNull TextComponent createDirect(final @NotNull String content) {
     return new TextComponentImpl(Collections.emptyList(), Style.empty(), content);
   }
@@ -90,17 +94,17 @@ static TextComponent create(final @NotNull List<? extends ComponentLike> childre
   @Override
   public @NotNull TextComponent content(final @NotNull String content) {
     if (Objects.equals(this.content, content)) return this;
-    return create(this.children, this.style, content);
+    return this.create0(this.children, this.style, content);
   }
 
   @Override
   public @NotNull TextComponent children(final @NotNull List<? extends ComponentLike> children) {
-    return create(children, this.style, this.content);
+    return this.create0(children, this.style, this.content);
   }
 
   @Override
   public @NotNull TextComponent style(final @NotNull Style style) {
-    return create(this.children, style, this.content);
+    return this.create0(this.children, style, this.content);
   }
 
   @Override
diff --git a/api/src/main/java/net/kyori/adventure/text/VirtualComponent.java b/api/src/main/java/net/kyori/adventure/text/VirtualComponent.java
new file mode 100644
index 0000000000..619e1d337c
--- /dev/null
+++ b/api/src/main/java/net/kyori/adventure/text/VirtualComponent.java
@@ -0,0 +1,46 @@
+/*
+ * This file is part of adventure, licensed under the MIT License.
+ *
+ * Copyright (c) 2017-2022 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;
+
+import org.jetbrains.annotations.ApiStatus;
+import org.jetbrains.annotations.NotNull;
+
+/**
+ * A virtual component.
+ *
+ * @since 4.13.0
+ */
+@ApiStatus.Experimental
+public interface VirtualComponent extends TextComponent {
+  /**
+   * Gets the virtual value.
+   *
+   * <p>This property is transient, and not guaranteed to survive during any sort of transformations.</p>
+   *
+   * @return the virtual value
+   * @since 4.13.0
+   */
+  @ApiStatus.Experimental
+  @NotNull VirtualComponentHolder<?> virtual();
+}
diff --git a/api/src/main/java/net/kyori/adventure/text/VirtualComponentHolder.java b/api/src/main/java/net/kyori/adventure/text/VirtualComponentHolder.java
new file mode 100644
index 0000000000..c1cbc0ef48
--- /dev/null
+++ b/api/src/main/java/net/kyori/adventure/text/VirtualComponentHolder.java
@@ -0,0 +1,45 @@
+/*
+ * This file is part of adventure, licensed under the MIT License.
+ *
+ * Copyright (c) 2017-2022 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;
+
+import org.jetbrains.annotations.ApiStatus;
+import org.jetbrains.annotations.UnknownNullability;
+
+/**
+ * A holder for a virtual component value.
+ *
+ * @param <V> the stored value type
+ * @since 4.13.0
+ */
+@ApiStatus.Experimental
+public interface VirtualComponentHolder<V> {
+  /**
+   * Gets the stored value.
+   *
+   * @return the stored value
+   * @since 4.13.0
+   */
+  @ApiStatus.Experimental
+  @UnknownNullability V unbox();
+}
diff --git a/api/src/main/java/net/kyori/adventure/text/VirtualComponentImpl.java b/api/src/main/java/net/kyori/adventure/text/VirtualComponentImpl.java
new file mode 100644
index 0000000000..d61308bc94
--- /dev/null
+++ b/api/src/main/java/net/kyori/adventure/text/VirtualComponentImpl.java
@@ -0,0 +1,52 @@
+/*
+ * This file is part of adventure, licensed under the MIT License.
+ *
+ * Copyright (c) 2017-2022 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;
+
+import java.util.Collections;
+import java.util.List;
+import net.kyori.adventure.text.format.Style;
+import org.jetbrains.annotations.NotNull;
+
+final class VirtualComponentImpl extends TextComponentImpl implements VirtualComponent {
+  static VirtualComponent createVirtual(final @NotNull VirtualComponentHolder<?> virtual) {
+    return new VirtualComponentImpl(Collections.emptyList(), Style.empty(), "", virtual);
+  }
+
+  private final VirtualComponentHolder<?> virtual;
+
+  private VirtualComponentImpl(final @NotNull List<Component> children, final @NotNull Style style, final @NotNull String content, final @NotNull VirtualComponentHolder<?> virtual) {
+    super(children, style, content);
+    this.virtual = virtual;
+  }
+
+  @Override
+  VirtualComponent create0(final @NotNull List<? extends ComponentLike> children, final @NotNull Style style, final @NotNull String content) {
+    return new VirtualComponentImpl(ComponentLike.asComponents(children, IS_NOT_EMPTY), style, content, this.virtual);
+  }
+
+  @Override
+  public @NotNull VirtualComponentHolder<?> virtual() {
+    return this.virtual;
+  }
+}
diff --git a/api/src/main/java/net/kyori/adventure/text/renderer/AbstractComponentRenderer.java b/api/src/main/java/net/kyori/adventure/text/renderer/AbstractComponentRenderer.java
index d3ef31b214..58fb9551cf 100644
--- a/api/src/main/java/net/kyori/adventure/text/renderer/AbstractComponentRenderer.java
+++ b/api/src/main/java/net/kyori/adventure/text/renderer/AbstractComponentRenderer.java
@@ -33,6 +33,7 @@
 import net.kyori.adventure.text.StorageNBTComponent;
 import net.kyori.adventure.text.TextComponent;
 import net.kyori.adventure.text.TranslatableComponent;
+import net.kyori.adventure.text.VirtualComponent;
 import org.jetbrains.annotations.NotNull;
 
 /**
@@ -43,7 +44,10 @@
  */
 public abstract class AbstractComponentRenderer<C> implements ComponentRenderer<C> {
   @Override
-  public @NotNull Component render(final @NotNull Component component, final @NotNull C context) {
+  public @NotNull Component render(@NotNull Component component, final @NotNull C context) {
+    if (component instanceof VirtualComponent) {
+      component = this.renderVirtual((VirtualComponent) component, context);
+    }
     if (component instanceof TextComponent) {
       return this.renderText((TextComponent) component, context);
     } else if (component instanceof TranslatableComponent) {
@@ -129,6 +133,17 @@ public abstract class AbstractComponentRenderer<C> implements ComponentRenderer<
    */
   protected abstract @NotNull Component renderText(final @NotNull TextComponent component, final @NotNull C context);
 
+  /**
+   * Renders a virtual component.
+   *
+   * @param component the component
+   * @param context the context
+   * @return the rendered component
+   */
+  protected @NotNull Component renderVirtual(final @NotNull VirtualComponent component, final @NotNull C context) {
+    return component;
+  }
+
   /**
    * Renders a translatable component.
    *