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