New approach for chat processing

Created different providers for regex, range and string matching
Handling one match at a time per provider

TODO:
Formatters should be executed one by one in declaration order
Add enitre message by default
Loop until all of the providers are finished
Ignore any areas returned by the providers

#71
This commit is contained in:
Norbi Peti 2020-02-08 01:44:19 +01:00
parent b452170ff7
commit 0a4cdd01ce
No known key found for this signature in database
GPG key ID: DBA4C4549A927E56
9 changed files with 214 additions and 106 deletions

View file

@ -49,7 +49,6 @@ public class ChatProcessing {
private static final Pattern CONSOLE_PING_PATTERN = Pattern.compile("(?i)" + Pattern.quote("@console"));
private static final Pattern HASHTAG_PATTERN = Pattern.compile("#(\\w+)");
private static final Pattern URL_PATTERN = Pattern.compile("(http[\\w:/?=$\\-_.+!*'(),&]+(?:#[\\w]+)?)");
public static final Pattern ENTIRE_MESSAGE_PATTERN = Pattern.compile(".+");
private static final Pattern UNDERLINED_PATTERN = Pattern.compile("__");
private static final Pattern ITALIC_PATTERN = Pattern.compile("\\*");
private static final Pattern ITALIC_PATTERN_2 = Pattern.compile("_");
@ -64,7 +63,10 @@ public class ChatProcessing {
private static final Pattern WORD_PATTERN = Pattern.compile("\\S+");
private static boolean pingedconsole = false;
public static final ChatFormatter ESCAPE_FORMATTER = ChatFormatter.builder("escape", ESCAPE_PATTERN).build();
/**
* A special range formatter that removes the effect of the next formatter
*/
public static final ChatFormatter ESCAPE_FORMATTER = ChatFormatter.builder("escape", ESCAPE_PATTERN).type(ChatFormatter.Type.Range).build();
private static ArrayList<ChatFormatter> commonFormatters = Lists.newArrayList(
ChatFormatter.builder("bold", BOLD_PATTERN).bold(true).removeCharCount((short) 2).type(ChatFormatter.Type.Range)

View file

@ -0,0 +1,31 @@
package buttondevteam.chat.components.formatter.formatting;
import buttondevteam.chat.commands.ucmds.admin.DebugCommand;
import java.util.ArrayList;
import java.util.Arrays;
public final class ChatFormatUtils {
private ChatFormatUtils() {}
static void sendMessageWithPointer(String str, int... pointer) {
DebugCommand.SendDebugMessage(str);
StringBuilder sb = new StringBuilder(str.length());
Arrays.sort(pointer);
for (int i = 0; i < pointer.length; i++) {
for (int j = 0; j < pointer[i] - (i > 0 ? pointer[i - 1] + 1 : 0); j++)
sb.append(' ');
if (pointer[i] == (i > 0 ? pointer[i - 1] : -1))
continue;
sb.append('^');
}
DebugCommand.SendDebugMessage(sb.toString());
}
/**
* Check if the given start and end position is inside any of the ranges
*/
static boolean isInRange(int start, int end, ArrayList<int[]> ranges) {
return ranges.stream().anyMatch(range -> range[1] >= start && range[0] <= end - 1);
}
}

View file

@ -5,14 +5,12 @@ import buttondevteam.chat.components.formatter.ChatProcessing;
import buttondevteam.lib.architecture.ConfigData;
import buttondevteam.lib.architecture.IHaveConfig;
import buttondevteam.lib.chat.Color;
import buttondevteam.lib.chat.Priority;
import lombok.Builder;
import lombok.Data;
import lombok.val;
import java.util.*;
import java.util.function.Predicate;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
@ -28,16 +26,6 @@ import java.util.stream.Collectors;
@Builder
public final class ChatFormatter {
Pattern regex;
boolean italic;
boolean bold;
boolean underlined;
boolean strikethrough;
boolean obfuscated;
Color color;
TriFunc<String, ChatFormatter, FormattedSection, String> onmatch;
String openlink;
@Builder.Default
Priority priority = Priority.Normal;
@Builder.Default
short removeCharCount = 0;
@Builder.Default
@ -64,18 +52,6 @@ public final class ChatFormatter {
return config.getData(name + ".enabled", true);
}
public enum Type {
Normal,
/**
* Matches a start and an end section which gets converted to one section (for example see italics)
*/
Range,
/**
* Exclude matching area from further processing (besides this formatter)
*/
Excluder
}
@FunctionalInterface
public interface TriFunc<T1, T2, T3, R> {
R apply(T1 x1, T2 x2, T3 x3);
@ -101,8 +77,8 @@ public final class ChatFormatter {
*/
val remchars = new ArrayList<int[]>();
header("Range section conversion");
sections = convertRangeSections(str, sections, remchars);
header("Range section creation");
sections = createRangeSections(str, sections, formatters, remchars);
header("Adding remove chars (RC)"); // Important to add after the range section conversion
addRemChars(sections, remchars, str);
@ -117,28 +93,6 @@ public final class ChatFormatter {
private static void createSections(List<ChatFormatter> formatters, String str, ArrayList<FormattedSection> sections,
boolean excluders) {
for (ChatFormatter formatter : formatters) {
if (excluders == (formatter.type != Type.Excluder))
continue; //If we're looking at excluders and this isn't one, skip - or vica-versa
Matcher matcher = formatter.regex.matcher(str);
while (matcher.find()) {
DebugCommand.SendDebugMessage("Found match from " + matcher.start() + " to " + (matcher.end() - 1));
DebugCommand.SendDebugMessage("With " + (excluders ? "excluder " : "") + "formatter: " + formatter);
sendMessageWithPointer(str, matcher.start(), matcher.end() - 1);
if (formatter.regex != ChatProcessing.ENTIRE_MESSAGE_PATTERN && sections.stream().anyMatch(fs -> fs.type == Type.Excluder && (fs.End >= matcher.start() && fs.Start <= matcher.end() - 1))) {
DebugCommand.SendDebugMessage("Ignoring formatter because of an excluder");
continue; //Exclude areas matched by excluders - Range sections are correctly handled afterwards
}
ArrayList<String> groups = new ArrayList<>();
for (int i = 0; i < matcher.groupCount(); i++)
groups.add(matcher.group(i + 1));
if (groups.size() > 0)
DebugCommand.SendDebugMessage("First group: " + groups.get(0));
FormattedSection section = new FormattedSection(formatter, matcher.start(), matcher.end() - 1, groups,
formatter.type);
sections.add(section);
}
}
}
private static void newCombine(String str, ArrayList<FormattedSection> sections, ArrayList<int[]> remchars) {
@ -154,7 +108,7 @@ public final class ChatFormatter {
}
}
private static ArrayList<FormattedSection> convertRangeSections(String str, ArrayList<FormattedSection> sections, ArrayList<int[]> remchars) {
private static ArrayList<FormattedSection> createRangeSections(String str, List<ChatFormatter> formatters, ArrayList<int[]> remchars) {
ArrayList<FormattedSection> combined = new ArrayList<>();
Map<ChatFormatter, FormattedSection> nextSection = new HashMap<>();
boolean escaped = false;
@ -172,7 +126,7 @@ public final class ChatFormatter {
combined.add(section); // The above will delete the \
DebugCommand.SendDebugMessage("Added section: " + section);
}
sendMessageWithPointer(str, section.Start, section.End);
ChatFormatUtils.sendMessageWithPointer(str, section.Start, section.End);
continue;
}
if (!escaped) {
@ -183,17 +137,17 @@ public final class ChatFormatter {
*/
if (takenByBigGuy || formatter.removeCharCount < takenEnd - takenStart) {
DebugCommand.SendDebugMessage("Lose: " + section);
sendMessageWithPointer(str, section.Start, section.End);
ChatFormatUtils.sendMessageWithPointer(str, section.Start, section.End);
DebugCommand.SendDebugMessage("And win: " + takenFormatter);
continue; // The current section loses
}
nextSection.remove(takenFormatter); // The current section wins
DebugCommand.SendDebugMessage("Win: " + section);
sendMessageWithPointer(str, section.Start, section.End);
ChatFormatUtils.sendMessageWithPointer(str, section.Start, section.End);
DebugCommand.SendDebugMessage("And lose: " + takenFormatter);
}
boolean hasFormatter = nextSection.containsKey(formatter);
if (!hasFormatter) {
/*if (!hasFormatter) {
val ff = formatter;
val cfo = nextSection.keySet().stream().filter(f -> f.removeCharCount > ff.removeCharCount).findAny();
if (cfo.isPresent()) {
@ -205,7 +159,7 @@ public final class ChatFormatter {
continue; //Not the formatter we're looking for - TODO: It doesn't fix the problem of italics at the end
}
}
}
}*/
takenStart = section.Start;
takenEnd = section.Start + formatter.removeCharCount;
takenFormatter = formatter;
@ -218,19 +172,19 @@ public final class ChatFormatter {
combined.add(s);
takenByBigGuy = true;
DebugCommand.SendDebugMessage("Finished section: " + s);
sendMessageWithPointer(str, s.Start, s.End);
ChatFormatUtils.sendMessageWithPointer(str, s.Start, s.End);
} else {
DebugCommand.SendDebugMessage("Adding next section: " + section);
sendMessageWithPointer(str, section.Start, section.End);
ChatFormatUtils.sendMessageWithPointer(str, section.Start, section.End);
nextSection.put(formatter, section);
takenByBigGuy = false;
}
DebugCommand
.SendDebugMessage("New area taken: (" + takenStart + "-" + takenEnd + ") " + takenFormatter);
sendMessageWithPointer(str, takenStart, takenEnd);
ChatFormatUtils.sendMessageWithPointer(str, takenStart, takenEnd);
} else {
DebugCommand.SendDebugMessage("Skipping section: " + section); // This will keep the text (character)
sendMessageWithPointer(str, section.Start, section.End);
ChatFormatUtils.sendMessageWithPointer(str, section.Start, section.End);
escaped = false; // Reset escaping if applied, like if we're at the '*' in '\*'
}
}
@ -250,7 +204,7 @@ public final class ChatFormatter {
.forEach(remchars::add);
DebugCommand.SendDebugMessage("Added remchars:");
DebugCommand.SendDebugMessage(remchars.stream().map(Arrays::toString).collect(Collectors.joining("; ")));
sendMessageWithPointer(str,
ChatFormatUtils.sendMessageWithPointer(str,
remchars.stream().flatMapToInt(Arrays::stream).toArray());
}
@ -271,15 +225,15 @@ public final class ChatFormatter {
lastSection = lastSect;
}
DebugCommand.SendDebugMessage("Combining sections " + firstSection);
sendMessageWithPointer(str, firstSection.Start, firstSection.End);
ChatFormatUtils.sendMessageWithPointer(str, firstSection.Start, firstSection.End);
DebugCommand.SendDebugMessage(" and " + lastSection);
sendMessageWithPointer(str, lastSection.Start, lastSection.End);
ChatFormatUtils.sendMessageWithPointer(str, lastSection.Start, lastSection.End);
if (firstSection.Start == lastSection.Start && firstSection.End == lastSection.End) {
firstSection.Formatters.addAll(lastSection.Formatters);
firstSection.Matches.addAll(lastSection.Matches);
firstSection.type = lastSection.type;
DebugCommand.SendDebugMessage("To section " + firstSection);
sendMessageWithPointer(str, firstSection.Start, firstSection.End);
ChatFormatUtils.sendMessageWithPointer(str, firstSection.Start, firstSection.End);
sections.remove(i);
i = 0;
sortSections(sections);
@ -305,7 +259,7 @@ public final class ChatFormatter {
Predicate<FormattedSection> removeIfNeeded = s -> {
if (s.Start < 0 || s.End < 0 || s.Start > s.End) {
DebugCommand.SendDebugMessage(" Removed: " + s);
sendMessageWithPointer(str, s.Start, s.End);
ChatFormatUtils.sendMessageWithPointer(str, s.Start, s.End);
sections.remove(s);
return true;
}
@ -315,15 +269,15 @@ public final class ChatFormatter {
DebugCommand.SendDebugMessage("To sections");
if (!removeIfNeeded.test(firstSection)) {
DebugCommand.SendDebugMessage(" 1:" + firstSection + "");
sendMessageWithPointer(str, firstSection.Start, firstSection.End);
ChatFormatUtils.sendMessageWithPointer(str, firstSection.Start, firstSection.End);
}
if (!removeIfNeeded.test(section)) {
DebugCommand.SendDebugMessage(" 2:" + section + "");
sendMessageWithPointer(str, section.Start, section.End);
ChatFormatUtils.sendMessageWithPointer(str, section.Start, section.End);
}
if (!removeIfNeeded.test(lastSection)) {
DebugCommand.SendDebugMessage(" 3:" + lastSection);
sendMessageWithPointer(str, lastSection.Start, lastSection.End);
ChatFormatUtils.sendMessageWithPointer(str, lastSection.Start, lastSection.End);
}
i = 0;
}
@ -332,7 +286,7 @@ public final class ChatFormatter {
for (int j = i - 1; j <= i + 1; j++) {
if (j < sections.size() && sections.get(j).End < sections.get(j).Start) {
DebugCommand.SendDebugMessage("Removing section: " + sections.get(j));
sendMessageWithPointer(str, sections.get(j).Start, sections.get(j).End);
ChatFormatUtils.sendMessageWithPointer(str, sections.get(j).Start, sections.get(j).End);
sections.remove(j);
j--;
i = 0;
@ -349,7 +303,7 @@ public final class ChatFormatter {
String originaltext;
int start = section.Start, end = section.End;
DebugCommand.SendDebugMessage("Start: " + start + " - End: " + end);
sendMessageWithPointer(str, start, end);
ChatFormatUtils.sendMessageWithPointer(str, start, end);
/*DebugCommand.SendDebugMessage("RCS: "+remchars.stream().filter(rc -> rc[0] <= start && start <= rc[1]).count());
DebugCommand.SendDebugMessage("RCE: "+remchars.stream().filter(rc -> rc[0] <= end && end <= rc[1]).count());
DebugCommand.SendDebugMessage("RCI: "+remchars.stream().filter(rc -> start < rc[0] || rc[1] < end).count());*/
@ -432,20 +386,6 @@ public final class ChatFormatter {
: Integer.compare(s1.Start, s2.Start));
}
private static void sendMessageWithPointer(String str, int... pointer) {
DebugCommand.SendDebugMessage(str);
StringBuilder sb = new StringBuilder(str.length());
Arrays.sort(pointer);
for (int i = 0; i < pointer.length; i++) {
for (int j = 0; j < pointer[i] - (i > 0 ? pointer[i - 1] + 1 : 0); j++)
sb.append(' ');
if (pointer[i] == (i > 0 ? pointer[i - 1] : -1))
continue;
sb.append('^');
}
DebugCommand.SendDebugMessage(sb.toString());
}
private static void header(String message) {
DebugCommand.SendDebugMessage("\n--------\n" + message + "\n--------\n");
}

View file

@ -0,0 +1,18 @@
package buttondevteam.chat.components.formatter.formatting;
import buttondevteam.lib.chat.Color;
import lombok.Builder;
import lombok.Data;
@Data
@Builder
public class FormatSettings {
boolean italic;
boolean bold;
boolean underlined;
boolean strikethrough;
boolean obfuscated;
Color color;
ChatFormatter.TriFunc<String, ChatFormatter, FormattedSection, String> onmatch;
String openlink;
}

View file

@ -1,36 +1,17 @@
package buttondevteam.chat.components.formatter.formatting;
import java.util.ArrayList;
import java.util.Collection;
public class FormattedSection {
public int Start;
public int End;
public ArrayList<ChatFormatter> Formatters = new ArrayList<ChatFormatter>();
public FormatSettings Settings;
public ArrayList<String> Matches = new ArrayList<String>();
public ChatFormatter.Type type;
FormattedSection(ChatFormatter formatter, int start, int end, ArrayList<String> matches, ChatFormatter.Type type) {
FormattedSection(FormatSettings settings, int start, int end, ArrayList<String> matches) {
Start = start;
End = end;
Formatters.add(formatter);
Settings = settings;
Matches.addAll(matches);
this.type = type;
}
FormattedSection(Collection<ChatFormatter> formatters, int start, int end, ArrayList<String> matches,
ChatFormatter.Type type) {
Start = start;
End = end;
Formatters.addAll(formatters);
Matches.addAll(matches);
this.type = type;
}
@Override
public String toString() {
return "Section(" + Start + ", " + End + ", formatters: " +
Formatters.toString() + ", matches: " + Matches.toString() + ", " +
type + ")";
}
}

View file

@ -0,0 +1,11 @@
package buttondevteam.chat.components.formatter.formatting;
import javax.annotation.Nullable;
import java.util.ArrayList;
public interface MatchProvider {
@Nullable
FormattedSection getNextSection(String message, ArrayList<int[]> ignoredAreas, ArrayList<int[]> removedCharacters);
boolean isFinished();
}

View file

@ -0,0 +1,39 @@
package buttondevteam.chat.components.formatter.formatting;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import java.util.ArrayList;
@RequiredArgsConstructor
public class RangeMatchProvider implements MatchProvider {
private final String pattern;
private final FormatSettings settings;
@Getter
private boolean finished;
private int nextIndex = 0;
private FormattedSection startedSection;
@SuppressWarnings("DuplicatedCode")
@Override
public FormattedSection getNextSection(String message, ArrayList<int[]> ignoredAreas, ArrayList<int[]> removedCharacters) {
int i, len;
do {
i = message.indexOf(pattern, nextIndex);
len = pattern.length();
nextIndex = i + len; //Set for the next loop if it's escaped or for the next method call
} while (i > 0 && message.charAt(i - 1) == '\\');
if (i == -1) {
finished = true; //Won't find any more - unfinished sections will be garbage collected
return null;
}
removedCharacters.add(new int[]{i, i + len - 1});
if (startedSection == null) {
startedSection = new FormattedSection(settings, i, i + len - 1, new ArrayList<>(0));
return null;
} else {
startedSection.End = i + len - 1;
return startedSection;
}
}
}

View file

@ -0,0 +1,44 @@
package buttondevteam.chat.components.formatter.formatting;
import buttondevteam.chat.commands.ucmds.admin.DebugCommand;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import javax.annotation.Nullable;
import java.util.ArrayList;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@RequiredArgsConstructor
public class RegexMatchProvider implements MatchProvider {
private final Pattern pattern;
private final FormatSettings settings;
private Matcher matcher;
@Getter
private boolean finished;
@Nullable
@Override
public FormattedSection getNextSection(String message, ArrayList<int[]> ignoredAreas, ArrayList<int[]> removedCharacters) {
if (matcher == null)
matcher = pattern.matcher(message);
if (!matcher.find()) {
finished = true;
return null;
}
int start = matcher.start(), end = matcher.end() - 1;
DebugCommand.SendDebugMessage("Found regex match from " + start + " to " + end);
DebugCommand.SendDebugMessage("With settings: " + settings);
ChatFormatUtils.sendMessageWithPointer(message, start, end);
if (ChatFormatUtils.isInRange(start, end, ignoredAreas)) {
DebugCommand.SendDebugMessage("Formatter is in ignored area, skipping");
return null; //Not setting finished to true, so it will go to the next match
}
ArrayList<String> groups = new ArrayList<>();
for (int i = 0; i < matcher.groupCount(); i++)
groups.add(matcher.group(i + 1));
if (groups.size() > 0)
DebugCommand.SendDebugMessage("First group: " + groups.get(0));
return new FormattedSection(settings, matcher.start(), matcher.end() - 1, groups);
}
}

View file

@ -0,0 +1,42 @@
package buttondevteam.chat.components.formatter.formatting;
import lombok.Getter;
import javax.annotation.Nullable;
import java.util.ArrayList;
public class StringMatchProvider implements MatchProvider {
private final String[] strings;
private final FormatSettings settings;
@Getter
private boolean finished;
private int nextIndex = 0;
/**
* Matches the given strings in the order given
*
* @param settings The format settings
* @param strings The strings to match in the correct order
*/
public StringMatchProvider(FormatSettings settings, String... strings) {
this.settings = settings;
this.strings = strings;
}
@Nullable
@Override
public FormattedSection getNextSection(String message, ArrayList<int[]> ignoredAreas, ArrayList<int[]> removedCharacters) {
int i = -1, len = 0;
for (String string : strings) {
i = message.indexOf(string, nextIndex);
len = string.length();
if (i != -1) break;
}
if (i == -1) {
finished = true; //Won't find any more
return null;
}
nextIndex = i + len;
return new FormattedSection(settings, i, i + len - 1, new ArrayList<>(0));
}
}