diff --git a/src/main/java/me/melontini/commander/impl/expression/library/ExpressionLibraryLoader.java b/src/main/java/me/melontini/commander/impl/expression/library/ExpressionLibraryLoader.java index 5020f3b..5b6a5a1 100644 --- a/src/main/java/me/melontini/commander/impl/expression/library/ExpressionLibraryLoader.java +++ b/src/main/java/me/melontini/commander/impl/expression/library/ExpressionLibraryLoader.java @@ -57,6 +57,11 @@ public class ExpressionLibraryLoader extends JsonCodecDataLoader expressions) { public static final Codec CODEC = RecordCodecBuilder.create(data -> data.group( Codec.BOOL.fieldOf("replace").forGetter(Shelf::replace), diff --git a/src/main/java/me/melontini/commander/impl/expression/macro/DynamicMacro.java b/src/main/java/me/melontini/commander/impl/expression/macro/DynamicMacro.java index 98c2d4d..1935987 100644 --- a/src/main/java/me/melontini/commander/impl/expression/macro/DynamicMacro.java +++ b/src/main/java/me/melontini/commander/impl/expression/macro/DynamicMacro.java @@ -1,16 +1,16 @@ package me.melontini.commander.impl.expression.macro; import java.util.Map; -import java.util.function.BiFunction; +import lombok.extern.log4j.Log4j2; import me.melontini.commander.api.expression.BrigadierMacro; import net.minecraft.loot.context.LootContext; -public record DynamicMacro( - String original, BiFunction, StringBuilder> start) +@Log4j2 +public record DynamicMacro(String original, PatternParser.Appender start) implements BrigadierMacro { @Override public String build(LootContext context, Map params) { - return start.apply(context, params).toString(); + return start.build(context, params, new StringBuilder()).toString(); } } diff --git a/src/main/java/me/melontini/commander/impl/expression/macro/PatternParser.java b/src/main/java/me/melontini/commander/impl/expression/macro/PatternParser.java index 7260208..df82696 100644 --- a/src/main/java/me/melontini/commander/impl/expression/macro/PatternParser.java +++ b/src/main/java/me/melontini/commander/impl/expression/macro/PatternParser.java @@ -8,21 +8,19 @@ import com.ezylang.evalex.data.types.NumberValue; import com.google.common.collect.ImmutableMap; import com.mojang.serialization.DataResult; import java.math.RoundingMode; +import java.util.ArrayList; +import java.util.List; import java.util.Map; import java.util.function.BiFunction; -import java.util.function.Consumer; import java.util.function.Function; -import java.util.regex.Matcher; -import java.util.regex.Pattern; import me.melontini.commander.api.expression.BrigadierMacro; import me.melontini.commander.impl.expression.EvalUtils; +import me.melontini.dark_matter.api.base.util.Result; import net.minecraft.loot.context.LootContext; import org.jetbrains.annotations.Nullable; public class PatternParser { - public static final Pattern PATTERN = - Pattern.compile("\\$(?:\\(([a-z]+)\\))?\\{\\{([^{}]*)\\}\\}"); public static final Map> CONVERTERS = ImmutableMap.of( "bool", v -> BooleanValue.of(v.getBooleanValue()), @@ -30,39 +28,103 @@ public class PatternParser { "int", v -> NumberValue.of(v.getNumberValue().setScale(0, RoundingMode.DOWN)), "double", v -> NumberValue.of(v.getNumberValue())); - public static final int CAST = 1; - public static final int EXPRESSION = 2; - public static DataResult parse(String input) { - Matcher matcher = PATTERN.matcher(input); - if (matcher.results().findAny().isEmpty()) return DataResult.success(new ConstantMacro(input)); - matcher.reset(); - - BiFunction, StringBuilder> start = - (context, params) -> new StringBuilder(); - while (matcher.find()) { - var result = parseExpression(matcher.group(EXPRESSION), matcher.group(CAST)); - if (result.error().isPresent()) return result.map(e -> null); - - var func = result.result().orElseThrow(); - var cmd = sb(b -> matcher.appendReplacement(b, "")); - var fin = start; - start = (context, params) -> fin.apply(context, params) - .append(cmd) - .append(EvalUtils.toMacroString(func.apply(context, params))); + StringReader reader = new StringReader(input); + + List list = new ArrayList<>(); + StringBuilder start = new StringBuilder(); + while (reader.hasNext()) { + char c = reader.read(); + if (c == '$') { + var result = processMacroStart(reader); + if (result.error().isPresent()) + return DataResult.error(() -> result.error().orElseThrow()); + if (result.value().isPresent()) { + var expression = result.value().orElseThrow(); + var fin = start.toString(); + list.add((context, params, builder) -> builder + .append(fin) + .append(EvalUtils.toMacroString(expression.apply(context, params)))); + start = new StringBuilder(); + continue; + } + } + start.append(c); + } + if (list.isEmpty()) { + return DataResult.success(new ConstantMacro(input)); } - var cmd = sb(matcher::appendTail); - var fin = start; - start = (context, params) -> fin.apply(context, params).append(cmd); + var fin = start.toString(); + if (list.size() == 1) { + var appender = list.get(0); + return DataResult.success(new DynamicMacro( + input, + (context, params, builder) -> appender.build(context, params, builder).append(fin))); + } + list.add((context, params, builder) -> builder.append(fin)); + return DataResult.success(new DynamicMacro( + input, + list.stream() + .reduce((a1, a2) -> (context, params, builder) -> + a2.build(context, params, a1.build(context, params, builder))) + .orElseThrow())); + } - return DataResult.success(new DynamicMacro(input, start)); + public interface Appender { + StringBuilder build(LootContext context, Map params, StringBuilder builder); } - private static String sb(Consumer consumer) { - var b = new StringBuilder(); - consumer.accept(b); - return b.toString(); + public static Result, EvaluationValue>, String> + processMacroStart(StringReader reader) { + int start = reader.pointer(); + + StringBuilder cast = null; + if (reader.peek() == '(') + cast: + { + reader.skip(); + cast = new StringBuilder(); + while (reader.hasNext()) { + char c = reader.read(); + if (c == ')') break cast; + cast.append(c); + } + return Result.error("Dangling parentheses '(' at index %s".formatted(start)); + } + + StringBuilder expression = null; + if (reader.peek() == '{' && reader.peek(1) == '{') + expression: + { + reader.skip(2); + expression = new StringBuilder(); + while (reader.hasNext()) { + char c = reader.read(); + if (c == '}' && reader.canRead(1) && reader.peek() == '}') { + reader.skip(); + break expression; + } + expression.append(c); + } + return Result.error("Dangling braces '{{' at index %s".formatted(start)); + } + + if (expression == null) return Result.empty(); + if (cast != null) { + String s = cast.toString(); + if (s.isBlank()) return Result.error("Illegal empty cast at index %s".formatted(start)); + for (char c : s.toCharArray()) { + if (!Character.isLetter(c)) + return Result.error( + "Illegal cast (%s). Must only contain letters and no whitespace!".formatted(s)); + } + if (!CONVERTERS.containsKey(s)) return Result.error("No such cast (%s)".formatted(s)); + } + + var result = parseExpression(expression.toString(), cast == null ? null : cast.toString()); + if (result.error().isPresent()) return Result.error(result.error().get().message()); + return Result.ok(result.result().orElseThrow()); } public static DataResult, EvaluationValue>> @@ -72,10 +134,54 @@ public class PatternParser { .map(exp -> (context, params) -> evaluate(context, exp, params)); var c = CONVERTERS.get(cast); - if (c == null) return DataResult.error(() -> "Unknown cast type %s".formatted(cast)); return EvalUtils.parseExpression(expression).map(exp -> (context, params) -> { var evalResult = evaluate(context, exp, params); return evalResult.getValue() == null ? null : c.apply(evalResult); }); } + + public static class StringReader { + private final String input; + private int pointer = 0; + + public StringReader(String input) { + this.input = input; + } + + public char peek() { + return this.peek(0); + } + + public char peek(int offset) { + return this.input.charAt(pointer + offset); + } + + public char read() { + return this.input.charAt(pointer++); + } + + public void skip(int offset) { + pointer += offset; + } + + public void skip() { + skip(1); + } + + public boolean canRead(int length) { + return pointer + length <= this.input.length(); + } + + public boolean hasNext() { + return canRead(1); + } + + public int pointer() { + return pointer; + } + + public void pointer(int pointer) { + this.pointer = pointer; + } + } } diff --git a/src/testmod/java/me/melontini/commander/test/ExpressionLibraryTest.java b/src/testmod/java/me/melontini/commander/test/ExpressionLibraryTest.java index bf8514c..388e9cf 100644 --- a/src/testmod/java/me/melontini/commander/test/ExpressionLibraryTest.java +++ b/src/testmod/java/me/melontini/commander/test/ExpressionLibraryTest.java @@ -10,13 +10,11 @@ public class ExpressionLibraryTest implements ServerTestEntrypoint { @HandyTest void testLibraryInExpressions(ServerTestContext context) { - context - .server() - .getCommandManager() - .executeWithPrefix(context.server().getCommandSource(), "/time set day"); + var r = context.submitAndWait(server -> { + server.getCommandManager().executeWithPrefix(server.getCommandSource(), "/time set day"); + return ExpressionTest.parse("library.cmd:is_day").eval(ExpressionTest.emptyContext(context)); + }); - Assertions.assertThat( - ExpressionTest.parse("library.cmd:is_day").eval(ExpressionTest.emptyContext(context))) - .isEqualTo(Expression.Result.convert(true)); + Assertions.assertThat(r).isEqualTo(Expression.Result.convert(true)); } }