Add processing, translation and file layers, major refactoring
- Added LanguageManager class from AreaShop to manage saving/loading language files - Added Transifex class from AreaShop to convert language files downloaded from Transifex - Added Message class from AreaShop to process messages: - Easily get, modify and send messages by a builder pattern - Insert message variables (allows message composition) - Insert variables based on index of the argument - Insert variables from a given ReplacementProvider - Split the parser, internal representation and generators into seperate classes: - The internal representation (InteractiveMessage) only encodes the structure and formatting of a message, knowing nothing about how it got build and nothing about sending it - The parser takes some input and builds an InteractiveMessage (this way multiple parsers can be implemented) - Generators take care of producing something that can be sent by reading an InteractiveMessage (ConsoleGenerator and TellrawGenerator are available) - Should make it easier to setup unit testing (implement equals() of InteractiveMessage and after that test parsers and generators separately) - Refactor and improve performance
This commit is contained in:
1 parent
892b7723dc
commit
16bb949691
22 files changed
+2064
-827
No files matched your search
@@ -1,5 +0,0 @@
|
||||
/bin
|
||||
|
||||
.metadata
|
||||
.classpath
|
||||
.project
|
||||
@@ -1,26 +0,0 @@
|
||||
|
||||
public class ConverterTest {
|
||||
|
||||
public static void main(String[] args) {
|
||||
String in = "";
|
||||
|
||||
in += "Hey there![break]" + '\n';
|
||||
in += "Welcome to FancyMessageFormat! :)" + '\n';
|
||||
|
||||
String out = FancyMessageFormatConverter.convertToJSON(in);
|
||||
System.out.println(out);
|
||||
|
||||
in = "";
|
||||
in += "We are [underline]able[/underline][break]" + '\n';
|
||||
in += "To do some [red][b]really[/b][white] " + '\n';
|
||||
in += "[blue][i]CrAZY[/i] " + '\n';
|
||||
in += " hover: Yiiih[yellow]aaaaa!" + '\n';
|
||||
in += " hover: [blue]Aiaiaiai[green]aiaiaiai!" + '\n';
|
||||
in += "stuff with your text - " + '\n';
|
||||
in += "while keeping it quite readable at the same time. [red][b]Awesome,[/b][blue] isn't it?" + '\n';
|
||||
|
||||
out = FancyMessageFormatConverter.convertToJSON(in);
|
||||
System.out.println(out);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,782 +0,0 @@
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedList;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* FancyMessageFormat converter, a library that enables to convert
|
||||
* messages in the FancyMessageFormat to Minecraft's bulky tellraw
|
||||
* format.
|
||||
*
|
||||
* @author NLThijs48
|
||||
* @author Tobias aka Phoenix | http://www.phoenix-iv.de
|
||||
*/
|
||||
public class FancyMessageFormatConverter {
|
||||
|
||||
private static final char TAG_BEFORE = '[';
|
||||
private static final char TAG_AFTER = ']';
|
||||
private static final char END_TAG_INDICATOR = '/';
|
||||
|
||||
/** The special character that prefixes all basic chat formatting codes. */
|
||||
private static final char SIMPLE_FORMAT_PREFIX_CHAR = '\u00A7';
|
||||
|
||||
/** Resets all previous chat colors or formats. */
|
||||
private static final char SIMPLE_FORMAT_RESET_CHAR = 'r';
|
||||
|
||||
/** Lookup table for all continuous tags (marked by []) */
|
||||
private static final HashMap<String, Tag> BRACKET_TAG_LIST = new HashMap<String, Tag>();
|
||||
|
||||
/** Lookup table for all interactive tags */
|
||||
private static final HashMap<String, Tag> INTERACTIVE_TAG_LIST = new HashMap<String, Tag>();
|
||||
|
||||
static {
|
||||
// Enlist all possible tags
|
||||
// (They go into a HashMap for lookup purposes)
|
||||
cacheTags(BRACKET_TAG_LIST, Color.class);
|
||||
cacheTags(BRACKET_TAG_LIST, FormatType.class);
|
||||
cacheTags(BRACKET_TAG_LIST, FormatCloseTag.class);
|
||||
cacheTags(BRACKET_TAG_LIST, ControlTag.class);
|
||||
// Interactive tags
|
||||
cacheTags(INTERACTIVE_TAG_LIST, ClickType.class);
|
||||
cacheTags(INTERACTIVE_TAG_LIST, HoverType.class);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Puts all constants in the given Tag class into the given lookup table.
|
||||
*/
|
||||
private static <T extends Tag> void cacheTags(HashMap<String, Tag> tagList, Class<T> tags) {
|
||||
for (Tag tag : tags.getEnumConstants()) {
|
||||
for(String key : tag.getTags()) {
|
||||
tagList.put(key, tag);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// ------------------------------------------------------------------------------------------
|
||||
// ------------------------------- Public / Interface -------------------------------
|
||||
// ------------------------------------------------------------------------------------------
|
||||
|
||||
|
||||
/**
|
||||
* Parses the given FancyMessageFormat message to a JSON array that can be
|
||||
* used with the tellraw command and the like.
|
||||
*/
|
||||
public static String convertToJSON(final String message) {
|
||||
return convertToJSON(Arrays.asList(message));
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the given FancyMessageFormat message to a JSON array that can be
|
||||
* used with the tellraw command and the like.
|
||||
*
|
||||
* @param inputLines Input message split at line breaks.
|
||||
*/
|
||||
public static String convertToJSON(final Iterable<String> inputLines) {
|
||||
ArrayList<String> lines = cleanInputString(inputLines);
|
||||
|
||||
LinkedList<InteractiveMessagePart> message = parse(lines);
|
||||
StringBuilder sb = new StringBuilder();
|
||||
if (message.size() == 1) {
|
||||
sb.append(message.getFirst().toJSON());
|
||||
} else {
|
||||
sb.append("{text=\"\",extra:[");
|
||||
for (InteractiveMessagePart messagePart : message) {
|
||||
sb.append(messagePart.toJSON());
|
||||
sb.append(',');
|
||||
}
|
||||
sb.deleteCharAt(sb.length() - 1);
|
||||
sb.append("]}");
|
||||
}
|
||||
|
||||
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Parses the given FancyMessageFormat message to a String containing control characters
|
||||
* for formatting that can be used for console outputs, but also for normal player
|
||||
* messages.
|
||||
* <p>
|
||||
* The returned message will only contain colors, bold, italic, underlining and 'magic'
|
||||
* characters. Hovers and other advanced tellraw tags will be skipped.
|
||||
*
|
||||
* @param inputLines Input message split at line breaks.
|
||||
*/
|
||||
public static String convertToConsole(final String message) {
|
||||
return convertToConsole(Arrays.asList(message));
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the given FancyMessageFormat message to a String containing control characters
|
||||
* for formatting that can be used for console outputs, but also for normal player
|
||||
* messages.
|
||||
* <p>
|
||||
* The returned message will only contain colors, bold, italic, underlining and 'magic'
|
||||
* characters. Hovers and other advanced tellraw tags will be skipped.
|
||||
*/
|
||||
public static String convertToConsole(final Iterable<String> inputLines) {
|
||||
ArrayList<String> lines = cleanInputString(inputLines);
|
||||
StringBuilder result = new StringBuilder();
|
||||
|
||||
Color currentColor = Color.WHITE;
|
||||
Set<FormatType> currentFormatting = new HashSet<FormatType>();
|
||||
boolean formatReset = false;
|
||||
|
||||
for (String line : lines) {
|
||||
if (isTaggedInteractive(line)) continue; // Skip. Ignore advanced formatting.
|
||||
|
||||
while (!line.isEmpty()) {
|
||||
TaggedContent tagged = getNextTag(line, true);
|
||||
String text = line;
|
||||
Tag tag = null;
|
||||
if (tagged != null) {
|
||||
text = tagged.precedingContent;
|
||||
tag = tagged.tag;
|
||||
line = tagged.subsequentContent;
|
||||
} else {
|
||||
line = "";
|
||||
}
|
||||
if (!text.isEmpty() && formatReset) {
|
||||
// Reset all formatting
|
||||
result.append(SIMPLE_FORMAT_PREFIX_CHAR);
|
||||
result.append(SIMPLE_FORMAT_RESET_CHAR);
|
||||
// Re-apply active formats
|
||||
if (currentColor != Color.WHITE) {
|
||||
result.append(SIMPLE_FORMAT_PREFIX_CHAR);
|
||||
result.append(currentColor.getNativeFormattingCode());
|
||||
}
|
||||
for(FormatType format : currentFormatting) {
|
||||
result.append(SIMPLE_FORMAT_PREFIX_CHAR);
|
||||
result.append(format.getNativeFormattingCode());
|
||||
}
|
||||
formatReset = false;
|
||||
}
|
||||
result.append(text);
|
||||
if (tag != null) {
|
||||
if (tag == ControlTag.BREAK) {
|
||||
result.append('\n');
|
||||
result.append(SIMPLE_FORMAT_PREFIX_CHAR);
|
||||
result.append(SIMPLE_FORMAT_RESET_CHAR);
|
||||
currentColor = Color.WHITE;
|
||||
currentFormatting.clear();
|
||||
formatReset = false;
|
||||
line = "";
|
||||
} else if (tag.getClass() == Color.class) {
|
||||
Color color = (Color) tag;
|
||||
result.append(SIMPLE_FORMAT_PREFIX_CHAR);
|
||||
result.append(color.getNativeFormattingCode());
|
||||
currentColor = color;
|
||||
} else if (tag.getClass() == FormatType.class) {
|
||||
FormatType format = (FormatType) tag;
|
||||
result.append(SIMPLE_FORMAT_PREFIX_CHAR);
|
||||
result.append(format.getNativeFormattingCode());
|
||||
currentFormatting.add(format);
|
||||
} else if (tag.getClass() == FormatCloseTag.class) {
|
||||
FormatCloseTag format = (FormatCloseTag) tag;
|
||||
currentFormatting.remove(format.closes);
|
||||
formatReset = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result.toString();
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// ------------------------------------------------------------------------------------------
|
||||
// ------------------------------- Private functions -------------------------------
|
||||
// ------------------------------------------------------------------------------------------
|
||||
|
||||
|
||||
/**
|
||||
* <ul>
|
||||
* <li>Splits lines at line breaks (creating a new line in the Array).
|
||||
* <li>Removes empty lines at the beginning.
|
||||
* <li>Removes lines with properties in front of the first text-line.
|
||||
* </ul>
|
||||
*/
|
||||
private static ArrayList<String> cleanInputString(Iterable<String> inputLines) {
|
||||
// Split lines at line breaks
|
||||
// In the end we will have a list with one line per element
|
||||
ArrayList<String> lines = new ArrayList<>();
|
||||
for (String line : inputLines) {
|
||||
lines.addAll(Arrays.asList(line.split("\\r?\\n")));
|
||||
}
|
||||
|
||||
// Remove any special lines at the start (a real text line should be first)
|
||||
while (!lines.isEmpty() && isTaggedInteractive(lines.get(0))) {
|
||||
lines.remove(0);
|
||||
}
|
||||
|
||||
return lines;
|
||||
}
|
||||
|
||||
|
||||
private static LinkedList<InteractiveMessagePart> parse(final Iterable<String> inputLines) {
|
||||
LinkedList<InteractiveMessagePart> message = new LinkedList<InteractiveMessagePart>();
|
||||
|
||||
Color currentColor = Color.WHITE;
|
||||
Set<FormatType> currentFormatting = new HashSet<FormatType>();
|
||||
|
||||
lineLoop: for (String line : inputLines) {
|
||||
InteractiveMessagePart messagePart;
|
||||
TaggedContent interactiveTag = getInteractiveTag(line);
|
||||
boolean isTextLine = interactiveTag == null;
|
||||
boolean isHoverLine = false;
|
||||
|
||||
if (isTextLine) {
|
||||
messagePart = new InteractiveMessagePart();
|
||||
message.add(messagePart);
|
||||
}
|
||||
else /* if Interactive formatting */ {
|
||||
messagePart = message.getLast();
|
||||
Tag tag = interactiveTag.tag;
|
||||
if (tag instanceof ClickType) {
|
||||
messagePart.clickType = (ClickType) interactiveTag.tag;
|
||||
messagePart.clickContent = interactiveTag.subsequentContent;
|
||||
} else if (tag instanceof HoverType) {
|
||||
line = interactiveTag.subsequentContent;
|
||||
isHoverLine = true;
|
||||
if (messagePart.hoverType != tag) {
|
||||
// Hover type changed
|
||||
messagePart.hoverContent = new LinkedList<TextMessagePart>();
|
||||
messagePart.hoverType = (HoverType) tag;
|
||||
}
|
||||
// Add hover content below
|
||||
}
|
||||
}
|
||||
|
||||
if (isTextLine || isHoverLine) {
|
||||
// Parse inline tags
|
||||
|
||||
Color currentLineColor = currentColor;
|
||||
Set<FormatType> currentLineFormatting = currentFormatting;
|
||||
LinkedList<TextMessagePart> targetList = messagePart.content;
|
||||
boolean parseBreak = true;
|
||||
if (isHoverLine) {
|
||||
// Reset - use own
|
||||
currentLineColor = Color.WHITE;
|
||||
currentLineFormatting = new HashSet<FormatType>();
|
||||
targetList = messagePart.hoverContent;
|
||||
parseBreak = false;
|
||||
|
||||
// Add line break after previous hover line
|
||||
if (!targetList.isEmpty()) {
|
||||
targetList.getLast().text += '\n';
|
||||
}
|
||||
}
|
||||
|
||||
// Split into pieces at places where formatting changes
|
||||
while (!line.isEmpty()) {
|
||||
String textToAdd = null;
|
||||
TaggedContent nextTag = getNextTag(line, parseBreak);
|
||||
boolean tagged = nextTag != null;
|
||||
|
||||
if (!tagged) {
|
||||
textToAdd = line;
|
||||
line = "";
|
||||
} else {
|
||||
textToAdd = nextTag.precedingContent;
|
||||
line = nextTag.subsequentContent;
|
||||
}
|
||||
|
||||
if (!textToAdd.isEmpty()) {
|
||||
// Add a text part with the correct formatting
|
||||
TextMessagePart part = new TextMessagePart();
|
||||
part.text = textToAdd;
|
||||
part.formatTypes = new HashSet<FormatType>(currentLineFormatting);
|
||||
part.color = currentLineColor;
|
||||
targetList.add(part);
|
||||
}
|
||||
|
||||
// Handle the change in formatting if a Tag has been detected (this needs to be after creating the InteractiveMessagePart)
|
||||
if (tagged) {
|
||||
// Handle the formatting tag
|
||||
Tag tag = nextTag.tag;
|
||||
if (tag instanceof Color) {
|
||||
currentLineColor = (Color) tag;
|
||||
} else if (tag instanceof FormatType) {
|
||||
currentLineFormatting.add((FormatType) tag);
|
||||
} else if (tag instanceof FormatCloseTag) {
|
||||
currentLineFormatting.remove(((FormatCloseTag) tag).closes);
|
||||
} else if (tag == ControlTag.BREAK) {
|
||||
targetList.getLast().text += '\n';
|
||||
continue lineLoop;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!isHoverLine) {
|
||||
// Adapt global attributes
|
||||
currentColor = currentLineColor;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return message;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Searches and returns the first continuous tag found in the given String.
|
||||
* @return The tag (plus its preceding and subsequent content) if found.
|
||||
* Null if nothing is found.
|
||||
*/
|
||||
private static TaggedContent getNextTag(String line, boolean parseBreak) {
|
||||
for (int startIndex = 0; startIndex < line.length(); startIndex++) {
|
||||
int start = line.indexOf(TAG_BEFORE, startIndex);
|
||||
if (start != -1) {
|
||||
int end = line.indexOf(TAG_AFTER, start);
|
||||
if (end != -1) {
|
||||
String inBetween = line.substring(start+1, end).toLowerCase();
|
||||
if (BRACKET_TAG_LIST.containsKey(inBetween)) {
|
||||
Tag tag = BRACKET_TAG_LIST.get(inBetween);
|
||||
if (tag == ControlTag.ESCAPE) {
|
||||
// Ignore next char
|
||||
line = line.substring(0, start) + line.substring(end + 1);
|
||||
startIndex = start;
|
||||
} else if (!parseBreak && tag == ControlTag.ESCAPE) {
|
||||
// Ignore break
|
||||
startIndex = end + 1;
|
||||
} else {
|
||||
String previousContent = line.substring(0, start);
|
||||
String subsequentContent = line.substring(end + 1);
|
||||
return new TaggedContent(previousContent, tag, subsequentContent);
|
||||
}
|
||||
} else {
|
||||
startIndex = start;
|
||||
}
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* If the given line defines an interactive property (e.g. "hover: myText")
|
||||
* the tag / property will be returned. Otherwise null is returned.
|
||||
*/
|
||||
private static TaggedContent getInteractiveTag(String line) {
|
||||
for (int index = 0; index < line.length(); index++) {
|
||||
char c = line.charAt(index);
|
||||
if (c == ' ' || c == '\t') {
|
||||
// Ignore (Skip spacing)
|
||||
} else {
|
||||
int end = line.indexOf(": ", index);
|
||||
if (end != -1) {
|
||||
String inBetween = line.substring(index, end).toLowerCase();
|
||||
if (INTERACTIVE_TAG_LIST.containsKey(inBetween)) {
|
||||
Tag tag = INTERACTIVE_TAG_LIST.get(inBetween);
|
||||
String subsequentContent = line.substring(end + 2);
|
||||
return new TaggedContent(null, tag, subsequentContent);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
private static boolean isTaggedInteractive(String line) {
|
||||
return getInteractiveTag(line) != null;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Produce a string in double quotes with backslash sequences in all the
|
||||
* right places.
|
||||
* @param string A String
|
||||
* @return A String correctly formatted for insertion in a JSON text.
|
||||
*/
|
||||
/*
|
||||
* Copyright (c) 2002 JSON.org
|
||||
* Licensed under the Apache License, Version 2.0
|
||||
*/
|
||||
private static String quoteStringJson(String string) {
|
||||
if (string == null || string.length() == 0) {
|
||||
return "\"\"";
|
||||
}
|
||||
|
||||
char c = 0;
|
||||
int i;
|
||||
int len = string.length();
|
||||
StringBuilder sb = new StringBuilder(len + 4);
|
||||
String t;
|
||||
|
||||
sb.append('"');
|
||||
for (i = 0; i < len; i += 1) {
|
||||
c = string.charAt(i);
|
||||
switch (c) {
|
||||
case '\\':
|
||||
case '"':
|
||||
sb.append('\\');
|
||||
sb.append(c);
|
||||
break;
|
||||
case '/':
|
||||
sb.append('\\');
|
||||
sb.append(c);
|
||||
break;
|
||||
case '\b':
|
||||
sb.append("\\b");
|
||||
break;
|
||||
case '\t':
|
||||
sb.append("\\t");
|
||||
break;
|
||||
case '\n':
|
||||
sb.append("\\n");
|
||||
break;
|
||||
case '\f':
|
||||
sb.append("\\f");
|
||||
break;
|
||||
case '\r':
|
||||
sb.append("\\r");
|
||||
break;
|
||||
default:
|
||||
if (c < ' ') {
|
||||
t = "000" + Integer.toHexString(c);
|
||||
sb.append("\\u" + t.substring(t.length() - 4));
|
||||
} else {
|
||||
sb.append(c);
|
||||
}
|
||||
}
|
||||
}
|
||||
sb.append('"');
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// ------------------------------------------------------------------------------------------
|
||||
// ------------------------------- Helper classes -------------------------------
|
||||
// ------------------------------------------------------------------------------------------
|
||||
|
||||
|
||||
private static class TaggedContent {
|
||||
final String precedingContent;
|
||||
final Tag tag;
|
||||
final String subsequentContent;
|
||||
|
||||
public TaggedContent(String pre, Tag tag, String sub) {
|
||||
this.precedingContent = pre;
|
||||
this.tag = tag;
|
||||
this.subsequentContent = sub;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Holds a string with basic (non-interactive) formatting.
|
||||
*/
|
||||
private static class TextMessagePart {
|
||||
String text = "";
|
||||
Color color = Color.WHITE;
|
||||
Set<FormatType> formatTypes = new HashSet<FormatType>();
|
||||
|
||||
|
||||
String toJSON() {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append('{');
|
||||
sb.append("text:").append(quoteStringJson(text));
|
||||
if (color != Color.WHITE) {
|
||||
sb.append(",color:").append(color.jsonValue);
|
||||
}
|
||||
for (FormatType formatting : formatTypes) {
|
||||
sb.append(',');
|
||||
sb.append(formatting.jsonKey).append(':');
|
||||
sb.append("true");
|
||||
}
|
||||
sb.append('}');
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
boolean hasFormatting() {
|
||||
return !(color == Color.WHITE && formatTypes.isEmpty());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Holds a string with interactive formatting.
|
||||
*/
|
||||
private static class InteractiveMessagePart {
|
||||
|
||||
LinkedList<TextMessagePart> content = new LinkedList<TextMessagePart>();
|
||||
|
||||
// Click
|
||||
ClickType clickType = null;
|
||||
String clickContent = "";
|
||||
|
||||
// Hover
|
||||
HoverType hoverType = null;
|
||||
LinkedList<TextMessagePart> hoverContent = null;
|
||||
|
||||
|
||||
String toJSON() {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
if (content.size() == 1) {
|
||||
// Add attributes to TextMessagePart object
|
||||
sb.append(content.getFirst().toJSON());
|
||||
sb.deleteCharAt(sb.length() - 1);
|
||||
} else {
|
||||
sb.append('{');
|
||||
sb.append("text=\"\",extra:[");
|
||||
for (TextMessagePart textPart : content) {
|
||||
sb.append(textPart.toJSON());
|
||||
sb.append(',');
|
||||
}
|
||||
sb.deleteCharAt(sb.length() - 1);
|
||||
sb.append(']');
|
||||
}
|
||||
if (clickType != null) {
|
||||
sb.append(',');
|
||||
sb.append("clickEvent:{");
|
||||
sb.append("action:").append(clickType.getJsonKey()).append(',');
|
||||
sb.append("value:").append(quoteStringJson(clickContent));
|
||||
sb.append('}');
|
||||
}
|
||||
if (hoverType != null) {
|
||||
sb.append(',');
|
||||
sb.append("hoverEvent:{");
|
||||
sb.append("action:").append(hoverType.getJsonKey()).append(',');
|
||||
sb.append("value:");
|
||||
if (hoverContent.size() == 1) {
|
||||
TextMessagePart hoverPart = hoverContent.getFirst();
|
||||
if (hoverPart.hasFormatting()) {
|
||||
sb.append(hoverPart.toJSON());
|
||||
} else {
|
||||
sb.append(quoteStringJson(hoverPart.text));
|
||||
}
|
||||
} else {
|
||||
sb.append('[');
|
||||
for (TextMessagePart hoverPart : hoverContent) {
|
||||
sb.append(hoverPart.toJSON());
|
||||
sb.append(',');
|
||||
}
|
||||
sb.deleteCharAt(sb.length() - 1);
|
||||
sb.append(']');
|
||||
}
|
||||
sb.append('}');
|
||||
}
|
||||
sb.append('}');
|
||||
return sb.toString();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
// --------------------------------------- Tags ---------------------------------------
|
||||
|
||||
|
||||
interface Tag {
|
||||
/** Tag text(s) used in the FancyMessageFormat (The text between '[' and ']') */
|
||||
String[] getTags();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Indicates formatting that is applied until explicitly stopped.
|
||||
* Can also be used in simple Minecraft messages (Non-JSON).
|
||||
*/
|
||||
interface ContinuousTag extends Tag {
|
||||
/**
|
||||
* The character that defines upcoming formatting in a native (non-JSON) Minecraft message.
|
||||
*/
|
||||
char getNativeFormattingCode();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Indicates formatting that allows cursor interaction. Requires the
|
||||
* Minecraft JSON / tellraw format.
|
||||
*/
|
||||
interface InteractiveMessageTag extends Tag {
|
||||
String getJsonKey();
|
||||
}
|
||||
|
||||
|
||||
static enum Color implements ContinuousTag {
|
||||
WHITE('f'),
|
||||
BLACK('0'),
|
||||
BLUE('9'),
|
||||
DARK_BLUE('1'),
|
||||
GREEN('a'),
|
||||
DARK_GREEN('2'),
|
||||
AQUA('b'),
|
||||
DARK_AQUA('3'),
|
||||
RED('c'),
|
||||
DARK_RED('4'),
|
||||
LIGHT_PURPLE('d'),
|
||||
DARK_PURPLE('5'),
|
||||
YELLOW('e'),
|
||||
GOLD('6'),
|
||||
GRAY('7'),
|
||||
DARK_GRAY('8');
|
||||
|
||||
final char bytecode;
|
||||
final String jsonValue;
|
||||
final String[] tags;
|
||||
|
||||
private Color(char bytecode) {
|
||||
this.bytecode = bytecode;
|
||||
this.jsonValue = this.name().toLowerCase();
|
||||
this.tags = new String[] {this.name().toLowerCase()};
|
||||
}
|
||||
|
||||
@Override
|
||||
public String[] getTags() {
|
||||
return tags;
|
||||
}
|
||||
|
||||
@Override
|
||||
public char getNativeFormattingCode() {
|
||||
return bytecode;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
static enum FormatType implements ContinuousTag {
|
||||
BOLD('l', "bold", "b", "bold"),
|
||||
ITALIC('o', "italic", "i", "italic"),
|
||||
UNDERLINE('n', "underlined", "u", "underline"),
|
||||
STRIKETHROUGH('s', "strikethrough", "s", "strikethrough"),
|
||||
OBFUSCATE('k', "obfuscated", "obfuscate");
|
||||
|
||||
final char bytecode;
|
||||
final String jsonKey;
|
||||
final String[] tags;
|
||||
|
||||
FormatType(char bytecode, String jsonKey, String... tags) {
|
||||
this.bytecode = bytecode;
|
||||
this.jsonKey = jsonKey;
|
||||
this.tags = tags;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String[] getTags() {
|
||||
return tags;
|
||||
}
|
||||
|
||||
@Override
|
||||
public char getNativeFormattingCode() {
|
||||
return bytecode;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
static enum FormatCloseTag implements Tag {
|
||||
BOLD_END(FormatType.BOLD),
|
||||
ITALIC_END(FormatType.ITALIC),
|
||||
UNDERLINE_END(FormatType.UNDERLINE),
|
||||
STRIKETHROUGH_END(FormatType.STRIKETHROUGH),
|
||||
OBFUSCATE_END(FormatType.OBFUSCATE);
|
||||
|
||||
/** Formatting that is stopped at this point */
|
||||
final FormatType closes;
|
||||
private final String[] tags;
|
||||
|
||||
private FormatCloseTag(FormatType openingTag) {
|
||||
this.closes = openingTag;
|
||||
|
||||
// Auto-generate close tags
|
||||
tags = new String[closes.tags.length];
|
||||
for (int i = 0; i < tags.length; i++) {
|
||||
tags[i] = END_TAG_INDICATOR + closes.tags[i];
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String[] getTags() {
|
||||
return tags;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
static enum ControlTag implements Tag {
|
||||
BREAK("break"),
|
||||
ESCAPE("esc");
|
||||
|
||||
private final String[] tags;
|
||||
|
||||
private ControlTag(String... tags) {
|
||||
this.tags = tags;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String[] getTags() {
|
||||
return tags;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Types of clicking
|
||||
*/
|
||||
static enum ClickType implements InteractiveMessageTag {
|
||||
LINK("open_url", "link"),
|
||||
COMMAND("run_command", "command"),
|
||||
SUGGEST("suggest_command", "suggest");
|
||||
|
||||
private final String jsonKey;
|
||||
private final String[] tags;
|
||||
|
||||
private ClickType(String jsonKey, String... tags) {
|
||||
this.jsonKey = jsonKey;
|
||||
this.tags = tags;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String[] getTags() {
|
||||
return tags;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getJsonKey() {
|
||||
return jsonKey;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
static enum HoverType implements InteractiveMessageTag {
|
||||
HOVER;
|
||||
|
||||
@Override
|
||||
public String[] getTags() {
|
||||
return new String[] {"hover"};
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getJsonKey() {
|
||||
return "show_text";
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,8 +1,12 @@
|
||||
FancyMessageFormat
|
||||
==================
|
||||
InteractiveMessaging
|
||||
====================
|
||||
|
||||
A format for text in config files to make use of the fancy Minecraft chat features like click, hover, suggest, etc.
|
||||
|
||||
## Authors
|
||||
* **Thijs aka NLThijs48:** Initial code structure, refactoring and getting it production ready
|
||||
* **Tobias aka Phoenix:** Initial code structure and initial parser
|
||||
|
||||
## Requirements
|
||||
* Support for click effects, when a player clicks on text it will execute something
|
||||
* Execute a command (will always be executed by the player itself)
|
||||
|
||||
@@ -2,28 +2,28 @@
|
||||
#### Colors & Formatting:
|
||||
* Colors and formatting normally go on until changed/removed, but they reset on new lines
|
||||
* Color tags:
|
||||
* [white], [black], [yellow], [gold], [aqua], [dark_aqua], [blue], [dark_blue], [light_purple], [dark_purple], [red], [dark_red], [green], [dark_green], [gray], [dark_gray]
|
||||
* `[white]`, `[black]`, `[yellow]`, `[gold]`, `[aqua]`, `[dark_aqua]`, `[blue]`, `[dark_blue]`, `[light_purple]`, `[dark_purple]`, `[red]`, `[dark_red]`, `[green]`, `[dark_green]`, `[gray]`, `[dark_gray]`
|
||||
* When you use a color it will override the last one
|
||||
* Formatting tags:
|
||||
* [bold], [italic], [underlined], [strikethrough], [obfuscated]
|
||||
* `[bold]`, `[italic]`, `[underlined]`, `[strikethrough]`, `[obfuscated]`
|
||||
* When you use a formatting tag it will enable this formatting
|
||||
* When you use a close tag of a format it will disable that formatting
|
||||
* The color and formatting inside a hover is seperate (start with nothing, resets each line)
|
||||
* Tags are not case sensitive
|
||||
* The following tags are equivalents:
|
||||
* [strikethrough] = [s]
|
||||
* [bold] = [b]
|
||||
* [italic] = [i]
|
||||
* [underline] = [u]
|
||||
* `[strikethrough]` = `[s]`
|
||||
* `[bold]` = `[b]`
|
||||
* `[italic]` = `[i]`
|
||||
* `[underline]` = `[u]`
|
||||
|
||||
#### Breaks (newlines):
|
||||
* The break symbol is: [break]
|
||||
* Using it will cause the text to wrap to the next line in chat, so if [break][break] is used it will leave an empty line
|
||||
* The break symbol is: `[break]`
|
||||
* Using it will cause the text to wrap to the next line in chat, so if `[break][break]` is used it will leave an empty line
|
||||
* The end of a list element (string) does NOT automatically result in a break in the chat
|
||||
* After using [break] there may not be more text in this line, from there only more [break] tags are allowed (more text can go in a new list entry)
|
||||
* After using `[break]` there may not be more text in this line, from there only more `[break]` tags are allowed (more text can go in a new list entry)
|
||||
|
||||
#### Special effect tags:
|
||||
* Tags are for adding special effects to a part of the text, used as the following: " link: http://google.com"
|
||||
* Tags are for adding special effects to a part of the text, used as the following: ` link: http://google.com`
|
||||
* Supported tags:
|
||||
* Link: If clicked it will cause a “Do you want to visit this website?” popup
|
||||
* Hover: If hovered over with the mouse it will give information in a little box
|
||||
@@ -35,8 +35,8 @@
|
||||
* Tags are not case sensitive
|
||||
|
||||
#### Escaping:
|
||||
* [esc] can be used to escape 1 character, so if you want to have "link: example.com" as text you need a [esc] in front of this. Then the first character will be escaped and since "ink: " is not a valid tag anymore it will be shown as text instead of being used as link on the previous text part.
|
||||
* Placing "[esc][esc]" in the text will result in the text "[esc]" being printed, the first escape will be handled as such, this will escape [ and therefore the text behind this will be "esc]" and this is just text
|
||||
* `[esc]` can be used to escape 1 character, so if you want to have `link: example.com` as text you need a `[esc]` in front of this. Then the first character will be escaped and since `ink: ` is not a valid tag anymore it will be shown as text instead of being used as link on the previous text part.
|
||||
* Placing `[esc][esc]` in the text will result in the text `[esc]` being printed, the first escape will be handled as such, this will escape `[` and therefore the text behind this will be `esc]` and this is just text
|
||||
* Also works for variables
|
||||
|
||||
## Guidelines
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<groupId>me.wiefferink</groupId>
|
||||
<artifactId>interactivemessenger</artifactId>
|
||||
<version>1.0</version>
|
||||
<name>InteractiveMessenger</name>
|
||||
|
||||
<properties>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
<maven.compiler.source>1.7</maven.compiler.source>
|
||||
<maven.compiler.target>1.7</maven.compiler.target>
|
||||
</properties>
|
||||
|
||||
<repositories>
|
||||
<repository>
|
||||
<id>spigot-repo</id>
|
||||
<url>https://hub.spigotmc.org/nexus/content/repositories/snapshots/</url>
|
||||
</repository>
|
||||
</repositories>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.bukkit</groupId>
|
||||
<artifactId>bukkit</artifactId>
|
||||
<version>1.8-R0.1-SNAPSHOT</version>
|
||||
<type>jar</type>
|
||||
<scope>provided</scope>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<directory>target</directory>
|
||||
<finalName>${name}</finalName>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
<version>3.5.1</version>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
||||
@@ -0,0 +1,101 @@
|
||||
package me.wiefferink.interactivemessenger.generators;
|
||||
|
||||
import me.wiefferink.interactivemessenger.message.InteractiveMessage;
|
||||
import me.wiefferink.interactivemessenger.message.InteractiveMessagePart;
|
||||
import me.wiefferink.interactivemessenger.message.TextMessagePart;
|
||||
import me.wiefferink.interactivemessenger.message.enums.Color;
|
||||
import me.wiefferink.interactivemessenger.message.enums.Format;
|
||||
import org.bukkit.ChatColor;
|
||||
|
||||
import java.util.EnumMap;
|
||||
|
||||
public class ConsoleGenerator {
|
||||
|
||||
/**
|
||||
* Map Color to the native formatting codes used in chat (https://minecraft.gamepedia.com/Formatting_codes)
|
||||
*/
|
||||
private static final EnumMap<Color, Character> colorCode = new EnumMap<Color, Character>(Color.class) {{
|
||||
put(Color.BLACK, '0');
|
||||
put(Color.DARK_BLUE, '1');
|
||||
put(Color.DARK_GREEN, '2');
|
||||
put(Color.DARK_AQUA, '3');
|
||||
put(Color.DARK_RED, '4');
|
||||
put(Color.DARK_PURPLE, '5');
|
||||
put(Color.GOLD, '6');
|
||||
put(Color.GRAY, '7');
|
||||
put(Color.DARK_GRAY, '8');
|
||||
put(Color.BLUE, '9');
|
||||
put(Color.GREEN, 'a');
|
||||
put(Color.AQUA, 'b');
|
||||
put(Color.RED, 'c');
|
||||
put(Color.LIGHT_PURPLE, 'd');
|
||||
put(Color.YELLOW, 'e');
|
||||
put(Color.WHITE, 'f');
|
||||
}};
|
||||
|
||||
/**
|
||||
* Map Format to the native formatting codes used in chat (https://minecraft.gamepedia.com/Formatting_codes)
|
||||
*/
|
||||
private static final EnumMap<Format, Character> formatCode = new EnumMap<Format, Character>(Format.class) {{
|
||||
put(Format.BOLD, 'l');
|
||||
put(Format.ITALIC, 'o');
|
||||
put(Format.UNDERLINE, 'n');
|
||||
put(Format.STRIKETHROUGH, 's');
|
||||
put(Format.OBFUSCATE, 'k');
|
||||
}};
|
||||
|
||||
|
||||
/**
|
||||
* Parses the given message to a String containing control characters
|
||||
* for formatting that can be used for console outputs, but also for normal player
|
||||
* messages.
|
||||
* <p>
|
||||
* The returned message will only contain colors, bold, italic, underlining and 'magic'
|
||||
* characters. Hovers and other advanced tellraw enums will be skipped.
|
||||
* @param message The parsed InteractiveMessage
|
||||
* @return Plain message that can be send
|
||||
*/
|
||||
public static String generate(InteractiveMessage message) {
|
||||
StringBuilder result = new StringBuilder();
|
||||
for(InteractiveMessagePart part : message) {
|
||||
toSimpleString(part, result);
|
||||
}
|
||||
|
||||
return result.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Append the message content to the StringBuilder
|
||||
* @param sb The StringBuilder to append the message to
|
||||
*/
|
||||
private static StringBuilder toSimpleString(InteractiveMessagePart part, StringBuilder sb) {
|
||||
for(TextMessagePart textPart : part) {
|
||||
toSimpleString(textPart, sb);
|
||||
}
|
||||
|
||||
if(part.newline) {
|
||||
sb.append("\n");
|
||||
}
|
||||
return sb;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a simple colored/formatted string
|
||||
* @param sb The StringBuilder to append the result to
|
||||
* @return StringBuilder with the message appended
|
||||
*/
|
||||
private static StringBuilder toSimpleString(TextMessagePart part, StringBuilder sb) {
|
||||
// Color
|
||||
if(part.color != null) {
|
||||
sb.append(ChatColor.COLOR_CHAR).append(colorCode.get(part.color));
|
||||
}
|
||||
// Formatting
|
||||
for(Format format : part.formatting) {
|
||||
sb.append(ChatColor.COLOR_CHAR).append(formatCode.get(format));
|
||||
}
|
||||
// Text
|
||||
sb.append(part.text);
|
||||
return sb;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,235 @@
|
||||
package me.wiefferink.interactivemessenger.generators;
|
||||
|
||||
import me.wiefferink.interactivemessenger.message.InteractiveMessage;
|
||||
import me.wiefferink.interactivemessenger.message.InteractiveMessagePart;
|
||||
import me.wiefferink.interactivemessenger.message.TextMessagePart;
|
||||
import me.wiefferink.interactivemessenger.message.enums.Color;
|
||||
import me.wiefferink.interactivemessenger.message.enums.Format;
|
||||
import me.wiefferink.interactivemessenger.message.enums.Click;
|
||||
import me.wiefferink.interactivemessenger.message.enums.Hover;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.EnumMap;
|
||||
import java.util.List;
|
||||
|
||||
public class TellrawGenerator {
|
||||
|
||||
/**
|
||||
* Map Format to the JSON keys used in tellraw (https://minecraft.gamepedia.com/Commands#Raw_JSON_text)
|
||||
*/
|
||||
private static final EnumMap<Format, String> formatJsonKey = new EnumMap<Format, String>(Format.class) {{
|
||||
put(Format.BOLD, "bold");
|
||||
put(Format.ITALIC, "italic");
|
||||
put(Format.UNDERLINE, "underlined");
|
||||
put(Format.STRIKETHROUGH, "strikethrough");
|
||||
put(Format.OBFUSCATE, "obfuscated");
|
||||
}};
|
||||
|
||||
/**
|
||||
* Map Click to the JSON keys used in tellraw (https://minecraft.gamepedia.com/Commands#Raw_JSON_text)
|
||||
*/
|
||||
private static final EnumMap<Click, String> clickJsonKey = new EnumMap<Click, String>(Click.class) {{
|
||||
put(Click.COMMAND, "run_command");
|
||||
put(Click.LINK, "open_url");
|
||||
put(Click.SUGGEST, "suggest_command");
|
||||
}};
|
||||
|
||||
/**
|
||||
* Map Hover to the JSON keys used in tellraw (https://minecraft.gamepedia.com/Commands#Raw_JSON_text)
|
||||
*/
|
||||
private static final EnumMap<Hover, String> hoverJsonKey = new EnumMap<Hover, String>(Hover.class) {{
|
||||
put(Hover.HOVER, "show_text");
|
||||
}};
|
||||
|
||||
|
||||
/**
|
||||
* Parses the given message to a JSON array that can be
|
||||
* used with the tellraw command and the like.
|
||||
* @param message The parsed InteractiveMessage.
|
||||
* @return JSON string that can be send to a player (multiple means line breaks have been used)
|
||||
*/
|
||||
public static List<String> generate(InteractiveMessage message) {
|
||||
List<String> result = new ArrayList<>();
|
||||
|
||||
List<InteractiveMessagePart> combine = new ArrayList<>(); // Part that are combined to a line
|
||||
while(!message.isEmpty()) {
|
||||
InteractiveMessagePart part = message.removeFirst();
|
||||
combine.add(part);
|
||||
if(part.newline || message.isEmpty()) {
|
||||
if(combine.size() == 1) {
|
||||
StringBuilder nextLine = new StringBuilder("[");
|
||||
toJSON(combine.get(0), nextLine);
|
||||
nextLine.append("]");
|
||||
result.add(nextLine.toString());
|
||||
} else if(combine.size() > 1) {
|
||||
StringBuilder nextLine = new StringBuilder("[{\"text\":\"\",\"extra\":[");
|
||||
for(int i = 0; i < combine.size(); i++) {
|
||||
// Skip possibly last empty InteractiveMessagePart which has newline set
|
||||
if(!combine.get(i).isEmpty()) {
|
||||
if(i != 0) {
|
||||
nextLine.append(",");
|
||||
}
|
||||
toJSON(combine.get(i), nextLine);
|
||||
}
|
||||
}
|
||||
nextLine.append("]}]");
|
||||
result.add(nextLine.toString());
|
||||
}
|
||||
combine.clear();
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get a JSON component for this message part
|
||||
* @param sb The StringBuilder to append the result to
|
||||
* @return The StringBuilder where the JSON has been appended to
|
||||
*/
|
||||
private static StringBuilder toJSON(InteractiveMessagePart part, StringBuilder sb) {
|
||||
// Error case, should never happen, print something as safeguard
|
||||
if(part.size() == 0) {
|
||||
sb.append("{\"text\":\"\"}");
|
||||
return sb;
|
||||
}
|
||||
|
||||
if(part.size() == 1) {
|
||||
// Add attributes to TextMessagePart object
|
||||
toJSON(part.getFirst(), sb);
|
||||
sb.deleteCharAt(sb.length()-1);
|
||||
} else {
|
||||
sb.append('{');
|
||||
sb.append("\"text\":\"\",\"extra\":[");
|
||||
for(TextMessagePart textPart : part) {
|
||||
toJSON(textPart, sb);
|
||||
sb.append(',');
|
||||
}
|
||||
sb.deleteCharAt(sb.length()-1);
|
||||
sb.append(']');
|
||||
}
|
||||
|
||||
// Add click action
|
||||
if(part.onClick != null) {
|
||||
sb.append(',');
|
||||
sb.append("\"clickEvent\":{");
|
||||
sb.append("\"action\":\"").append(clickJsonKey.get(part.onClick)).append("\",");
|
||||
sb.append("\"value\":");
|
||||
printJsonString(part.clickContent, sb);
|
||||
sb.append('}');
|
||||
}
|
||||
|
||||
// Add hover action
|
||||
if(part.onHover != null) {
|
||||
sb.append(',');
|
||||
sb.append("\"hoverEvent\":{");
|
||||
sb.append("\"action\":\"").append(hoverJsonKey.get(part.onHover)).append("\",");
|
||||
sb.append("\"value\":");
|
||||
if(part.hoverContent.size() == 1) {
|
||||
TextMessagePart hoverPart = part.hoverContent.getFirst();
|
||||
if(hoverPart.hasFormatting()) {
|
||||
toJSON(hoverPart, sb);
|
||||
} else {
|
||||
printJsonString(hoverPart.text, sb);
|
||||
}
|
||||
} else {
|
||||
sb.append('[');
|
||||
for(TextMessagePart hoverPart : part.hoverContent) {
|
||||
toJSON(hoverPart, sb);
|
||||
sb.append(',');
|
||||
}
|
||||
sb.deleteCharAt(sb.length()-1);
|
||||
sb.append(']');
|
||||
}
|
||||
sb.append('}');
|
||||
}
|
||||
sb.append('}');
|
||||
return sb;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a JSON component for this message part
|
||||
* @param sb The StringBuilder to append the JSON result to
|
||||
* @return The StringBuilder where the result has been appended to
|
||||
*/
|
||||
private static StringBuilder toJSON(TextMessagePart part, StringBuilder sb) {
|
||||
sb.append('{');
|
||||
|
||||
// Text
|
||||
sb.append("\"text\":");
|
||||
printJsonString(part.text, sb);
|
||||
|
||||
// Color
|
||||
if(part.color != null && part.color != Color.WHITE) {
|
||||
sb.append(",\"color\":\"").append(part.color.name().toLowerCase()).append("\"");
|
||||
}
|
||||
|
||||
// Formatting
|
||||
for(Format formatting : part.formatting) {
|
||||
sb.append(",\"");
|
||||
sb.append(formatJsonKey.get(formatting)).append("\":");
|
||||
sb.append("true");
|
||||
}
|
||||
|
||||
sb.append('}');
|
||||
return sb;
|
||||
}
|
||||
|
||||
/**
|
||||
* Produce a string in double quotes with backslash sequences in all the right places.
|
||||
* @param string The string to print
|
||||
* @param sb The StringBuilder to add the quoted string to
|
||||
* @return sb
|
||||
*/
|
||||
private static StringBuilder printJsonString(String string, StringBuilder sb) {
|
||||
if(string == null || string.length() == 0) {
|
||||
sb.append("\"\"");
|
||||
return sb;
|
||||
}
|
||||
|
||||
sb.append('"');
|
||||
for(int i = 0; i < string.length(); i++) {
|
||||
char c = string.charAt(i);
|
||||
switch(c) {
|
||||
case '\\':
|
||||
case '"':
|
||||
sb.append('\\');
|
||||
sb.append(c);
|
||||
break;
|
||||
case '/':
|
||||
sb.append('\\');
|
||||
sb.append(c);
|
||||
break;
|
||||
case '\b':
|
||||
sb.append("\\b");
|
||||
break;
|
||||
case '\t':
|
||||
sb.append("\\t");
|
||||
break;
|
||||
case '\n':
|
||||
sb.append("\\n");
|
||||
break;
|
||||
case '\f':
|
||||
sb.append("\\f");
|
||||
break;
|
||||
case '\r':
|
||||
sb.append("\\r");
|
||||
break;
|
||||
default:
|
||||
if(c <= '\u001F' || (c >= '\u007F' && c <= '\u009F') || (c >= '\u2000' && c <= '\u20FF')) {
|
||||
String ss = Integer.toHexString(c);
|
||||
sb.append("\\u");
|
||||
for(int k = 0; k < 4-ss.length(); k++) {
|
||||
sb.append('0');
|
||||
}
|
||||
sb.append(ss.toUpperCase());
|
||||
} else {
|
||||
sb.append(c);
|
||||
}
|
||||
}
|
||||
}
|
||||
sb.append('"');
|
||||
return sb;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package me.wiefferink.interactivemessenger.message;
|
||||
|
||||
import java.util.LinkedList;
|
||||
|
||||
/**
|
||||
* Interactive message
|
||||
*/
|
||||
public class InteractiveMessage extends LinkedList<InteractiveMessagePart> {
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package me.wiefferink.interactivemessenger.message;
|
||||
|
||||
import me.wiefferink.interactivemessenger.message.enums.Click;
|
||||
import me.wiefferink.interactivemessenger.message.enums.Hover;
|
||||
|
||||
import java.util.LinkedList;
|
||||
|
||||
/**
|
||||
* Holds a string with interactive formatting.
|
||||
*/
|
||||
public class InteractiveMessagePart extends LinkedList<TextMessagePart> {
|
||||
|
||||
public boolean newline = false;
|
||||
|
||||
// Click
|
||||
public Click onClick = null;
|
||||
public String clickContent = "";
|
||||
|
||||
// Hover
|
||||
public Hover onHover = null;
|
||||
public LinkedList<TextMessagePart> hoverContent = null;
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "InteractiveMessagePart(textMessageParts:"+super.toString()+", onClick:"+onClick+", clickContent:"+clickContent+", onHover:"+onHover+", hoverContent:"+hoverContent+", newline:"+newline+")";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package me.wiefferink.interactivemessenger.message;
|
||||
|
||||
import me.wiefferink.interactivemessenger.message.enums.Color;
|
||||
import me.wiefferink.interactivemessenger.message.enums.Format;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* Holds a string with basic (non-interactive) formatting.
|
||||
*/
|
||||
public class TextMessagePart {
|
||||
public String text = "";
|
||||
public Color color = null;
|
||||
public Set<Format> formatting = new HashSet<>();
|
||||
|
||||
/**
|
||||
* Check if this part has formatting
|
||||
* @return true if this part has formatting, otherwise false
|
||||
*/
|
||||
public boolean hasFormatting() {
|
||||
return !(color == Color.WHITE && formatting.isEmpty());
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "TextMessagePart(text:"+text+", color:"+color+", formatting:"+formatting.toString()+")";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package me.wiefferink.interactivemessenger.message.enums;
|
||||
|
||||
public enum Click {
|
||||
LINK,
|
||||
COMMAND,
|
||||
SUGGEST
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package me.wiefferink.interactivemessenger.message.enums;
|
||||
|
||||
public enum Color {
|
||||
WHITE,
|
||||
BLACK,
|
||||
BLUE,
|
||||
DARK_BLUE,
|
||||
GREEN,
|
||||
DARK_GREEN,
|
||||
AQUA,
|
||||
DARK_AQUA,
|
||||
RED,
|
||||
DARK_RED,
|
||||
LIGHT_PURPLE,
|
||||
DARK_PURPLE,
|
||||
YELLOW,
|
||||
GOLD,
|
||||
GRAY,
|
||||
DARK_GRAY
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
package me.wiefferink.interactivemessenger.message.enums;
|
||||
|
||||
public enum Control {
|
||||
BREAK,
|
||||
RESET
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package me.wiefferink.interactivemessenger.message.enums;
|
||||
|
||||
public enum Format {
|
||||
BOLD,
|
||||
ITALIC,
|
||||
UNDERLINE,
|
||||
STRIKETHROUGH,
|
||||
OBFUSCATE
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
package me.wiefferink.interactivemessenger.message.enums;
|
||||
|
||||
// TODO support show_item, show_achievement, show_entity
|
||||
public enum Hover {
|
||||
HOVER
|
||||
}
|
||||
@@ -0,0 +1,435 @@
|
||||
package me.wiefferink.interactivemessenger.parsers;
|
||||
|
||||
import me.wiefferink.interactivemessenger.generators.ConsoleGenerator;
|
||||
import me.wiefferink.interactivemessenger.message.InteractiveMessage;
|
||||
import me.wiefferink.interactivemessenger.message.InteractiveMessagePart;
|
||||
import me.wiefferink.interactivemessenger.message.TextMessagePart;
|
||||
import me.wiefferink.interactivemessenger.message.enums.Control;
|
||||
import me.wiefferink.interactivemessenger.message.enums.Color;
|
||||
import me.wiefferink.interactivemessenger.message.enums.Format;
|
||||
import me.wiefferink.interactivemessenger.message.enums.Click;
|
||||
import me.wiefferink.interactivemessenger.message.enums.Hover;
|
||||
import org.bukkit.ChatColor;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
public class YamlParser {
|
||||
|
||||
public static final char ESCAPE_CHAR = '\\';
|
||||
public static final char SIMPLE_FORMAT_RESET_CHAR = 'r';
|
||||
public static final Pattern tagPattern = Pattern.compile("\\[[/a-zA-Z1-9]+\\]|[&"+ChatColor.COLOR_CHAR+"][1-9a-zA-Z]");
|
||||
|
||||
// Lookup table for all continuous enums ([<tag>])
|
||||
private static final HashMap<String, Object> BRACKET_TAGS = new HashMap<String, Object>() {{
|
||||
// Colors
|
||||
cacheTags(this, Color.class);
|
||||
put("grey", Color.GRAY);
|
||||
put("darkgrey", Color.DARK_GRAY);
|
||||
put("dark_grey", Color.DARK_GRAY);
|
||||
|
||||
// Formatting
|
||||
cacheTags(this, Format.class);
|
||||
for(Format tag : Format.values()) {
|
||||
put((tag.name().charAt(0)+"").toLowerCase(), tag);
|
||||
}
|
||||
put("strike", Format.STRIKETHROUGH);
|
||||
|
||||
// Control
|
||||
cacheTags(this, Control.class);
|
||||
}};
|
||||
|
||||
// Lookup table for all interactive enums (<tag>:)
|
||||
private static final HashMap<String, Object> INTERACTIVE_TAGS = new HashMap<String, Object>() {{
|
||||
cacheTags(this, Click.class);
|
||||
cacheTags(this, Hover.class);
|
||||
}};
|
||||
|
||||
// Lookup table for all native enums (&<tag> and §<tag>)
|
||||
private static final HashMap<String, Object> NATIVE_TAGS = new HashMap<String, Object>() {{
|
||||
put("0", Color.BLACK);
|
||||
put("1", Color.DARK_BLUE);
|
||||
put("2", Color.DARK_GREEN);
|
||||
put("3", Color.DARK_AQUA);
|
||||
put("4", Color.DARK_RED);
|
||||
put("5", Color.DARK_PURPLE);
|
||||
put("6", Color.GOLD);
|
||||
put("7", Color.GRAY);
|
||||
put("8", Color.DARK_GRAY);
|
||||
put("9", Color.BLUE);
|
||||
put("a", Color.GREEN);
|
||||
put("b", Color.AQUA);
|
||||
put("c", Color.RED);
|
||||
put("d", Color.LIGHT_PURPLE);
|
||||
put("e", Color.YELLOW);
|
||||
put("f", Color.WHITE);
|
||||
|
||||
put("l", Format.BOLD);
|
||||
put("m", Format.STRIKETHROUGH);
|
||||
put("o", Format.ITALIC);
|
||||
put("n", Format.UNDERLINE);
|
||||
put("k", Format.OBFUSCATE);
|
||||
|
||||
put("r", Control.RESET);
|
||||
}};
|
||||
|
||||
/**
|
||||
* Puts all values of the given Enum into the given lookup table.
|
||||
*/
|
||||
private static <T extends Enum> void cacheTags(HashMap<String, Object> tagList, Class<T> tags) {
|
||||
for(Enum tag : tags.getEnumConstants()) {
|
||||
tagList.put(tag.name().toLowerCase(), tag);
|
||||
tagList.put(tag.name().toLowerCase().replace("_", ""), tag);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse input lines into an InteractiveMessage
|
||||
* @param input The input to parse
|
||||
* @return InteractiveMessage representing the parsed input
|
||||
*/
|
||||
public static InteractiveMessage parse(List<String> input) {
|
||||
return parse(input, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse input lines into an InteractiveMessage
|
||||
* @param input The input to parse
|
||||
* @param doInteractives true to parse interactive enums (hover, click, etc.), false to skip them
|
||||
* @return InteractiveMessage representing the parsed input
|
||||
*/
|
||||
public static InteractiveMessage parse(List<String> input, boolean doInteractives) {
|
||||
InteractiveMessage message = new InteractiveMessage();
|
||||
|
||||
Color currentColor = null;
|
||||
Set<Format> currentFormatting = new HashSet<>();
|
||||
|
||||
lineLoop:
|
||||
for(String line : input) {
|
||||
InteractiveMessagePart messagePart;
|
||||
TaggedContent interactiveTag = getInteractiveTag(line);
|
||||
boolean isTextLine = interactiveTag == null;
|
||||
if(!doInteractives && !isTextLine) {
|
||||
continue;
|
||||
}
|
||||
boolean isHoverLine = false;
|
||||
|
||||
if(isTextLine) {
|
||||
messagePart = new InteractiveMessagePart();
|
||||
message.add(messagePart);
|
||||
} else /* if Interactive formatting */ {
|
||||
if(message.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
messagePart = message.getLast();
|
||||
Object tag = interactiveTag.tag;
|
||||
if(tag instanceof Click) {
|
||||
messagePart.onClick = (Click)interactiveTag.tag;
|
||||
messagePart.clickContent = interactiveTag.subsequentContent;
|
||||
} else if(tag instanceof Hover) {
|
||||
line = interactiveTag.subsequentContent;
|
||||
isHoverLine = true;
|
||||
if(messagePart.onHover != tag) {
|
||||
// Hover type changed
|
||||
messagePart.hoverContent = new LinkedList<>();
|
||||
messagePart.onHover = (Hover)tag;
|
||||
}
|
||||
// Add hover content below
|
||||
}
|
||||
}
|
||||
|
||||
if(isTextLine || isHoverLine) {
|
||||
// Parse inline enums
|
||||
Color currentLineColor = currentColor;
|
||||
Set<Format> currentLineFormatting = currentFormatting;
|
||||
LinkedList<TextMessagePart> targetList = messagePart;
|
||||
boolean parseBreak = true;
|
||||
if(isHoverLine) {
|
||||
// Reset - use own
|
||||
currentLineColor = null;
|
||||
currentLineFormatting = new HashSet<>();
|
||||
targetList = messagePart.hoverContent;
|
||||
parseBreak = false;
|
||||
|
||||
// Add line break after previous hover line
|
||||
if(!targetList.isEmpty()) {
|
||||
targetList.getLast().text += '\n';
|
||||
}
|
||||
}
|
||||
|
||||
// Split into pieces at places where formatting changes
|
||||
while(!line.isEmpty()) {
|
||||
String textToAdd;
|
||||
TaggedContent nextTag = getNextTag(line, parseBreak);
|
||||
boolean tagged = nextTag != null;
|
||||
|
||||
if(!tagged) {
|
||||
textToAdd = line;
|
||||
line = "";
|
||||
} else {
|
||||
textToAdd = nextTag.precedingContent;
|
||||
line = nextTag.subsequentContent;
|
||||
}
|
||||
|
||||
// Add a text part with the correct formatting
|
||||
if(!textToAdd.isEmpty()) {
|
||||
TextMessagePart part = new TextMessagePart();
|
||||
part.text = unescape(textToAdd);
|
||||
part.formatting = new HashSet<>(currentLineFormatting);
|
||||
part.color = currentLineColor;
|
||||
targetList.add(part);
|
||||
}
|
||||
|
||||
// Handle the change in formatting if a Tag has been detected (this needs to be after creating the InteractiveMessagePart)
|
||||
if(tagged) {
|
||||
// Handle the formatting tag
|
||||
Object tag = nextTag.tag;
|
||||
if(tag instanceof Color) {
|
||||
currentLineColor = (Color)tag;
|
||||
} else if(tag instanceof Format) {
|
||||
if(nextTag.closing) {
|
||||
currentLineFormatting.remove(tag);
|
||||
} else {
|
||||
currentLineFormatting.add((Format)tag);
|
||||
}
|
||||
} else if(tag == Control.BREAK) {
|
||||
if(isHoverLine && !targetList.isEmpty()) {
|
||||
targetList.getLast().text += "\n";
|
||||
} else {
|
||||
messagePart.newline = true;
|
||||
currentLineFormatting.clear();
|
||||
continue lineLoop;
|
||||
}
|
||||
} else if(tag == Control.RESET) {
|
||||
currentLineFormatting.clear();
|
||||
currentLineColor = Color.WHITE;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(!isHoverLine) {
|
||||
// Adapt global attributes
|
||||
currentColor = currentLineColor;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Cleanup empty parts (some parts only affected the formatting of next parts, but do not have actual content)
|
||||
Iterator<InteractiveMessagePart> it = message.iterator();
|
||||
while(it.hasNext()) {
|
||||
InteractiveMessagePart check = it.next();
|
||||
if(check.size() == 0 && !check.newline) {
|
||||
it.remove();
|
||||
}
|
||||
}
|
||||
return message;
|
||||
}
|
||||
|
||||
/**
|
||||
* Insert a message at the specified position
|
||||
* @param message The current message
|
||||
* @param insert The message to insert
|
||||
* @param line The line number to insert at
|
||||
* @param start The start of the variable to replace
|
||||
* @param end The end of the variable to replace
|
||||
*/
|
||||
public static void insertMessage(List<String> message, List<String> insert, int line, int start, int end) {
|
||||
if(insert == null || line < 0 || line >= message.size() || start < 0 || end < 0) {
|
||||
return;
|
||||
}
|
||||
String lineContent = message.remove(line);
|
||||
if(start > lineContent.length() || end > lineContent.length()) {
|
||||
message.add(line, lineContent); // Repair damage
|
||||
return;
|
||||
}
|
||||
if(isTaggedInteractive(lineContent)) {
|
||||
lineContent = lineContent.replace("", "");
|
||||
// TODO check we can get rid of the dependency on ConsoleGenerator
|
||||
message.add(line, lineContent.substring(0, start) + ConsoleGenerator.generate(YamlParser.parse(insert)) + lineContent.substring(end));
|
||||
return;
|
||||
}
|
||||
|
||||
// Find interactive lines meant for this message
|
||||
List<String> interactives = new ArrayList<>();
|
||||
while(line < message.size() && isTaggedInteractive(message.get(line))) {
|
||||
interactives.add(message.remove(line));
|
||||
}
|
||||
|
||||
// Split the line and add the parts
|
||||
int at = line;
|
||||
if(start > 0) {
|
||||
message.add(line, lineContent.substring(0, start));
|
||||
at++;
|
||||
message.addAll(at, interactives);
|
||||
at += interactives.size();
|
||||
}
|
||||
message.addAll(at, insert);
|
||||
at += insert.size();
|
||||
message.addAll(at, interactives);
|
||||
at += interactives.size();
|
||||
if(end < lineContent.length()) {
|
||||
message.add(at, lineContent.substring(end));
|
||||
at++;
|
||||
message.addAll(at, interactives);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Searches and returns the first continuous tag found in the given String.
|
||||
* @return The tag (plus its preceding and subsequent content) if found.
|
||||
* Null if nothing is found.
|
||||
*/
|
||||
private static TaggedContent getNextTag(String line, boolean parseBreak) {
|
||||
// TODO do something with parseBreak or remove
|
||||
// Tags containing letters and numbers and native format (captures too much, but will continue search)
|
||||
Matcher matcher = tagPattern.matcher(line);
|
||||
while(matcher.find()) {
|
||||
boolean closing = false;
|
||||
Object tag;
|
||||
if(matcher.group().startsWith("&") || matcher.group().startsWith(ChatColor.COLOR_CHAR+"")) {
|
||||
// Skip escaped color enums
|
||||
if(matcher.start() > 0 && line.charAt(matcher.start()-1) == ESCAPE_CHAR) {
|
||||
continue;
|
||||
}
|
||||
tag = NATIVE_TAGS.get(matcher.group().charAt(1)+"".toLowerCase());
|
||||
} else {
|
||||
String content = matcher.group().substring(1, matcher.group().length()-1).toLowerCase();
|
||||
tag = BRACKET_TAGS.get(content);
|
||||
|
||||
// Try closing tag
|
||||
if(tag == null && content.length() > 0 && content.charAt(0) == '/') {
|
||||
tag = BRACKET_TAGS.get(content.substring(1));
|
||||
closing = true;
|
||||
// Control enums cannot be closed
|
||||
if(tag instanceof Control) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Continue search if we found something like [abc] or &w that is not a tag
|
||||
if(tag != null) {
|
||||
return new TaggedContent(line.substring(0, matcher.start()), tag, line.substring(matcher.end()), closing);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* If the given line defines an interactive property (e.g. "hover: myText")
|
||||
* the tag / property will be returned. Otherwise null is returned.
|
||||
* @param line The line to try and get the interactive tag for
|
||||
* @return TaggedContent object with the interactive tag type and split or null if not found
|
||||
*/
|
||||
private static TaggedContent getInteractiveTag(String line) {
|
||||
for(int index = 0; index < line.length(); index++) {
|
||||
char c = line.charAt(index);
|
||||
if(c == ' ' || c == '\t') {
|
||||
// Ignore (Skip spacing)
|
||||
} else {
|
||||
int end = line.indexOf(": ", index);
|
||||
if(end != -1) {
|
||||
String inBetween = line.substring(index, end).toLowerCase();
|
||||
if(INTERACTIVE_TAGS.containsKey(inBetween)) {
|
||||
Object tag = INTERACTIVE_TAGS.get(inBetween);
|
||||
String subsequentContent = line.substring(end+2);
|
||||
return new TaggedContent(null, tag, subsequentContent, false);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a line is an advanced declaration like hover or command
|
||||
* @param line The line to check
|
||||
* @return true if the line is interactive, false when it is a text line
|
||||
*/
|
||||
private static boolean isTaggedInteractive(String line) {
|
||||
return getInteractiveTag(line) != null;
|
||||
}
|
||||
|
||||
/**
|
||||
* - Splits lines at line breaks (creating a new line in the Array).
|
||||
* - Removes empty lines at the beginning.
|
||||
* - Removes lines with properties in front of the first text-line.
|
||||
* @param inputLines The lines to clean
|
||||
* @return cleaned lines
|
||||
*/
|
||||
private static ArrayList<String> cleanInputString(List<String> inputLines) {
|
||||
// Split li|nes at line breaks
|
||||
// In the end we will have a list with one line per element
|
||||
ArrayList<String> lines = new ArrayList<>();
|
||||
for(String line : inputLines) {
|
||||
lines.addAll(Arrays.asList(line.split("\\r?\\n")));
|
||||
}
|
||||
|
||||
// Remove any special lines at the start (a real text line should be first)
|
||||
while(!lines.isEmpty() && isTaggedInteractive(lines.get(0))) {
|
||||
lines.remove(0);
|
||||
}
|
||||
|
||||
return lines;
|
||||
}
|
||||
|
||||
// Characters that should be escaped
|
||||
private static final List<String> toEscape = Arrays.asList(
|
||||
ESCAPE_CHAR+"", // Used for escapes, must be first
|
||||
"%", // Variables
|
||||
"]", // Formatting and control enums
|
||||
"&", // Traditional color formatting
|
||||
ChatColor.COLOR_CHAR+"" // Legacy formatting enums
|
||||
);
|
||||
|
||||
/**
|
||||
* Escape a (user provided) string for including it in a message
|
||||
* @param message The message part to escape
|
||||
* @return The escaped message part
|
||||
*/
|
||||
public static String escape(String message) {
|
||||
for(String escape : toEscape) {
|
||||
message = message.replace(escape, ESCAPE_CHAR+escape);
|
||||
}
|
||||
return message;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the escaping of control characters in a (user provided) string
|
||||
* @param message The message part to escape
|
||||
* @return The escaped message part
|
||||
*/
|
||||
public static String unescape(String message) {
|
||||
for(String escape : toEscape) {
|
||||
message = message.replace(ESCAPE_CHAR+escape, escape);
|
||||
}
|
||||
return message;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Represents a tag that has been found in a line
|
||||
*/
|
||||
private static class TaggedContent {
|
||||
final String precedingContent;
|
||||
final Object tag;
|
||||
final boolean closing;
|
||||
final String subsequentContent;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
* @param pre Content that appeared before the tag
|
||||
* @param tag The Tag that has been found (formatting or control)
|
||||
* @param sub Content found after the tag
|
||||
*/
|
||||
public TaggedContent(String pre, Object tag, String sub, boolean closing) {
|
||||
this.precedingContent = pre;
|
||||
this.tag = tag;
|
||||
this.subsequentContent = sub;
|
||||
this.closing = closing;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,735 @@
|
||||
package me.wiefferink.interactivemessenger.processing;
|
||||
|
||||
import me.wiefferink.interactivemessenger.generators.ConsoleGenerator;
|
||||
import me.wiefferink.interactivemessenger.generators.TellrawGenerator;
|
||||
import me.wiefferink.interactivemessenger.parsers.YamlParser;
|
||||
import me.wiefferink.interactivemessenger.source.MessageProvider;
|
||||
import org.apache.commons.lang.StringUtils;
|
||||
import org.apache.commons.lang.exception.ExceptionUtils;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.ChatColor;
|
||||
import org.bukkit.command.CommandSender;
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.logging.Logger;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
public class Message {
|
||||
|
||||
// CONFIGURATION
|
||||
private static boolean useInteractiveMessages = true;
|
||||
private static boolean useColorsInConsole = false;
|
||||
private static Logger logger = null;
|
||||
private static MessageProvider messageProvider = null;
|
||||
|
||||
// Define the symbols used for variables
|
||||
public static final String VARIABLESTART = "%";
|
||||
public static final String VARIABLEEND = "%";
|
||||
public static final String LANGUAGEVARIABLE = "lang:";
|
||||
public static final Pattern variablePattern = Pattern.compile(Pattern.quote(VARIABLESTART)+"[a-zA-Z]+"+Pattern.quote(VARIABLEEND));
|
||||
public static final Pattern variables = Pattern.compile(
|
||||
Pattern.quote(VARIABLESTART)+
|
||||
Pattern.quote(LANGUAGEVARIABLE)+"[a-zA-Z-]+"+ // Language key
|
||||
"(\\|(.*?\\|)+)?"+ // Optional message arguments
|
||||
Pattern.quote(VARIABLEEND)
|
||||
);
|
||||
|
||||
private static HashMap<Integer, Pattern> indexPatterns = new HashMap<>();
|
||||
|
||||
/**
|
||||
* Get a cached variable index tagPattern
|
||||
* @param index The index of the variable tagPattern
|
||||
* @return The tagPattern for the given index
|
||||
*/
|
||||
private static Pattern getIndexPattern(int index) {
|
||||
Pattern result = indexPatterns.get(index);
|
||||
if(result == null) {
|
||||
result = Pattern.compile(Pattern.quote(VARIABLESTART)+index+Pattern.quote(VARIABLEEND));
|
||||
indexPatterns.put(index, result);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
// Language variable used to insert a prefix
|
||||
public static final String CHATLANGUAGEVARIABLE = "prefix";
|
||||
// Maximum number of replacement rounds (the replaced value can have variables again)
|
||||
public static final int REPLACEMENTLIMIT = 100;
|
||||
// Limit of the client is 32767 for the complete message
|
||||
public static final int MAXIMUMJSONLENGTH = 30000;
|
||||
// If sending a fancy message does not work we disable it for this run of the server
|
||||
private static boolean fancyWorks = true;
|
||||
|
||||
// INSTANCE VARIABLES
|
||||
private List<String> message;
|
||||
private Object[] replacements;
|
||||
private String key = null;
|
||||
private boolean doLanguageReplacements = true;
|
||||
private boolean inline = false;
|
||||
|
||||
/**
|
||||
* Initialize the Message class
|
||||
* @param provider The provider to use for getting messages based on keys
|
||||
* @param theLogger The logger to use for logging warning and error messages
|
||||
*/
|
||||
public static void init(MessageProvider provider, Logger theLogger) {
|
||||
messageProvider = provider;
|
||||
logger = theLogger;
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal use only
|
||||
*/
|
||||
private Message() {
|
||||
message = new ArrayList<>();
|
||||
}
|
||||
|
||||
/**
|
||||
* Empty message object
|
||||
* @return this
|
||||
*/
|
||||
public static Message empty() {
|
||||
return new Message();
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct a message from a language key
|
||||
* Requires a MessageProvider to be set
|
||||
* @param key The key of the message to use
|
||||
* @return this
|
||||
*/
|
||||
public static Message fromKey(String key) {
|
||||
return new Message().setMessageFromKey(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct a message from a string
|
||||
* @param message The message to use
|
||||
* @return this
|
||||
*/
|
||||
public static Message fromString(String message) {
|
||||
return new Message().setMessage(message);
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct a message from a string list
|
||||
* @param message The message to use
|
||||
* @return this
|
||||
*/
|
||||
public static Message fromList(List<String> message) {
|
||||
return new Message().setMessage(message);
|
||||
}
|
||||
|
||||
/**
|
||||
* Enable or disable the use of fancy messages
|
||||
* @param enabled true to enable, false to disable
|
||||
*/
|
||||
public static void useFancyMessages(boolean enabled) {
|
||||
useInteractiveMessages = enabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* Enable or disable the use of colors when sending a message to a target that is not a Player (console, log, etcetera)
|
||||
* @param enabled true to enable, false to disable
|
||||
*/
|
||||
public static void useColorsInConsole(boolean enabled) {
|
||||
useColorsInConsole = enabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the message with all replacements done
|
||||
* @return Message as a list
|
||||
*/
|
||||
public List<String> get() {
|
||||
doReplacements();
|
||||
return message;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the message with all replacements done
|
||||
* @param limit the limit to hold to
|
||||
* @return Message as a list
|
||||
*/
|
||||
private List<String> get(Limit limit) {
|
||||
if(limit.reached()) {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
doReplacements(limit);
|
||||
return message;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the message with all replacements done
|
||||
* @return Message as a string
|
||||
*/
|
||||
public String getSingle() {
|
||||
doReplacements();
|
||||
return StringUtils.join(message, "");
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the raw message without replacing anything
|
||||
* @return The message
|
||||
*/
|
||||
public List<String> getRaw() {
|
||||
return message;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get raw message as string
|
||||
* @return The raw message
|
||||
*/
|
||||
public String getSingleRaw() {
|
||||
return StringUtils.join(message, "");
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a plain string for the message (for example for using in the console)
|
||||
* @return The message as simple string
|
||||
*/
|
||||
public String getPlain() {
|
||||
doReplacements();
|
||||
return ConsoleGenerator.generate(YamlParser.parse(message));
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the default prefix to the message
|
||||
* @param doIt true if the prefix should be added, otherwise false
|
||||
* @return this
|
||||
*/
|
||||
public Message prefix(boolean doIt) {
|
||||
if(doIt) {
|
||||
message.add(0, VARIABLESTART+LANGUAGEVARIABLE+CHATLANGUAGEVARIABLE+VARIABLEEND);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the default prefix to the message
|
||||
* @return this
|
||||
*/
|
||||
public Message prefix() {
|
||||
return prefix(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the replacements to apply to the message
|
||||
* @param replacements The replacements to apply
|
||||
* - GeneralRegion: All region replacements are applied
|
||||
* - Message: Message is inserted
|
||||
* - other: index tag is replaced, like %0%
|
||||
* @return this
|
||||
*/
|
||||
public Message replacements(Object... replacements) {
|
||||
this.replacements = replacements;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Append lines to the message
|
||||
* @param lines The lines to append
|
||||
* @return this
|
||||
*/
|
||||
public Message append(List<String> lines) {
|
||||
message.addAll(lines);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Append a message to this message
|
||||
* @param message The message to append
|
||||
* @return this
|
||||
*/
|
||||
public Message append(Message message) {
|
||||
return append(message.get());
|
||||
}
|
||||
|
||||
/**
|
||||
* Append lines to the message
|
||||
* @param line The line to append
|
||||
* @return this
|
||||
*/
|
||||
public Message append(String line) {
|
||||
message.add(line);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Turn off language replacements for this message
|
||||
* @return this
|
||||
*/
|
||||
public Message noLanguageReplacements() {
|
||||
doLanguageReplacements = false;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark this message as inline, used for insertion into other messages
|
||||
* @return this
|
||||
*/
|
||||
public Message inline() {
|
||||
inline = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Send the message to a target
|
||||
* @param target The target to send the message to (Player, CommandSender, Logger)
|
||||
* @return this
|
||||
*/
|
||||
public Message send(Object target) {
|
||||
if(message == null || message.size() == 0 || (message.size() == 1 && message.get(0).length() == 0) || target == null) {
|
||||
return this;
|
||||
}
|
||||
doReplacements();
|
||||
if(target instanceof Player) {
|
||||
boolean sendPlain = true;
|
||||
if(useInteractiveMessages && fancyWorks) {
|
||||
try {
|
||||
boolean result = true;
|
||||
List<String> jsonMessages = TellrawGenerator.generate(YamlParser.parse(message));
|
||||
for(String jsonMessage : jsonMessages) {
|
||||
if(jsonMessage.length() > MAXIMUMJSONLENGTH) {
|
||||
error("Message with key", key, "could not be send, results in a JSON string that is too big to send to the client, start of the message:", getMessageStart(this, 200));
|
||||
return this;
|
||||
}
|
||||
result &= Bukkit.dispatchCommand(Bukkit.getConsoleSender(), "tellraw "+((Player)target).getName()+" "+jsonMessage);
|
||||
}
|
||||
sendPlain = !result;
|
||||
fancyWorks = result;
|
||||
} catch(Exception e) {
|
||||
fancyWorks = false;
|
||||
error("Sending fancy message did not work, falling back to plain messages. Message key:", key, ", error:", ExceptionUtils.getStackTrace(e));
|
||||
}
|
||||
}
|
||||
if(sendPlain) { // Fancy messages disabled or broken
|
||||
((Player)target).sendMessage(ConsoleGenerator.generate(YamlParser.parse(message)));
|
||||
}
|
||||
} else {
|
||||
String plainMessage = ConsoleGenerator.generate(YamlParser.parse(message));
|
||||
// Strip colors if disabled
|
||||
if(!useColorsInConsole) {
|
||||
plainMessage = ChatColor.stripColor(plainMessage);
|
||||
}
|
||||
|
||||
// Send to the target
|
||||
if(target instanceof CommandSender) {
|
||||
((CommandSender)target).sendMessage(plainMessage);
|
||||
} else if(target instanceof Logger) {
|
||||
((Logger)target).info(plainMessage);
|
||||
} else {
|
||||
warn("Could not send message (key: "+key+") because the target ("+target.getClass().getName()+") is not recognized, message: "+plainMessage);
|
||||
}
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
|
||||
// INTERNAL METHODS
|
||||
|
||||
/**
|
||||
* Set the internal message
|
||||
* @param message The message to set
|
||||
* @return this
|
||||
*/
|
||||
private Message setMessage(List<String> message) {
|
||||
this.message = message;
|
||||
if(this.message == null) {
|
||||
this.message = new ArrayList<>();
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the internal message with a key
|
||||
* @param key The message key to get the message for
|
||||
* @return this
|
||||
*/
|
||||
private Message setMessageFromKey(String key) {
|
||||
this.key = key;
|
||||
if(messageProvider == null) {
|
||||
error("Tried to get message with key", key+", but there is no MessageProvider!");
|
||||
} else {
|
||||
this.setMessage(messageProvider.getMessage(key));
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the internal message with a string
|
||||
* @param message The message to set
|
||||
* @return this
|
||||
*/
|
||||
private Message setMessage(String message) {
|
||||
List<String> list = new ArrayList<>();
|
||||
list.add(message);
|
||||
return this.setMessage(list);
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply all replacements to the message
|
||||
* @return this
|
||||
*/
|
||||
public Message doReplacements() {
|
||||
Limit limit = new Limit(REPLACEMENTLIMIT, this);
|
||||
doReplacements(limit);
|
||||
//depthPrint(limit, "Replacing took", System.currentTimeMillis()-limit.started, "milliseconds", this);
|
||||
return this;
|
||||
}
|
||||
|
||||
private Message doReplacements(Limit limit) {
|
||||
if(limit.reached()) {
|
||||
return this;
|
||||
}
|
||||
|
||||
limit.depth++;
|
||||
|
||||
//depthPrint(limit, ">>> doReplacements:", message, limit);
|
||||
// Replace variables until they are all gone, or when the limit is reached
|
||||
try {
|
||||
List<String> outerOriginal;
|
||||
// Repeat replacements for if language replacements introduced new variables
|
||||
int fullRounds = 0;
|
||||
do {
|
||||
outerOriginal = new ArrayList<>(message);
|
||||
List<String> innerOriginal;
|
||||
limit.left--;
|
||||
if(limit.reached()) {
|
||||
break;
|
||||
}
|
||||
|
||||
// Do argument replacements
|
||||
do {
|
||||
innerOriginal = new ArrayList<>(message);
|
||||
if(limit.reached()) {
|
||||
break;
|
||||
}
|
||||
replaceArgumentVariables(limit);
|
||||
} while(!message.equals(innerOriginal));
|
||||
|
||||
// Do language replacements
|
||||
if(doLanguageReplacements) {
|
||||
do {
|
||||
innerOriginal = new ArrayList<>(message);
|
||||
if(limit.reached()) {
|
||||
break;
|
||||
}
|
||||
replaceLanguageVariables(limit);
|
||||
} while(!message.equals(innerOriginal));
|
||||
}
|
||||
|
||||
fullRounds++;
|
||||
} while(!message.equals(outerOriginal));
|
||||
|
||||
// Increase limit by one to compensate for the last round where no replacements have been done
|
||||
if(!limit.reached() && fullRounds >= 1) {
|
||||
limit.left++;
|
||||
}
|
||||
} catch(StackOverflowError e) {
|
||||
limit.left = 0;
|
||||
limit.notified = true;
|
||||
error("Too many recursive replacements for message with key: "+limit.message.key+" (probably includes itself as replacement), start of the message: "+getMessageStart(limit.message, 200));
|
||||
}
|
||||
limit.depth--;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Replace argument variables in a message
|
||||
* The arguments to apply as replacements:
|
||||
* - If it is a GeneralRegion the replacements of the region will be applied
|
||||
* - Else the parameter will replace its number surrounded with VARIABLESTART and VARIABLEEND
|
||||
*/
|
||||
private void replaceArgumentVariables(Limit limit) {
|
||||
limit.depth++;
|
||||
//depthPrint(limit, ">>> replaceArgumentVariables:", message, limit);
|
||||
if(message == null || message.size() == 0 || replacements == null || limit.reached()) {
|
||||
//depthPrint(limit, "quick return");
|
||||
limit.depth--;
|
||||
return;
|
||||
}
|
||||
|
||||
for(int i = 0; i < message.size(); i++) {
|
||||
int number = 0;
|
||||
for(Object param : replacements) {
|
||||
String line = message.get(i);
|
||||
if(param != null) {
|
||||
if(param instanceof ReplacementProvider) {
|
||||
// Find the first non-escaped named variable
|
||||
Matcher matcher = variablePattern.matcher(line);
|
||||
int startAt = 0;
|
||||
while(matcher.find()) {
|
||||
// Check for escaping
|
||||
int beforeAt = matcher.start()-1;
|
||||
if(beforeAt >= 0 && line.charAt(beforeAt) == YamlParser.ESCAPE_CHAR) {
|
||||
//depthPrint(limit, "skipping named variable:", matcher.group(), limit);
|
||||
continue;
|
||||
}
|
||||
//depthPrint(limit, "replacing named variable:", matcher.group());
|
||||
|
||||
// Insert replacement provided by the ReplacementProvider
|
||||
Object replacement = ((ReplacementProvider)param).provideReplacement(matcher.group().substring(1, matcher.group().length()-1));
|
||||
if(replacement != null) {
|
||||
String result = "";
|
||||
// Prefix
|
||||
if(matcher.start() > 0) {
|
||||
result += line.substring(0, matcher.start());
|
||||
}
|
||||
// Replacement
|
||||
String add = replacement.toString();
|
||||
result += add;
|
||||
// Suffix
|
||||
if(matcher.end() < line.length()) {
|
||||
result += line.substring(matcher.end());
|
||||
}
|
||||
|
||||
message.set(i, result);
|
||||
line = result;
|
||||
int matcherStart = matcher.start();
|
||||
matcher = variablePattern.matcher(line);
|
||||
matcher.region(matcherStart+add.length(), line.length());
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Find first non-escaped numbered variable
|
||||
Matcher matcher = getIndexPattern(number).matcher(line);
|
||||
while(matcher.find()) {
|
||||
// Check for escaping
|
||||
int beforeAt = matcher.start()-1;
|
||||
if(beforeAt >= 0 && line.charAt(beforeAt) == YamlParser.ESCAPE_CHAR) {
|
||||
//depthPrint(limit, "skipping indexed variable:", matcher.group(), limit);
|
||||
continue;
|
||||
}
|
||||
//depthPrint(limit, "replacing indexed variable:", matcher.group());
|
||||
|
||||
// Insert another Message
|
||||
if(param instanceof Message) {
|
||||
int startDiff = message.size()-i;
|
||||
//depthPrint(limit, "insert message raw:", ((Message)param).message);
|
||||
Message mParam = (Message)param;
|
||||
|
||||
// Insert inline
|
||||
if(mParam.inline) {
|
||||
message.set(i, insert(line, mParam.getSingle(), matcher.start(), matcher.end()));
|
||||
}
|
||||
|
||||
// Insert as message
|
||||
else {
|
||||
List<String> insertMessage = ((Message)param).get(limit);
|
||||
//depthPrint(limit, "insert message resolved:", ((Message)param).message);
|
||||
if(limit.reached()) {
|
||||
limit.depth--;
|
||||
return;
|
||||
}
|
||||
YamlParser.insertMessage(message, insertMessage, i, matcher.start(), matcher.end());
|
||||
// Skip to end of insert
|
||||
i = message.size()-startDiff;
|
||||
}
|
||||
}
|
||||
|
||||
// Insert a simple string
|
||||
else {
|
||||
// Insert it inline, assuming this might be user input, therefore escaping it
|
||||
//depthPrint(limit, "insert string:", param.toString());
|
||||
message.set(i, insert(line, YamlParser.escape(param.toString()), matcher.start(), matcher.end()));
|
||||
}
|
||||
break; // Maximum of one replacement
|
||||
}
|
||||
number++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
limit.depth--;
|
||||
}
|
||||
|
||||
/**
|
||||
* Insert a string into another one, replacing a part of the base
|
||||
* @param base The base string to insert into
|
||||
* @param insert The string to insert
|
||||
* @param start The start of the region to replace
|
||||
* @param end The end of the region to replace
|
||||
* @return The formatted string
|
||||
*/
|
||||
private String insert(String base, String insert, int start, int end) {
|
||||
String newMessage = "";
|
||||
if(start > 0) {
|
||||
newMessage += base.substring(0, start);
|
||||
}
|
||||
newMessage += insert;
|
||||
if(end < base.length()) {
|
||||
newMessage += base.substring(end);
|
||||
}
|
||||
return newMessage;
|
||||
}
|
||||
|
||||
/**
|
||||
* Replace all language variables in a message
|
||||
*/
|
||||
private void replaceLanguageVariables(Limit limit) {
|
||||
limit.depth++;
|
||||
//depthPrint(limit, ">>> replaceLanguageVariables:", message, limit);
|
||||
if(message == null || message.size() == 0 || limit.reached()) {
|
||||
//depthPrint(limit, "quick return");
|
||||
limit.depth--;
|
||||
return;
|
||||
}
|
||||
|
||||
for(int i = 0; i < message.size(); i++) {
|
||||
Matcher matcher = variables.matcher(message.get(i));
|
||||
while(matcher.find()) {
|
||||
// Check for escaping
|
||||
int beforeAt = matcher.start()-1;
|
||||
if(beforeAt >= 0 && message.get(i).charAt(beforeAt) == YamlParser.ESCAPE_CHAR) {
|
||||
//depthPrint(limit, "skipping variable:", matcher.group(), limit);
|
||||
continue;
|
||||
}
|
||||
//depthPrint(limit, "replacing variable:", matcher.group());
|
||||
|
||||
// Parse arguments
|
||||
String variable = matcher.group();
|
||||
String key;
|
||||
Object[] arguments = null;
|
||||
if(variable.contains("|")) {
|
||||
key = variable.substring(VARIABLESTART.length()+LANGUAGEVARIABLE.length(), variable.indexOf("|"));
|
||||
String[] stringArguments = variable.substring(variable.indexOf("|")+1, variable.length()-VARIABLEEND.length()).split("\\|");
|
||||
// Wrap arguments in Message object to prevent escaping
|
||||
arguments = new Message[stringArguments.length];
|
||||
for(int argumentIndex = 0; argumentIndex < stringArguments.length; argumentIndex++) {
|
||||
// Marks as inline to prevent spreading the language variable onto multiple lines
|
||||
arguments[argumentIndex] = Message.fromString(stringArguments[argumentIndex]).inline();
|
||||
}
|
||||
} else {
|
||||
key = variable.substring(VARIABLESTART.length()+LANGUAGEVARIABLE.length(), variable.length()-VARIABLEEND.length());
|
||||
}
|
||||
Message insert = Message.fromKey(key);
|
||||
if(arguments != null) {
|
||||
insert.replacements(arguments);
|
||||
}
|
||||
|
||||
// Insert message
|
||||
int startDiff = message.size()-i;
|
||||
List<String> insertMessage = insert.get(limit);
|
||||
if(limit.reached()) {
|
||||
limit.depth--;
|
||||
return;
|
||||
}
|
||||
YamlParser.insertMessage(message, insertMessage, i, matcher.start(), matcher.end());
|
||||
// Skip to end of insert
|
||||
i = message.size()-startDiff;
|
||||
break; // Maximum of one replacement
|
||||
}
|
||||
}
|
||||
limit.depth--;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "Message(key:"+key+", message:"+message+")";
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Class to store a limit
|
||||
*/
|
||||
private class Limit {
|
||||
public int left;
|
||||
public int depth;
|
||||
public boolean notified = false;
|
||||
public Message message;
|
||||
public long started;
|
||||
|
||||
/**
|
||||
* Set the initial limit
|
||||
* @param count The limit to use
|
||||
* @param message The message this limit is started for
|
||||
*/
|
||||
public Limit(int count, Message message) {
|
||||
this.left = count;
|
||||
this.depth = 0;
|
||||
this.message = message;
|
||||
this.started = System.currentTimeMillis();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the limit is reached
|
||||
* @return true if the limit is reached, otherwise false
|
||||
*/
|
||||
public boolean reached() {
|
||||
boolean reached = left <= 0;
|
||||
if(reached && !notified) {
|
||||
notified = true;
|
||||
error("Reached replacement limit, probably has replacements loops, problematic message key: "+message.key+", first characters of the message: "+getMessageStart(message, 200));
|
||||
}
|
||||
return reached;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "Limit(left: "+left+", notified: "+notified+", depth: "+depth+", message.key: "+message.key+")";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get a start of the message with a maximum length
|
||||
* @param message The message
|
||||
* @param maximumLength The maximum length to return
|
||||
* @return The start of the message with at most maximumLength characters
|
||||
*/
|
||||
private static String getMessageStart(Message message, int maximumLength) {
|
||||
String messageStart = "";
|
||||
for(int i = 0; i < message.getRaw().size() && messageStart.length() < maximumLength; i++) {
|
||||
messageStart += message.getRaw().get(i).substring(0, Math.min(maximumLength, message.getRaw().get(i).length()));
|
||||
}
|
||||
return messageStart.substring(0, Math.min(maximumLength, messageStart.length()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Debug method to print indented messages
|
||||
* @param limit The limit to use for the depth
|
||||
* @param message The message to print indented
|
||||
*/
|
||||
private static void depthPrint(Limit limit, Object... message) {
|
||||
String indent = "";
|
||||
for(int i = 0; i < limit.depth; i++) {
|
||||
indent += " ";
|
||||
}
|
||||
Object[] iMessage = new Object[message.length+1];
|
||||
iMessage[0] = indent;
|
||||
System.arraycopy(message, 0, iMessage, 1, message.length);
|
||||
info(iMessage);
|
||||
}
|
||||
|
||||
/**
|
||||
* Print information to the console
|
||||
* @param message The message to print
|
||||
*/
|
||||
public static void info(Object... message) {
|
||||
if(logger != null) {
|
||||
logger.info(StringUtils.join(message, " "));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Print a warning to the console
|
||||
* @param message The message to print
|
||||
*/
|
||||
public static void warn(Object... message) {
|
||||
if(logger != null) {
|
||||
logger.warning(StringUtils.join(message, " "));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Print an error to the console
|
||||
* @param message The messagfe to print
|
||||
*/
|
||||
public static void error(Object... message) {
|
||||
if(logger != null) {
|
||||
logger.severe(StringUtils.join(message, " "));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package me.wiefferink.interactivemessenger.processing;
|
||||
|
||||
/**
|
||||
* Provide replacement for a class to insert into messages
|
||||
*/
|
||||
public interface ReplacementProvider {
|
||||
/**
|
||||
* Get the replacement for a variable
|
||||
* @param variable The variable to replace
|
||||
* @return The replacement for the variable, or null if empty
|
||||
*/
|
||||
Object provideReplacement(String variable);
|
||||
}
|
||||
@@ -0,0 +1,208 @@
|
||||
package me.wiefferink.interactivemessenger.source;
|
||||
|
||||
import com.google.common.base.Charsets;
|
||||
import me.wiefferink.interactivemessenger.processing.Message;
|
||||
import me.wiefferink.interactivemessenger.translation.Transifex;
|
||||
import org.apache.commons.lang.exception.ExceptionUtils;
|
||||
import org.bukkit.configuration.file.YamlConfiguration;
|
||||
import org.bukkit.plugin.java.JavaPlugin;
|
||||
|
||||
import java.io.*;
|
||||
import java.net.URISyntaxException;
|
||||
import java.util.*;
|
||||
import java.util.zip.ZipEntry;
|
||||
import java.util.zip.ZipFile;
|
||||
|
||||
public class LanguageManager implements MessageProvider {
|
||||
private JavaPlugin plugin;
|
||||
private Map<String, List<String>> currentLanguage, defaultLanguage;
|
||||
private File languageFolder;
|
||||
private String jarLanguagePath;
|
||||
private List<String> chatPrefix;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
* @param plugin The plugin creating this LanguageManager (used for logging and finding the language files in the jar)
|
||||
* @param jarLanguagePath The path in the jar to the folder with the language files
|
||||
* @param currentLanguageName The name of the language that should be active (without '.yml')
|
||||
* @param defaultLanguageName The name of the language that
|
||||
*/
|
||||
public LanguageManager(JavaPlugin plugin, String jarLanguagePath, String currentLanguageName, String defaultLanguageName, List<String> chatPrefix) {
|
||||
this.plugin = plugin;
|
||||
this.jarLanguagePath = jarLanguagePath;
|
||||
this.chatPrefix = chatPrefix;
|
||||
this.languageFolder = new File(plugin.getDataFolder() + File.separator + jarLanguagePath);
|
||||
|
||||
Message.init(this, plugin.getLogger());
|
||||
saveDefaults();
|
||||
currentLanguage = loadLanguage(currentLanguageName);
|
||||
if(defaultLanguageName.equals(currentLanguageName)) {
|
||||
defaultLanguage = currentLanguage;
|
||||
} else {
|
||||
defaultLanguage = loadLanguage(defaultLanguageName);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the message for a certain key (result can be modified)
|
||||
* @param key The key of the message to get
|
||||
* @return The message as a list of strings
|
||||
*/
|
||||
@Override
|
||||
public List<String> getMessage(String key) {
|
||||
List<String> message;
|
||||
if(key.equalsIgnoreCase(Message.CHATLANGUAGEVARIABLE)) {
|
||||
message = chatPrefix;
|
||||
} else if(currentLanguage.containsKey(key)) {
|
||||
message = currentLanguage.get(key);
|
||||
} else {
|
||||
message = defaultLanguage.get(key);
|
||||
}
|
||||
if(message == null) {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
return new ArrayList<>(message);
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves the default language files if not already present
|
||||
*/
|
||||
private void saveDefaults() {
|
||||
// Create the language folder if it not exists
|
||||
File langFolder;
|
||||
if(!languageFolder.exists()) {
|
||||
if(!languageFolder.mkdirs()) {
|
||||
Message.warn("Could not create language directory: "+languageFolder.getAbsolutePath());
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
// Read jar as ZIP file
|
||||
File jarPath = new File(plugin.getClass().getProtectionDomain().getCodeSource().getLocation().toURI());
|
||||
ZipFile jar = new ZipFile(jarPath);
|
||||
Enumeration<? extends ZipEntry> entries = jar.entries();
|
||||
|
||||
// Each entry is a file or directory
|
||||
while(entries.hasMoreElements()) {
|
||||
ZipEntry entry = entries.nextElement();
|
||||
|
||||
// Filter to YAML files in the language directory
|
||||
if(!entry.isDirectory() && entry.getName().startsWith(jarLanguagePath+"/") && entry.getName().endsWith(".yml")) {
|
||||
// Save the file to disk
|
||||
File targetFile = new File(languageFolder.getAbsolutePath() + File.separator + entry.getName().substring(entry.getName().lastIndexOf("/")));
|
||||
try(
|
||||
InputStream input = jar.getInputStream(entry);
|
||||
OutputStream output = new FileOutputStream(targetFile)
|
||||
) {
|
||||
int read;
|
||||
byte[] bytes = new byte[1024];
|
||||
while((read = input.read(bytes)) != -1) {
|
||||
output.write(bytes, 0, read);
|
||||
}
|
||||
input.close();
|
||||
output.close();
|
||||
} catch(IOException e) {
|
||||
Message.warn("Something went wrong saving a default language file: "+targetFile.getAbsolutePath());
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch(URISyntaxException e) {
|
||||
Message.error("Failed to find location of jar file:", ExceptionUtils.getStackTrace(e));
|
||||
} catch(IOException e) {
|
||||
Message.error("Failed to read zip file:", ExceptionUtils.getStackTrace(e));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads the specified language
|
||||
* @param key The language to load
|
||||
* @return Map with the messages loaded from the file
|
||||
*/
|
||||
private Map<String, List<String>> loadLanguage(String key) {
|
||||
return loadLanguage(key, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads the specified language
|
||||
* @param key The language to load
|
||||
* @param convert try conversion or not (infinite recursion prevention)
|
||||
* @return Map with the messages loaded from the file
|
||||
*/
|
||||
private Map<String, List<String>> loadLanguage(String key, boolean convert) {
|
||||
Map<String, List<String>> result = new HashMap<>();
|
||||
|
||||
// Load the language file
|
||||
boolean convertFromTransifex = false;
|
||||
File file = new File(languageFolder.getAbsolutePath()+File.separator+key+".yml");
|
||||
try(
|
||||
InputStreamReader reader = new InputStreamReader(new FileInputStream(file), Charsets.UTF_8)
|
||||
) {
|
||||
// Detect empty language files, happens when the YAML parsers prints an exception (it does return an empty YamlConfiguration though)
|
||||
YamlConfiguration ymlFile = YamlConfiguration.loadConfiguration(reader);
|
||||
if(ymlFile.getKeys(false).isEmpty()) {
|
||||
Message.warn("Language file "+key+".yml has zero messages.");
|
||||
return result;
|
||||
}
|
||||
|
||||
// Retrieve the messages from the YAML file and create the result
|
||||
if(!convert || !Transifex.needsConversion(ymlFile)) {
|
||||
for(String messageKey : ymlFile.getKeys(false)) {
|
||||
if(ymlFile.isList(messageKey)) {
|
||||
result.put(messageKey, new ArrayList<>(ymlFile.getStringList(messageKey)));
|
||||
} else {
|
||||
result.put(messageKey, new ArrayList<>(Collections.singletonList(ymlFile.getString(messageKey))));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
convertFromTransifex = true;
|
||||
}
|
||||
} catch(IOException e) {
|
||||
Message.warn("Could not load language file: "+file.getAbsolutePath());
|
||||
}
|
||||
|
||||
// Do conversion (after block above closed the reader)
|
||||
if(convertFromTransifex) {
|
||||
if(!Transifex.convertFrom(file)) {
|
||||
Message.warn("Failed to convert "+file.getName()+" from the Transifex layout to the AreaShop layout, check the errors above");
|
||||
}
|
||||
return loadLanguage(key, false);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
package me.wiefferink.interactivemessenger.source;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Provide messages based on keys, for example from a language file
|
||||
*/
|
||||
public interface MessageProvider {
|
||||
|
||||
/**
|
||||
* Get the message that is linked to the specified key
|
||||
* @param key The key of the message to get
|
||||
* @return A list with the lines of the message
|
||||
*/
|
||||
List<String> getMessage(String key);
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
package me.wiefferink.interactivemessenger.translation;
|
||||
|
||||
import me.wiefferink.interactivemessenger.processing.Message;
|
||||
import org.apache.commons.lang.StringUtils;
|
||||
import org.apache.commons.lang.exception.ExceptionUtils;
|
||||
import org.bukkit.configuration.file.YamlConfiguration;
|
||||
|
||||
import java.io.*;
|
||||
|
||||
/**
|
||||
* Convert to and from a format suitable for Transifex (online collaborative translation platform)
|
||||
*/
|
||||
public class Transifex {
|
||||
|
||||
/**
|
||||
* Test if a YamlConfiguration is downloaded from Transifex and needs to be converted
|
||||
* @param configuration The configuration to check
|
||||
* @return true if the file needs to be converted, otherwise false
|
||||
*/
|
||||
public static boolean needsConversion(YamlConfiguration configuration) {
|
||||
// Detect language files downloaded from Transifex and convert them
|
||||
if(configuration.getKeys(false).size() == 1) {
|
||||
for(String languageKey : configuration.getKeys(false)) {
|
||||
if(configuration.isConfigurationSection(languageKey)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a language file downloaded from Transifex into a proper format
|
||||
* @param inputFile The input file to
|
||||
* @return true if the conversion was successful, otherwise false
|
||||
*/
|
||||
public static boolean convertFrom(File inputFile) {
|
||||
if(inputFile == null || !inputFile.exists() || !inputFile.isFile()) {
|
||||
warn("Could not convert Transifix file, it does not exist");
|
||||
return false;
|
||||
}
|
||||
Message.info("Transifex conversion of", inputFile.getAbsolutePath());
|
||||
|
||||
File newFile = new File(inputFile.getAbsolutePath()+".new");
|
||||
try(
|
||||
BufferedReader reader = new BufferedReader(new FileReader(inputFile));
|
||||
BufferedWriter writer = new BufferedWriter(new FileWriter(newFile))
|
||||
) {
|
||||
// Create target file
|
||||
newFile.createNewFile();
|
||||
|
||||
// Add translation strings
|
||||
String line = reader.readLine();
|
||||
boolean first = true;
|
||||
String lastLineStart = null;
|
||||
while(line != null) {
|
||||
// Skip line with the language on it
|
||||
if(first) {
|
||||
first = false;
|
||||
line = reader.readLine();
|
||||
continue;
|
||||
}
|
||||
// Skip comment lines (normally there are none)
|
||||
if(line.startsWith("#")) {
|
||||
line = reader.readLine();
|
||||
continue;
|
||||
}
|
||||
// Strip whitespace at the front
|
||||
if(line.length() > 2) {
|
||||
line = line.substring(2);
|
||||
}
|
||||
// Add empty lines between messages of a different type (the part before - decides the type)
|
||||
if(lastLineStart != null && !line.trim().isEmpty() && !line.startsWith(lastLineStart) && !line.trim().startsWith("-")) {
|
||||
writer.newLine();
|
||||
}
|
||||
// Write line to the new file
|
||||
if(!line.trim().isEmpty()) {
|
||||
// Detect special fancyformat lines
|
||||
if(line.contains("\\n")) {
|
||||
String[] specials = line.split("\\\\n");
|
||||
//print("Contains newline: "+line+", split into "+specials.length+" parts");
|
||||
// Write identifier
|
||||
writer.write(specials[0].substring(0, specials[0].indexOf(":")+1));
|
||||
writer.newLine();
|
||||
// Strip identifier from first part
|
||||
specials[0] = specials[0].substring(specials[0].indexOf(":")+3);
|
||||
// Write special lines
|
||||
for(String special : specials) {
|
||||
writer.write(" - \"");
|
||||
if(special.startsWith("\\t")) {
|
||||
special = special.substring(2);
|
||||
writer.write(" ");
|
||||
}
|
||||
writer.write(special);
|
||||
if(!special.endsWith("\"")) {
|
||||
writer.write("\"");
|
||||
}
|
||||
writer.newLine();
|
||||
}
|
||||
} else {
|
||||
// Simply copy the line
|
||||
writer.write(line);
|
||||
writer.newLine();
|
||||
}
|
||||
}
|
||||
// Update last line start
|
||||
String[] splittedLine = line.split("-");
|
||||
if(splittedLine.length > 0 && !line.trim().isEmpty()) {
|
||||
lastLineStart = splittedLine[0];
|
||||
}
|
||||
line = reader.readLine();
|
||||
}
|
||||
} catch(IOException e) {
|
||||
warn("Could not convert Transifix file:", ExceptionUtils.getStackTrace(e));
|
||||
newFile.delete();
|
||||
return false;
|
||||
}
|
||||
|
||||
// Move current file to .old, move new file to original location
|
||||
File oldFile = new File(inputFile.getAbsolutePath()+".transifex");
|
||||
if(oldFile.exists() && oldFile.isFile()) {
|
||||
if(!oldFile.delete()) {
|
||||
warn("Could not remove existing old file:", oldFile.getAbsolutePath());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if(!inputFile.renameTo(oldFile)) {
|
||||
warn("Could not move input file to .old, from", inputFile.getAbsolutePath(), "to", oldFile.getAbsolutePath());
|
||||
return false;
|
||||
}
|
||||
if(!newFile.renameTo(inputFile)) {
|
||||
warn("Could not move new file to original file location, from", newFile.getAbsolutePath(), "to", inputFile.getAbsolutePath());
|
||||
oldFile.renameTo(inputFile); // Try to recover original file
|
||||
return false;
|
||||
}
|
||||
Message.info("Converted "+inputFile.getName()+" from the Transifex layout to the AreaShop layout, original saved in "+oldFile.getName());
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Print a warning to the console
|
||||
* @param message The message to print
|
||||
*/
|
||||
private static void warn(String... message) {
|
||||
Message.warn("[Transifex language file conversion]", StringUtils.join(message, " "));
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user