From 2c8a84f118854abb7becc7863d743a68d9c85f4a Mon Sep 17 00:00:00 2001 From: Tako Schotanus Date: Wed, 18 Mar 2026 14:14:54 +0100 Subject: [PATCH] fix: some more minor markup fixes and refactorings --- .../twinkle/demos/BouncingTwinkleDemo.java | 2 +- .../codejive/twinkle/fluent/MarkupParser.java | 2 +- .../fluent/impl/DefaultMarkupParser.java | 105 ++++++++++++++---- .../twinkle/fluent/impl/FluentImpl.java | 2 +- .../fluent/TestDefaultMarkupParser.java | 11 +- .../codejive/twinkle/fluent/TestFluent.java | 17 +++ 6 files changed, 110 insertions(+), 29 deletions(-) diff --git a/examples/src/main/java/org/codejive/twinkle/demos/BouncingTwinkleDemo.java b/examples/src/main/java/org/codejive/twinkle/demos/BouncingTwinkleDemo.java index 52654fc..1750e65 100644 --- a/examples/src/main/java/org/codejive/twinkle/demos/BouncingTwinkleDemo.java +++ b/examples/src/main/java/org/codejive/twinkle/demos/BouncingTwinkleDemo.java @@ -103,7 +103,7 @@ public static void main(String[] args) throws Exception { Fluent f = writer.fluent(); f.at(2, 0).markup("{green}[ {white}%s{green} ]", size); f.at(size.width() / 2 - 3, 0) - .markup("{green}[ {blue}{ul}{%s}Twinkle{/}{/ul}{green} ]", URL); + .markup("{green}[ {blue}{ul}{$1}Twinkle{/}{/ul}{green} ]", URL); f.at(size.width() - 12, 0) .markup("{green}[ {+}{white}fps %s{-} ]", Math.round(fps.average())); f.at(textX, textY).color(textColor).text(text).done(); diff --git a/twinkle-text/src/main/java/org/codejive/twinkle/fluent/MarkupParser.java b/twinkle-text/src/main/java/org/codejive/twinkle/fluent/MarkupParser.java index ac64ed3..9649f2b 100644 --- a/twinkle-text/src/main/java/org/codejive/twinkle/fluent/MarkupParser.java +++ b/twinkle-text/src/main/java/org/codejive/twinkle/fluent/MarkupParser.java @@ -3,5 +3,5 @@ /** An interface for possible markup parser implementations that can be used for the `Fluent` */ public interface MarkupParser { - void parse(Fluent fluent, String textWithMarkup); + void parse(Fluent fluent, String textWithMarkup, Object... args); } diff --git a/twinkle-text/src/main/java/org/codejive/twinkle/fluent/impl/DefaultMarkupParser.java b/twinkle-text/src/main/java/org/codejive/twinkle/fluent/impl/DefaultMarkupParser.java index 32e0cda..2486ce6 100644 --- a/twinkle-text/src/main/java/org/codejive/twinkle/fluent/impl/DefaultMarkupParser.java +++ b/twinkle-text/src/main/java/org/codejive/twinkle/fluent/impl/DefaultMarkupParser.java @@ -1,7 +1,9 @@ package org.codejive.twinkle.fluent.impl; +import java.io.IOException; import java.util.HashMap; import java.util.Map; +import java.util.function.Consumer; import java.util.regex.Matcher; import java.util.regex.Pattern; import org.codejive.twinkle.ansi.Color; @@ -20,30 +22,23 @@ public class DefaultMarkupParser implements MarkupParser { private static Map colors; - private static final Pattern markupPattern = Pattern.compile("(? lastIndex) { - append(fluent, textWithMarkup.substring(lastIndex, matcher.start())); - } - // Handle the markup content - String markupContent = matcher.group(1); - handleMarkup(fluent, markupContent); - lastIndex = matcher.end(); - } - // Append any remaining text after the last markup - if (lastIndex < textWithMarkup.length()) { - append(fluent, textWithMarkup.substring(lastIndex, textWithMarkup.length())); - } + applyPattern( + fluent::plain, + markupPattern, + "{", + textWithMarkup, + markup -> handleMarkup(fluent, markup, args)); } - protected void handleMarkup(Fluent fluent, String markup) { + protected void handleMarkup(Fluent fluent, String markup, Object... args) { + markup = applySubstitutions(markup, args); + if (tryStyles(fluent, markup)) { return; } @@ -89,6 +84,48 @@ protected void handleMarkup(Fluent fluent, String markup) { } } + private String applySubstitutions(String markup, Object... args) { + if (markup.contains("$")) { + StringBuilder sb = new StringBuilder(); + applyPattern(sb::append, varPattern, "$", markup, vn -> applyVar(sb, vn, args)); + return sb.toString(); + } + return markup; + } + + protected void applyVar(Appendable appendable, String varName, Object... args) { + try { + if (varName.startsWith("/")) { + appendable.append("/").append(getVar(varName.substring(1), args)); + } else { + appendable.append(getVar(varName, args)); + } + } catch (IOException e) { + // Ignore + } + } + + protected String getVar(String varName, Object... args) { + try { + int num = Integer.parseInt(varName) - 1; + if (num >= args.length) { + throw new IndexOutOfBoundsException( + "Variable substitution index out of bounds: " + + num + + " is larger than the number of provided arguments: " + + args.length); + } + if (num < 0) { + throw new IndexOutOfBoundsException( + "Variable substitution index must be 1 or higher"); + } + return String.valueOf(args[num]); + } catch (NumberFormatException e) { + // Ignore + } + return ""; + } + private boolean tryStyles(NegatableCommands fluentNeg, String markup) { switch (markup.toLowerCase()) { case "bold": @@ -281,11 +318,31 @@ private Color tryColorByName(String markup) { return colors.get(lmarkup); } - protected void append(Fluent fluent, String text) { - try { - fluent.plain(text); - } catch (Exception e) { - // We simply ignore errors + protected void applyPattern( + Consumer appendable, + Pattern pattern, + String token, + String textWithMarkup, + Consumer handler) { + int lastIndex = 0; + Matcher matcher = pattern.matcher(textWithMarkup); + while (matcher.find()) { + // Append text before the markup + if (matcher.start() > lastIndex) { + String txt = textWithMarkup.substring(lastIndex, matcher.start()); + txt = txt.replace(token + token, token); + appendable.accept(txt); + } + // Handle the markup content + String markupContent = matcher.group(1); + handler.accept(markupContent); + lastIndex = matcher.end(); + } + // Append any remaining text after the last markup + if (lastIndex < textWithMarkup.length()) { + String txt = textWithMarkup.substring(lastIndex); + txt = txt.replace(token + token, token); + appendable.accept(txt); } } } diff --git a/twinkle-text/src/main/java/org/codejive/twinkle/fluent/impl/FluentImpl.java b/twinkle-text/src/main/java/org/codejive/twinkle/fluent/impl/FluentImpl.java index 72935bc..6159b0d 100644 --- a/twinkle-text/src/main/java/org/codejive/twinkle/fluent/impl/FluentImpl.java +++ b/twinkle-text/src/main/java/org/codejive/twinkle/fluent/impl/FluentImpl.java @@ -109,7 +109,7 @@ public FluentImpl plain(@NonNull String text) { public Fluent markup(@NonNull Object obj, Object... args) { StringBuilder sb = new StringBuilder(); FluentImpl f = new FluentImpl(sb, currentStyle); - f.markupParser.parse(f, obj.toString()); + f.markupParser.parse(f, obj.toString(), args); Formatter fmt = new Formatter(appendable); fmt.format(sb.toString(), args); diff --git a/twinkle-text/src/test/java/org/codejive/twinkle/fluent/TestDefaultMarkupParser.java b/twinkle-text/src/test/java/org/codejive/twinkle/fluent/TestDefaultMarkupParser.java index 224c3c4..509d181 100644 --- a/twinkle-text/src/test/java/org/codejive/twinkle/fluent/TestDefaultMarkupParser.java +++ b/twinkle-text/src/test/java/org/codejive/twinkle/fluent/TestDefaultMarkupParser.java @@ -28,8 +28,8 @@ private static class Setup { final FluentImpl fluent = FluentImpl.of(sb, Style.DEFAULT); final DefaultMarkupParser markup = new DefaultMarkupParser(); - void parse(String text) { - markup.parse(fluent, text); + void parse(String text, Object... args) { + markup.parse(fluent, text, args); } String result() { @@ -441,4 +441,11 @@ public void testTextWithFormatting() { s.parse("{i}%s{/i}"); assertThat(s.result()).isEqualTo(Ansi.italic() + "%s" + Ansi.italicOff()); } + + @Test + public void testTextWithArgumentInMarkup() { + Setup s = new Setup(); + s.parse("{i}{$1}{/i}", "bold"); + assertThat(s.result()).isEqualTo(Ansi.italic() + Ansi.bold() + Ansi.italicOff()); + } } diff --git a/twinkle-text/src/test/java/org/codejive/twinkle/fluent/TestFluent.java b/twinkle-text/src/test/java/org/codejive/twinkle/fluent/TestFluent.java index 0cb3ec8..dbfc6ab 100644 --- a/twinkle-text/src/test/java/org/codejive/twinkle/fluent/TestFluent.java +++ b/twinkle-text/src/test/java/org/codejive/twinkle/fluent/TestFluent.java @@ -570,6 +570,23 @@ public void testFormatMarkupOrdering() { assertThat(stringFluent.toString()).isEqualTo(""); } + @Test + public void testTextWithFormattingInMarkup() { + Fluent stringFluent = Fluent.string(); + // Markup is invalid so will be ignored (removed) + stringFluent.markup("{i}{%s}{/i}", "bold"); + assertThat(stringFluent.toString()).isEqualTo(Ansi.italic() + Ansi.italicOff()); + } + + @Test + public void testTextWithArgumentInMarkup() { + Fluent stringFluent = Fluent.string(); + // Markup is invalid so will be ignored (removed) + stringFluent.markup("{i}{$1}{/i}", "bold"); + assertThat(stringFluent.toString()) + .isEqualTo(Ansi.italic() + Ansi.bold() + Ansi.italicOff()); + } + @Test public void testMarkupAppendable() { RecordingAppendable recapp = new RecordingAppendable();