feat(io): semantic styled-text model (Style, Span, StyledText)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,13 @@
|
|||||||
|
package thb.jeanluc.adventure.io.text;
|
||||||
|
|
||||||
|
/** A run of text carrying a single semantic {@link Style}. */
|
||||||
|
public record Span(String text, Style style) {
|
||||||
|
public Span {
|
||||||
|
if (text == null) {
|
||||||
|
throw new IllegalArgumentException("text must not be null");
|
||||||
|
}
|
||||||
|
if (style == null) {
|
||||||
|
throw new IllegalArgumentException("style must not be null");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
package thb.jeanluc.adventure.io.text;
|
||||||
|
|
||||||
|
/** Semantic output roles. Renderers map each role to concrete colours/attributes. */
|
||||||
|
public enum Style { PLAIN, HEADING, ITEM, NPC, EXIT, DANGER, DIM }
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
package thb.jeanluc.adventure.io.text;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/** An ordered, immutable list of styled {@link Span}s. Built via {@link Builder}. */
|
||||||
|
public final class StyledText {
|
||||||
|
|
||||||
|
private final List<Span> spans;
|
||||||
|
|
||||||
|
private StyledText(List<Span> spans) {
|
||||||
|
this.spans = List.copyOf(spans);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return the spans in order (unmodifiable). */
|
||||||
|
public List<Span> spans() {
|
||||||
|
return spans;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return all span text concatenated, without any styling. */
|
||||||
|
public String plainText() {
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
for (Span s : spans) {
|
||||||
|
sb.append(s.text());
|
||||||
|
}
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return a styled text of a single {@link Style#PLAIN} span. */
|
||||||
|
public static StyledText of(String s) {
|
||||||
|
return new StyledText(List.of(new Span(s, Style.PLAIN)));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Builder builder() {
|
||||||
|
return new Builder();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Fluent builder; one method per {@link Style} role. */
|
||||||
|
public static final class Builder {
|
||||||
|
private final List<Span> spans = new ArrayList<>();
|
||||||
|
|
||||||
|
private Builder add(String text, Style style) {
|
||||||
|
spans.add(new Span(text, style));
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder plain(String s) { return add(s, Style.PLAIN); }
|
||||||
|
public Builder heading(String s) { return add(s, Style.HEADING); }
|
||||||
|
public Builder item(String s) { return add(s, Style.ITEM); }
|
||||||
|
public Builder npc(String s) { return add(s, Style.NPC); }
|
||||||
|
public Builder exit(String s) { return add(s, Style.EXIT); }
|
||||||
|
public Builder danger(String s) { return add(s, Style.DANGER); }
|
||||||
|
public Builder dim(String s) { return add(s, Style.DIM); }
|
||||||
|
|
||||||
|
public StyledText build() {
|
||||||
|
return new StyledText(spans);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
package thb.jeanluc.adventure.io.text;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||||
|
|
||||||
|
class StyledTextTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void builder_collectsSpansInOrder() {
|
||||||
|
StyledText t = StyledText.builder()
|
||||||
|
.plain("You take ").item("the brass lamp").plain(".").build();
|
||||||
|
assertThat(t.spans()).hasSize(3);
|
||||||
|
assertThat(t.spans().get(1).style()).isEqualTo(Style.ITEM);
|
||||||
|
assertThat(t.spans().get(1).text()).isEqualTo("the brass lamp");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void plainText_concatenatesAllSpanText() {
|
||||||
|
StyledText t = StyledText.builder()
|
||||||
|
.plain("You take ").item("the brass lamp").plain(".").build();
|
||||||
|
assertThat(t.plainText()).isEqualTo("You take the brass lamp.");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void of_createsSinglePlainSpan() {
|
||||||
|
StyledText t = StyledText.of("hello");
|
||||||
|
assertThat(t.spans()).hasSize(1);
|
||||||
|
assertThat(t.spans().getFirst().style()).isEqualTo(Style.PLAIN);
|
||||||
|
assertThat(t.plainText()).isEqualTo("hello");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void span_rejectsNulls() {
|
||||||
|
assertThatThrownBy(() -> new Span(null, Style.PLAIN))
|
||||||
|
.isInstanceOf(IllegalArgumentException.class);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user