forked from Mwexim/skript-parser
-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathExpressionElement.java
More file actions
396 lines (382 loc) · 17.8 KB
/
ExpressionElement.java
File metadata and controls
396 lines (382 loc) · 17.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
package io.github.syst3ms.skriptparser.pattern;
import io.github.syst3ms.skriptparser.lang.Expression;
import io.github.syst3ms.skriptparser.lang.Literal;
import io.github.syst3ms.skriptparser.lang.Variable;
import io.github.syst3ms.skriptparser.lang.base.ConditionalExpression;
import io.github.syst3ms.skriptparser.log.ErrorType;
import io.github.syst3ms.skriptparser.log.SkriptLogger;
import io.github.syst3ms.skriptparser.parsing.MatchContext;
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.util.ClassUtils;
import io.github.syst3ms.skriptparser.util.StringUtils;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
/**
* A variable/expression, declared in syntax using {@literal %type%}
* Has :
* <ul>
* <li>a {@link List} of {@link PatternType}</li>
* <li>a field determining what type of values this expression accepts : literals, expressions or both ({@literal %*type%}, {@literal %~type%} and {@literal %type%} respectively)</li>
* <li>a flag determining whether the expression resorts to default expressions or not, defaulting to {@literal null} instead</li>
* <li>a flag determining whether the expression accepts condition expressions or not</li>
* </ul>
* @see PatternType
* @see Literal
* @see ConditionalExpression
*/
public class ExpressionElement implements PatternElement {
private final List<PatternType<?>> types;
private final Acceptance acceptance;
private final boolean nullable;
private final boolean acceptsConditional;
public ExpressionElement(List<PatternType<?>> types, Acceptance acceptance, boolean nullable, boolean acceptsConditional) {
this.types = types;
this.acceptance = acceptance;
this.nullable = nullable;
this.acceptsConditional = acceptsConditional;
}
@Override
public int match(String s, int index, MatchContext context) {
var typeArray = types.toArray(new PatternType<?>[0]);
if (index >= s.length()) {
return -1;
}
var logger = context.getLogger();
var source = context.getSource();
var possibilityIndex = context.getPatternIndex();
var flattened = PatternElement.flatten(context.getOriginalElement());
while (source.isPresent() && possibilityIndex + 1 >= flattened.size()) {
flattened = PatternElement.flatten(source.get().getOriginalElement());
possibilityIndex = source.get().getPatternIndex();
source = source.get().getSource();
}
// We look at what could possibly be after the expression in the current syntax
var possibleInputs = PatternElement.getPossibleInputs(flattened.subList(possibilityIndex + 1, flattened.size()));
for (var possibleInput : possibleInputs) { // We iterate over those possibilities
if (possibleInput instanceof TextElement) {
var text = ((TextElement) possibleInput).getText();
if (text.isEmpty())
continue;
if (text.equals("\0")) { // End of line
if (index == 0) {
return -1;
}
var toParse = s.substring(index).strip();
var expression = parse(toParse, typeArray, context.getParserState(), logger);
if (expression.isPresent()) {
context.addExpression(expression.get());
return index + toParse.length();
}
return -1;
}
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);
if (expression.isPresent()) {
context.addExpression(expression.get());
return index + toParse.length();
}
i = findTextWithBoundary(s, text.strip(), i + 1);
}
} else if (possibleInput instanceof RegexGroup) {
var m = ((RegexGroup) possibleInput).getPattern().matcher(s).region(index, s.length());
while (m.lookingAt()) {
var i = m.start();
if (i == -1) {
continue;
}
var toParse = s.substring(index, i);
if (toParse.length() == context.getOriginalPattern().length())
continue;
var expression = parse(toParse, typeArray, context.getParserState(), logger);
if (expression.isPresent()) {
context.addExpression(expression.get());
return index + toParse.length();
}
}
} else {
assert possibleInput instanceof ExpressionElement;
// 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("\0")) {
// End of line marker - parse the rest and we're done
var rest = s.substring(index);
var splits = splitAtSpaces(rest);
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;
}
}
}
}
return -1;
} else {
var bound = StringUtils.indexOfIgnoreCase(s, text, index);
if (bound == -1) {
continue;
}
var rest = s.substring(index, bound);
var splits = splitAtSpaces(rest);
for (var split : splits) {
var i = StringUtils.indexOfIgnoreCase(s, split, index);
if (i != -1) {
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 i;
}
}
}
}
}
}
}
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<String> splitAtSpaces(String s) {
List<String> split = new ArrayList<>();
var sb = new StringBuilder();
var charArray = s.toCharArray();
for (var i = 0; i < charArray.length; i++) {
var c = charArray[i];
if (c == ' ') {
if (sb.length() > 0) {
split.add(sb.toString());
sb.setLength(0);
}
} else if (c == '(') {
var enclosed = StringUtils.getEnclosedText(s, '(', ')', i)
.map(en -> "(" + en + ")");
sb.append(enclosed.orElse("("));
i += enclosed.map(s1 -> s1.length() + 1).orElse(0);
} else {
sb.append(c);
}
}
if (sb.length() > 0) {
split.add(sb.toString());
}
return split;
}
@SuppressWarnings("unchecked")
private <T> Optional<? extends Expression<? extends T>> parse(String s, PatternType<?>[] types, ParserState parserState, SkriptLogger logger) {
for (var type : types) {
Optional<? extends Expression<? extends T>> expression;
logger.recurse();
if (type.equals(SyntaxParser.BOOLEAN_PATTERN_TYPE)) {
// NOTE : conditions call parseBooleanExpression straight away
expression = (Optional<? extends Expression<? extends T>>) SyntaxParser.parseBooleanExpression(
s,
acceptsConditional ? SyntaxParser.MAYBE_CONDITIONAL : SyntaxParser.NOT_CONDITIONAL,
parserState,
logger
);
} else {
expression = SyntaxParser.parseExpression(s, (PatternType<T>) type, parserState, logger);
}
logger.callback();
if (expression.isEmpty())
continue;
expression = expression.filter(e -> {
switch (acceptance) {
case ALL:
break;
case EXPRESSIONS_ONLY:
if (Literal.isLiteral(e)) {
logger.error("Only expressions are allowed, found literal " + s, ErrorType.SEMANTIC_ERROR);
return false;
}
break;
case LITERALS_ONLY:
if (!Literal.isLiteral(e)) {
logger.error("Only literals are allowed, found expression " + s, ErrorType.SEMANTIC_ERROR);
return false;
}
break;
case VARIABLES_ONLY:
if (!(e instanceof Variable)) {
logger.error("Only variables are allowed, found " + s, ErrorType.SEMANTIC_ERROR);
return false;
}
break;
}
return true;
});
/*
* If multiple types are possible, and a variable is matched, it will have the type
* of the first option, but all options should be supported.
* We manually change the return type of the variable awaiting a major overhaul.
* TODO create a better type system for this that supports union types
*/
expression.ifPresent(e -> {
if (e instanceof Variable && types.length > 1) {
var typeClasses = Arrays.stream(types).map(val -> val.getType().getTypeClass()).toArray(Class[]::new);
((Variable<?>) e).setReturnType(ClassUtils.getCommonSuperclass(typeClasses));
}
});
return expression;
}
return Optional.empty();
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (!(obj instanceof ExpressionElement)) {
return false;
} else {
var e = (ExpressionElement) obj;
return types.equals(e.types) && acceptance == e.acceptance && acceptsConditional == e.acceptsConditional;
}
}
@Override
public String toString() {
var sb = new StringBuilder("%");
if (nullable)
sb.append('-');
switch (acceptance) {
case ALL:
break;
case EXPRESSIONS_ONLY:
sb.append('~');
break;
case LITERALS_ONLY:
sb.append('*');
break;
case VARIABLES_ONLY:
sb.append('^');
break;
}
if (acceptsConditional)
sb.append('=');
sb.append(
String.join(
"/",
types.stream().map(PatternType::toString).toArray(CharSequence[]::new)
)
);
return sb.append("%").toString();
}
public List<PatternType<?>> getTypes() {
return types;
}
public enum Acceptance {
ALL,
EXPRESSIONS_ONLY,
LITERALS_ONLY,
VARIABLES_ONLY
}
}