diff --git a/src/main/java/io/github/syst3ms/skriptparser/expressions/ExprClaudeTest.java b/src/main/java/io/github/syst3ms/skriptparser/expressions/ExprClaudeTest.java new file mode 100644 index 000000000..e122ae827 --- /dev/null +++ b/src/main/java/io/github/syst3ms/skriptparser/expressions/ExprClaudeTest.java @@ -0,0 +1,40 @@ +package io.github.syst3ms.skriptparser.expressions; + +import io.github.syst3ms.skriptparser.Parser; +import io.github.syst3ms.skriptparser.lang.Expression; +import io.github.syst3ms.skriptparser.lang.TriggerContext; +import io.github.syst3ms.skriptparser.parsing.ParseContext; + +public class ExprClaudeTest implements Expression { + + static { + Parser.getMainRegistration().newExpression(ExprClaudeTest.class, String.class, + true, "claude test %number% [of] %number%") + .noDoc() + .register(); + } + + private Expression first, second; + + @Override + public boolean init(Expression[] expressions, int matchedPattern, ParseContext parseContext) { + first = (Expression) expressions[0]; + second = (Expression) expressions[1]; + return true; + } + + @Override + public String[] getValues(TriggerContext ctx) { + Number number = this.first.getSingle(ctx).orElse(null); + Number other = this.second.getSingle(ctx).orElse(null); + if (number != null && other != null) { + return new String[]{"claude test " + number + " " + other}; + } + return new String[]{}; + } + + @Override + public String toString(TriggerContext ctx, boolean debug) { + return ""; + } +} diff --git a/src/main/java/io/github/syst3ms/skriptparser/file/FileParser.java b/src/main/java/io/github/syst3ms/skriptparser/file/FileParser.java index 6e709ba2b..6b6dbfc14 100644 --- a/src/main/java/io/github/syst3ms/skriptparser/file/FileParser.java +++ b/src/main/java/io/github/syst3ms/skriptparser/file/FileParser.java @@ -30,11 +30,18 @@ public class FileParser { */ public static List parseFileLines(String fileName, List lines, int expectedIndentation, int lastLine, SkriptLogger logger) { List elements = new ArrayList<>(); + boolean multiLineComment = false; for (var i = 0; i < lines.size(); i++) { var line = lines.get(i); + + // Multi line comment sections + if (!line.isBlank() && line.startsWith("###")) { + multiLineComment = !multiLineComment; + } + String content = removeComments(line); - if (content.isEmpty()) { + if (multiLineComment || content.isEmpty()) { elements.add(new VoidElement(fileName, lastLine + i, expectedIndentation)); continue; } diff --git a/src/main/java/io/github/syst3ms/skriptparser/lang/ExpressionList.java b/src/main/java/io/github/syst3ms/skriptparser/lang/ExpressionList.java index eedb88f13..6bb031550 100644 --- a/src/main/java/io/github/syst3ms/skriptparser/lang/ExpressionList.java +++ b/src/main/java/io/github/syst3ms/skriptparser/lang/ExpressionList.java @@ -1,6 +1,7 @@ package io.github.syst3ms.skriptparser.lang; import io.github.syst3ms.skriptparser.parsing.ParseContext; +import io.github.syst3ms.skriptparser.types.changers.ChangeMode; import io.github.syst3ms.skriptparser.util.ClassUtils; import org.jetbrains.annotations.Contract; import org.jetbrains.annotations.Nullable; @@ -209,4 +210,25 @@ public Expression getSource() { return source != null ? source : this; } + @Override + public Optional[]> acceptsChange(ChangeMode mode) { + // Check if all expressions in the list accept this change mode + for (var expr : expressions) { + if (expr.acceptsChange(mode).isEmpty()) { + return Optional.empty(); + } + } + // If all expressions accept the change, return the accepted types from the first expression + // (assuming all expressions have compatible types) + return expressions[0].acceptsChange(mode); + } + + @Override + public void change(TriggerContext ctx, ChangeMode mode, Object[] changeWith) throws UnsupportedOperationException { + // Apply the change to all expressions in the list + for (var expr : expressions) { + expr.change(ctx, mode, changeWith); + } + } + } diff --git a/src/main/java/io/github/syst3ms/skriptparser/lang/Variable.java b/src/main/java/io/github/syst3ms/skriptparser/lang/Variable.java index 1a4b68fb8..d240187f9 100644 --- a/src/main/java/io/github/syst3ms/skriptparser/lang/Variable.java +++ b/src/main/java/io/github/syst3ms/skriptparser/lang/Variable.java @@ -424,6 +424,11 @@ public boolean isIndexLoop(String s) { @Override public String toString(TriggerContext ctx, boolean debug) { + // When in debug mode or using a dummy context, show the variable name instead of its value + if (debug || ctx == TriggerContext.DUMMY) { + String l = this.local ? "_" : ""; + return "{" + l + name.toString(ctx) + "}"; + } return TypeManager.toString(getValues(ctx)); } diff --git a/src/main/java/io/github/syst3ms/skriptparser/parsing/SyntaxParser.java b/src/main/java/io/github/syst3ms/skriptparser/parsing/SyntaxParser.java index c0362edc6..6953a2cd6 100644 --- a/src/main/java/io/github/syst3ms/skriptparser/parsing/SyntaxParser.java +++ b/src/main/java/io/github/syst3ms/skriptparser/parsing/SyntaxParser.java @@ -356,7 +356,7 @@ private static Optional> matchExpressionIn */ public static Optional> parseListLiteral(String s, PatternType expectedType, ParserState parserState, SkriptLogger logger) { assert !expectedType.isSingle(); - if (!s.contains(",") && !s.contains("and") && !s.contains("nor") && !s.contains("or")) + if (!s.contains(",") && !s.matches(".*\\b(?:and|n?or)\\b.*")) return Optional.empty(); List parts = new ArrayList<>(); var m = LIST_SPLIT_PATTERN.matcher(s); diff --git a/src/main/java/io/github/syst3ms/skriptparser/pattern/ExpressionElement.java b/src/main/java/io/github/syst3ms/skriptparser/pattern/ExpressionElement.java index 098f10847..7f7f6f360 100644 --- a/src/main/java/io/github/syst3ms/skriptparser/pattern/ExpressionElement.java +++ b/src/main/java/io/github/syst3ms/skriptparser/pattern/ExpressionElement.java @@ -78,7 +78,7 @@ public int match(String s, int index, MatchContext context) { } return -1; } - var i = StringUtils.indexOfIgnoreCase(s, text, index); + var i = findTextWithBoundary(s, text.strip(), index); while (i != -1) { var toParse = s.substring(index, i).strip(); var expression = parse(toParse, typeArray, context.getParserState(), logger); @@ -86,7 +86,7 @@ public int match(String s, int index, MatchContext context) { context.addExpression(expression.get()); return index + toParse.length(); } - i = StringUtils.indexOfIgnoreCase(s, text, i + 1); + i = findTextWithBoundary(s, text.strip(), i + 1); } } else if (possibleInput instanceof RegexGroup) { var m = ((RegexGroup) possibleInput).getPattern().matcher(s).region(index, s.length()); @@ -106,23 +106,93 @@ public int match(String s, int index, MatchContext context) { } } else { assert possibleInput instanceof ExpressionElement; - var nextPossibleInputs = PatternElement.getPossibleInputs(flattened.subList(context.getPatternIndex() + 1, flattened.size())); + // Find the index of the next expression element in the flattened list + // We need to find the first ExpressionElement after the current position + var expressionIndex = -1; + for (var i = possibilityIndex + 1; i < flattened.size(); i++) { + var elem = flattened.get(i); + // Skip optional groups and look inside them + if (elem instanceof OptionalGroup) { + var inner = PatternElement.flatten(((OptionalGroup) elem).getElement()); + if (inner.stream().anyMatch(e -> e instanceof ExpressionElement)) { + continue; // Skip optional groups containing expressions + } + } else if (elem instanceof ExpressionElement) { + expressionIndex = i; + break; + } + } + if (expressionIndex == -1) { + continue; + } + // When the expression is the last element, nextPossibleInputs will contain "\0" (end of line) + // which we handle below, so we should NOT skip this case! + var nextPossibleInputs = PatternElement.getPossibleInputs(flattened.subList(expressionIndex + 1, flattened.size())); if (nextPossibleInputs.stream().anyMatch(pe -> !(pe instanceof TextElement))) { continue; } for (var nextPossibleInput : nextPossibleInputs) { var text = ((TextElement) nextPossibleInput).getText(); - if (text.equals("")) { + if (text.equals("\0")) { + // End of line marker - parse the rest and we're done var rest = s.substring(index); var splits = splitAtSpaces(rest); - for (var split : splits) { - var i = StringUtils.indexOfIgnoreCase(s, split, index); - if (i != -1) { - var toParse = s.substring(index, i); - var expression = parse(toParse, typeArray, context.getParserState(), logger); - if (expression.isPresent()) { - context.addExpression(expression.get()); - return index + toParse.length(); + if (splits.isEmpty()) { + return -1; + } + // Try parsing progressively larger prefixes + for (var splitCount = 1; splitCount < splits.size(); splitCount++) { + var endIndex = index; + for (var j = 0; j < splitCount; j++) { + var splitIndex = s.indexOf(splits.get(j), endIndex); + if (splitIndex == -1) { + break; + } + endIndex = splitIndex + splits.get(j).length(); + } + while (endIndex < s.length() && Character.isWhitespace(s.charAt(endIndex))) { + endIndex++; + } + if (endIndex > index) { + var toParse = s.substring(index, endIndex).strip(); + if (!toParse.isEmpty()) { + var expression = parse(toParse, typeArray, context.getParserState(), logger); + if (expression.isPresent()) { + context.addExpression(expression.get()); + return endIndex; + } + } + } + } + return -1; + } else if (text.isEmpty() || text.isBlank()) { + var rest = s.substring(index); + var splits = splitAtSpaces(rest); + if (splits.isEmpty()) { + return -1; + } + // Try parsing progressively larger prefixes (first 1 token, then first 2 tokens, etc.) + for (var splitCount = 1; splitCount < splits.size(); splitCount++) { + var endIndex = index; + for (var j = 0; j < splitCount; j++) { + var splitIndex = s.indexOf(splits.get(j), endIndex); + if (splitIndex == -1) { + break; + } + endIndex = splitIndex + splits.get(j).length(); + } + // Find the start of the next token (skip whitespace) + while (endIndex < s.length() && Character.isWhitespace(s.charAt(endIndex))) { + endIndex++; + } + if (endIndex > index) { + var toParse = s.substring(index, endIndex).strip(); + if (!toParse.isEmpty()) { + var expression = parse(toParse, typeArray, context.getParserState(), logger); + if (expression.isPresent()) { + context.addExpression(expression.get()); + return endIndex; + } } } } @@ -137,11 +207,14 @@ public int match(String s, int index, MatchContext context) { for (var split : splits) { var i = StringUtils.indexOfIgnoreCase(s, split, index); if (i != -1) { - var toParse = s.substring(index, i); + var toParse = s.substring(index, i).strip(); + if (toParse.isEmpty()) { + continue; + } var expression = parse(toParse, typeArray, context.getParserState(), logger); if (expression.isPresent()) { context.addExpression(expression.get()); - return index + toParse.length(); + return i; } } } @@ -152,6 +225,36 @@ public int match(String s, int index, MatchContext context) { return -1; } + /** + * Finds the index of text in a string, respecting word boundaries for keywords like "or", "and", "nor". + * @param s the string to search in + * @param text the text to find + * @param start the starting index + * @return the index where text was found, or -1 if not found + */ + private int findTextWithBoundary(String s, String text, int start) { + if (text.isEmpty()) { + return -1; + } + // Check if this is a keyword that needs word boundary checking + var lowerText = text.toLowerCase(); + var needsBoundaryCheck = lowerText.equals("or") || lowerText.equals("and") || lowerText.equals("nor"); + + var i = StringUtils.indexOfIgnoreCase(s, text, start); + while (i != -1 && needsBoundaryCheck) { + // Check word boundaries + var beforeIsWordChar = i > 0 && Character.isLetterOrDigit(s.charAt(i - 1)); + var afterIsWordChar = (i + text.length() < s.length()) && Character.isLetterOrDigit(s.charAt(i + text.length())); + + if (!beforeIsWordChar && !afterIsWordChar) { + return i; // Valid word boundary match + } + // Try next occurrence + i = StringUtils.indexOfIgnoreCase(s, text, i + 1); + } + return i; + } + private List splitAtSpaces(String s) { List split = new ArrayList<>(); var sb = new StringBuilder(); diff --git a/src/test/java/io/github/syst3ms/skriptparser/ClaudeDebugTest.java b/src/test/java/io/github/syst3ms/skriptparser/ClaudeDebugTest.java new file mode 100644 index 000000000..f7ab31fff --- /dev/null +++ b/src/test/java/io/github/syst3ms/skriptparser/ClaudeDebugTest.java @@ -0,0 +1,56 @@ +package io.github.syst3ms.skriptparser; + +import io.github.syst3ms.skriptparser.lang.Expression; +import io.github.syst3ms.skriptparser.log.SkriptLogger; +import io.github.syst3ms.skriptparser.parsing.ParserState; +import io.github.syst3ms.skriptparser.parsing.SyntaxParser; +import io.github.syst3ms.skriptparser.types.PatternType; +import io.github.syst3ms.skriptparser.types.TypeManager; +import org.junit.Test; + +import static org.junit.Assert.fail; + +public class ClaudeDebugTest { + static { + TestRegistration.register(); + } + + @Test + public void testClaudePattern() { + var logger = new SkriptLogger(true); + var parserState = new ParserState(); + + // Test case 1: with space between numbers + String input1 = "claude test 1 2"; + var type = TypeManager.getByClass(String.class).orElseThrow(); + var patternType = new PatternType<>(type, true); + + System.out.println("Testing: " + input1); + var result1 = SyntaxParser.parseExpression(input1, patternType, parserState, logger); + + if (result1.isEmpty()) { + System.out.println("FAILED to parse: " + input1); + fail("Failed to parse: " + input1); + } else { + System.out.println("SUCCESS: " + input1); + Expression expr = result1.get(); + System.out.println(" Parsed expression: " + expr.getClass().getSimpleName()); + } + + // Test case 2: with "of" between numbers + logger.clearErrors(); + logger.clearLogs(); + String input2 = "claude test 1 of 2"; + System.out.println("\nTesting: " + input2); + var result2 = SyntaxParser.parseExpression(input2, patternType, parserState, logger); + + if (result2.isEmpty()) { + System.out.println("FAILED to parse: " + input2); + fail("Failed to parse: " + input2); + } else { + System.out.println("SUCCESS: " + input2); + Expression expr = result2.get(); + System.out.println(" Parsed expression: " + expr.getClass().getSimpleName()); + } + } +}