Skip to content

Commit 9f2261e

Browse files
committed
Add debug command for component strings
1 parent 0bd40f5 commit 9f2261e

2 files changed

Lines changed: 335 additions & 18 deletions

File tree

common/src/main/java/dev/terminalmc/chatnotify/command/Commands.java

Lines changed: 69 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,14 @@
2323
import dev.terminalmc.chatnotify.ChatNotify;
2424
import dev.terminalmc.chatnotify.gui.screen.RootScreen;
2525
import dev.terminalmc.chatnotify.util.Unicode;
26+
import dev.terminalmc.chatnotify.util.text.ParseUtil;
2627
import net.minecraft.client.Minecraft;
2728
import net.minecraft.commands.CommandBuildContext;
2829
import net.minecraft.network.chat.Component;
2930

31+
import java.io.IOException;
32+
import java.nio.file.Files;
33+
import java.nio.file.Path;
3034
import java.util.List;
3135

3236
import static net.minecraft.commands.Commands.argument;
@@ -71,27 +75,74 @@ public static <S> void register(CommandDispatcher<S> dispatcher, CommandBuildCon
7175
mc.tell(() -> mc.setScreen(new RootScreen(mc.screen)));
7276
return Command.SINGLE_SUCCESS;
7377
})
74-
.then(literal("format")
75-
.then(argument("pattern", StringArgumentType.greedyString())
76-
.suggests(((ctx, builder) -> {
77-
String[] split = ctx.getInput().split("format ");
78-
String input = split.length < 2 ? "" : split[1];
79-
String pre = input.endsWith("$") ? input : input + "$";
80-
FORMAT_CODES.forEach((s) -> builder.suggest(pre + s));
81-
return builder.buildFuture();
82-
}))
83-
.executes(ctx -> {
84-
String pattern = StringArgumentType.getString(ctx, "pattern");
85-
Component text = Component.literal(pattern.replaceAll(
86-
"\\$",
87-
Unicode.SECTION.str
88-
));
78+
.then(literal("debug")
79+
.then(literal("format")
80+
.then(argument("pattern", StringArgumentType.greedyString())
81+
.suggests(((ctx, builder) -> {
82+
String[] split = ctx.getInput().split("format ");
83+
String input = split.length < 2 ? "" : split[1];
84+
String pre = input.endsWith("$") ? input : input + "$";
85+
FORMAT_CODES.forEach((s) -> builder.suggest(pre + s));
86+
return builder.buildFuture();
87+
}))
88+
.executes(ctx -> {
89+
String pattern = StringArgumentType.getString(
90+
ctx, "pattern"
91+
);
92+
Component text = Component.literal(pattern.replaceAll(
93+
"\\$",
94+
Unicode.SECTION.str
95+
));
8996

90-
mc.gui.getChat().addMessage(text);
91-
return Command.SINGLE_SUCCESS;
92-
})
97+
mc.gui.getChat().addMessage(text);
98+
return Command.SINGLE_SUCCESS;
99+
})
100+
)
101+
)
102+
.then(literal("parse")
103+
.then(literal("string")
104+
.then(argument("string", StringArgumentType.greedyString())
105+
.executes(ctx -> {
106+
String string =
107+
StringArgumentType.getString(
108+
ctx,
109+
"string"
110+
);
111+
Component text =
112+
ParseUtil.parseMutableComponent(string);
113+
114+
mc.gui.getChat().addMessage(text);
115+
return Command.SINGLE_SUCCESS;
116+
})
117+
)
118+
)
119+
.then(literal("file")
120+
.then(argument("path", StringArgumentType.greedyString())
121+
.executes(ctx -> {
122+
String path = StringArgumentType.getString(
123+
ctx,
124+
"path"
125+
);
126+
parseFromPath(mc, path);
127+
128+
return Command.SINGLE_SUCCESS;
129+
})
130+
)
131+
)
93132
)
94133
)
95134
);
96135
}
136+
137+
private static void parseFromPath(Minecraft mc, String path) {
138+
try {
139+
List<String> lines = Files.readAllLines(Path.of(path));
140+
for (String line : lines) {
141+
Component text = ParseUtil.parseMutableComponent(line.strip());
142+
mc.gui.getChat().addMessage(text);
143+
}
144+
} catch (IOException e) {
145+
throw new RuntimeException(e);
146+
}
147+
}
97148
}
Lines changed: 266 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,266 @@
1+
package dev.terminalmc.chatnotify.util.text;
2+
3+
import dev.terminalmc.chatnotify.ChatNotify;
4+
import net.minecraft.network.chat.*;
5+
import net.minecraft.network.chat.ClickEvent.Action;
6+
import net.minecraft.resources.ResourceLocation;
7+
8+
import java.util.ArrayList;
9+
import java.util.List;
10+
import java.util.Optional;
11+
import java.util.regex.Matcher;
12+
import java.util.regex.Pattern;
13+
14+
public class ParseUtil {
15+
16+
/**
17+
* Attempts to reverse {@link MutableComponent#toString}.
18+
* <p/>
19+
* Behavior is undefined if the string contains any mismatched brackets or braces.
20+
*/
21+
public static MutableComponent parseMutableComponent(String string) {
22+
//
23+
// Guards!
24+
//
25+
string = string.strip();
26+
if (string.length() < 5) {
27+
ChatNotify.LOG.warn("Stripped string is less than 5 characters: {}", string);
28+
return Component.literal(string);
29+
}
30+
31+
//
32+
// Scan for mismatched brackets or braces
33+
//
34+
int bracketDepth = 0;
35+
int braceDepth = 0;
36+
for (int i = 0; i < string.length(); i++) {
37+
char c = string.charAt(i);
38+
switch (c) {
39+
case '[' -> bracketDepth++;
40+
case ']' -> bracketDepth--;
41+
case '{' -> braceDepth++;
42+
case '}' -> braceDepth--;
43+
}
44+
}
45+
if (bracketDepth != 0 || braceDepth != 0) {
46+
ChatNotify.LOG.error("String contains mismatched brackets or braces: {}", string);
47+
return Component.literal(string);
48+
}
49+
50+
//
51+
// Split into core and meta
52+
//
53+
String coreStr = string;
54+
String metaStr = null;
55+
56+
if (string.endsWith("]")) {
57+
int depth = 1;
58+
59+
int i;
60+
for (i = string.length() - 2; i >= 0; i--) {
61+
char c = string.charAt(i);
62+
switch (c) {
63+
case ']' -> depth++;
64+
case '[' -> depth--;
65+
}
66+
if (depth == 0)
67+
break;
68+
}
69+
70+
coreStr = string.substring(0, i);
71+
metaStr = string.substring(i + 1, string.length() - 1);
72+
}
73+
74+
//
75+
// Parse core
76+
//
77+
MutableComponent core;
78+
if (coreStr.startsWith("empty")) {
79+
// empty
80+
core = Component.empty();
81+
} else if (coreStr.startsWith("literal{")) {
82+
// literal{<string>}
83+
core = Component.literal(coreStr.substring(8, coreStr.length() - 1));
84+
} else if (coreStr.startsWith("translation{")) {
85+
// translation{key='<string>'[, fallback='<string>'], args=[<arg1>, <arg2>]}
86+
String substring = coreStr.substring(12, coreStr.length() - 1);
87+
String pattern = "^key='(.*?)', (?:fallback='(.*?)', )?args=\\[(.*?)]$";
88+
Matcher matcher = Pattern.compile(pattern).matcher(substring);
89+
if (matcher.matches()) {
90+
String key = matcher.group(1);
91+
String fallback = matcher.group(2);
92+
List<String> argStrings = splitTopLevel(matcher.group(3), ", ");
93+
94+
Object[] args = new Object[argStrings.size()];
95+
for (int i = 0; i < args.length; i++) {
96+
args[i] = parseMutableComponent(argStrings.get(i));
97+
}
98+
99+
if (fallback == null) {
100+
core = Component.translatable(key, args);
101+
} else {
102+
core = Component.translatable(key, fallback, args);
103+
}
104+
} else {
105+
ChatNotify.LOG.error(
106+
"Translation string does not match regex pattern: {}",
107+
substring
108+
);
109+
core = Component.literal(coreStr);
110+
}
111+
} else if (coreStr.startsWith("score{")) {
112+
// score{name='<name>', objective='<objective>'}
113+
String substring = coreStr.substring(6, coreStr.length() - 1);
114+
String pattern = "^name='(.*?)', objective='(.*?)'$";
115+
Matcher matcher = Pattern.compile(pattern).matcher(substring);
116+
if (matcher.matches()) {
117+
String name = matcher.group(1);
118+
String objective = matcher.group(2);
119+
120+
core = Component.score(name, objective);
121+
} else {
122+
ChatNotify.LOG.error("Score string does not match regex pattern: {}", substring);
123+
core = Component.literal(coreStr);
124+
}
125+
} else if (coreStr.startsWith("keybind{")) {
126+
// keybind{<name>}
127+
String substring = coreStr.substring(8, coreStr.length() - 1);
128+
core = Component.keybind(substring);
129+
} else if (coreStr.startsWith("pattern{")) {
130+
// pattern{<pattern>}
131+
String substring = coreStr.substring(8, coreStr.length() - 1);
132+
core = Component.selector(substring, Optional.empty());
133+
} else {
134+
// nbt
135+
core = Component.literal(coreStr);
136+
}
137+
138+
//
139+
// Parse meta
140+
//
141+
if (metaStr != null) {
142+
String pattern = "^(?:style=\\{(.*)}(?:, )?)?(?:siblings=\\[(.*)])?$";
143+
Matcher matcher = Pattern.compile(pattern).matcher(metaStr);
144+
if (matcher.matches()) {
145+
String styleStr = matcher.group(1);
146+
String siblingsStr = matcher.group(2);
147+
148+
if (styleStr != null) {
149+
core.setStyle(parseStyle(styleStr));
150+
}
151+
152+
if (siblingsStr != null) {
153+
List<String> siblings = splitTopLevel(siblingsStr, ", ");
154+
for (String sibling : siblings) {
155+
core.append(parseMutableComponent(sibling));
156+
}
157+
}
158+
} else {
159+
ChatNotify.LOG.error("Meta string does not match regex pattern: {}", metaStr);
160+
return Component.literal(string);
161+
}
162+
}
163+
164+
return core;
165+
}
166+
167+
/**
168+
* Attempts to reverse {@link Style#toString}.
169+
* <p/>
170+
* Behavior is undefined if the string contains any mismatched brackets or braces.
171+
*/
172+
public static Style parseStyle(String string) {
173+
Style style = Style.EMPTY;
174+
List<String> split = splitTopLevel(string, ",");
175+
176+
for (String s : split) {
177+
if (s.isBlank())
178+
continue;
179+
180+
if (s.startsWith("color=")) {
181+
style = style.withColor(TextColor.parseColor(s.substring(6)).getOrThrow());
182+
} else if (s.startsWith("clickEvent=")) {
183+
String substring = s.substring(11);
184+
String pattern = "^ClickEvent\\{action=(\\w+), value='(.*)'}$";
185+
Matcher matcher = Pattern.compile(pattern).matcher(substring);
186+
if (matcher.matches()) {
187+
String action = matcher.group(1);
188+
String value = matcher.group(2);
189+
style = style.withClickEvent(new ClickEvent(Action.valueOf(action), value));
190+
} else {
191+
ChatNotify.LOG.warn(
192+
"ClickEvent string does not match regex pattern: {}",
193+
substring
194+
);
195+
style = style.withClickEvent(new ClickEvent(
196+
Action.SUGGEST_COMMAND,
197+
"Sample click text"
198+
));
199+
}
200+
} else if (s.startsWith("hoverEvent=")) {
201+
style = style.withHoverEvent(new HoverEvent(
202+
HoverEvent.Action.SHOW_TEXT,
203+
Component.literal("Sample hover text")
204+
));
205+
} else if (s.startsWith("insertion=")) {
206+
style = style.withInsertion(s.substring(10));
207+
} else if (s.startsWith("font=")) {
208+
style = style.withFont(ResourceLocation.parse(s.substring(5)));
209+
} else if (s.equals("bold")) {
210+
style = style.withBold(true);
211+
} else if (s.equals("italic")) {
212+
style = style.withItalic(true);
213+
} else if (s.equals("underlined")) {
214+
style = style.withUnderlined(true);
215+
} else if (s.equals("strikethrough")) {
216+
style = style.withStrikethrough(true);
217+
} else if (s.equals("obfuscated")) {
218+
style = style.withObfuscated(true);
219+
}
220+
}
221+
222+
return style;
223+
}
224+
225+
/**
226+
* Splits the string on occurrences of the delimiter that occur outside any brackets or braces.
227+
* <p/>
228+
* Assumes the input string does not contain mismatched brackets or braces.
229+
*
230+
* @param string the string to split.
231+
* @param delimiter the delimiter to split on.
232+
* @return the list of parts.
233+
*/
234+
public static List<String> splitTopLevel(String string, String delimiter) {
235+
List<String> parts = new ArrayList<>();
236+
StringBuilder builder = new StringBuilder();
237+
int bracketDepth = 0;
238+
int braceDepth = 0;
239+
240+
for (int i = 0; i < string.length(); i++) {
241+
char c = string.charAt(i);
242+
switch (c) {
243+
case '[' -> bracketDepth++;
244+
case ']' -> bracketDepth--;
245+
case '{' -> braceDepth++;
246+
case '}' -> braceDepth--;
247+
}
248+
249+
if (bracketDepth == 0 && braceDepth == 0 && string.startsWith(delimiter, i)) {
250+
// Encountered a top-level delimiter, add builder as new part and reset
251+
parts.add(builder.toString());
252+
builder.setLength(0);
253+
i += delimiter.length() - 1;
254+
} else {
255+
builder.append(c);
256+
}
257+
}
258+
259+
// Add final part
260+
if (!builder.isEmpty()) {
261+
parts.add(builder.toString());
262+
}
263+
264+
return parts;
265+
}
266+
}

0 commit comments

Comments
 (0)