From 85c1c280e6b5bb63ed516d0caeffbb08d920c472 Mon Sep 17 00:00:00 2001
From: Janne Koschinski <janne@kuschku.de>
Date: Sun, 14 Feb 2016 19:01:07 +0100
Subject: [PATCH] Fixed issues with the mIRC color serializing/deserializing

---
 .../ui/chat/chatview/ChatMessageRenderer.java |   2 +-
 .../ui/chat/drawer/BufferItem.java            |   9 +-
 .../ui/editor/AdvancedEditor.java             |   9 +-
 .../quasseldroid_ng/ui/theme/AppContext.java  |  15 +
 .../editor => util/irc/format}/BoldSpan.java  |   2 +-
 .../irc/format/IrcFormatDeserializer.java     | 304 ++++++++++++++++++
 .../irc/{ => format}/IrcFormatHelper.java     |   7 +-
 .../irc/format/IrcFormatSerializer.java}      |   6 +-
 .../irc/format}/ItalicSpan.java               |   2 +-
 .../java/de/kuschku/util/ui/MessageUtil.java  | 191 -----------
 10 files changed, 335 insertions(+), 212 deletions(-)
 rename app/src/main/java/de/kuschku/{quasseldroid_ng/ui/editor => util/irc/format}/BoldSpan.java (95%)
 create mode 100644 app/src/main/java/de/kuschku/util/irc/format/IrcFormatDeserializer.java
 rename app/src/main/java/de/kuschku/util/irc/{ => format}/IrcFormatHelper.java (95%)
 rename app/src/main/java/de/kuschku/{quasseldroid_ng/ui/editor/FormattingHelper.java => util/irc/format/IrcFormatSerializer.java} (97%)
 rename app/src/main/java/de/kuschku/{quasseldroid_ng/ui/editor => util/irc/format}/ItalicSpan.java (95%)
 delete mode 100644 app/src/main/java/de/kuschku/util/ui/MessageUtil.java

diff --git a/app/src/main/java/de/kuschku/quasseldroid_ng/ui/chat/chatview/ChatMessageRenderer.java b/app/src/main/java/de/kuschku/quasseldroid_ng/ui/chat/chatview/ChatMessageRenderer.java
index fdbf8fe17..b9d21cfbf 100644
--- a/app/src/main/java/de/kuschku/quasseldroid_ng/ui/chat/chatview/ChatMessageRenderer.java
+++ b/app/src/main/java/de/kuschku/quasseldroid_ng/ui/chat/chatview/ChatMessageRenderer.java
@@ -29,8 +29,8 @@ import android.support.annotation.UiThread;
 import de.kuschku.libquassel.localtypes.buffers.Buffer;
 import de.kuschku.libquassel.message.Message;
 import de.kuschku.quasseldroid_ng.ui.theme.AppContext;
-import de.kuschku.util.irc.IrcFormatHelper;
 import de.kuschku.util.irc.IrcUserUtils;
+import de.kuschku.util.irc.format.IrcFormatHelper;
 
 import static de.kuschku.util.AndroidAssert.assertNotNull;
 
diff --git a/app/src/main/java/de/kuschku/quasseldroid_ng/ui/chat/drawer/BufferItem.java b/app/src/main/java/de/kuschku/quasseldroid_ng/ui/chat/drawer/BufferItem.java
index c466e13cf..60514b98f 100644
--- a/app/src/main/java/de/kuschku/quasseldroid_ng/ui/chat/drawer/BufferItem.java
+++ b/app/src/main/java/de/kuschku/quasseldroid_ng/ui/chat/drawer/BufferItem.java
@@ -40,7 +40,6 @@ import de.kuschku.libquassel.message.Message;
 import de.kuschku.libquassel.primitives.types.BufferInfo;
 import de.kuschku.quasseldroid_ng.R;
 import de.kuschku.quasseldroid_ng.ui.theme.AppContext;
-import de.kuschku.util.ui.MessageUtil;
 
 public class BufferItem extends SecondaryDrawerItem {
     @NonNull
@@ -144,14 +143,10 @@ public class BufferItem extends SecondaryDrawerItem {
     }
 
     @Override
-    public void onPostBindView(IDrawerItem drawerItem, @NonNull View view) {
+    public void onPostBindView(IDrawerItem drawerItem, View view) {
         super.onPostBindView(drawerItem, view);
 
         if (getDescription() != null && getDescription().getText() != null)
-            ((TextView) view.findViewById(R.id.material_drawer_description)).setText(MessageUtil.parseStyleCodes(
-                    context.themeUtil(),
-                    getDescription().getText(),
-                    context.settings().mircColors.or(true)
-            ));
+            ((TextView) view.findViewById(R.id.material_drawer_description)).setText(context.deserializer().formatString(getDescription().getText()));
     }
 }
diff --git a/app/src/main/java/de/kuschku/quasseldroid_ng/ui/editor/AdvancedEditor.java b/app/src/main/java/de/kuschku/quasseldroid_ng/ui/editor/AdvancedEditor.java
index 427a29df9..4293222e4 100644
--- a/app/src/main/java/de/kuschku/quasseldroid_ng/ui/editor/AdvancedEditor.java
+++ b/app/src/main/java/de/kuschku/quasseldroid_ng/ui/editor/AdvancedEditor.java
@@ -31,14 +31,15 @@ import android.text.style.UnderlineSpan;
 import android.widget.EditText;
 
 import de.kuschku.quasseldroid_ng.ui.theme.AppContext;
+import de.kuschku.util.irc.format.BoldSpan;
+import de.kuschku.util.irc.format.ItalicSpan;
 
 public class AdvancedEditor {
+    private final AppContext context;
     private final EditText editText;
-    @NonNull
-    private final FormattingHelper helper;
 
     public AdvancedEditor(AppContext context, EditText editText) {
-        this.helper = new FormattingHelper(context);
+        this.context = context;
         this.editText = editText;
     }
 
@@ -140,6 +141,6 @@ public class AdvancedEditor {
 
     @NonNull
     public String toFormatString() {
-        return helper.toEscapeCodes(editText.getText());
+        return context.serializer().toEscapeCodes(editText.getText());
     }
 }
diff --git a/app/src/main/java/de/kuschku/quasseldroid_ng/ui/theme/AppContext.java b/app/src/main/java/de/kuschku/quasseldroid_ng/ui/theme/AppContext.java
index 3ccec30be..9cd1af445 100644
--- a/app/src/main/java/de/kuschku/quasseldroid_ng/ui/theme/AppContext.java
+++ b/app/src/main/java/de/kuschku/quasseldroid_ng/ui/theme/AppContext.java
@@ -26,12 +26,16 @@ import android.support.annotation.NonNull;
 import de.kuschku.libquassel.BusProvider;
 import de.kuschku.libquassel.client.Client;
 import de.kuschku.quasseldroid_ng.ui.chat.Settings;
+import de.kuschku.util.irc.format.IrcFormatDeserializer;
+import de.kuschku.util.irc.format.IrcFormatSerializer;
 
 public class AppContext {
     private ThemeUtil themeUtil;
     private Settings settings;
     private Client client;
     private BusProvider provider;
+    private IrcFormatDeserializer deserializer;
+    private IrcFormatSerializer serializer;
 
     public ThemeUtil themeUtil() {
         return themeUtil;
@@ -39,6 +43,9 @@ public class AppContext {
 
     public void setThemeUtil(ThemeUtil themeUtil) {
         this.themeUtil = themeUtil;
+
+        this.serializer = new IrcFormatSerializer(this);
+        this.deserializer = new IrcFormatDeserializer(this);
     }
 
     @NonNull
@@ -88,4 +95,12 @@ public class AppContext {
         setProvider(provider);
         return this;
     }
+
+    public IrcFormatDeserializer deserializer() {
+        return deserializer;
+    }
+
+    public IrcFormatSerializer serializer() {
+        return serializer;
+    }
 }
diff --git a/app/src/main/java/de/kuschku/quasseldroid_ng/ui/editor/BoldSpan.java b/app/src/main/java/de/kuschku/util/irc/format/BoldSpan.java
similarity index 95%
rename from app/src/main/java/de/kuschku/quasseldroid_ng/ui/editor/BoldSpan.java
rename to app/src/main/java/de/kuschku/util/irc/format/BoldSpan.java
index c5014797c..3e1342368 100644
--- a/app/src/main/java/de/kuschku/quasseldroid_ng/ui/editor/BoldSpan.java
+++ b/app/src/main/java/de/kuschku/util/irc/format/BoldSpan.java
@@ -19,7 +19,7 @@
  * with this program.  If not, see <http://www.gnu.org/licenses/>.
  */
 
-package de.kuschku.quasseldroid_ng.ui.editor;
+package de.kuschku.util.irc.format;
 
 import android.graphics.Typeface;
 import android.text.style.StyleSpan;
diff --git a/app/src/main/java/de/kuschku/util/irc/format/IrcFormatDeserializer.java b/app/src/main/java/de/kuschku/util/irc/format/IrcFormatDeserializer.java
new file mode 100644
index 000000000..1540be165
--- /dev/null
+++ b/app/src/main/java/de/kuschku/util/irc/format/IrcFormatDeserializer.java
@@ -0,0 +1,304 @@
+/*
+ * QuasselDroid - Quassel client for Android
+ * Copyright (C) 2016 Janne Koschinski
+ * Copyright (C) 2016 Ken Børge Viktil
+ * Copyright (C) 2016 Magnus Fjell
+ * Copyright (C) 2016 Martin Sandsmark <martin.sandsmark@kde.org>
+ *
+ * This program is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License as published by the Free
+ * Software Foundation, either version 3 of the License, or (at your option)
+ * any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package de.kuschku.util.irc.format;
+
+
+import android.support.annotation.ColorInt;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.text.SpannableStringBuilder;
+import android.text.Spanned;
+import android.text.style.BackgroundColorSpan;
+import android.text.style.ForegroundColorSpan;
+import android.text.style.UnderlineSpan;
+
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Set;
+import java.util.Stack;
+
+import de.kuschku.quasseldroid_ng.ui.theme.AppContext;
+
+import static de.kuschku.util.AndroidAssert.assertNotNull;
+
+/**
+ * A helper class to turn mIRC formatted Strings into Android’s SpannableStrings with the same
+ * color and format codes
+ */
+public class IrcFormatDeserializer {
+    public static final int CODE_BOLD = 0x02;
+    public static final int CODE_COLOR = 0x03;
+    public static final int CODE_ITALIC = 0x1D;
+    public static final int CODE_UNDERLINE = 0x1F;
+    public static final int CODE_SWAP = 0x16;
+    public static final int CODE_RESET = 0x0F;
+
+    private final AppContext context;
+
+    public IrcFormatDeserializer(AppContext context) {
+        this.context = context;
+    }
+
+    /**
+     * Try to read a number from a String in specified bounds
+     *
+     * @param str   String to be read from
+     * @param start Start index (inclusive)
+     * @param end   End index (exclusive)
+     * @return The byte represented by the digits read from the string
+     */
+    public static byte readNumber(@NonNull String str, int start, int end) {
+        String result = str.substring(start, end);
+        if (result.isEmpty())
+            return -1;
+        else
+            return (byte) Integer.parseInt(result, 10);
+    }
+
+    /**
+     * @param str   String to be searched in
+     * @param start Start position (inclusive)
+     * @return Index of first character that is not a digit
+     */
+    private static int findEndOfNumber(@NonNull String str, int start) {
+        Set<Character> validCharCodes = new HashSet<>(Arrays.asList('0', '1', '2', '3', '4', '5', '6', '7', '8', '9'));
+        String searchFrame = str.substring(start);
+        int i;
+        for (i = 0; i < 2 && i < searchFrame.length(); i++) {
+            if (!validCharCodes.contains(searchFrame.charAt(i))) {
+                break;
+            }
+        }
+        return start + i;
+    }
+
+    @Nullable
+    private static IrcFormat fromId(char id) {
+        switch (id) {
+            case CODE_BOLD:
+                return new BoldIrcFormat();
+            case CODE_ITALIC:
+                return new ItalicIrcFormat();
+            case CODE_UNDERLINE:
+                return new UnderlineIrcFormat();
+            default:
+                return null;
+        }
+    }
+
+    /**
+     * Function to handle mIRC formatted strings
+     *
+     * @param str mIRC formatted String
+     * @return a CharSequence with Android’s span format representing the input string
+     */
+    @NonNull
+    public CharSequence formatString(@NonNull String str) {
+        SpannableStringBuilder plainText = new SpannableStringBuilder();
+        Stack<FormatDescription> stack = new Stack<>();
+        boolean colorize = context.settings().mircColors.get();
+
+        // Iterating over every character
+        for (int i = 0; i < str.length(); i++) {
+            char character = str.charAt(i);
+            switch (character) {
+                case CODE_BOLD:
+                case CODE_ITALIC:
+                case CODE_UNDERLINE: {
+                    if (!colorize) continue;
+
+                    // If there is an element on stack with the same code, close it
+                    if (!stack.empty() && stack.peek().format.id() == character) {
+                        stack.pop().apply(plainText, plainText.length());
+
+                        // Otherwise create a new one
+                    } else {
+                        IrcFormat format = fromId(character);
+                        assertNotNull(format);
+                        stack.push(new FormatDescription(plainText.length(), format));
+                    }
+                }
+                break;
+                case CODE_COLOR: {
+                    if (!colorize) continue;
+
+                    int foregroundStart = i + 1;
+                    int foregroundEnd = findEndOfNumber(str, foregroundStart);
+                    // If we have a foreground element
+                    if (foregroundEnd > foregroundStart) {
+                        byte foreground = readNumber(str, foregroundStart, foregroundEnd);
+
+                        byte background = -1;
+                        int backgroundEnd = -1;
+                        // If we have a background code, read it
+                        if (str.length() > foregroundEnd && str.charAt(foregroundEnd) == ',') {
+                            backgroundEnd = findEndOfNumber(str, foregroundEnd + 1);
+                            background = readNumber(str, foregroundEnd + 1, backgroundEnd);
+                        }
+                        // If previous element was also a color element, try to reuse background
+                        if (!stack.empty() && stack.peek().format.id() == CODE_COLOR) {
+                            // Apply old format
+                            FormatDescription oldFormat = stack.pop();
+                            oldFormat.apply(plainText, plainText.length());
+                            // Reuse old background, if possible
+                            if (background == -1)
+                                background = ((ColorIrcFormat) oldFormat.format).background;
+                        }
+                        // Add new format
+                        stack.push(new FormatDescription(plainText.length(), new ColorIrcFormat(foreground, background)));
+
+                        // i points in front of the next character
+                        i = ((backgroundEnd == -1) ? foregroundEnd : backgroundEnd) - 1;
+
+                        // Otherwise assume this is a closing tag
+                    } else if (!stack.empty() && stack.peek().format.id() == CODE_COLOR) {
+                        stack.pop().apply(plainText, plainText.length());
+                    }
+                }
+                break;
+                case CODE_SWAP: {
+                    if (!colorize) continue;
+
+                    // If we have a color tag before, apply it, and create a new one with swapped colors
+                    if (!stack.empty() && stack.peek().format.id() == CODE_COLOR) {
+                        FormatDescription format = stack.pop();
+                        format.apply(plainText, plainText.length());
+                        stack.push(new FormatDescription(plainText.length(), ((ColorIrcFormat) format.format).copySwapped()));
+                    }
+                }
+                break;
+                case CODE_RESET: {
+                    if (!colorize) continue;
+
+                    // End all formatting tags
+                    while (!stack.empty()) {
+                        stack.pop().apply(plainText, plainText.length());
+                    }
+                }
+                break;
+                default: {
+                    // Just append it, if it’s not special
+                    plainText.append(character);
+                }
+            }
+        }
+
+        // End all formatting tags
+        while (!stack.empty()) {
+            stack.pop().apply(plainText, plainText.length());
+        }
+        return plainText;
+    }
+
+    private interface IrcFormat {
+        void applyTo(@NonNull SpannableStringBuilder editable, int from, int to);
+
+        byte id();
+    }
+
+    public interface ColorSupplier {
+        @ColorInt
+        int ircColor(byte color);
+    }
+
+    private static class FormatDescription {
+        public final int start;
+        @NonNull
+        public final IrcFormat format;
+
+        public FormatDescription(int start, @NonNull IrcFormat format) {
+            this.start = start;
+            this.format = format;
+        }
+
+        public void apply(@NonNull SpannableStringBuilder editable, int end) {
+            format.applyTo(editable, start, end);
+        }
+    }
+
+    private static class ItalicIrcFormat implements IrcFormat {
+        @Override
+        public void applyTo(@NonNull SpannableStringBuilder editable, int from, int to) {
+            editable.setSpan(new ItalicSpan(), from, to, Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
+        }
+
+        @Override
+        public byte id() {
+            return CODE_ITALIC;
+        }
+    }
+
+    private static class UnderlineIrcFormat implements IrcFormat {
+        @Override
+        public void applyTo(@NonNull SpannableStringBuilder editable, int from, int to) {
+            editable.setSpan(new UnderlineSpan(), from, to, Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
+        }
+
+        @Override
+        public byte id() {
+            return CODE_UNDERLINE;
+        }
+    }
+
+    private static class BoldIrcFormat implements IrcFormat {
+        @Override
+        public void applyTo(@NonNull SpannableStringBuilder editable, int from, int to) {
+            editable.setSpan(new BoldSpan(), from, to, Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
+        }
+
+        @Override
+        public byte id() {
+            return CODE_BOLD;
+        }
+    }
+
+    private class ColorIrcFormat implements IrcFormat {
+        private final byte foreground;
+        private final byte background;
+
+        public ColorIrcFormat(byte foreground, byte background) {
+            this.foreground = foreground;
+            this.background = background;
+        }
+
+        @Override
+        public void applyTo(@NonNull SpannableStringBuilder editable, int from, int to) {
+            int[] mircColors = context.themeUtil().res.mircColors;
+            if (foreground != -1) {
+                editable.setSpan(new ForegroundColorSpan(mircColors[foreground % 16]), from, to, Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
+            }
+            if (background != -1) {
+                editable.setSpan(new BackgroundColorSpan(mircColors[background % 16]), from, to, Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
+            }
+        }
+
+        @NonNull
+        public ColorIrcFormat copySwapped() {
+            return new ColorIrcFormat(background, foreground);
+        }
+
+        @Override
+        public byte id() {
+            return CODE_COLOR;
+        }
+    }
+}
diff --git a/app/src/main/java/de/kuschku/util/irc/IrcFormatHelper.java b/app/src/main/java/de/kuschku/util/irc/format/IrcFormatHelper.java
similarity index 95%
rename from app/src/main/java/de/kuschku/util/irc/IrcFormatHelper.java
rename to app/src/main/java/de/kuschku/util/irc/format/IrcFormatHelper.java
index b538a376f..21fadd263 100644
--- a/app/src/main/java/de/kuschku/util/irc/IrcFormatHelper.java
+++ b/app/src/main/java/de/kuschku/util/irc/format/IrcFormatHelper.java
@@ -19,10 +19,9 @@
  * with this program.  If not, see <http://www.gnu.org/licenses/>.
  */
 
-package de.kuschku.util.irc;
+package de.kuschku.util.irc.format;
 
 
-import android.app.Activity;
 import android.content.ActivityNotFoundException;
 import android.content.Context;
 import android.content.Intent;
@@ -47,7 +46,7 @@ import java.util.regex.Pattern;
 
 import de.kuschku.quasseldroid_ng.R;
 import de.kuschku.quasseldroid_ng.ui.theme.AppContext;
-import de.kuschku.util.ui.MessageUtil;
+import de.kuschku.util.irc.IrcUserUtils;
 
 public class IrcFormatHelper {
     @NonNull
@@ -85,7 +84,7 @@ public class IrcFormatHelper {
     public CharSequence formatIrcMessage(@NonNull String message) {
         List<FutureClickableSpan> spans = new LinkedList<>();
 
-        SpannableString str = new SpannableString(MessageUtil.parseStyleCodes(context.themeUtil(), message, context.settings().mircColors.get()));
+        SpannableString str = new SpannableString(context.deserializer().formatString(message));
         Matcher urlMatcher = urlPattern.matcher(str);
         while (urlMatcher.find()) {
             spans.add(new FutureClickableSpan(new CustomURLSpan(urlMatcher.group()), urlMatcher.start(), urlMatcher.end()));
diff --git a/app/src/main/java/de/kuschku/quasseldroid_ng/ui/editor/FormattingHelper.java b/app/src/main/java/de/kuschku/util/irc/format/IrcFormatSerializer.java
similarity index 97%
rename from app/src/main/java/de/kuschku/quasseldroid_ng/ui/editor/FormattingHelper.java
rename to app/src/main/java/de/kuschku/util/irc/format/IrcFormatSerializer.java
index e40bde252..df9bcc4db 100644
--- a/app/src/main/java/de/kuschku/quasseldroid_ng/ui/editor/FormattingHelper.java
+++ b/app/src/main/java/de/kuschku/util/irc/format/IrcFormatSerializer.java
@@ -19,7 +19,7 @@
  * with this program.  If not, see <http://www.gnu.org/licenses/>.
  */
 
-package de.kuschku.quasseldroid_ng.ui.editor;
+package de.kuschku.util.irc.format;
 
 import android.support.annotation.NonNull;
 import android.text.Spanned;
@@ -32,10 +32,10 @@ import java.util.Locale;
 
 import de.kuschku.quasseldroid_ng.ui.theme.AppContext;
 
-public class FormattingHelper {
+public class IrcFormatSerializer {
     private final AppContext context;
 
-    public FormattingHelper(AppContext context) {
+    public IrcFormatSerializer(AppContext context) {
         this.context = context;
     }
 
diff --git a/app/src/main/java/de/kuschku/quasseldroid_ng/ui/editor/ItalicSpan.java b/app/src/main/java/de/kuschku/util/irc/format/ItalicSpan.java
similarity index 95%
rename from app/src/main/java/de/kuschku/quasseldroid_ng/ui/editor/ItalicSpan.java
rename to app/src/main/java/de/kuschku/util/irc/format/ItalicSpan.java
index 7a8a638cc..96c93a85b 100644
--- a/app/src/main/java/de/kuschku/quasseldroid_ng/ui/editor/ItalicSpan.java
+++ b/app/src/main/java/de/kuschku/util/irc/format/ItalicSpan.java
@@ -19,7 +19,7 @@
  * with this program.  If not, see <http://www.gnu.org/licenses/>.
  */
 
-package de.kuschku.quasseldroid_ng.ui.editor;
+package de.kuschku.util.irc.format;
 
 import android.graphics.Typeface;
 import android.text.style.StyleSpan;
diff --git a/app/src/main/java/de/kuschku/util/ui/MessageUtil.java b/app/src/main/java/de/kuschku/util/ui/MessageUtil.java
deleted file mode 100644
index 856ecac77..000000000
--- a/app/src/main/java/de/kuschku/util/ui/MessageUtil.java
+++ /dev/null
@@ -1,191 +0,0 @@
-/*
- * QuasselDroid - Quassel client for Android
- * Copyright (C) 2016 Janne Koschinski
- * Copyright (C) 2016 Ken Børge Viktil
- * Copyright (C) 2016 Magnus Fjell
- * Copyright (C) 2016 Martin Sandsmark <martin.sandsmark@kde.org>
- *
- * This program is free software: you can redistribute it and/or modify it
- * under the terms of the GNU General Public License as published by the Free
- * Software Foundation, either version 3 of the License, or (at your option)
- * any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along
- * with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-package de.kuschku.util.ui;
-
-import android.graphics.Typeface;
-import android.support.annotation.NonNull;
-import android.text.Spannable;
-import android.text.SpannableString;
-import android.text.SpannableStringBuilder;
-import android.text.style.BackgroundColorSpan;
-import android.text.style.ForegroundColorSpan;
-import android.text.style.StyleSpan;
-import android.text.style.UnderlineSpan;
-
-import de.kuschku.quasseldroid_ng.ui.theme.ThemeUtil;
-
-public class MessageUtil {
-    // Transparent in ARGB
-    private static final int COLOR_TRANSPARENT = 0x00000000;
-
-    /**
-     * Parse mIRC style codes in IrcMessage
-     */
-    @NonNull
-    public static SpannableString parseStyleCodes(@NonNull ThemeUtil themeUtil, @NonNull String content, boolean parse) {
-        if (!parse) {
-            return new SpannableString(content
-                    .replaceAll("\\x02", "")
-                    .replaceAll("\\x0F", "")
-                    .replaceAll("\\x1D", "")
-                    .replaceAll("\\x1F", "")
-                    .replaceAll("\\x03[0-9]{1,2}(,[0-9]{1,2})?", "")
-                    .replaceAll("\\x03", ""));
-        }
-
-        final char boldIndicator = 2;
-        final char normalIndicator = 15;
-        final char italicIndicator = 29;
-        final char underlineIndicator = 31;
-        final char colorIndicator = 3;
-
-        if (content.indexOf(boldIndicator) == -1
-                && content.indexOf(italicIndicator) == -1
-                && content.indexOf(underlineIndicator) == -1
-                && content.indexOf(colorIndicator) == -1)
-            return new SpannableString(content);
-
-        SpannableStringBuilder newString = new SpannableStringBuilder(content);
-
-        int start, end, endSearchOffset, startIndicatorLength, style, fg, bg;
-        while (true) {
-            content = newString.toString();
-            end = -1;
-            startIndicatorLength = 1;
-            style = 0;
-            fg = -1;
-            bg = -1;
-
-            // Colors?
-            start = content.indexOf(colorIndicator);
-
-            if (start != -1) {
-                // Note that specifying colour codes here is optional, as the same indicator will cancel existing colours
-                endSearchOffset = start + 1;
-                if (endSearchOffset < content.length()) {
-                    if (Character.isDigit(content.charAt(endSearchOffset))) {
-                        if (endSearchOffset + 1 < content.length() && Character.isDigit(content.charAt(endSearchOffset + 1))) {
-                            fg = Integer.parseInt(content.substring(endSearchOffset, endSearchOffset + 2));
-                            endSearchOffset += 2;
-                        } else {
-                            fg = Integer.parseInt(content.substring(endSearchOffset, endSearchOffset + 1));
-                            endSearchOffset += 1;
-                        }
-
-                        if (endSearchOffset < content.length() && content.charAt(endSearchOffset) == ',') {
-                            if (endSearchOffset + 1 < content.length() && Character.isDigit(content.charAt(endSearchOffset + 1))) {
-                                endSearchOffset++;
-                                if (endSearchOffset + 1 < content.length() && Character.isDigit(content.charAt(endSearchOffset + 1))) {
-                                    bg = Integer.parseInt(content.substring(endSearchOffset, endSearchOffset + 2));
-                                    endSearchOffset += 2;
-                                } else {
-                                    bg = Integer.parseInt(content.substring(endSearchOffset, endSearchOffset + 1));
-                                    endSearchOffset += 1;
-                                }
-                            }
-                        }
-                    }
-                }
-                startIndicatorLength = endSearchOffset - start;
-
-                end = content.indexOf(colorIndicator, endSearchOffset);
-            }
-
-            if (start == -1) {
-                start = content.indexOf(boldIndicator);
-                if (start != -1) {
-                    end = content.indexOf(boldIndicator, start + 1);
-                    style = Typeface.BOLD;
-                }
-            }
-
-            if (start == -1) {
-                start = content.indexOf(italicIndicator);
-                if (start != -1) {
-                    end = content.indexOf(italicIndicator, start + 1);
-                    style = Typeface.ITALIC;
-                }
-            }
-
-            if (start == -1) {
-                start = content.indexOf(underlineIndicator);
-                if (start != -1) {
-                    end = content.indexOf(underlineIndicator, start + 1);
-                    style = -1;
-                }
-            }
-
-            if (start == -1)
-                break;
-
-            int norm = content.indexOf(normalIndicator, start + 1);
-            if (norm != -1 && (end == -1 || norm < end))
-                end = norm;
-
-            if (end == -1)
-                end = content.length();
-
-            if (end - (start + startIndicatorLength) > 0) {
-                // Only set spans if there's any text between start & end
-                if (style == -1) {
-                    newString.setSpan(new UnderlineSpan(), start, end, Spannable.SPAN_INCLUSIVE_EXCLUSIVE);
-                } else {
-                    newString.setSpan(new StyleSpan(style), start, end, Spannable.SPAN_INCLUSIVE_EXCLUSIVE);
-                }
-
-                if (fg != -1 && themeUtil.res.mircColors[fg] != COLOR_TRANSPARENT) {
-                    newString.setSpan(new ForegroundColorSpan(themeUtil.res.mircColors[fg]), start, end,
-                            Spannable.SPAN_INCLUSIVE_EXCLUSIVE);
-                }
-                if (bg != -1 && themeUtil.res.mircColors[fg] != COLOR_TRANSPARENT) {
-                    newString.setSpan(new BackgroundColorSpan(themeUtil.res.mircColors[bg]), start, end,
-                            Spannable.SPAN_INCLUSIVE_EXCLUSIVE);
-                }
-            }
-
-            // Intentionally don't remove "normal" indicators or color here, as they are multi-purpose
-            if (end < content.length() && (content.charAt(end) == boldIndicator
-                    || content.charAt(end) == italicIndicator
-                    || content.charAt(end) == underlineIndicator))
-                newString.delete(end, end + 1);
-
-            newString.delete(start, start + startIndicatorLength);
-        }
-
-        // NOW we remove the "normal" and color indicator
-        while (true) {
-            content = newString.toString();
-            int normPos = content.indexOf(normalIndicator);
-            if (normPos != -1)
-                newString.delete(normPos, normPos + 1);
-
-            int colorPos = content.indexOf(colorIndicator);
-            if (colorPos != -1)
-                newString.delete(colorPos, colorPos + 1);
-
-            if (normPos == -1 && colorPos == -1)
-                break;
-        }
-
-        return new SpannableString(newString);
-    }
-}
-- 
GitLab