From d57a1ab9a45be3472d40d7bdf5003cc9ffd8d275 Mon Sep 17 00:00:00 2001 From: Jean-Luc Makiola Date: Sun, 31 May 2026 20:14:22 +0200 Subject: [PATCH] feat(io): semantic styled-text model (Style, Span, StyledText) Co-Authored-By: Claude Opus 4.8 (1M context) --- .../thb/jeanluc/adventure/io/text/Span.java | 13 ++++ .../thb/jeanluc/adventure/io/text/Style.java | 4 ++ .../jeanluc/adventure/io/text/StyledText.java | 59 +++++++++++++++++++ .../adventure/io/text/StyledTextTest.java | 38 ++++++++++++ 4 files changed, 114 insertions(+) create mode 100644 Semesterprojekt/src/main/java/thb/jeanluc/adventure/io/text/Span.java create mode 100644 Semesterprojekt/src/main/java/thb/jeanluc/adventure/io/text/Style.java create mode 100644 Semesterprojekt/src/main/java/thb/jeanluc/adventure/io/text/StyledText.java create mode 100644 Semesterprojekt/src/test/java/thb/jeanluc/adventure/io/text/StyledTextTest.java diff --git a/Semesterprojekt/src/main/java/thb/jeanluc/adventure/io/text/Span.java b/Semesterprojekt/src/main/java/thb/jeanluc/adventure/io/text/Span.java new file mode 100644 index 0000000..9f01f06 --- /dev/null +++ b/Semesterprojekt/src/main/java/thb/jeanluc/adventure/io/text/Span.java @@ -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"); + } + } +} diff --git a/Semesterprojekt/src/main/java/thb/jeanluc/adventure/io/text/Style.java b/Semesterprojekt/src/main/java/thb/jeanluc/adventure/io/text/Style.java new file mode 100644 index 0000000..02d67f9 --- /dev/null +++ b/Semesterprojekt/src/main/java/thb/jeanluc/adventure/io/text/Style.java @@ -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 } diff --git a/Semesterprojekt/src/main/java/thb/jeanluc/adventure/io/text/StyledText.java b/Semesterprojekt/src/main/java/thb/jeanluc/adventure/io/text/StyledText.java new file mode 100644 index 0000000..0065c03 --- /dev/null +++ b/Semesterprojekt/src/main/java/thb/jeanluc/adventure/io/text/StyledText.java @@ -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 spans; + + private StyledText(List spans) { + this.spans = List.copyOf(spans); + } + + /** @return the spans in order (unmodifiable). */ + public List 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 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); + } + } +} diff --git a/Semesterprojekt/src/test/java/thb/jeanluc/adventure/io/text/StyledTextTest.java b/Semesterprojekt/src/test/java/thb/jeanluc/adventure/io/text/StyledTextTest.java new file mode 100644 index 0000000..7b188e4 --- /dev/null +++ b/Semesterprojekt/src/test/java/thb/jeanluc/adventure/io/text/StyledTextTest.java @@ -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); + } +}