package tc.oc.commons.core.chat; import java.net.MalformedURLException; import java.net.URI; import java.net.URISyntaxException; import java.net.URL; import java.text.AttributedCharacterIterator; import java.text.MessageFormat; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Iterator; import java.util.List; import java.util.Objects; import java.util.function.Supplier; import java.util.stream.Collectors; import java.util.stream.IntStream; import java.util.stream.Stream; import javax.annotation.Nullable; import com.google.common.base.Function; import com.google.common.collect.ImmutableList; import com.google.common.collect.Lists; import net.md_5.bungee.api.ChatColor; import net.md_5.bungee.api.chat.BaseComponent; import net.md_5.bungee.api.chat.ClickEvent; import net.md_5.bungee.api.chat.HoverEvent; import net.md_5.bungee.api.chat.TextComponent; import net.md_5.bungee.api.chat.TranslatableComponent; import static com.google.common.base.Preconditions.checkNotNull; /** * Utils for working with {@link BaseComponent}s */ public abstract class Components { public static BaseComponent[] fromLegacyTextMulti(String legacyText) { return TextComponent.fromLegacyText(ChatColor.translateAlternateColorCodes('`', legacyText)); } /** * Convert text with legacy formatting codes to a {@link Component} */ public static Component fromLegacyText(String legacyText) { return new Component(fromLegacyTextMulti(legacyText)); } private static final Component SPACE = new Component(" "); private static final Component NEWLINE = new Component("\n"); public static BaseComponent blank() { return BlankComponent.INSTANCE; } public static BaseComponent space() { return SPACE; } public static BaseComponent newline() { return NEWLINE; } public static List<BaseComponent> repeat(Supplier<BaseComponent> component, int amount) { return IntStream.rangeClosed(1, amount).mapToObj(i -> component.get()).collect(Collectors.toList()); } public static boolean isBlank(@Nullable BaseComponent c) { return c == null || c instanceof BlankComponent; } /** * See {@link #format(MessageFormat, List)} */ public static BaseComponent[] format(String format, BaseComponent... arguments) { return format(new MessageFormat(format), arguments); } /** * See {@link #format(MessageFormat, List)} */ public static BaseComponent[] format(String format, List<BaseComponent> arguments) { return format(new MessageFormat(format), arguments); } /** * See {@link #format(MessageFormat, List)} */ public static BaseComponent[] format(MessageFormat format, BaseComponent... arguments) { return format(format, Arrays.asList(arguments)); } /** * Render the given {@link MessageFormat} to component form, using the given arguments. * This is equivalent to {@link MessageFormat#format} but a component tree is generated, * instead of a String, and the arguments are included directly in the tree. * * To accomplish this, the message is first rendered to a String with placeholder arguments. * Then {@link MessageFormat#formatToCharacterIterator} is used to figure out where the * arguments appear in the result, and the text between them is spliced out and used to * build the component tree along with the actual arguments. */ public static BaseComponent[] format(MessageFormat format, List<BaseComponent> arguments) { if(arguments == null || arguments.isEmpty()) { return new BaseComponent[] { new TextComponent(format.format(null, new StringBuffer(), null).toString()) }; } List<BaseComponent> parts = new ArrayList<>(arguments.size() * 2 + 1); Object[] dummies = new Object[arguments.size()]; StringBuffer sb = format.format(dummies, new StringBuffer(), null); AttributedCharacterIterator iter = format.formatToCharacterIterator(dummies); while(iter.getIndex() < iter.getEndIndex()) { int end = iter.getRunLimit(); Integer index = (Integer) iter.getAttribute(MessageFormat.Field.ARGUMENT); if(index == null) { parts.add(new TextComponent(sb.substring(iter.getIndex(), end))); } else { parts.add(arguments.get(index)); } iter.setIndex(end); } return parts.toArray(new BaseComponent[parts.size()]); } /** * Recursively compare the given components for equality */ public static boolean equals(HoverEvent a, HoverEvent b) { return (a == b) || (a != null && b != null && a.getAction() == b.getAction() && equals(a.getValue(), b.getValue())); } /** * Recursively compare the given components for equality */ public static boolean equals(ClickEvent a, ClickEvent b) { return (a == b) || (a != null && b != null && a.getAction() == b.getAction() && Objects.equals(a.getValue(), b.getValue())); } /** * Recursively compare the given components for equality */ public static boolean equals(BaseComponent a, BaseComponent b) { return (a == b) || (a != null && b != null && Objects.equals(a.isBoldRaw(), b.isBoldRaw()) && Objects.equals(a.isItalicRaw(), b.isItalicRaw()) && Objects.equals(a.isObfuscatedRaw(), b.isItalicRaw()) && Objects.equals(a.isStrikethroughRaw(), b.isItalicRaw()) && Objects.equals(a.isUnderlinedRaw(), b.isItalicRaw()) && equals(a.getClickEvent(), b.getClickEvent()) && equals(a.getHoverEvent(), b.getHoverEvent())); } /** * Recursively compare the given components for equality */ public static boolean equals(BaseComponent[] a, BaseComponent[] b) { if(a == b) return true; if(a == null || b == null) return false; if(a.length != b.length) return false; for(int i = 0; i < a.length; i++) { if(!equals(a[i], b[i])) return false; } return true; } /** * Recursively compare the given components for equality */ public static boolean equals(Collection<BaseComponent> a, Collection<BaseComponent> b) { if(a == b) return true; if(a == null || b == null) return false; if(a.size() != b.size()) return false; for(Iterator<BaseComponent> ia = a.iterator(), ib = b.iterator(); ia.hasNext() && ib.hasNext(); ) { if(!equals(ia.next(), ib.next())) return false; } return true; } public static BaseComponent color(BaseComponent c, @Nullable ChatColor color) { c.setColor(color); return c; } public static BaseComponent bold(BaseComponent c, @Nullable Boolean yes) { c.setBold(yes); return c; } public static BaseComponent italic(BaseComponent c, @Nullable Boolean yes) { c.setItalic(yes); return c; } public static BaseComponent underlined(BaseComponent c, @Nullable Boolean yes) { c.setUnderlined(yes); return c; } public static BaseComponent strikethrough(BaseComponent c, @Nullable Boolean yes) { c.setStrikethrough(yes); return c; } public static BaseComponent obfuscated(BaseComponent c, @Nullable Boolean yes) { c.setObfuscated(yes); return c; } public static BaseComponent clickEvent(BaseComponent c, @Nullable ClickEvent event) { c.setClickEvent(event); return c; } public static BaseComponent clickEvent(BaseComponent c, ClickEvent.Action action, String value) { c.setClickEvent(new ClickEvent(action, value)); return c; } public static BaseComponent hoverEvent(BaseComponent c, @Nullable HoverEvent event) { c.setHoverEvent(event); return c; } public static BaseComponent hoverEvent(BaseComponent c, HoverEvent.Action action, BaseComponent...values) { c.setHoverEvent(new HoverEvent(action, values)); return c; } public static BaseComponent extra(BaseComponent c, BaseComponent...extras) { for(BaseComponent extra : extras) { checkNotNull(extra); c.addExtra(extra); } return c; } public static BaseComponent addFormats(BaseComponent component, ChatColor... formats) { return addFormats(component, Arrays.asList(formats)); } public static BaseComponent addFormats(BaseComponent component, Iterable<ChatColor> formats) { for(ChatColor format : formats) { checkNotNull(format); switch(format) { case BOLD: component.setBold(true); break; case ITALIC: component.setItalic(true); break; case UNDERLINE: component.setUnderlined(true); break; case STRIKETHROUGH: component.setStrikethrough(true); break; case MAGIC: component.setObfuscated(true); break; case RESET: throw new IllegalArgumentException("Cannot add format " + format); default: component.setColor(format); break; } } return component; } public static BaseComponent removeFormats(BaseComponent component, ChatColor... formats) { for(ChatColor format : formats) { checkNotNull(format); switch(format) { case BOLD: component.setBold(false); break; case ITALIC: component.setItalic(false); break; case UNDERLINE: component.setUnderlined(false); break; case STRIKETHROUGH: component.setStrikethrough(false); break; case MAGIC: component.setObfuscated(false); break; default: throw new IllegalArgumentException("Cannot remove format " + format); } } return component; } public static boolean hasFormat(BaseComponent c) { return c.getColorRaw() != null || c.isBoldRaw() != null || c.isItalicRaw() != null || c.isUnderlinedRaw() != null || c.isStrikethroughRaw() != null || c.isObfuscatedRaw() != null || c.getClickEvent() != null || c.getHoverEvent() != null; } public static void copyFormat(BaseComponent from, BaseComponent to) { to.setColor(from.getColorRaw()); to.setBold(from.isBoldRaw()); to.setItalic(from.isItalicRaw()); to.setUnderlined(from.isUnderlinedRaw()); to.setStrikethrough(from.isStrikethroughRaw()); to.setObfuscated(from.isObfuscatedRaw()); } public static void copyEvents(BaseComponent from, BaseComponent to) { to.setClickEvent(from.getClickEvent()); to.setHoverEvent(from.getHoverEvent()); } public static void copyFormatAndEvents(BaseComponent from, BaseComponent to) { copyFormat(from, to); copyEvents(from, to); } public static void softMergeFormat(BaseComponent from, BaseComponent to) { if(to.getColorRaw() == null) to.setColor(from.getColorRaw()); if(to.isBoldRaw() == null) to.setBold(from.isBoldRaw()); if(to.isItalicRaw() == null) to.setItalic(from.isItalicRaw()); if(to.isUnderlinedRaw() == null) to.setUnderlined(from.isUnderlinedRaw()); if(to.isStrikethroughRaw() == null) to.setStrikethrough(from.isStrikethroughRaw()); if(to.isObfuscatedRaw() == null) to.setObfuscated(from.isObfuscatedRaw()); if(to.getClickEvent() == null) to.setClickEvent(from.getClickEvent()); if(to.getHoverEvent() == null) to.setHoverEvent(from.getHoverEvent()); } public static void hardMergeFormat(BaseComponent from, BaseComponent to) { if(from.getColorRaw() != null) to.setColor(from.getColorRaw()); if(from.isBoldRaw() != null) to.setBold(from.isBoldRaw()); if(from.isItalicRaw() != null) to.setItalic(from.isItalicRaw()); if(from.isUnderlinedRaw() != null) to.setUnderlined(from.isUnderlinedRaw()); if(from.isStrikethroughRaw() != null) to.setStrikethrough(from.isStrikethroughRaw()); if(from.isObfuscatedRaw() != null) to.setObfuscated(from.isObfuscatedRaw()); if(from.getClickEvent() != null) to.setClickEvent(from.getClickEvent()); if(from.getHoverEvent() != null) to.setHoverEvent(from.getHoverEvent()); } public static void copyLastFormat(String legacy, BaseComponent to) { int length = legacy.length(); // Search backwards from the end as it is faster for (int index = length - 1; index > -1; index--) { char section = legacy.charAt(index); if (section == ChatColor.COLOR_CHAR && index < length - 1) { char c = legacy.charAt(index + 1); ChatColor color = ChatColor.getByChar(c); if (color != null) { addFormats(to, color); // Once we find a color or reset we can stop searching if(color.equals(ChatColor.RESET)) break; if((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F')) break; } } } } public static void addExtra(BaseComponent c, List<BaseComponent> extra) { if(extra != null) { if(c.getExtra() == null) { c.setExtra(extra); } else { c.getExtra().addAll(extra); } } } public static BaseComponent shallowCopy(BaseComponent original) { if(original instanceof TextComponent) { return shallowCopy((TextComponent) original); } else if(original instanceof TranslatableComponent) { return shallowCopy((TranslatableComponent) original); } else { throw new IllegalArgumentException("Don't know how to copy a " + original.getClass().getName()); } } public static TextComponent shallowCopy(TextComponent original) { TextComponent copy = new TextComponent(original.getText()); copyFormatAndEvents(original, copy); copy.setExtra(original.getExtra()); return copy; } public static TranslatableComponent shallowCopy(TranslatableComponent original) { TranslatableComponent copy = new TranslatableComponent(original.getTranslate()); copy.setWith(original.getWith()); copyFormatAndEvents(original, copy); copy.setExtra(original.getExtra()); return copy; } public static BaseComponent concat(BaseComponent... components) { switch(components.length) { case 0: return blank(); case 1: return components[0]; default: return new Component(components); } } public static BaseComponent link(String protocol, String host, String path, ChatColor...formats) { try { return link(new URL(protocol, host, path), formats); } catch(MalformedURLException e) { throw new IllegalStateException(e); } } public static BaseComponent link(URL url, ChatColor...formats) { try { final URI uri = url.toURI(); // The encoded form escapes all illegal characters e.g. " " becomes "%20", // which is required by the client. final String encoded = url.toExternalForm(); // The display form leaves the illegal chars in the path, which tends to look nicer. // It also removes any trailing slash. final String display = encoded .replace(uri.getRawPath(), uri.getPath()) .replaceAll("/$", ""); final Component c = new Component(display).clickEvent(ClickEvent.Action.OPEN_URL, encoded); if(formats.length == 0) { c.color(ChatColor.BLUE).underlined(true); } else { c.add(formats); } return c; } catch(URISyntaxException e) { return blank(); } } public static int pixelWidth(Collection<BaseComponent> components, boolean bold) { int width = 0; for(BaseComponent component : components) { width += pixelWidth(component); } return width; } public static int pixelWidth(BaseComponent component, boolean bold) { if(component.isBoldRaw() != null) { bold = component.isBold(); } int width = 0; if(component instanceof TextComponent) { String text = ((TextComponent) component).getText(); width += ChatUtils.pixelWidth(text, bold); } if(component.getExtra() != null) { width += pixelWidth(component.getExtra(), bold); } return width; } public static int pixelWidth(BaseComponent component) { return pixelWidth(component, false); } public static List<BaseComponent> transform(List<String> strings) { return Lists.transform(strings, new Function<String, BaseComponent>() { @Override public BaseComponent apply(String s) { return new Component(s); } }); } public static BaseComponent join(BaseComponent delimiter, Collection<? extends BaseComponent> elements) { Component c = new Component(); boolean first = true; for(BaseComponent el : elements) { if(!first) { c.extra(delimiter); } c.extra(el); first = false; } return c; } public static List<BaseComponent> wordWrap(BaseComponent text, int width) { return wordWrap(new ArrayList<>(), text, width); } public static List<BaseComponent> wordWrap(List<BaseComponent> lines, BaseComponent text, int width) { for(String line : ChatUtils.wordWrap(text.toLegacyText(), width)) { lines.add(concat(TextComponent.fromLegacyText(line))); } return lines; } public static BaseComponent naturalList(Stream<? extends BaseComponent> elements) { return naturalList(elements.collect(Collectors.toList())); } public static BaseComponent naturalList(Collection<? extends BaseComponent> elements) { switch(elements.size()) { case 0: return Components.blank(); case 1: return elements.iterator().next(); case 2: return new TranslatableComponent("misc.list.pair", elements.toArray()); default: Iterator<? extends BaseComponent> iter = elements.iterator(); BaseComponent a = new TranslatableComponent("misc.list.start", iter.next(), iter.next()); BaseComponent b = iter.next(); while(iter.hasNext()) { a = new TranslatableComponent("misc.list.middle", a, b); b = iter.next(); } return new TranslatableComponent("misc.list.end", a, b); } } public static BaseComponent warning(BaseComponent content) { return new Component(ChatColor.RED).extra(new Component(" \u26a0 ", ChatColor.YELLOW), content); } }