Skip to content

Commit

Permalink
Merge pull request #898 from KyoriPowered/feat/ansi-component-serializer
Browse files Browse the repository at this point in the history
ANSIComponentSerializer
  • Loading branch information
rymiel authored Apr 25, 2023
2 parents 7c19212 + 570310d commit 62ca6f4
Show file tree
Hide file tree
Showing 7 changed files with 410 additions and 1 deletion.
3 changes: 3 additions & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,9 @@ slf4jtest = "com.github.valfirst:slf4j-test:2.9.0" # Specific versions are neede
# text-serializer-gson
gson = "com.google.code.gson:gson:2.8.0"

# text-serializer-ansi
ansi = "net.kyori:ansi:1.0.0"

# tests
junit-api = { module = "org.junit.jupiter:junit-jupiter-api" }
junit-bom = { module = "org.junit:junit-bom", version.ref = "junit" }
Expand Down
3 changes: 2 additions & 1 deletion settings.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,8 @@ sequenceOf(
"text-serializer-gson",
"text-serializer-gson-legacy-impl",
"text-serializer-legacy",
"text-serializer-plain"
"text-serializer-plain",
"text-serializer-ansi",
).forEach {
include("adventure-$it")
project(":adventure-$it").projectDir = file(it)
Expand Down
10 changes: 10 additions & 0 deletions text-serializer-ansi/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
plugins {
id("adventure.common-conventions")
}

dependencies {
api(projects.adventureApi)
api(libs.ansi)
}

applyJarMetadata("net.kyori.adventure.text.serializer.ansi")
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
/*
* This file is part of adventure, licensed under the MIT License.
*
* Copyright (c) 2017-2023 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.serializer.ansi;

import java.util.function.Consumer;
import net.kyori.adventure.builder.AbstractBuilder;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.flattener.ComponentFlattener;
import net.kyori.adventure.text.serializer.ComponentEncoder;
import net.kyori.adventure.util.PlatformAPI;
import net.kyori.ansi.ColorLevel;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;

/**
* A serializer which emits <a href="https://en.wikipedia.org/wiki/ANSI_escape_code">ANSI escape sequences</a>.
*
* <p>Note that this serializer does NOT support deserialization.</p>
*
* @since 4.14.0
*/
public interface ANSIComponentSerializer extends ComponentEncoder<Component, String> {
/**
* Gets a component serializer for serialization to a string using ANSI escape codes.
*
* <p>Note that this serializer does NOT support deserialization.</p>
*
* @return a component serializer for serialization with ANSI escape sequences.
* @since 4.14.0
*/
static @NotNull ANSIComponentSerializer ansi() {
return ANSIComponentSerializerImpl.Instances.INSTANCE;
}

/**
* Create a new builder.
*
* @return a new ANSI serializer builder
* @since 4.14.0
*/
static ANSIComponentSerializer.@NotNull Builder builder() {
return new ANSIComponentSerializerImpl.BuilderImpl();
}

/**
* A builder for the ANSI component serializer.
*
* @since 4.14.0
*/
interface Builder extends AbstractBuilder<ANSIComponentSerializer> {
/**
* Sets the default color level used when serializing.
*
* <p>By default, this serializer will use {@link ColorLevel#compute()} to try to detect the color level of the terminal being used.</p>
*
* @param colorLevel the color level
* @return this builder
* @see ColorLevel
* @since 4.14.0
*/
@NotNull Builder colorLevel(final @NotNull ColorLevel colorLevel);

/**
* Sets the component flattener instance to use when traversing the component for serialization.
*
* <p>By default, this serializer will use {@link ComponentFlattener#basic()}.</p>
*
* @param componentFlattener the flattener instance.
* @return this builder
* @since 4.14.0
*/
@NotNull Builder flattener(final @NotNull ComponentFlattener componentFlattener);

/**
* Builds the serializer.
*
* @return the built serializer
*/
@Override
@NotNull ANSIComponentSerializer build();
}

/**
* A {@link ANSIComponentSerializer} service provider.
*
* @since 4.14.0
*/
@ApiStatus.Internal
@PlatformAPI
interface Provider {
/**
* Provides a {@link ANSIComponentSerializer}.
*
* @return a {@link ANSIComponentSerializer}
* @since 4.8.0
*/
@ApiStatus.Internal
@PlatformAPI
@NotNull ANSIComponentSerializer ansi();

/**
* Completes the building process of {@link Builder}.
*
* @return a {@link Consumer}
* @since 4.8.0
*/
@ApiStatus.Internal
@PlatformAPI
@NotNull Consumer<Builder> builder();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
/*
* This file is part of adventure, licensed under the MIT License.
*
* Copyright (c) 2017-2023 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.serializer.ansi;

import java.util.Optional;
import java.util.function.Consumer;
import net.kyori.adventure.key.Key;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.flattener.ComponentFlattener;
import net.kyori.adventure.text.flattener.FlattenerListener;
import net.kyori.adventure.text.format.Style;
import net.kyori.adventure.text.format.TextColor;
import net.kyori.adventure.text.format.TextDecoration;
import net.kyori.adventure.util.Services;
import net.kyori.ansi.ANSIComponentRenderer;
import net.kyori.ansi.ColorLevel;
import net.kyori.ansi.StyleOps;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.annotations.Range;

final class ANSIComponentSerializerImpl implements ANSIComponentSerializer {
private static final Optional<Provider> SERVICE = Services.service(Provider.class);

static final Consumer<Builder> BUILDER = SERVICE
.map(Provider::builder)
.orElseGet(() -> builder -> {
// NOOP
});

final ColorLevel colorLevel;
final ComponentFlattener flattener;

static final class Instances {
static final ANSIComponentSerializer INSTANCE = SERVICE
.map(Provider::ansi)
.orElseGet(() -> new ANSIComponentSerializerImpl(ColorLevel.compute(), ComponentFlattener.basic()));
}

ANSIComponentSerializerImpl(final ColorLevel colorLevel, final ComponentFlattener flattener) {
this.colorLevel = colorLevel;
this.flattener = flattener;
}

@Override
public @NotNull String serialize(final @NotNull Component component) {
final ANSIComponentRenderer.ToString<Style> renderer = ANSIComponentRenderer.toString(ComponentStyleOps.INSTANCE);
ComponentFlattener.basic().flatten(component, new ANSIFlattenerListener(renderer));
renderer.complete();
return renderer.asString();
}

static StyleOps.State mapState(final TextDecoration.State state) {
switch (state) {
case NOT_SET:
return StyleOps.State.UNSET;
case FALSE:
return StyleOps.State.FALSE;
case TRUE:
return StyleOps.State.TRUE;
}
throw new IllegalStateException("Decoration state is not valid");
}

static class ComponentStyleOps implements StyleOps<Style> {
static final ComponentStyleOps INSTANCE = new ComponentStyleOps();

@Override
public State bold(final @NotNull Style style) {
return mapState(style.decoration(TextDecoration.BOLD));
}

@Override
public State italics(final @NotNull Style style) {
return mapState(style.decoration(TextDecoration.ITALIC));
}

@Override
public State underlined(final @NotNull Style style) {
return mapState(style.decoration(TextDecoration.UNDERLINED));
}

@Override
public State strikethrough(final @NotNull Style style) {
return mapState(style.decoration(TextDecoration.STRIKETHROUGH));
}

@Override
public State obfuscated(final @NotNull Style style) {
return mapState(style.decoration(TextDecoration.OBFUSCATED));
}

@Override
public @Range(from = -1L, to = 16777215L) int color(final @NotNull Style style) {
final TextColor color = style.color();
return color == null ? -1 : color.value();
}

@Override
public @Nullable String font(final @NotNull Style style) {
final Key font = style.font();
return font == null ? null : font.asString();
}
}

static class ANSIFlattenerListener implements FlattenerListener {
private final ANSIComponentRenderer<Style> renderer;

ANSIFlattenerListener(final ANSIComponentRenderer<Style> renderer) {
this.renderer = renderer;
}

@Override
public void pushStyle(final @NotNull Style style) {
this.renderer.pushStyle(style);
}

@Override
public void component(final @NotNull String text) {
this.renderer.text(text);
}

@Override
public void popStyle(final @NotNull Style style) {
this.renderer.popStyle(style);
}
}

static final class BuilderImpl implements ANSIComponentSerializer.Builder {
private ColorLevel colorLevel = ColorLevel.compute();
private ComponentFlattener flattener = ComponentFlattener.basic();

BuilderImpl() {
BUILDER.accept(this);
}

@Override
public @NotNull Builder colorLevel(final @NotNull ColorLevel colorLevel) {
this.colorLevel = colorLevel;
return this;
}

@Override
public @NotNull Builder flattener(final @NotNull ComponentFlattener componentFlattener) {
this.flattener = componentFlattener;
return this;
}

@Override
public @NotNull ANSIComponentSerializer build() {
return new ANSIComponentSerializerImpl(this.colorLevel, this.flattener);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/*
* This file is part of adventure, licensed under the MIT License.
*
* Copyright (c) 2023 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.
*/
/**
* Serialization to ANSI escape sequences. Does not support deserialization.
*/
package net.kyori.adventure.text.serializer.ansi;
Loading

0 comments on commit 62ca6f4

Please sign in to comment.