diff --git a/src/main/java/ch/njol/skript/effects/EffReplace.java b/src/main/java/ch/njol/skript/effects/EffReplace.java index 293326a8e5c..44c52beea39 100644 --- a/src/main/java/ch/njol/skript/effects/EffReplace.java +++ b/src/main/java/ch/njol/skript/effects/EffReplace.java @@ -144,11 +144,13 @@ private void replace(Event event, Object[] needles, Expression haystackExpr) replaceFunction = haystackString -> { for (Pattern pattern : patterns) { Matcher matcher = pattern.matcher(haystackString); - if (replaceFirst) { - haystackString = matcher.replaceFirst(replacement); - } else { - haystackString = matcher.replaceAll(replacement); - } + try { // Throws IndexOutOfBounds on improper use of regex groups in replacement + if (replaceFirst) { + haystackString = matcher.replaceFirst(replacement); + } else { + haystackString = matcher.replaceAll(replacement); + } + } catch (Exception ignored) {} } return haystackString; }; diff --git a/src/main/java/org/skriptlang/skript/common/CommonModule.java b/src/main/java/org/skriptlang/skript/common/CommonModule.java index c3569dc0bc1..762ec04c158 100644 --- a/src/main/java/org/skriptlang/skript/common/CommonModule.java +++ b/src/main/java/org/skriptlang/skript/common/CommonModule.java @@ -32,7 +32,8 @@ protected void loadSelf(SkriptAddon addon) { register(addon, ExprColorFromHexCode::register, ExprHexCode::register, - ExprRecursiveSize::register + ExprRecursiveSize::register, + ExprReplace::register ); } diff --git a/src/main/java/org/skriptlang/skript/common/elements/expressions/ExprReplace.java b/src/main/java/org/skriptlang/skript/common/elements/expressions/ExprReplace.java new file mode 100644 index 00000000000..ca10539d897 --- /dev/null +++ b/src/main/java/org/skriptlang/skript/common/elements/expressions/ExprReplace.java @@ -0,0 +1,166 @@ +package org.skriptlang.skript.common.elements.expressions; + +import ch.njol.skript.SkriptConfig; +import ch.njol.skript.doc.Description; +import ch.njol.skript.doc.Example; +import ch.njol.skript.doc.Name; +import ch.njol.skript.doc.Since; +import ch.njol.skript.lang.Expression; +import ch.njol.skript.lang.SkriptParser.ParseResult; +import ch.njol.skript.lang.SyntaxStringBuilder; +import ch.njol.skript.lang.util.SimpleExpression; +import ch.njol.util.Kleenean; +import ch.njol.util.StringUtils; +import org.bukkit.event.Event; +import org.jetbrains.annotations.Nullable; +import org.skriptlang.skript.registration.DefaultSyntaxInfos; +import org.skriptlang.skript.registration.SyntaxRegistry; + +import java.util.ArrayList; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +@Name("Text Replacement") +@Description("Performs a text replacement on a given value, returning the result. Supports regex and case sensitive replacement.") +@Example("send \"Welcome [player]\" where \"[player]\" is replaced with \"%player%\" to player") +@Example(""" + # Function for sanitizing user inputs + # Strips the input of any non-alphanumeric characters using regex + function sanitizeInput(input: string) :: string: + return {_input} where regex pattern "\\W" is replaced with "" + """) +@Example(""" + # Function to convert &# hex color codes to <# > (mini message format) + function colorFormat(input: string) :: string: + return {_input} where all instances of regex pattern "&#([a-fA-F0-9]{6})" are replaced with "<#$1>" + """) +@Example(""" + # Very simple chat censor + on chat: + set message to message where all instances of "idiot", "noob" are replaced with "****" + set message to message where regex "\\b(idiot|noob)\\b" is replaced with "****" # Regex version using word boundaries for better results + """) + +@Since("INSERT VERSION") +public class ExprReplace extends SimpleExpression { + + public static void register(SyntaxRegistry registry) { + registry.register( + SyntaxRegistry.EXPRESSION, + DefaultSyntaxInfos.Expression.builder(ExprReplace.class, String.class) + .addPatterns( + "%strings% where [(first:[the] first instance[s]|all instances) of] %strings% (is|are) replaced with %string% [regex:using regex|case:with case sensitivity]", + "%strings% where [(first:[the] first instance[s]|all instances) of] regex [pattern[s]] %strings% (is|are) replaced with %string%" + ) + .supplier(ExprReplace::new) + .build() + ); + } + + private Expression needleExpr; + private Expression haystackExpr; + private Expression replacementExpr; + + private boolean isFirst; + private boolean isRegex = false; + private boolean isCaseSensitive = false; + + @SuppressWarnings("unchecked") + @Override + public boolean init(Expression[] expr, int matchedPattern, Kleenean isDelayed, ParseResult parseResult) { + + haystackExpr = (Expression) expr[0]; + needleExpr = (Expression) expr[1]; + replacementExpr = (Expression) expr[2]; + + if (matchedPattern == 1 || parseResult.hasTag("regex")) { + isRegex = true; + } + + isFirst = parseResult.hasTag("first"); + + if (SkriptConfig.caseSensitive.value() || parseResult.hasTag("case")) { + isCaseSensitive = true; + } + return true; + } + + @Override + protected String @Nullable [] get(Event event) { + String replacement = replacementExpr.getSingle(event); + String[] needles = needleExpr.getArray(event); + String[] haystacks = haystackExpr.getArray(event); + + if (replacement == null) { + return haystacks; + } + + List result = new ArrayList<>(haystacks.length); + + if (isRegex) { + List patterns = new ArrayList<>(needles.length); + for (String needle : needles) { + try { // Pre compile regex for use with multiple haystacks + patterns.add(Pattern.compile(needle)); + } catch (Exception ignored) { + } + } + + for (String haystack : haystacks) { + for (Pattern pattern : patterns) { + Matcher matcher = pattern.matcher(haystack); + try { // Throws IndexOutOfBounds on improper use of regex groups in replacement + if (isFirst) { + haystack = matcher.replaceFirst(replacement); + } else { + haystack = matcher.replaceAll(replacement); + } + } catch (Exception ignored) {} + } + result.add(haystack); + } + } else { + for (String haystack : haystacks) { + for (String needle : needles) { + if (isFirst) { + haystack = StringUtils.replaceFirst(haystack, needle, replacement, isCaseSensitive); + } else { + haystack = StringUtils.replace(haystack, needle, replacement, isCaseSensitive); + } + } + result.add(haystack); + } + } + + return result.toArray(new String[0]); + } + + @Override + public boolean isSingle() { + return haystackExpr.isSingle(); + } + + + @Override + public Class getReturnType() { + return String.class; + } + + @Override + public String toString(@Nullable Event event, boolean debug) { + SyntaxStringBuilder builder = new SyntaxStringBuilder(event, debug); + + builder.append("replace"); + if (isFirst) + builder.append("first"); + if (isRegex) + builder.append("regex"); + builder.append(needleExpr, "in", haystackExpr, "with", replacementExpr); + if (isCaseSensitive) + builder.append("with case sensitivity"); + + return builder.toString(); + } + +} diff --git a/src/test/skript/tests/syntaxes/expressions/ExprReplace.sk b/src/test/skript/tests/syntaxes/expressions/ExprReplace.sk new file mode 100644 index 00000000000..a89b81c1cd9 --- /dev/null +++ b/src/test/skript/tests/syntaxes/expressions/ExprReplace.sk @@ -0,0 +1,35 @@ +test "replace expression": + + assert "hello this is a test" where "hello" is replaced with "goodbye" is "goodbye this is a test" with "replace" + + + assert "hello this is a test" where regex pattern "[el]" is replaced with "!" is "h!!!o this is a t!st" with "regex replace pattern 1" + + assert "hello this is a test" where "[el]" is replaced with "!" using regex is "h!!!o this is a t!st" with "regex replace pattern 2" + + + set {_test::*} to "hello", "this", "is", "a", "test" + + assert {_test::*} where "hello", "tes" are replaced with "!" is ("!", "this", "is", "a", "!t") with "replace list" + + assert {_test::*} where regex "[el]" is replaced with "!" is "h!!!o", "this", "is", "a", "t!st" with "regex replace list" + + + assert "aaaaAAAAbbbbBBBB" where "a" is replaced with "!" with case sensitivity is "!!!!AAAAbbbbBBBB" with "replace case sensitivity" + + + assert "test hello test hello this is a test" where first instance of "hello" is replaced with "!" is "test ! test hello this is a test" with "replace first" + + assert "test hello test hello this is a test" where all instances of "hello" are replaced with "!" is "test ! test ! this is a test" with "replace all" + + + assert "abc abcd" where first instance of regex "[ac]" is replaced with "!" is "!bc abcd" with "regex replace first" + + assert "abc abcd" where first instance of regex "[ac]", "[acd]" is replaced with "!" is "!b! abcd" with "regex replace multi first" + + + set {_test::*} to "hello", "this", "is", "a", "test" + + assert {_test::*} where first instance of "hel", "this" are replaced with "!" is ("!lo", "!", "is", "a", "test") with "first replace multi list" + + assert {_test::*} where first instances of regex patterns "[he]", "l" are replaced with "!" is ("!e!lo", "t!is", "is", "a", "t!st") with "first regex replace multi list"