/* * This file is part of NucleusFramework for Bukkit, licensed under the MIT License (MIT). * * Copyright (c) JCThePants (www.jcwhatever.com) * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package com.jcwhatever.nucleus.utils.text.format; import com.jcwhatever.nucleus.utils.PreCon; import com.jcwhatever.nucleus.utils.performance.pool.IPoolElementFactory; import com.jcwhatever.nucleus.utils.performance.pool.IPoolRecycleHandler; import com.jcwhatever.nucleus.utils.performance.pool.SimplePool; import com.jcwhatever.nucleus.utils.text.TextColor; import com.jcwhatever.nucleus.utils.text.TextFormat; import com.jcwhatever.nucleus.utils.text.components.IChatComponent; import com.jcwhatever.nucleus.utils.text.components.IChatMessage; import com.jcwhatever.nucleus.utils.text.components.IChatModifier; import com.jcwhatever.nucleus.utils.text.components.SimpleChatComponent; import com.jcwhatever.nucleus.utils.text.components.SimpleChatModifier; import com.jcwhatever.nucleus.utils.text.dynamic.IDynamicText; import com.jcwhatever.nucleus.utils.text.format.TextFormatterSettings.FormatPolicy; import com.jcwhatever.nucleus.utils.text.format.args.IFormatterArg; import javax.annotation.Nullable; import java.util.HashMap; import java.util.Iterator; import java.util.Map; /** * Replaces tags and unicode escapes in text. * * <p>Tags consist of enclosing curly braces with tag text inside.</p> * * <p>Numbers are used to mark the index of the parameter that should be * placed where the tag is. ie {0} is parameter 1 (index 0)</p> * * <p>{@link TextColor} constant names are automatically added as tags and * replaced with the equivalent color code. ie {RED} is replaced with the * Minecraft color code for red.</p> * * <p>Tags that don't match a parameter index or other defined tag formatter * are ignored and added as is.</p> * * <p>Comments can be added to tags by inserting a colon. Useful for including * the purpose of the format tag for documentation purposes. * ie {0: This part of the tag is a comment and is ignored}</p> * */ public class TextFormatter { private static Map<String, ITagFormatter> _colors = new HashMap<>(22); static { Iterator<TextFormat> iterator = TextFormat.formatIterator(); while (iterator.hasNext()) { final TextFormat format = iterator.next(); _colors.put(format.getTagName(), new ITagFormatter() { @Override public String getTag() { return format.getTagName(); } @Override public void append(IFormatterAppendable output, String tag) { setModifier(format, output); } }); } } private static ITagFormatter ERASER = new ITagFormatter() { @Override public String getTag() { return "#ERASER#"; } @Override public void append(IFormatterAppendable output, String rawTag) { // do nothing } }; private static void setModifier(TextFormat format,IFormatterAppendable appendable) { IChatModifier modifier = appendable.getModifier(); switch (format.getTagName()) { case "BOLD": modifier.setBold(true); return; case "ITALIC": modifier.setItalic(true); return; case "MAGIC": modifier.setMagic(true); return; case "STRIKETHROUGH": modifier.setStrikeThrough(true); return; case "UNDERLINE": modifier.setUnderline(true); return; case "RESET": if (appendable instanceof FormatResultBuffer) { ((FormatResultBuffer) appendable).reset(); } appendable.getModifier().reset(); return; } if (format instanceof TextColor) { modifier.setColor((TextColor)format); } } private final Thread _homeThread; private final TextFormatterSettings _settings; private final SimplePool<FormatResultBuffer> _bufferPool = new SimplePool<FormatResultBuffer>(10, new IPoolElementFactory<FormatResultBuffer>() { @Override public FormatResultBuffer create() { return new FormatResultBuffer(); } }, new IPoolRecycleHandler<FormatResultBuffer>() { @Override public void onRecycle(FormatResultBuffer buffer) { buffer.hardReset(); } }); /** * Constructor. * * @param settings The formatter settings */ public TextFormatter(TextFormatterSettings settings) { _settings = settings; _homeThread = Thread.currentThread(); } /** * Format text. * * @param template The template text. * @param params The parameters to add. * * @return The formatted string. */ public ITextFormatterResult format(CharSequence template, Object... params) { PreCon.notNull(template); return format(_settings, template, params); } /** * Format text. * * @param settings Custom formatter settings to use. * @param template The template text. * @param params The parameters to add. * * @return The formatted string. */ public ITextFormatterResult format(TextFormatterSettings settings, CharSequence template, Object... params) { PreCon.notNull(template); if (isHomeThread()) { FormatResultBuffer buffer = _bufferPool.retrieve(); ITextFormatterResult result = format(new ParseContext(settings, buffer, template, params), false); _bufferPool.recycle(buffer); return result; } else { return format(new ParseContext(settings, new FormatResultBuffer(), template, params), false); } } /** * Format text using a custom set of formatters. * * @param context The parsing context. * @param isFormattingArgs True if formatting arguments, otherwise false. * * @return The format result. */ private ITextFormatterResult format(ParseContext context, boolean isFormattingArgs) { if (context.template instanceof ITextFormatterResult && context.params.length == 0) { return (ITextFormatterResult) context.template; } if (!context.shouldFormat()) { if (isFormattingArgs) { context.buffer.append(context.template); } else { context.result.append(new SimpleChatComponent(context.template)); context.result.finishResult(context.settings); } return context.result; } FormatResultBuffer buffer = context.buffer; StringBuilder tagBuffer = context.tagBuffer; TextFormatterSettings settings = context.settings; TextFormatterResult result = context.result; TextParser parser = context.parser; while (!parser.isFinished()) { char ch = parser.next(); if (ch == 0) break; // handle format codes if (ch == TextFormat.CHAR && TextFormat.isFormatChar(parser.peek(1))) { appendFormatCode(context); parser.skip(1); continue; } // check for tag opening if (ch == '{') { // parse tag String tag = parseTag(context); // update index position parser.skip(tagBuffer.length()); // template ended before tag was closed if (tag == null) { buffer.append('{'); buffer.append(tagBuffer); } // tag parsed else { parser.skip(1); // add 1 for closing brace appendReplacement(context, tag); } } else if (ch == '\n' || ch == '\r') { if (settings.getLineReturnPolicy() != FormatPolicy.REMOVE) { buffer.newLine(); } } else if (ch == '\\' && parser.peek(1) != 0) { processBackslash(context); } else { if (settings.isEscaped(ch)) buffer.append('\\'); // append next character buffer.append(ch); } } if (buffer.isModified()) { buffer.reset(); } result.appendAll(buffer.results); if (!isFormattingArgs) { result.finishResult(settings); } return result; } private void appendFormatCode(ParseContext context) { FormatResultBuffer buffer = context.buffer; TextFormat format = TextFormat.fromFormatChar(context.parser.peek(1)); assert format != null; if (format instanceof TextColor) { context.result.setParsedColor(true); if (buffer.getModifier().getColor() != null) { buffer.reset(); } } setModifier(format, buffer); } /** * Parse a unicode character from the string */ private char parseUnicode(ParseContext context) { StringBuilder tagBuffer = context.tagBuffer; TextParser parser = context.parser; tagBuffer.setLength(0); int readCount = 0; while (parser.current() != 0) { if (readCount == 4) { break; } else { char ch = parser.peek(readCount + 1); if ("01234567890abcdefABCDEF".indexOf(ch) == -1) return 0; tagBuffer.append(ch); } readCount++; } if (tagBuffer.length() == 4) { try { return (char) Integer.parseInt(tagBuffer.toString(), 16); } catch (NumberFormatException ignore) { return 0; } } return 0; } /* * Parse a single tag from the template */ private String parseTag(ParseContext context) { StringBuilder tagBuffer = context.tagBuffer; TextParser parser = context.parser; tagBuffer.setLength(0); int i = 1; while (parser.current() != 0) { char ch = context.parser.peek(i); if (ch == 0) return null; if (ch == '}') { return tagBuffer.toString(); } else { tagBuffer.append(ch); } i++; } return null; } /* * Append replacement text for a tag */ private void appendReplacement(ParseContext context, String tag) { TextFormatterSettings settings = context.settings; TextFormatterResult result = context.result; FormatResultBuffer buffer = context.buffer; StringBuilder tagBuffer = context.tagBuffer; Object[] params = context.params; Map<String, ITagFormatter> formatters = context.formatters; boolean isNumber = !tag.isEmpty(); tagBuffer.setLength(0); // parse out tag from comment section for (int i=0; i < tag.length(); i++) { char ch = tag.charAt(i); // done at comment character if (ch == ':') { break; } // append next tag character else { tagBuffer.append(ch); // check if the character is a number if (isNumber && !Character.isDigit(ch)) { isNumber = false; } } } String parsedTag = tagBuffer.toString(); if (isNumber) { int index = Integer.parseInt(parsedTag); // make sure number is in the range of the provided parameters. if (params.length <= index) { reappendTag(buffer, tag); } // replace number with parameter argument. else { Object param = params[index]; if (param instanceof IDynamicText) { param = ((IDynamicText) param).nextText(); } else if (param instanceof IFormatterArg) { IChatModifier modifier = new SimpleChatModifier(buffer.getModifier()); if (buffer.isModified()) { buffer.reset(); } ((IFormatterArg) param).getComponents(buffer.results); buffer.reset(modifier); return; } else if (param instanceof IChatComponent) { IChatModifier modifier = new SimpleChatModifier(buffer.getModifier()); if (buffer.isModified()) { buffer.reset(); } buffer.results.add((IChatComponent) param); buffer.reset(modifier); return; } else if (param instanceof IChatMessage) { IChatModifier modifier = new SimpleChatModifier(buffer.getModifier()); if (buffer.isModified()) { buffer.reset(); } ((IChatMessage) param).getComponents(buffer.results); buffer.reset(modifier); return; } String toAppend = String.valueOf(param); // append parameter argument if (settings.isArgsFormatted()) { IChatModifier modifier = new SimpleChatModifier(buffer.getModifier()); ITextFormatterResult argResult = format(new ParseContext(context, toAppend), true); //result.appendAll(argResult); if ((argResult.isParsed() && argResult.isColorParsed()) || (!argResult.isParsed() && toAppend.indexOf(TextFormat.CHAR) != -1)) { // make sure colors from inserted text do not continue // into template text buffer.reset(modifier); } } else { boolean hasColorCode = toAppend.indexOf(TextFormat.CHAR) != -1; IChatModifier modifier = hasColorCode ? new SimpleChatModifier(buffer.getModifier()) : null; buffer.append(toAppend); if (hasColorCode) { // make sure colors from inserted text do not continue // into template text buffer.reset(modifier); } } } } else { // check for custom formatter ITagFormatter formatter = settings.getTagPolicy() == FormatPolicy.IGNORE ? null : getFormatter(parsedTag, formatters); if (formatter == null && settings.getColorPolicy() != FormatPolicy.IGNORE) { // check for color formatter formatter = getFormatter(parsedTag, _colors); if (formatter != null) { // remove color tag if color policy is remove if (settings.getColorPolicy() == FormatPolicy.REMOVE) { formatter = ERASER; } else { result.setParsedColor(true); } } } // remove tag if tag policy is remove else if (formatter != null && settings.getTagPolicy() == FormatPolicy.REMOVE) { formatter = ERASER; } if (formatter != null) { if (shouldResetAfterTag(formatter.getTag())) buffer.reset(); // formatter appends replacement text to format buffer formatter.append(buffer, tag); } else { // no formatter, append tag to result buffer reappendTag(buffer, tag); } } } private boolean shouldResetAfterTag(String tag) { TextFormat format = TextColor.fromName(tag); return format instanceof TextColor; } /** * Process an escape character. Returns the new index location. */ private void processBackslash(ParseContext context) { TextFormatterSettings settings = context.settings; FormatResultBuffer buffer = context.buffer; TextParser parser = context.parser; // make sure the backslash isn't escaped int s = 0; int bsCount = 0; while (parser.current() != 0) { if (parser.peek(s - 1) == '\\') { bsCount++; } else { break; } s--; } if (bsCount % 2 != 0) return; // look at next character char next = parser.peek(1); // handle new line character if ((next == 'n' || next == 'r') && settings.getLineReturnPolicy() != FormatPolicy.IGNORE) { if (settings.getLineReturnPolicy() != FormatPolicy.REMOVE) { buffer.newLine(); } parser.skip(1); } // handle unicode else if (next == 'u' && settings.getUnicodePolicy() != FormatPolicy.IGNORE) { parser.skip(1); char unicode = parseUnicode(context); if (unicode == 0) { // append non unicode text buffer.append("\\u"); buffer.incrementCharCount(2); } else { if (settings.getUnicodePolicy() != FormatPolicy.REMOVE) { buffer.append(unicode); buffer.incrementCharCount(1); } parser.skip(4); } } // unused backslash else { buffer.append('\\'); } } /** * Get a color formatter for the parsed tag. */ @Nullable private ITagFormatter getFormatter(String parsedTag, Map<String, ITagFormatter> formatters) { return formatters.get(parsedTag); } /* * Append raw tag to string builder */ private void reappendTag(FormatResultBuffer context, String tag) { context.append('{'); context.append(tag); context.append('}'); context.incrementCharCount(2 + tag.length()); } /* * Determine if the current thread is the thread * the formatter was instantiated on. */ private boolean isHomeThread() { return Thread.currentThread().equals(_homeThread); } }