diff --git a/app/src/main/java/de/kuschku/quasseldroid/util/irc/format/ChannelLinkSpan.kt b/app/src/main/java/de/kuschku/quasseldroid/util/irc/format/ChannelLinkSpan.kt
new file mode 100644
index 0000000000000000000000000000000000000000..7e9be6eb1b4e34f64c4774549abd4bbee97f1831
--- /dev/null
+++ b/app/src/main/java/de/kuschku/quasseldroid/util/irc/format/ChannelLinkSpan.kt
@@ -0,0 +1,47 @@
+/*
+ * Quasseldroid - Quassel client for Android
+ *
+ * Copyright (c) 2019 Janne Koschinski
+ * Copyright (c) 2019 The Quassel Project
+ *
+ * This program is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 3 as published
+ * by the Free Software Foundation.
+ *
+ * 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.quasseldroid.util.irc.format
+
+import android.text.TextPaint
+import android.text.style.ClickableSpan
+import android.view.View
+import de.kuschku.libquassel.protocol.NetworkId
+import de.kuschku.quasseldroid.ui.chat.ChatActivity
+
+class ChannelLinkSpan(
+  private val networkId: NetworkId,
+  private val text: String,
+  private val highlight: Boolean
+) : ClickableSpan() {
+  override fun updateDrawState(ds: TextPaint?) {
+    if (ds != null) {
+      if (!highlight) ds.color = ds.linkColor
+      ds.isUnderlineText = true
+    }
+  }
+
+  override fun onClick(widget: View) {
+    ChatActivity.launch(
+      widget.context,
+      networkId = networkId,
+      channel = text
+    )
+  }
+}
diff --git a/app/src/main/java/de/kuschku/quasseldroid/util/irc/format/ContentFormatter.kt b/app/src/main/java/de/kuschku/quasseldroid/util/irc/format/ContentFormatter.kt
index 116acfa76781b1dd13e97d0be6a540913d42033d..7c0ae3c1473c8624e9e0d7b907701050cc167868 100644
--- a/app/src/main/java/de/kuschku/quasseldroid/util/irc/format/ContentFormatter.kt
+++ b/app/src/main/java/de/kuschku/quasseldroid/util/irc/format/ContentFormatter.kt
@@ -22,20 +22,17 @@ package de.kuschku.quasseldroid.util.irc.format
 import android.content.Context
 import android.graphics.Typeface
 import android.text.SpannableString
-import android.text.Spanned
-import android.text.TextPaint
-import android.text.style.*
-import android.view.View
+import android.text.style.ForegroundColorSpan
+import android.text.style.StyleSpan
 import androidx.annotation.ColorInt
 import de.kuschku.libquassel.protocol.NetworkId
 import de.kuschku.libquassel.util.irc.HostmaskHelper
 import de.kuschku.libquassel.util.irc.SenderColorUtil
 import de.kuschku.quasseldroid.R
 import de.kuschku.quasseldroid.settings.MessageSettings
-import de.kuschku.quasseldroid.ui.chat.ChatActivity
 import de.kuschku.quasseldroid.util.helper.styledAttributes
-import de.kuschku.quasseldroid.util.irc.format.spans.IrcBackgroundColorSpan
-import de.kuschku.quasseldroid.util.irc.format.spans.IrcForegroundColorSpan
+import de.kuschku.quasseldroid.util.irc.format.model.FormatInfo
+import de.kuschku.quasseldroid.util.irc.format.model.IrcFormat
 import de.kuschku.quasseldroid.util.ui.SpanFormatter
 import org.intellij.lang.annotations.Language
 import javax.inject.Inject
@@ -80,59 +77,28 @@ class ContentFormatter @Inject constructor(
     getColor(0, 0)
   }
 
-  class QuasselURLSpan(text: String, private val highlight: Boolean) : URLSpan(text) {
-    override fun updateDrawState(ds: TextPaint?) {
-      if (ds != null) {
-        if (!highlight) ds.color = ds.linkColor
-        ds.isUnderlineText = true
-      }
-    }
-  }
-
-  class ChannelLinkSpan(private val networkId: NetworkId, private val text: String,
-                        private val highlight: Boolean) : ClickableSpan() {
-    override fun updateDrawState(ds: TextPaint?) {
-      if (ds != null) {
-        if (!highlight) ds.color = ds.linkColor
-        ds.isUnderlineText = true
-      }
-    }
-
-    override fun onClick(widget: View) {
-      ChatActivity.launch(
-        widget.context,
-        networkId = networkId,
-        channel = text
-      )
-    }
-  }
-
   fun formatContent(content: String,
                     highlight: Boolean = false,
                     showSpoilers: Boolean = false,
                     networkId: NetworkId?): CharSequence {
-    val formattedText = ircFormatDeserializer.formatString(content, messageSettings.colorizeMirc)
-    val text = SpannableString(formattedText)
+    val spans = mutableListOf<FormatInfo>()
+    val formattedText = SpannableString(
+      ircFormatDeserializer.formatString(
+        content,
+        messageSettings.colorizeMirc,
+        spans
+      )
+    )
 
     if (showSpoilers) {
-      val spans = mutableMapOf<Triple<Int, Int, Int>, MutableList<Any>>()
-      for (span in text.getSpans(0, text.length, IrcForegroundColorSpan::class.java)) {
-        val from = text.getSpanStart(span)
-        val to = text.getSpanEnd(span)
-        spans.getOrPut(Triple(from, to, span.getForegroundColor()), ::mutableListOf).add(span)
-      }
-      for (span in text.getSpans(0, text.length, IrcBackgroundColorSpan::class.java)) {
-        val from = text.getSpanStart(span)
-        val to = text.getSpanEnd(span)
-        spans.getOrPut(Triple(from, to, span.getBackgroundColor()), ::mutableListOf).add(span)
-      }
-      for (group in spans.values) {
-        if (group.size > 1 &&
-            group.any { it is ForegroundColorSpan } &&
-            group.any { it is BackgroundColorSpan }) {
-          for (span in group) {
-            text.removeSpan(span)
-          }
+      spans.removeAll {
+        when {
+          it.format is IrcFormat.Color ->
+            it.format.foreground == it.format.background
+          it.format is IrcFormat.Hex   ->
+            it.format.foreground == it.format.background
+          else                         ->
+            false
         }
       }
     }
@@ -140,11 +106,11 @@ class ContentFormatter @Inject constructor(
     for (result in urlPattern.findAll(formattedText)) {
       val group = result.groups[1]
       if (group != null) {
-        text.setSpan(
-          QuasselURLSpan(group.value, highlight), group.range.start,
+        spans.add(FormatInfo(
+          group.range.start,
           group.range.start + group.value.length,
-          Spanned.SPAN_INCLUSIVE_EXCLUSIVE
-        )
+          IrcFormat.Url(group.value, highlight)
+        ))
       }
     }
 
@@ -152,15 +118,21 @@ class ContentFormatter @Inject constructor(
       for (result in channelPattern.findAll(formattedText)) {
         val group = result.groups[1]
         if (group != null) {
-          text.setSpan(ChannelLinkSpan(networkId, group.value, highlight),
-                       group.range.start,
-                       group.range.endInclusive + 1,
-                       Spanned.SPAN_INCLUSIVE_EXCLUSIVE)
+          spans.add(FormatInfo(
+            group.range.start,
+            group.range.start + group.value.length,
+            IrcFormat.Channel(networkId, group.value, highlight)
+          ))
         }
       }
     }
 
-    return text
+    spans.reverse()
+    for (span in spans) {
+      span.apply(formattedText)
+    }
+
+    return formattedText
   }
 
   private fun formatNickNickImpl(nick: String, self: Boolean, colorize: Boolean,
diff --git a/app/src/main/java/de/kuschku/quasseldroid/util/irc/format/IrcFormatDeserializer.kt b/app/src/main/java/de/kuschku/quasseldroid/util/irc/format/IrcFormatDeserializer.kt
index a390248aed26cf0115b8406802c66cf28256922e..03e3aff35ba7989ad1b6b19317c23de63cb2917c 100644
--- a/app/src/main/java/de/kuschku/quasseldroid/util/irc/format/IrcFormatDeserializer.kt
+++ b/app/src/main/java/de/kuschku/quasseldroid/util/irc/format/IrcFormatDeserializer.kt
@@ -21,11 +21,12 @@ package de.kuschku.quasseldroid.util.irc.format
 
 import android.content.Context
 import android.text.SpannableStringBuilder
-import android.text.Spanned
 import de.kuschku.quasseldroid.R
 import de.kuschku.quasseldroid.util.compatibility.AndroidCrashFixer
 import de.kuschku.quasseldroid.util.helper.getColorCompat
-import de.kuschku.quasseldroid.util.irc.format.spans.*
+import de.kuschku.quasseldroid.util.irc.format.model.FormatDescription
+import de.kuschku.quasseldroid.util.irc.format.model.FormatInfo
+import de.kuschku.quasseldroid.util.irc.format.model.IrcFormat
 import javax.inject.Inject
 
 /**
@@ -70,19 +71,28 @@ class IrcFormatDeserializer(private val mircColors: IntArray) {
    * @param content mIRC formatted String
    * @return a CharSequence with Android’s span format representing the input string
    */
-  fun formatString(content: String?, colorize: Boolean): CharSequence {
+  fun formatString(content: String?, colorize: Boolean,
+                   output: MutableList<FormatInfo>? = null): CharSequence {
     if (content == null) return ""
 
     val str = AndroidCrashFixer.removeCrashableCharacters(content)
 
     val plainText = SpannableStringBuilder()
-    var bold: FormatDescription<BoldIrcFormat>? = null
-    var italic: FormatDescription<ItalicIrcFormat>? = null
-    var underline: FormatDescription<UnderlineIrcFormat>? = null
-    var strikethrough: FormatDescription<StrikethroughIrcFormat>? = null
-    var monospace: FormatDescription<MonospaceIrcFormat>? = null
-    var color: FormatDescription<ColorIrcFormat>? = null
-    var hexColor: FormatDescription<HexIrcFormat>? = null
+    var bold: FormatDescription<IrcFormat.Bold>? = null
+    var italic: FormatDescription<IrcFormat.Italic>? = null
+    var underline: FormatDescription<IrcFormat.Underline>? = null
+    var strikethrough: FormatDescription<IrcFormat.Strikethrough>? = null
+    var monospace: FormatDescription<IrcFormat.Monospace>? = null
+    var color: FormatDescription<IrcFormat.Color>? = null
+    var hexColor: FormatDescription<IrcFormat.Hex>? = null
+
+    fun applyFormat(desc: FormatDescription<IrcFormat>) {
+      if (output != null) {
+        output.add(FormatInfo(desc.start, plainText.length, desc.format))
+      } else {
+        desc.apply(plainText, plainText.length)
+      }
+    }
 
     // Iterating over every character
     var normalCount = 0
@@ -96,11 +106,12 @@ class IrcFormatDeserializer(private val mircColors: IntArray) {
 
           // If there is an element on stack with the same code, close it
           bold = if (bold != null) {
-            if (colorize) bold.apply(plainText, plainText.length)
+            if (colorize) applyFormat(bold)
             null
             // Otherwise create a new one
           } else {
-            FormatDescription(plainText.length, BoldIrcFormat())
+            FormatDescription(plainText.length,
+                              IrcFormat.Bold)
           }
         }
         CODE_ITALIC        -> {
@@ -109,11 +120,12 @@ class IrcFormatDeserializer(private val mircColors: IntArray) {
 
           // If there is an element on stack with the same code, close it
           italic = if (italic != null) {
-            if (colorize) italic.apply(plainText, plainText.length)
+            if (colorize) applyFormat(italic)
             null
             // Otherwise create a new one
           } else {
-            FormatDescription(plainText.length, ItalicIrcFormat())
+            FormatDescription(plainText.length,
+                              IrcFormat.Italic)
           }
         }
         CODE_UNDERLINE     -> {
@@ -122,11 +134,12 @@ class IrcFormatDeserializer(private val mircColors: IntArray) {
 
           // If there is an element on stack with the same code, close it
           underline = if (underline != null) {
-            if (colorize) underline.apply(plainText, plainText.length)
+            if (colorize) applyFormat(underline)
             null
             // Otherwise create a new one
           } else {
-            FormatDescription(plainText.length, UnderlineIrcFormat())
+            FormatDescription(plainText.length,
+                              IrcFormat.Underline)
           }
         }
         CODE_STRIKETHROUGH -> {
@@ -135,11 +148,12 @@ class IrcFormatDeserializer(private val mircColors: IntArray) {
 
           // If there is an element on stack with the same code, close it
           strikethrough = if (strikethrough != null) {
-            if (colorize) strikethrough.apply(plainText, plainText.length)
+            if (colorize) applyFormat(strikethrough)
             null
             // Otherwise create a new one
           } else {
-            FormatDescription(plainText.length, StrikethroughIrcFormat())
+            FormatDescription(plainText.length,
+                              IrcFormat.Strikethrough)
           }
         }
         CODE_MONOSPACE     -> {
@@ -148,11 +162,12 @@ class IrcFormatDeserializer(private val mircColors: IntArray) {
 
           // If there is an element on stack with the same code, close it
           monospace = if (monospace != null) {
-            if (colorize) monospace.apply(plainText, plainText.length)
+            if (colorize) applyFormat(monospace)
             null
             // Otherwise create a new one
           } else {
-            FormatDescription(plainText.length, MonospaceIrcFormat())
+            FormatDescription(plainText.length,
+                              IrcFormat.Monospace)
           }
         }
         CODE_COLOR         -> {
@@ -175,14 +190,14 @@ class IrcFormatDeserializer(private val mircColors: IntArray) {
             // If previous element was also a color element, try to reuse background
             if (color != null) {
               // Apply old format
-              if (colorize) color.apply(plainText, plainText.length)
+              if (colorize) applyFormat(color)
               // Reuse old background, if possible
               if (background.toInt() == -1)
                 background = color.format.background
             }
             // Add new format
             color = FormatDescription(
-              plainText.length, ColorIrcFormat(foreground, background, this.mircColors)
+              plainText.length, IrcFormat.Color(foreground, background, this.mircColors)
             )
 
             // i points in front of the next character
@@ -190,7 +205,7 @@ class IrcFormatDeserializer(private val mircColors: IntArray) {
 
             // Otherwise assume this is a closing tag
           } else if (color != null) {
-            if (colorize) color.apply(plainText, plainText.length)
+            if (colorize) applyFormat(color)
             color = null
           }
         }
@@ -214,20 +229,23 @@ class IrcFormatDeserializer(private val mircColors: IntArray) {
             // If previous element was also a color element, try to reuse background
             if (hexColor != null) {
               // Apply old format
-              if (colorize) hexColor.apply(plainText, plainText.length)
+              if (colorize) applyFormat(hexColor)
               // Reuse old background, if possible
               if (background == -1)
                 background = hexColor.format.background
             }
             // Add new format
-            hexColor = FormatDescription(plainText.length, HexIrcFormat(foreground, background))
+            hexColor = FormatDescription(plainText.length,
+                                         IrcFormat.Hex(
+                                           foreground,
+                                           background))
 
             // i points in front of the next character
             i = (if (backgroundEnd == -1) foregroundEnd else backgroundEnd) - 1
 
             // Otherwise assume this is a closing tag
           } else if (hexColor != null) {
-            if (colorize) hexColor.apply(plainText, plainText.length)
+            if (colorize) applyFormat(hexColor)
             hexColor = null
           }
         }
@@ -237,7 +255,7 @@ class IrcFormatDeserializer(private val mircColors: IntArray) {
 
           // If we have a color tag before, apply it, and create a new one with swapped colors
           if (color != null) {
-            if (colorize) color.apply(plainText, plainText.length)
+            if (colorize) applyFormat(color)
             color = FormatDescription(
               plainText.length, color.format.copySwapped()
             )
@@ -249,23 +267,23 @@ class IrcFormatDeserializer(private val mircColors: IntArray) {
 
           // End all formatting tags
           if (bold != null) {
-            if (colorize) bold.apply(plainText, plainText.length)
+            if (colorize) applyFormat(bold)
             bold = null
           }
           if (italic != null) {
-            if (colorize) italic.apply(plainText, plainText.length)
+            if (colorize) applyFormat(italic)
             italic = null
           }
           if (underline != null) {
-            if (colorize) underline.apply(plainText, plainText.length)
+            if (colorize) applyFormat(underline)
             underline = null
           }
           if (color != null) {
-            if (colorize) color.apply(plainText, plainText.length)
+            if (colorize) applyFormat(color)
             color = null
           }
           if (hexColor != null) {
-            if (colorize) hexColor.apply(plainText, plainText.length)
+            if (colorize) applyFormat(hexColor)
             hexColor = null
           }
         }
@@ -281,108 +299,29 @@ class IrcFormatDeserializer(private val mircColors: IntArray) {
 
     // End all formatting tags
     if (bold != null) {
-      if (colorize) bold.apply(plainText, plainText.length)
+      if (colorize) applyFormat(bold)
     }
     if (italic != null) {
-      if (colorize) italic.apply(plainText, plainText.length)
+      if (colorize) applyFormat(italic)
     }
     if (underline != null) {
-      if (colorize) underline.apply(plainText, plainText.length)
+      if (colorize) applyFormat(underline)
     }
     if (strikethrough != null) {
-      if (colorize) strikethrough.apply(plainText, plainText.length)
+      if (colorize) applyFormat(strikethrough)
     }
     if (monospace != null) {
-      if (colorize) monospace.apply(plainText, plainText.length)
+      if (colorize) applyFormat(monospace)
     }
     if (color != null) {
-      if (colorize) color.apply(plainText, plainText.length)
+      if (colorize) applyFormat(color)
     }
     if (hexColor != null) {
-      if (colorize) hexColor.apply(plainText, plainText.length)
+      if (colorize) applyFormat(hexColor)
     }
     return plainText
   }
 
-  private interface IrcFormat {
-    fun applyTo(editable: SpannableStringBuilder, from: Int, to: Int)
-  }
-
-  private class FormatDescription<out U : IrcFormat>(val start: Int, val format: U) {
-    fun apply(editable: SpannableStringBuilder, end: Int) {
-      format.applyTo(editable, start, end)
-    }
-  }
-
-  private class ItalicIrcFormat : IrcFormat {
-    override fun applyTo(editable: SpannableStringBuilder, from: Int, to: Int) {
-      editable.setSpan(IrcItalicSpan(), from, to, Spanned.SPAN_INCLUSIVE_EXCLUSIVE)
-    }
-  }
-
-  private class UnderlineIrcFormat : IrcFormat {
-    override fun applyTo(editable: SpannableStringBuilder, from: Int, to: Int) {
-      editable.setSpan(IrcUnderlineSpan(), from, to, Spanned.SPAN_INCLUSIVE_EXCLUSIVE)
-    }
-  }
-
-  private class StrikethroughIrcFormat : IrcFormat {
-    override fun applyTo(editable: SpannableStringBuilder, from: Int, to: Int) {
-      editable.setSpan(IrcStrikethroughSpan(), from, to, Spanned.SPAN_INCLUSIVE_EXCLUSIVE)
-    }
-  }
-
-  private class MonospaceIrcFormat : IrcFormat {
-    override fun applyTo(editable: SpannableStringBuilder, from: Int, to: Int) {
-      editable.setSpan(IrcMonospaceSpan(), from, to, Spanned.SPAN_INCLUSIVE_EXCLUSIVE)
-    }
-  }
-
-  private class BoldIrcFormat : IrcFormat {
-    override fun applyTo(editable: SpannableStringBuilder, from: Int, to: Int) {
-      editable.setSpan(IrcBoldSpan(), from, to, Spanned.SPAN_INCLUSIVE_EXCLUSIVE)
-    }
-  }
-
-  private inner class HexIrcFormat(val foreground: Int, val background: Int) : IrcFormat {
-    override fun applyTo(editable: SpannableStringBuilder, from: Int, to: Int) {
-      if (foreground >= 0) {
-        editable.setSpan(
-          IrcForegroundColorSpan.HEX(foreground or 0xFFFFFF.inv()), from, to,
-          Spanned.SPAN_INCLUSIVE_EXCLUSIVE
-        )
-      }
-      if (background >= 0) {
-        editable.setSpan(
-          IrcBackgroundColorSpan.HEX(background or 0xFFFFFF.inv()), from, to,
-          Spanned.SPAN_INCLUSIVE_EXCLUSIVE
-        )
-      }
-    }
-  }
-
-  private inner class ColorIrcFormat(val foreground: Byte, val background: Byte,
-                                     val mircColors: IntArray) : IrcFormat {
-    override fun applyTo(editable: SpannableStringBuilder, from: Int, to: Int) {
-      if (foreground.toInt() >= 0 && foreground.toInt() < mircColors.size) {
-        editable.setSpan(
-          IrcForegroundColorSpan.MIRC(foreground.toInt(), mircColors[foreground.toInt()]), from, to,
-          Spanned.SPAN_INCLUSIVE_EXCLUSIVE
-        )
-      }
-      if (background.toInt() >= 0 && background.toInt() < mircColors.size) {
-        editable.setSpan(
-          IrcBackgroundColorSpan.MIRC(background.toInt(), mircColors[background.toInt()]), from, to,
-          Spanned.SPAN_INCLUSIVE_EXCLUSIVE
-        )
-      }
-    }
-
-    fun copySwapped(): ColorIrcFormat {
-      return ColorIrcFormat(background, foreground, mircColors)
-    }
-  }
-
   companion object {
     private const val CODE_BOLD = 0x02.toChar()
     private const val CODE_COLOR = 0x03.toChar()
diff --git a/app/src/main/java/de/kuschku/quasseldroid/util/irc/format/QuasselURLSpan.kt b/app/src/main/java/de/kuschku/quasseldroid/util/irc/format/QuasselURLSpan.kt
new file mode 100644
index 0000000000000000000000000000000000000000..14921b3c7b6a677f399d1149c2631e9f951bdcc8
--- /dev/null
+++ b/app/src/main/java/de/kuschku/quasseldroid/util/irc/format/QuasselURLSpan.kt
@@ -0,0 +1,32 @@
+/*
+ * Quasseldroid - Quassel client for Android
+ *
+ * Copyright (c) 2019 Janne Koschinski
+ * Copyright (c) 2019 The Quassel Project
+ *
+ * This program is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 3 as published
+ * by the Free Software Foundation.
+ *
+ * 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.quasseldroid.util.irc.format
+
+import android.text.TextPaint
+import android.text.style.URLSpan
+
+class QuasselURLSpan(text: String, private val highlight: Boolean) : URLSpan(text) {
+  override fun updateDrawState(ds: TextPaint?) {
+    if (ds != null) {
+      if (!highlight) ds.color = ds.linkColor
+      ds.isUnderlineText = true
+    }
+  }
+}
diff --git a/app/src/main/java/de/kuschku/quasseldroid/util/irc/format/model/FormatDescription.kt b/app/src/main/java/de/kuschku/quasseldroid/util/irc/format/model/FormatDescription.kt
new file mode 100644
index 0000000000000000000000000000000000000000..add0deab4025e7f412ccf7b579caf0fb5c897ade
--- /dev/null
+++ b/app/src/main/java/de/kuschku/quasseldroid/util/irc/format/model/FormatDescription.kt
@@ -0,0 +1,28 @@
+/*
+ * Quasseldroid - Quassel client for Android
+ *
+ * Copyright (c) 2019 Janne Koschinski
+ * Copyright (c) 2019 The Quassel Project
+ *
+ * This program is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 3 as published
+ * by the Free Software Foundation.
+ *
+ * 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.quasseldroid.util.irc.format.model
+
+import android.text.SpannableStringBuilder
+
+class FormatDescription<out U : IrcFormat>(val start: Int, val format: U) {
+  fun apply(editable: SpannableStringBuilder, end: Int) {
+    format.applyTo(editable, start, end)
+  }
+}
diff --git a/app/src/main/java/de/kuschku/quasseldroid/util/irc/format/model/FormatInfo.kt b/app/src/main/java/de/kuschku/quasseldroid/util/irc/format/model/FormatInfo.kt
new file mode 100644
index 0000000000000000000000000000000000000000..76822446d9486635902c5ba47ff59bc93f743089
--- /dev/null
+++ b/app/src/main/java/de/kuschku/quasseldroid/util/irc/format/model/FormatInfo.kt
@@ -0,0 +1,28 @@
+/*
+ * Quasseldroid - Quassel client for Android
+ *
+ * Copyright (c) 2019 Janne Koschinski
+ * Copyright (c) 2019 The Quassel Project
+ *
+ * This program is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 3 as published
+ * by the Free Software Foundation.
+ *
+ * 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.quasseldroid.util.irc.format.model
+
+import android.text.Spannable
+
+data class FormatInfo(val start: Int, val end: Int, val format: IrcFormat) {
+  fun apply(editable: Spannable) {
+    format.applyTo(editable, start, end)
+  }
+}
diff --git a/app/src/main/java/de/kuschku/quasseldroid/util/irc/format/model/IrcFormat.kt b/app/src/main/java/de/kuschku/quasseldroid/util/irc/format/model/IrcFormat.kt
new file mode 100644
index 0000000000000000000000000000000000000000..9a5c5e35ad0ae9ae8b4a569c1cdf58f07621e27a
--- /dev/null
+++ b/app/src/main/java/de/kuschku/quasseldroid/util/irc/format/model/IrcFormat.kt
@@ -0,0 +1,150 @@
+/*
+ * Quasseldroid - Quassel client for Android
+ *
+ * Copyright (c) 2019 Janne Koschinski
+ * Copyright (c) 2019 The Quassel Project
+ *
+ * This program is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 3 as published
+ * by the Free Software Foundation.
+ *
+ * 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.quasseldroid.util.irc.format.model
+
+import android.text.Spannable
+import android.text.Spanned
+import de.kuschku.libquassel.protocol.NetworkId
+import de.kuschku.quasseldroid.util.irc.format.ChannelLinkSpan
+import de.kuschku.quasseldroid.util.irc.format.QuasselURLSpan
+import de.kuschku.quasseldroid.util.irc.format.spans.*
+
+sealed class IrcFormat {
+  abstract fun applyTo(editable: Spannable, from: Int, to: Int)
+
+  object Italic : IrcFormat() {
+    override fun applyTo(editable: Spannable, from: Int, to: Int) {
+      editable.setSpan(IrcItalicSpan(), from, to,
+                       Spanned.SPAN_INCLUSIVE_EXCLUSIVE)
+    }
+  }
+
+  object Underline : IrcFormat() {
+    override fun applyTo(editable: Spannable, from: Int, to: Int) {
+      editable.setSpan(IrcUnderlineSpan(), from, to,
+                       Spanned.SPAN_INCLUSIVE_EXCLUSIVE)
+    }
+  }
+
+  object Strikethrough : IrcFormat() {
+    override fun applyTo(editable: Spannable, from: Int, to: Int) {
+      editable.setSpan(IrcStrikethroughSpan(), from, to,
+                       Spanned.SPAN_INCLUSIVE_EXCLUSIVE)
+    }
+  }
+
+  object Monospace : IrcFormat() {
+    override fun applyTo(editable: Spannable, from: Int, to: Int) {
+      editable.setSpan(IrcMonospaceSpan(), from, to,
+                       Spanned.SPAN_INCLUSIVE_EXCLUSIVE)
+    }
+  }
+
+  object Bold : IrcFormat() {
+    override fun applyTo(editable: Spannable, from: Int, to: Int) {
+      editable.setSpan(IrcBoldSpan(), from, to,
+                       Spanned.SPAN_INCLUSIVE_EXCLUSIVE)
+    }
+  }
+
+  data class Hex(val foreground: Int, val background: Int) : IrcFormat() {
+    override fun applyTo(editable: Spannable, from: Int, to: Int) {
+      if (foreground >= 0) {
+        editable.setSpan(
+          IrcForegroundColorSpan.HEX(foreground or 0xFFFFFF.inv()), from, to,
+          Spanned.SPAN_INCLUSIVE_EXCLUSIVE
+        )
+      }
+      if (background >= 0) {
+        editable.setSpan(
+          IrcBackgroundColorSpan.HEX(background or 0xFFFFFF.inv()), from, to,
+          Spanned.SPAN_INCLUSIVE_EXCLUSIVE
+        )
+      }
+    }
+  }
+
+  data class Color(val foreground: Byte, val background: Byte,
+                   private val mircColors: IntArray) : IrcFormat() {
+    override fun applyTo(editable: Spannable, from: Int, to: Int) {
+      if (foreground.toInt() >= 0 && foreground.toInt() < mircColors.size) {
+        editable.setSpan(
+          IrcForegroundColorSpan.MIRC(foreground.toInt(),
+                                      mircColors[foreground.toInt()]),
+          from,
+          to,
+          Spanned.SPAN_INCLUSIVE_EXCLUSIVE
+        )
+      }
+      if (background.toInt() >= 0 && background.toInt() < mircColors.size) {
+        editable.setSpan(
+          IrcBackgroundColorSpan.MIRC(background.toInt(),
+                                      mircColors[background.toInt()]),
+          from,
+          to,
+          Spanned.SPAN_INCLUSIVE_EXCLUSIVE
+        )
+      }
+    }
+
+    fun copySwapped(): Color {
+      return Color(background, foreground, mircColors)
+    }
+
+    override fun toString(): String {
+      return "Color(foreground=$foreground, background=$background)"
+    }
+
+    override fun equals(other: Any?): Boolean {
+      if (this === other) return true
+      if (javaClass != other?.javaClass) return false
+
+      other as Color
+
+      if (foreground != other.foreground) return false
+      if (background != other.background) return false
+
+      return true
+    }
+
+    override fun hashCode(): Int {
+      var result = foreground.toInt()
+      result = 31 * result + background
+      return result
+    }
+  }
+
+  data class Url(val target: String, val highlight: Boolean) : IrcFormat() {
+    override fun applyTo(editable: Spannable, from: Int, to: Int) {
+      editable.setSpan(
+        QuasselURLSpan(target, highlight), from, to, Spanned.SPAN_INCLUSIVE_EXCLUSIVE
+      )
+    }
+  }
+
+  data class Channel(val networkId: NetworkId, val target: String, val highlight: Boolean) :
+    IrcFormat() {
+    override fun applyTo(editable: Spannable, from: Int, to: Int) {
+      editable.setSpan(
+        ChannelLinkSpan(networkId, target, highlight), from, to, Spanned.SPAN_INCLUSIVE_EXCLUSIVE
+      )
+    }
+  }
+}
diff --git a/app/src/test/java/de/kuschku/quasseldroid/util/irc/format/IrcFormatDeserializerTest.kt b/app/src/test/java/de/kuschku/quasseldroid/util/irc/format/IrcFormatDeserializerTest.kt
index 93e7a85759f242ba92b57d186d4d8869ab636f64..e980b77a5e882504becdb6b05d941920cfd24dc5 100644
--- a/app/src/test/java/de/kuschku/quasseldroid/util/irc/format/IrcFormatDeserializerTest.kt
+++ b/app/src/test/java/de/kuschku/quasseldroid/util/irc/format/IrcFormatDeserializerTest.kt
@@ -19,11 +19,9 @@
 
 package de.kuschku.quasseldroid.util.irc.format
 
-import android.text.Spanned
-import android.text.SpannedString
 import de.kuschku.quasseldroid.QuasseldroidTest
-import de.kuschku.quasseldroid.util.irc.format.spans.IrcBackgroundColorSpan
-import de.kuschku.quasseldroid.util.irc.format.spans.IrcForegroundColorSpan
+import de.kuschku.quasseldroid.util.irc.format.model.FormatInfo
+import de.kuschku.quasseldroid.util.irc.format.model.IrcFormat
 import org.junit.Assert.assertEquals
 import org.junit.Before
 import org.junit.Test
@@ -34,7 +32,7 @@ import org.robolectric.annotation.Config
 @Config(application = QuasseldroidTest::class)
 @RunWith(RobolectricTestRunner::class)
 class IrcFormatDeserializerTest {
-  lateinit var deserializer: IrcFormatDeserializer
+  private lateinit var deserializer: IrcFormatDeserializer
 
   @Before
   fun setUp() {
@@ -43,46 +41,25 @@ class IrcFormatDeserializerTest {
 
   @Test
   fun testMissingEndTag() {
-    val text = SpannedString.valueOf(deserializer.formatString(
-      "\u000301,01weeeeeeeeee",
-      colorize = true
-    ))
+    val spans = mutableListOf<FormatInfo>()
+    val text = deserializer.formatString(
+      content = "\u000301,01weeeeeeeeee",
+      colorize = true,
+      output = spans
+    )
+    assertEquals("weeeeeeeeee", text.toString())
     assertEquals(
       listOf(
-        SpanInfo(
-          from = 0,
-          to = 11,
-          flags = Spanned.SPAN_INCLUSIVE_EXCLUSIVE,
-          span = IrcForegroundColorSpan.MIRC(mircColor = 1, color = colors[1])
-        ),
-        SpanInfo(
-          from = 0,
-          to = 11,
-          flags = Spanned.SPAN_INCLUSIVE_EXCLUSIVE,
-          span = IrcBackgroundColorSpan.MIRC(mircColor = 1, color = colors[1])
+        FormatInfo(
+          start = 0,
+          end = 11,
+          format = IrcFormat.Color(1, 1, colors)
         )
       ),
-      text.allSpans<Any>()
+      spans
     )
   }
 
-  inline fun <reified T> Spanned.allSpans(): List<SpanInfo> =
-    getSpans(0, length, T::class.java).map {
-      SpanInfo(
-        from = getSpanStart(it),
-        to = getSpanEnd(it),
-        flags = getSpanFlags(it),
-        span = it
-      )
-    }
-
-  data class SpanInfo(
-    val from: Int,
-    val to: Int,
-    val flags: Int,
-    val span: Any?
-  )
-
   companion object {
     val colors = intArrayOf(
       0x00ffffff,